Commit 236b7fe8 by Aleksandr Tamakov

init

parents
*.iml
.gradle
/local.properties
/.idea/caches/build_file_checksums.ser
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
# Windows thumbnail db
Thumbs.db
# OSX files
# built application files
*.apk
*.ap_
*.bat
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
build/
/sources/base/injector/reports
# Local configuration file (sdk path, etc)
local.properties
# Eclipse project files
.classpath
.project
# Android Studio
.idea
/*/local.properties
/*/out
/*/*/build
build
/*/*/production
*.iws
*.ipr
*~
*.swp
/app/debug/
/app/release/
/build
\ No newline at end of file
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
id("dagger.hilt.android.plugin")
id("com.google.firebase.crashlytics")
id("com.google.gms.google-services")
id("kotlin-parcelize")
}
apply(from = "../tasks.gradle.kts")
val versionMajor = 1
val versionMinor = 0
val versionPatch = 0
android {
namespace = "com.isidroid.c23"
compileSdk = BuildVersions.COMPILE_SDK
val versionBuild = com.isidroid.gradle.getBuildVersion(project)
version = "${versionMajor}.${versionMinor}.${versionPatch}.${versionBuild}"
defaultConfig {
applicationId = "com.dynamix.teamprinter2"
minSdk = BuildVersions.MIN_SDK
targetSdk = BuildVersions.TARGET_SDK
versionCode = versionMajor * 1000000 + versionMinor * 100000 + versionPatch * 10000 + versionBuild
versionName = "$version"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
setProperty("archivesBaseName", "$applicationId.$versionName")
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
signingConfigs {
create("release") {
keyAlias = System.getenv("ISIDROID_KEY_ALIAS")
keyPassword = System.getenv("ISIDROID_KEY_PASSWORD")
storeFile = file(System.getenv("ISIDROID_KEYSTORE"))
storePassword = System.getenv("ISIDROID_KEYSTORE_PASSWORD")
}
}
buildTypes {
release {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("release")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = BuildVersions.KOTLIN_COMPILER_EXT_VERSION
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
testOptions {
reportDir = "$rootDir/test-reports"
resultsDir = "$rootDir/test-results"
}
flavorDimensionList.add("distributor")
productFlavors {
create("google"){
dimension = "distributor"
}
create("huawei"){
dimension = "distributor"
}
}
}
val googleImplementation by configurations
val huaweiImplementation by configurations
dependencies {
implementation(project(":feature:session"))
implementation(project(":feature:rendering"))
implementation(project(":feature:spot"))
implementation(project((":library:slider")))
implementation(project((":core:core")))
implementation(project((":ui:render_preview")))
implementation(project((":ui:compose_components")))
// google
implementation("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
implementation("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:${GoogleVersions.lifecycle}")
implementation("androidx.activity:activity-compose:${GoogleVersions.activityCompose}")
implementation("com.google.dagger:hilt-android:${GoogleVersions.hilt}")
implementation("androidx.hilt:hilt-navigation-compose:${GoogleVersions.hiltNavigationCompose}")
implementation("androidx.preference:preference-ktx:${GoogleVersions.preferences}")
implementation("androidx.room:room-runtime:${GoogleVersions.room}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
// geo location
implementation(project(":library:location"))
implementation(project(":ui:maps"))
// compose
implementation(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material:${GoogleVersions.material}")
implementation("androidx.navigation:navigation-compose:${GoogleVersions.navigationCompose}")
implementation("androidx.hilt:hilt-navigation-compose:${GoogleVersions.hiltNavigationCompose}")
implementation("androidx.constraintlayout:constraintlayout-compose:${GoogleVersions.constraint}")
// tools
implementation("com.jakewharton.timber:timber:${ToolsVersions.timber}")
// testing
testImplementation("junit:junit:${TestVersions.junit}")
androidTestImplementation("androidx.test.ext:junit:${TestVersions.junitExt}")
androidTestImplementation("androidx.test.espresso:espresso-core:${TestVersions.espressoCore}")
androidTestImplementation(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
{
"project_info": {
"project_number": "207645231385",
"firebase_url": "https://team-printer-office-561b8.firebaseio.com",
"project_id": "team-printer-office-561b8",
"storage_bucket": "team-printer-office-561b8.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:207645231385:android:346b5559e4fa6797",
"android_client_info": {
"package_name": "com.dynamix.teamprinter"
}
},
"oauth_client": [
{
"client_id": "207645231385-hv2kq37amnrfvh96g29nghpn6o6tlfp3.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.dynamix.teamprinter",
"certificate_hash": "af12952c13f390380e4d0c9c42f66e74719314b6"
}
},
{
"client_id": "207645231385-sji8rkugufs864b0dggor0qpnakqnc87.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.dynamix.teamprinter",
"certificate_hash": "6ff92641096252df3d9fb2dd8432bf6ab51d1407"
}
},
{
"client_id": "207645231385-u96krrj5qu06gui3qb950lk790p8ic7h.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCLHf7TFbDlvsaJ-OG29M1F99j5_oOnFaY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "207645231385-k7kaomj3qkdgu1gbohr6igbf01gdnm72.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "207645231385-tgs00p1s809s18bec0el83cvjpipa3up.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.dynamixsoftware.TeamPrinter-iOS"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:207645231385:android:b1bef82101eb96f7",
"android_client_info": {
"package_name": "com.dynamix.teamprinter.dev"
}
},
"oauth_client": [
{
"client_id": "207645231385-7ouioe7p5th2qo3ntkin8b7fjp9atm9q.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.dynamix.teamprinter.dev",
"certificate_hash": "6ff92641096252df3d9fb2dd8432bf6ab51d1407"
}
},
{
"client_id": "207645231385-u96krrj5qu06gui3qb950lk790p8ic7h.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCLHf7TFbDlvsaJ-OG29M1F99j5_oOnFaY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "207645231385-k7kaomj3qkdgu1gbohr6igbf01gdnm72.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "207645231385-tgs00p1s809s18bec0el83cvjpipa3up.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.dynamixsoftware.TeamPrinter-iOS"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:207645231385:android:357640f1bfaa54910ddd88",
"android_client_info": {
"package_name": "com.dynamix.teamprinter.spot"
}
},
"oauth_client": [
{
"client_id": "207645231385-u96krrj5qu06gui3qb950lk790p8ic7h.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCLHf7TFbDlvsaJ-OG29M1F99j5_oOnFaY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "207645231385-k7kaomj3qkdgu1gbohr6igbf01gdnm72.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "207645231385-tgs00p1s809s18bec0el83cvjpipa3up.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.dynamixsoftware.TeamPrinter-iOS"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:207645231385:android:722f71f816a2a97f0ddd88",
"android_client_info": {
"package_name": "com.dynamix.teamprinter2"
}
},
"oauth_client": [
{
"client_id": "207645231385-u96krrj5qu06gui3qb950lk790p8ic7h.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyCLHf7TFbDlvsaJ-OG29M1F99j5_oOnFaY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "207645231385-k7kaomj3qkdgu1gbohr6igbf01gdnm72.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "207645231385-tgs00p1s809s18bec0el83cvjpipa3up.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.dynamixsoftware.TeamPrinter-iOS"
}
}
]
}
}
}
],
"configuration_version": "1"
}
\ No newline at end of file
# 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
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "5310a974346e7b7c3f4fbbab21cba840",
"entities": [
{
"tableName": "Session",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "PrintProfile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `spotId` TEXT NOT NULL, `name` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `printWidth` INTEGER NOT NULL, `printHeight` INTEGER NOT NULL, `marginTop` INTEGER NOT NULL, `marginLeft` INTEGER NOT NULL, `dpix` INTEGER NOT NULL, `dpiy` INTEGER NOT NULL, `cost` REAL NOT NULL, `sort` INTEGER NOT NULL, `grayscale` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "spotId",
"columnName": "spotId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "width",
"columnName": "width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "printWidth",
"columnName": "printWidth",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "printHeight",
"columnName": "printHeight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "marginTop",
"columnName": "marginTop",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "marginLeft",
"columnName": "marginLeft",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dpix",
"columnName": "dpix",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dpiy",
"columnName": "dpiy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "sort",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "grayscale",
"columnName": "grayscale",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Spot",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, `code` TEXT NOT NULL, `address` TEXT NOT NULL, `status` TEXT, `distance` REAL NOT NULL, `lng` REAL NOT NULL, `lat` REAL NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "code",
"columnName": "code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "distance",
"columnName": "distance",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "lng",
"columnName": "lng",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "lat",
"columnName": "lat",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Spot_code",
"unique": false,
"columnNames": [
"code"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Spot_code` ON `${TABLE_NAME}` (`code`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5310a974346e7b7c3f4fbbab21cba840')"
]
}
}
\ No newline at end of file
package com.isidroid.c23
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.isidroid.c23", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name=".App"
android:theme="@style/Theme.Compose23"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Compose23">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBKI2W1CALiYHJcu8KNfYbhTZBpOZZKYPc" />
</application>
</manifest>
\ No newline at end of file
package com.isidroid.c23
import android.app.Application
import com.isidroid.c23.data.source.settings.Settings
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class App: Application() {
override fun onCreate() {
super.onCreate()
Settings.init(applicationContext)
Timber.plant(Timber.DebugTree())
}
}
\ No newline at end of file
package com.isidroid.c23
import kotlin.jvm.Throws
class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m)
\ No newline at end of file
package com.isidroid.c23
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.isidroid.c23.ext.isEdgeToEdge
import com.isidroid.c23.ui.navigation.AppNavHost
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.core.ext.printCurrentDestination
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb()),
navigationBarStyle = SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb()),
)
setContent {
ComposeApp()
}
}
}
@Composable
fun ComposeApp() {
val navController = rememberNavController()
val currentBackStack by navController.currentBackStackEntryAsState()
val isEdgeToEdge = currentBackStack?.destination?.route.isEdgeToEdge
LaunchedEffect(Unit) { navController.printCurrentDestination() }
val surfaceModifier = with(Modifier) {
if (!isEdgeToEdge)
navigationBarsPadding().systemBarsPadding()
else
this
}
AppTheme {
Surface(modifier = surfaceModifier) {
val horizontalPaddings = if (isEdgeToEdge) 0 else 16
AppNavHost(
navController = navController,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = horizontalPaddings.dp)
)
}
}
}
import android.Manifest
import android.content.Context
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.utils.hasPermission
import com.isidroid.utils.shouldShowRequestPermissionRationale
class PermissionState(
private val permission: String,
private val context: Context,
private val permissionLauncher: ManagedActivityResultLauncher<String, Boolean>?
) {
var hasPermission by mutableStateOf(context.hasPermission(permission))
private set
var shouldShowRationale by mutableStateOf(context.shouldShowRequestPermissionRationale(permission))
private set
fun launchPermissionRequest() {
shouldShowRationale = context.shouldShowRequestPermissionRationale(permission)
permissionLauncher?.launch(permission)
}
}
@Composable
fun rememberPermissionState(
permission: String,
launcher: ManagedActivityResultLauncher<String, Boolean>? = null
): PermissionState {
val context = LocalContext.current
return remember(permission, context) {
PermissionState(permission, context, launcher)
}
}
@Composable
fun PermissionRequestScreen() {
val permissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (permissionState.hasPermission) {
Text("Permission Granted!")
} else {
if (permissionState.shouldShowRationale) {
Text("We need access to your location for a better experience.")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
permissionState.launchPermissionRequest()
}) {
Text("Request Permission")
}
}
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
AppTheme {
PermissionRequestScreen()
}
}
package com.isidroid.c23.constant
import androidx.annotation.StringDef
@Retention(AnnotationRetention.SOURCE)
@StringDef(AppBuildType.DEV, AppBuildType.PROD, AppBuildType.STAGE, AppBuildType.MOCK)
annotation class AppBuildType {
companion object {
const val MOCK = "MOCK"
const val DEV = "DEV"
const val STAGE = "STAGE"
const val PROD = "PROD"
fun getServerUrl(type: String): String {
return when (type) {
MOCK -> "https://ya.ru"
// DEV -> BuildConfig.DEVELOP_URL
// STAGE -> BuildConfig.STAGE_URL
// PROD -> BuildConfig.PROD_URL
else -> throw IllegalStateException("Type '$type' is not defined")
}
}
}
}
\ No newline at end of file
package com.isidroid.c23.constant
@Retention(AnnotationRetention.SOURCE)
annotation class Argument {
companion object {
const val URI = "URI"
const val LATITUDE = "LATITUDE"
const val LONGITUDE = "LONGITUDE"
const val SPOT_ID = "SPOT_ID"
const val SPOT_CODE = "SPOT_CODE"
}
}
package com.isidroid.c23.data.mapper
import com.isidroid.location.model.LatLng2
import com.isidroid.ui.maps.model.MapMarker
internal fun LatLng2.transformToMapMarker(id: String) = MapMarker(id = id, name = "", lat = lat, lng = lng)
\ No newline at end of file
package com.isidroid.c23.data.source.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.isidroid.spot.data.source.local.dao.PrintProfileDao
import com.isidroid.spot.model.PrintProfile
import com.isidroid.session.data.source.local.SessionDao
import com.isidroid.session.model.Session
import com.isidroid.spot.model.Spot
import com.isidroid.spot.data.source.local.dao.SpotDao
@Database(
version = 1,
entities = [
Session::class,
PrintProfile::class,
Spot::class
],
exportSchema = true,
autoMigrations = [
// AutoMigration(from = 1, to = 2),
]
)
@TypeConverters(RoomConverters::class)
abstract class AppDataBase : RoomDatabase() {
abstract val sessionDao: SessionDao
abstract val printProfileDao: PrintProfileDao
abstract val spotDao: SpotDao
companion object {
@Volatile
private var INSTANCE: AppDataBase? = null
fun database(context: Context): AppDataBase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDataBase::class.java,
"database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
\ No newline at end of file
package com.isidroid.c23.data.source.local
import androidx.room.TypeConverter
import com.google.gson.GsonBuilder
import java.util.Date
object RoomConverters {
private val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ")
.setLenient()
.create()
@TypeConverter fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter fun dateToTimestamp(date: Date?): Long? = date?.time
}
\ No newline at end of file
package com.isidroid.c23.data.source.network
import android.os.Build
import com.isidroid.c23.BuildConfig
import com.isidroid.c23.data.source.settings.Settings
import com.isidroid.session.repository.SessionRepository
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
private const val HEADER_AUTH = "Authorization"
class AuthInterceptor(private val sessionRepository: SessionRepository) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = Settings.authToken
val request = requestBuilder(
originalRequest = chain.request(),
accessToken = token
).build()
val response = chain.proceed(request)
return when (response.code) {
401 -> refreshToken(chain, request, response)
else -> response
}
}
private fun refreshToken(
chain: Interceptor.Chain,
request: Request?,
response: Response
): Response {
if (request == null) return response
response.close()
// val newToken = sessionRepository.refreshAuthToken()
val newToken = ""
Settings.authToken = newToken
val builder = request.newBuilder()
.removeHeader(HEADER_AUTH)
.addHeader("app-version", BuildConfig.VERSION_NAME)
.addHeader("os-version", Build.MODEL)
authorizeCall(builder, newToken)
return chain.proceed(builder.build())
}
private fun requestBuilder(originalRequest: Request, accessToken: String?): Request.Builder {
val builder = originalRequest.newBuilder()
// if (request.method == "POST" || request.method == "PUT")
builder.addHeader("Accept", "application/json")
val skipAuth = originalRequest.headers.any { it.first == "skip_auth" }
if (!skipAuth) authorizeCall(builder, accessToken)
return builder
}
private fun authorizeCall(builder: Request.Builder, accessToken: String?) {
accessToken?.let {
builder.addHeader("Authorization", "Bearer $accessToken")
// builder.addHeader("X-Authorization-Token", accessToken)
}
}
}
\ No newline at end of file
package com.isidroid.c23.data.source.network
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.Response
import timber.log.Timber
class CacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val urlBuilder = request.url.newBuilder()
// .addQueryParameter("request_id", UUID.randomUUID().toString())
val cacheControl = CacheControl.Builder()
.noCache()
.build()
val requestBuilder = request.newBuilder()
.url(urlBuilder.build())
.cacheControl(cacheControl)
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
}
\ No newline at end of file
package com.isidroid.c23.data.source.settings
enum class SettingId {
// string
APP_BUILD_TYPE,
TOKEN,
}
\ No newline at end of file
package com.isidroid.c23.data.source.settings
import android.content.Context
import com.isidroid.c23.constant.AppBuildType
object Settings {
private val map = SettingsMap()
fun init(context: Context) {
map.init(context)
}
var buildType: String
get() = map.string(SettingId.APP_BUILD_TYPE, AppBuildType.PROD) ?: AppBuildType.PROD
set(value) = map.save(SettingId.APP_BUILD_TYPE, value)
var authToken: String?
get() = map.string(SettingId.TOKEN, null)
set(value) = map.save(SettingId.TOKEN, value)
}
\ No newline at end of file
package com.isidroid.c23.data.source.settings
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager
class SettingsMap {
lateinit var sp: SharedPreferences
private val mapBool = hashMapOf<String, Boolean>()
private val mapString = hashMapOf<String, String?>()
private val mapInt = hashMapOf<String, Int>()
private val mapLong = hashMapOf<String, Long>()
fun init(context: Context) {
sp = PreferenceManager.getDefaultSharedPreferences(context)
recreate()
}
private fun recreate() {
mapBool.clear()
mapString.clear()
mapInt.clear()
mapLong.clear()
sp.all.entries.forEach {
if (it.value is String?) mapString[it.key] = it.value as String?
if (it.value is Boolean) mapBool[it.key] = it.value as Boolean
if (it.value is Int) mapInt[it.key] = it.value as Int
if (it.value is Long) mapLong[it.key] = it.value as Long
}
}
fun string(key: SettingId, defaultValue: String?) = mapString[key.name] ?: defaultValue
fun bool(key: SettingId, defaultValue: Boolean) = mapBool[key.name] ?: defaultValue
fun int(key: SettingId, defaultValue: Int) = mapInt[key.name] ?: defaultValue
fun long(key: SettingId, defaultValue: Long) = mapLong[key.name] ?: defaultValue
fun save(key: SettingId, value: Any?) {
sp.edit(commit = true){
if (value is Boolean) putBoolean(key.name, value)
if (value is String?) putString(key.name, value)
if (value is Int) putInt(key.name, value)
if (value is Long) putLong(key.name, value)
}
recreate()
}
}
package com.isidroid.c23.di
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.isidroid.c23.data.source.network.AuthInterceptor
import com.isidroid.c23.data.source.network.CacheControlInterceptor
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.DiMock
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import com.isidroid.c23.ext.isMock
import com.isidroid.c23.utils.DateDeserializer
import com.isidroid.core.DiDebuggableBuild
import com.isidroid.network.ApiCacheControlInterceptor
import com.isidroid.network.ApiServerUrl
import com.isidroid.network.ApiTokenInterceptor
import com.isidroid.session.repository.SessionRepository
import okhttp3.Interceptor
import java.util.Date
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object AppModule {
@Provides @Singleton @DiMock
fun provideIsMock(): Boolean = isMock()
@Provides @Singleton @DiDebuggableBuild
fun provideIsDebuggableBuild(): Boolean = isMock() || isDebug()
@Provides @Singleton
fun providePreferences(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
}
\ No newline at end of file
package com.isidroid.c23.di
import android.content.Context
import com.isidroid.c23.data.source.local.AppDataBase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Provides @Singleton
fun providesAppDatabase(@ApplicationContext context: Context): AppDataBase = AppDataBase.database(context)
@Provides @Singleton
fun providesPaperDao(appDataBase: AppDataBase) = appDataBase.printProfileDao
@Provides @Singleton
fun providesSpotDao(appDataBase: AppDataBase) = appDataBase.spotDao
}
\ No newline at end of file
package com.isidroid.c23.di
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.isidroid.c23.data.source.network.AuthInterceptor
import com.isidroid.c23.data.source.network.CacheControlInterceptor
import com.isidroid.c23.utils.DateDeserializer
import com.isidroid.network.ApiCacheControlInterceptor
import com.isidroid.network.ApiServerUrl
import com.isidroid.network.ApiTokenInterceptor
import com.isidroid.session.repository.SessionRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import java.util.Date
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Singleton @Provides @ApiCacheControlInterceptor
fun provideApiCacheControlInterceptor(): Interceptor = CacheControlInterceptor()
@Singleton @Provides @ApiTokenInterceptor
fun provideAuthInterceptor(sessionRepository: SessionRepository): Interceptor = AuthInterceptor(sessionRepository)
@Provides @Singleton @ApiServerUrl
fun provideApiUrl() = "http://stage.teamprinter.com/"
@Singleton @Provides
fun provideGson(): Gson = GsonBuilder()
.setLenient()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ")
.registerTypeAdapter(Date::class.java, DateDeserializer())
.create()
}
\ No newline at end of file
package com.isidroid.c23.domain.use_case
import com.isidroid.spot.repository.ActiveSpotRepository
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ContentUseCase @Inject constructor(private val activeSpotRepository: ActiveSpotRepository) {
fun create() = flow {
val spot = activeSpotRepository.getDefaultSpot()
emit(spot)
}
}
\ No newline at end of file
package com.isidroid.c23.domain.use_case
import com.isidroid.c23.ext.isDebug
import com.isidroid.c23.ui.screen.home.HomeContract
import com.isidroid.core.FlowResult
import com.isidroid.session.repository.SessionRepository
import com.isidroid.spot.repository.ActiveSpotRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class HomeUseCase @Inject constructor(
private val sessionRepository: SessionRepository,
private val activeSpotRepository: ActiveSpotRepository
) {
fun createSession() = flow {
emit(FlowResult.Loading)
val maxDelay = if (isDebug()) 0 else 3_000
val startedAt = System.currentTimeMillis()
val hasSession = sessionRepository.hasCurrent
val hasDefaultSpot = activeSpotRepository.getDefaultSpot() != null
val ts = System.currentTimeMillis() - startedAt
val navigation = when {
!hasSession -> HomeContract.Effect.Navigation.ToLogin
!hasDefaultSpot -> HomeContract.Effect.Navigation.ToSelectSpot(
// lat = 37.2451678,
// lng = -121.9752617
lat = 37.2451678,
lng = -121.9752617,
)
else -> HomeContract.Effect.Navigation.ToSelectContent
}
val delay = maxDelay - ts
delay(delay)
emit(FlowResult.Success(navigation))
}
}
package com.isidroid.c23.domain.use_case
import android.Manifest
import android.content.Context
import com.isidroid.core.FlowResult
import com.isidroid.location.model.LatLng2
import com.isidroid.location.repository.LocationRepository
import com.isidroid.spot.model.RichSpot
import com.isidroid.spot.repository.ActiveSpotRepository
import com.isidroid.spot.repository.SpotRepository
import com.isidroid.utils.hasPermission
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
class MapUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val locationRepository: LocationRepository,
private val spotRepository: SpotRepository,
private val activeSpotRepository: ActiveSpotRepository,
) {
fun prepareData(lat: Double?, lng: Double?, spotCode: String?) = flow {
val hasPermissions = context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
if (!hasPermissions) {
emit(MapPrepareDto.NoPermissions)
return@flow
}
val location = parseLocation(lat = lat, lng = lng, spotCode = spotCode)
emit(MapPrepareDto.Location(lat = location.first, lng = location.second))
}
fun myLocation() = flow {
emit(locationRepository.getCurrentLocation())
}
fun findSpots(lat: Double, lng: Double, radius: Int) = flow {
emit(FlowResult.Loading)
val richSpotList = spotRepository.locateSpots(lat = lat, lng = lng, distance = radius)
val result = richSpotList.groupBy { LatLng2(it.spot.lat, it.spot.lng) }
emit(FlowResult.Success(result))
}
fun selectSpot(richSpot: RichSpot?) = flow {
// save spot and all information locally
richSpot ?: throw IllegalStateException("Failed to get Spot information")
spotRepository.save(richSpot)
activeSpotRepository.makeDefaultSpot(richSpot.spot.id)
emit(1)
}
private suspend fun parseLocation(lat: Double?, lng: Double?, spotCode: String? = null): Pair<Double, Double> {
val spot = spotRepository.findRichSpot(spotCode)?.spot ?: activeSpotRepository.getDefaultSpot()?.spot
return when {
spot != null -> Pair(spot.lat, spot.lng)
lat == null || lng == null -> locationRepository.getCurrentLocation()
else -> Pair(lat, lng)
}
}
sealed interface MapPrepareDto {
data object NoPermissions : MapPrepareDto
data class Location(val lat: Double, val lng: Double) : MapPrepareDto
}
}
package com.isidroid.c23.domain.use_case
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.ui.unit.IntSize
import com.isidroid.c23.R
import com.isidroid.c23.SpotHasNoPrintProfilesException
import com.isidroid.rendering.model.RenderResult
import com.isidroid.core.FlowResult
import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.spot.model.PrintProfile
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.ActiveSpotRepository
import com.isidroid.utils.saveToFile
import com.isidroid.utils.toBitmap
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import java.io.File
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RenderUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val renderRepository: RenderRepository,
private val spotRepository: ActiveSpotRepository
) {
fun loadSpot() = flow {
val richSpot = spotRepository.getDefaultSpot()
if (richSpot?.printProfiles.isNullOrEmpty()) throw SpotHasNoPrintProfilesException(context.getString(R.string.error_spot_has_no_printing_profiles))
emit(richSpot!!)
}
fun render(uri: Uri?, containerSize: IntSize?, printProfile: PrintProfile?, renderSettings: RenderSettingsV2?) = flow {
uri ?: throw IllegalStateException("Uri is null")
containerSize ?: throw IllegalStateException("Container size is null")
printProfile ?: throw IllegalStateException("Paper is null")
renderSettings ?: throw IllegalStateException("renderSettings is null")
emit(FlowResult.Loading)
val bitmap = uri.toBitmap(context) ?: throw IllegalStateException("Can't create bitmap from uri $uri")
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
bitmap.saveToFile(file)
bitmap.recycle()
val updatedSettings = renderSettings.copy(filePath = file.absolutePath)
val previewContainer = renderRepository.preparePreviewContainer(
dpiX = printProfile.dpix,
dpiY = printProfile.dpiy,
filePath = updatedSettings.filePath,
paperHeightMm = printProfile.height,
paperWidthMm = printProfile.width,
printOrientation = updatedSettings.orientation,
viewWidth = containerSize.width,
viewHeight = containerSize.height
)
val picture = BitmapFactory.decodeFile(updatedSettings.filePath)
val result = renderRepository.renderBitmap(
width = printProfile.width,
height = printProfile.height,
printWidth = printProfile.printWidth,
printHeight = printProfile.printHeight,
marginTop = printProfile.marginTop,
marginLeft = printProfile.marginLeft,
dpix = printProfile.dpix,
dpiy = printProfile.dpiy,
renderSettingsV2 = updatedSettings.copy(canvasWidth = previewContainer.width, canvasHeight = previewContainer.height),
picture = picture,
)
result.saveToFile(file)
val renderResult = RenderResult(width = previewContainer.width, height = previewContainer.height, filePath = file.absolutePath)
emit(FlowResult.Success(renderResult))
}
}
package com.isidroid.c23.ext
import android.content.Context
import android.content.Intent
import com.isidroid.c23.MainActivity
fun Context.restartApp() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
Runtime.getRuntime().exit(0)
}
\ No newline at end of file
package com.isidroid.c23.ext
import androidx.compose.ui.graphics.Color
import com.isidroid.c23.BuildConfig
import com.isidroid.c23.constant.AppBuildType
import com.isidroid.c23.data.source.settings.Settings
import kotlin.random.Random
fun isStage() = Settings.buildType == AppBuildType.STAGE
fun isDev() = Settings.buildType == AppBuildType.DEV
fun isMock() = Settings.buildType == AppBuildType.MOCK
fun isDebug() = BuildConfig.DEBUG || isDev() || isStage() || isMock()
fun randomColor(): Color {
val random = Random.Default
return Color(
red = random.nextFloat(),
green = random.nextFloat(),
blue = random.nextFloat(),
alpha = 1.0f
)
}
\ No newline at end of file
package com.isidroid.c23.ext
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.navArgument
import com.isidroid.c23.ui.navigation.Content
import com.isidroid.c23.ui.navigation.Map
import com.isidroid.c23.ui.navigation.RenderPreview
val String?.isEdgeToEdge
get() = arrayOf<String>(
RenderPreview.route,
Content.route,
Map.route
).any {
this?.contains(it) == true
}
@Composable
fun BasicNavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
builder: NavGraphBuilder.() -> Unit
) {
val screenTransitionDuration = 200
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
enterTransition = {
fadeIn(tween(screenTransitionDuration))
},
exitTransition = {
fadeOut(tween(screenTransitionDuration))
},
popEnterTransition = {
fadeIn(tween(screenTransitionDuration))
},
popExitTransition = {
fadeOut(tween(screenTransitionDuration))
},
builder = builder
)
}
internal fun makeNavArgument(name: String, type: NavType<*>, nullable: Boolean = true, defaultValue: Any? = null) = navArgument(name){
this.type = type
this.nullable = nullable
this.defaultValue = defaultValue
}
\ No newline at end of file
package com.isidroid.c23.ext
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
import com.isidroid.rendering.model.RenderSettingsV2
fun renderPreviewDefaultSettings(greyscale: Boolean) = RenderSettingsV2(
scale = 1f,
greyscale = greyscale,
isRealSize = false,
orientation = PrintOrientation.AUTO,
printSize = PrintSize.FIT_TO_PRINTABLE_AREA,
showPrintableArea = true
)
\ No newline at end of file
package com.isidroid.c23.ui._component
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@Composable
fun PickGalleryComponent(
hash: String,
onPictureReady: (List<Uri>) -> Unit,
type: String = "image/*"
) {
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
onPictureReady(uris)
}
LaunchedEffect(hash) { launcher.launch(type) }
}
\ No newline at end of file
package com.isidroid.c23.ui._component
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
@Composable
fun ShortOfficeInfoComponent(
officeName: String,
modifier: Modifier,
containerClick: () -> Unit,
buttonClick: (() -> Unit)? = containerClick,
@StringRes titleRes: Int,
@StringRes buttonTextRes: Int,
bottomColor: Color = MaterialTheme.colorScheme.secondaryContainer,
paddingBottom: Dp? = null
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.background(bottomColor)
.padding(vertical = 12.dp)
.padding(horizontal = 12.dp)
.clickable { containerClick() },
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.LocationOn,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
Text(
text = footerText(stringResource(id = titleRes), officeName),
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp),
lineHeight = 20.sp
)
if (buttonClick != null)
Button(
shape = RoundedCornerShape(24.dp),
onClick = buttonClick,
) {
Text(stringResource(id = buttonTextRes).uppercase())
}
}
if (paddingBottom != null)
Box(
modifier = Modifier
.fillMaxWidth()
.height(paddingBottom)
.background(bottomColor)
)
}
}
private fun footerText(title: String, officeName: String) = buildAnnotatedString {
withStyle(style = SpanStyle(fontStyle = FontStyle.Italic, fontSize = 14.sp)) {
append(title)
}
append("\n")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp)) {
append(officeName)
}
}
@Preview
@Composable
private fun FooterPreview() {
Surface {
ShortOfficeInfoComponent(
modifier = Modifier,
officeName = "Default Office Name",
containerClick = {},
bottomColor = MaterialTheme.colorScheme.secondaryContainer,
titleRes = R.string.app_name,
buttonTextRes = R.string.app_name,
)
}
}
\ No newline at end of file
package com.isidroid.c23.ui._component
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.isidroid.c23.BuildConfig
import java.io.File
import java.util.Objects
@Composable
fun TakePictureComponent(
hash: String,
onPictureTaken: (String) -> Unit,
onPermissionDenied: () -> Unit,
) {
val context = LocalContext.current
val file = File.createTempFile(hash, ".jpg")
val uri = FileProvider.getUriForFile(
Objects.requireNonNull(context),
BuildConfig.APPLICATION_ID + ".fileprovider", file
)
val hasCameraPermission by remember { mutableStateOf(ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) }
var capturedImageUri by remember { mutableStateOf(Uri.EMPTY) }
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { capturedImageUri = uri }
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) cameraLauncher.launch(uri)
else onPermissionDenied()
}
LaunchedEffect(hasCameraPermission, hash) {
if (!hasCameraPermission)
permissionLauncher.launch(Manifest.permission.CAMERA)
else
cameraLauncher.launch(uri)
}
LaunchedEffect(capturedImageUri.path) {
if (capturedImageUri.path?.isNotEmpty() == true)
onPictureTaken(capturedImageUri.path.orEmpty())
}
}
\ No newline at end of file
package com.isidroid.c23.ui._component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TopAppBarComponent(
text: String,
modifier: Modifier = Modifier,
alpha: Float = 1f,
onNavigationClick: (() -> Unit)? = null,
colors: TopAppBarColors? = null
) {
CenterAlignedTopAppBar(
modifier = modifier.height(64.dp),
title = {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
)
},
colors = colors ?: TopAppBarDefaults.topAppBarColors().copy(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = alpha)),
navigationIcon = {
if (onNavigationClick != null)
Icon(Icons.AutoMirrored.Rounded.KeyboardArrowLeft, contentDescription = null, modifier = Modifier.clickable { onNavigationClick() })
}
)
}
\ No newline at end of file
package com.isidroid.c23.ui.navigation
interface NavDirection {
val route: String
}
object Home : NavDirection {
override val route: String = "Home"
}
object RenderPreview : NavDirection {
override val route: String = "RenderPreview"
}
object Content: NavDirection {
override val route: String = "Content"
}
object Map: NavDirection {
override val route: String = "Map"
}
\ No newline at end of file
package com.isidroid.c23.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import com.isidroid.c23.constant.Argument
import com.isidroid.c23.ext.BasicNavHost
import com.isidroid.c23.ext.isDebug
import com.isidroid.c23.ext.makeNavArgument
import com.isidroid.c23.ui.navigation.destinations.ContentScreenDestination
import com.isidroid.c23.ui.navigation.destinations.HomeScreenDestination
import com.isidroid.c23.ui.navigation.destinations.MapScreenDestination
import com.isidroid.c23.ui.navigation.destinations.RenderScreenDestination
@Composable
fun AppNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
val startDestination = if (isDebug()) Home.route else Home.route
BasicNavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
composable(route = Home.route) { HomeScreenDestination(navController) }
composable(route = Content.route) { ContentScreenDestination(navController) }
composable(
route = routeRenderPreview(uris = "{${Argument.URI}}"),
arguments = listOf(makeNavArgument(Argument.URI, NavType.StringType)),
content = { RenderScreenDestination(navController) }
)
composable(
route = routeMap(lat = "{${Argument.LATITUDE}}", lng = "{${Argument.LONGITUDE}}", spotCode = "{${Argument.SPOT_CODE}}"),
arguments = listOf(
makeNavArgument(Argument.LATITUDE, NavType.StringType, nullable = true),
makeNavArgument(Argument.LONGITUDE, NavType.StringType, nullable = true),
makeNavArgument(Argument.SPOT_CODE, NavType.StringType, nullable = true),
),
content = { MapScreenDestination(navController) }
)
}
}
package com.isidroid.c23.ui.navigation
import androidx.core.net.toUri
import com.isidroid.c23.constant.Argument
internal fun routeSelectContent() = Content.route.toUri()
.toString()
internal fun routeRenderPreview(uris: String = Argument.URI) = RenderPreview.route.toUri().buildUpon()
.appendQueryParameter(Argument.URI, uris)
.toString()
internal fun routeMap(lat: String? = null, lng: String? = null, spotCode: String? = null) = Map.route.toUri().buildUpon()
.appendQueryParameter(Argument.LATITUDE, lat)
.appendQueryParameter(Argument.LONGITUDE, lng)
.appendQueryParameter(Argument.SPOT_CODE, spotCode)
.toString()
package com.isidroid.c23.ui.navigation.destinations
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.navigation.routeMap
import com.isidroid.c23.ui.navigation.routeRenderPreview
import com.isidroid.c23.ui.screen.content.ContentContract
import com.isidroid.c23.ui.screen.content.ContentScreen
import com.isidroid.c23.ui.screen.content.ContentViewModel
import com.isidroid.c23.ui.screen.home.HomeScreen
import com.isidroid.c23.ui.screen.home.HomeViewModel
import com.isidroid.core.ext.navigateSingleTopTo
@Composable
fun ContentScreenDestination(navController: NavHostController) {
val viewModel: ContentViewModel = hiltViewModel()
ContentScreen(
state = viewModel.viewState,
effectFlow = viewModel.effect,
onEventSent = { event -> viewModel.setEvent(event) },
onNavigationRequested = { effect ->
when (effect) {
is ContentContract.Effect.Navigation.ToRenderPreview -> navController.navigate(routeRenderPreview(uris = effect.uris))
is ContentContract.Effect.Navigation.ToMap -> navController.navigateSingleTopTo(routeMap(effect.lat?.toString(), effect.lng?.toString(), effect.spotId))
ContentContract.Effect.Navigation.ToBack -> navController.popBackStack()
}
}
)
}
\ No newline at end of file
package com.isidroid.c23.ui.navigation.destinations
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.navigation.routeMap
import com.isidroid.c23.ui.navigation.routeSelectContent
import com.isidroid.c23.ui.screen.home.HomeContract
import com.isidroid.c23.ui.screen.home.HomeScreen
import com.isidroid.c23.ui.screen.home.HomeViewModel
import com.isidroid.core.ext.navigateSingleTopTo
@Composable
fun HomeScreenDestination(navController: NavHostController) {
val viewModel: HomeViewModel = hiltViewModel()
HomeScreen(
state = viewModel.viewState,
effectFlow = viewModel.effect,
onNavigationRequested = { effect ->
val route = when (effect) {
HomeContract.Effect.Navigation.ToLogin -> TODO("Session is not implemented")
HomeContract.Effect.Navigation.ToSelectContent -> routeSelectContent()
is HomeContract.Effect.Navigation.ToSelectSpot -> routeMap(lat = effect.lat?.toString(), lng = effect.lng?.toString(), spotCode = effect.spotCode)
}
navController.navigateSingleTopTo(route, isLaunchSingleTop = true, isInclusive = true)
}
)
}
\ No newline at end of file
package com.isidroid.c23.ui.navigation.destinations
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.navigation.routeSelectContent
import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.c23.ui.screen.map.MapScreen
import com.isidroid.c23.ui.screen.map.MapViewModel
import com.isidroid.core.ext.navigateSingleTopTo
@Composable
fun MapScreenDestination(navController: NavHostController) {
val viewModel: MapViewModel = hiltViewModel()
MapScreen(
state = viewModel.viewState,
effectFlow = viewModel.effect,
onEventSent = { event -> viewModel.setEvent(event) },
spotResultStateFlow = viewModel.spotResultStateFlow,
onNavigationRequested = { effect ->
when (effect) {
MapContract.Effect.Navigation.ToBack -> navController.popBackStack()
MapContract.Effect.Navigation.ToSelectContent -> navController.navigateSingleTopTo(routeSelectContent())
}
},
)
}
\ No newline at end of file
package com.isidroid.c23.ui.navigation.destinations
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.isidroid.c23.ui.navigation.routeMap
import com.isidroid.c23.ui.screen.render_preview.RenderContract
import com.isidroid.c23.ui.screen.render_preview.RenderPreviewScreen
import com.isidroid.c23.ui.screen.render_preview.RenderViewModel
@Composable
fun RenderScreenDestination(navController: NavController) {
val viewModel: RenderViewModel = hiltViewModel()
RenderPreviewScreen(
state = viewModel.viewState,
renderResultsFlow = viewModel.renderResultsFlow,
effectFlow = viewModel.effect,
onEventSent = { event -> viewModel.setEvent(event) },
onNavigationRequested = { effect ->
when (effect) {
RenderContract.Effect.Navigation.ToBack -> navController.popBackStack()
RenderContract.Effect.Navigation.ToMap -> navController.navigate(routeMap())
}
}
)
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.content
import android.net.Uri
import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState
import com.isidroid.spot.model.RichSpot
class ContentContract {
sealed interface Event : ViewEvent {
data class PickContent(val photo: Boolean = false, val documents: Boolean = false) : Event
data class MultipleContents(val uris: List<Uri>) : Event
data class OpenMap(val lat: Double? = null, val lng: Double? = null, val spotCode: String? = null) : Event
data object GoBack: Event
}
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
data class ToRenderPreview(val uris: String) : Navigation
data class ToMap(val lat: Double? = null, val lng: Double? = null, val spotId: String? = null) : Navigation
data object ToBack: Navigation
}
}
data class State(
val galleryHash: String? = null,
val documentHash: String? = null,
val richSpot: RichSpot? = null
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.content
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContentScreen(
state: State<ContentContract.State>,
effectFlow: Flow<ContentContract.Effect>?,
onEventSent: (event: ContentContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: ContentContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier
) {
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
onEventSent(ContentContract.Event.MultipleContents(uris))
}
BackHandler {
onEventSent(ContentContract.Event.GoBack)
}
LaunchedEffect(state.value.galleryHash) { if (state.value.galleryHash != null) launcher.launch("image/*") }
LaunchedEffect(state.value.documentHash) { if (state.value.documentHash != null) launcher.launch("document/pdf") }
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is ContentContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.select_content),
colors = TopAppBarDefaults.topAppBarColors(
// containerColor = MaterialTheme.colorScheme.primary,
// titleContentColor = MaterialTheme.colorScheme.onPrimary
)
)
}
) { paddingValues ->
Column(modifier = Modifier.fillMaxSize()) {
Row(
Modifier
.padding(paddingValues)
.fillMaxWidth()
) {
BlockComponent(
text = stringResource(id = R.string.select_content_photos),
image = painterResource(id = R.drawable.content_type_photo),
onClick = { onEventSent(ContentContract.Event.PickContent(photo = true)) },
)
BlockComponent(
text = stringResource(id = R.string.select_content_doc),
image = painterResource(id = R.drawable.content_type_doc),
onClick = { onEventSent(ContentContract.Event.PickContent(documents = true)) },
)
}
Spacer(modifier = Modifier.weight(1f))
Footer(state, paddingValues, onEventSent)
}
}
}
@Composable
private fun Footer(
state: State<ContentContract.State>,
paddingValues: PaddingValues,
onEventSent: (event: ContentContract.Event) -> Unit
) {
val richSpot = state.value.richSpot ?: return
ShortOfficeInfoComponent(
officeName = richSpot.spot.name,
modifier = Modifier.fillMaxWidth(),
containerClick = { onEventSent(ContentContract.Event.OpenMap()) },
buttonClick = null,
titleRes = R.string.select_content_footer_title,
buttonTextRes = R.string.app_name,
paddingBottom = paddingValues.calculateBottomPadding(),
)
}
@Composable
private fun RowScope.BlockComponent(
text: String,
image: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier
.weight(1f)
.height(160.dp)
.padding(2.dp)
.clip(RoundedCornerShape(6.dp))
.clickable { onClick() }
) {
Image(
painter = image,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.background(Color.Black.copy(alpha = .65f))
) {
Text(
text = text,
color = Color.White,
modifier = Modifier.padding(start = 16.dp, top = 2.dp),
fontSize = 16.sp,
style = MaterialTheme.typography.headlineSmall,
letterSpacing = .96.sp,
fontWeight = FontWeight.Medium
)
}
}
}
@Composable
@Preview
private fun BlockPreview() {
Surface {
Row {
BlockComponent(
text = "Photos",
image = painterResource(id = R.drawable.content_type_photo),
onClick = { }
)
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.content
import android.net.Uri
import androidx.lifecycle.viewModelScope
import androidx.room.util.copy
import com.isidroid.c23.domain.use_case.ContentUseCase
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.vm.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
class ContentViewModel @Inject constructor(
private val useCase: ContentUseCase
) : BaseViewModel<ContentContract.Event, ContentContract.State, ContentContract.Effect>() {
init {
viewModelScope.launch { create() }
}
override val isDebug: Boolean = isDebug()
override fun setInitialState(): ContentContract.State = ContentContract.State()
override suspend fun handleEvents(event: ContentContract.Event) {
when (event) {
is ContentContract.Event.MultipleContents -> onContentSelected(event.uris)
is ContentContract.Event.PickContent -> setState {
copy(
galleryHash = if (event.photo) UUID.randomUUID().toString() else null,
documentHash = if (event.documents) UUID.randomUUID().toString() else null,
)
}
is ContentContract.Event.OpenMap -> setEffect { ContentContract.Effect.Navigation.ToMap(event.lat, event.lng, event.spotCode) }
ContentContract.Event.GoBack -> setEffect { ContentContract.Effect.Navigation.ToBack }
}
}
private fun onContentSelected(uris: List<Uri>) {
if (uris.isEmpty())
return
setState { copy(galleryHash = null, documentHash = null) }
setEffect { ContentContract.Effect.Navigation.ToRenderPreview(uris.joinToString()) }
}
private suspend fun create() {
useCase.create().flowOn(Dispatchers.IO).collect { data ->
if (data == null)
setEffect { ContentContract.Effect.Navigation.ToMap() }
else
setState { copy(richSpot = data) }
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.home
import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState
class HomeContract {
sealed interface Event : ViewEvent {
}
data class State(
val isLoading: Boolean = true
) : ViewState
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
data object ToLogin : Navigation
data class ToSelectSpot(val lat: Double? = null, val lng: Double? = null, val spotCode: String? = null) : Navigation
data object ToSelectContent : Navigation
}
}
}
package com.isidroid.c23.ui.screen.home
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import coil.compose.rememberAsyncImagePainter
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.isidroid.c23.R
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.render_preview.RenderPreviewComponent
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
@Composable
fun HomeScreen(
state: State<HomeContract.State>,
effectFlow: Flow<HomeContract.Effect>?,
onNavigationRequested: (navigationEffect: HomeContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier
) {
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is HomeContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
if (state.value.isLoading) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.startapp))
LottieAnimation(
composition = composition,
// progress = { progress },
iterations = LottieConstants.IterateForever,
modifier = modifier
.fillMaxSize()
.padding(48.dp)
)
}
}
package com.isidroid.c23.ui.screen.home
import androidx.lifecycle.viewModelScope
import com.isidroid.c23.domain.use_case.HomeUseCase
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val useCase: HomeUseCase
) : BaseViewModel<HomeContract.Event, HomeContract.State, HomeContract.Effect>() {
init {
viewModelScope.launch { onCreate() }
}
override val isDebug: Boolean = isDebug()
override fun setInitialState(): HomeContract.State = HomeContract.State()
override suspend fun handleEvents(event: HomeContract.Event) {}
// handle events
private suspend fun onCreate() {
useCase.createSession()
.flowOn(Dispatchers.IO)
.catchTimber { }
.collect { res ->
when (res) {
FlowResult.Loading -> setState { copy(isLoading = true) }
is FlowResult.Success -> setEffect { res.result }
}
}
}
}
package com.isidroid.c23.ui.screen.map
import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState
import com.isidroid.spot.model.RichSpot
class MapContract {
sealed interface Event : ViewEvent {
data object PermissionGranted: Event
data object ToBack: Event
data object MoveCameraToMyLocation: Event
data class CameraMove(val lat: Double, val lng: Double, val radius: Int) : Event
data class ClickOnMarker(val markerId: String?): Event
data class SelectSpot(val id: String): Event
}
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
data object ToBack: Navigation
data object ToSelectContent: Navigation
}
}
data class State(
val lat: Double? = null,
val lng: Double? = null,
val requestLocationPermission: Boolean = false,
val spotCode: String? = null,
val spotsInfoList: List<RichSpot>? = null
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.map
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.c23.ui.screen.map._components.RequestLocationPermissionComponent
import com.isidroid.c23.ui.screen.map._components.TPMapComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapScreen(
state: State<MapContract.State>,
effectFlow: Flow<MapContract.Effect>?,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: MapContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: MapContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is MapContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
RequestLocationPermissionComponent(
state = state,
onGranted = { onEventSent(MapContract.Event.PermissionGranted) }
)
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.appbar_select_spot),
alpha = .65f,
onNavigationClick = { onEventSent(MapContract.Event.ToBack) }
)
}
) { paddingValues ->
TPMapComponent(
state = state,
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(paddingValues),
onEventSent = onEventSent,
mapMarkersStateFlow = spotResultStateFlow
)
OfficeInfoComponent(
state = state,
containerClick = { onEventSent(MapContract.Event.ClickOnMarker(null)) },
buttonClick = { onEventSent(MapContract.Event.SelectSpot(it)) },
modifier = Modifier.padding(bottom = 0.dp),
paddingBottom = paddingValues.calculateBottomPadding(),
)
}
}
@Composable
private fun OfficeInfoComponent(
state: State<MapContract.State>,
containerClick: () -> Unit,
buttonClick: (String) -> Unit,
modifier: Modifier = Modifier,
paddingBottom: Dp = 0.dp
) {
val spotsInfoList = state.value.spotsInfoList
val isVisible = !spotsInfoList.isNullOrEmpty()
val bottomColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = .75f)
Box(modifier = modifier.fillMaxSize()) {
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(initialOffsetY = { it * 2 }),
exit = slideOutVertically(targetOffsetY = { it }),
modifier = Modifier.align(Alignment.BottomCenter)
) {
Column(modifier = Modifier.fillMaxWidth()) {
if (!spotsInfoList.isNullOrEmpty()) {
LazyColumn(modifier = Modifier) {
items(spotsInfoList) { richSpot ->
ShortOfficeInfoComponent(
officeName = buildString {
append(richSpot.spot.name)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp),
containerClick = containerClick,
buttonClick = { buttonClick(richSpot.spot.id) },
titleRes = R.string.preview_footer_title,
buttonTextRes = R.string.select_spot,
bottomColor = bottomColor,
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(paddingBottom)
.background(bottomColor)
)
}
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.map
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.isidroid.c23.constant.Argument
import com.isidroid.c23.data.mapper.transformToMapMarker
import com.isidroid.c23.domain.use_case.MapUseCase
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import com.isidroid.location.model.LatLng2
import com.isidroid.spot.model.RichSpot
import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
class MapViewModel @Inject constructor(
private val useCase: MapUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel<MapContract.Event, MapContract.State, MapContract.Effect>() {
private var _findSpotsJob: Job? = null
private val _data = hashMapOf<String, RichSpot>()
private var _markers = hashMapOf<String, List<RichSpot>>()
private val _spotResultStateFlow = MutableStateFlow<List<MapMarker>>(emptyList())
val spotResultStateFlow = _spotResultStateFlow.asStateFlow()
init {
viewModelScope.launch { listenArguments(savedStateHandle) }
}
override val isDebug: Boolean = isDebug()
override fun setInitialState() = MapContract.State()
override suspend fun handleEvents(event: MapContract.Event) {
when (event) {
MapContract.Event.PermissionGranted -> prepareData(viewState.value.lat, viewState.value.lng, viewState.value.spotCode)
is MapContract.Event.CameraMove -> onCameraMoved(event.lat, event.lng, event.radius)
is MapContract.Event.ClickOnMarker -> clickOnMarker(event.markerId)
is MapContract.Event.SelectSpot -> selectSpot(event.id)
MapContract.Event.ToBack -> setEffect { MapContract.Effect.Navigation.ToBack }
MapContract.Event.MoveCameraToMyLocation -> moveToMyLocation()
}
}
// handle events
private suspend fun listenArguments(savedStateHandle: SavedStateHandle) {
val flowLat = savedStateHandle.getStateFlow<String?>(Argument.LATITUDE, null)
val flowLng = savedStateHandle.getStateFlow<String?>(Argument.LONGITUDE, null)
val flowSpotCode = savedStateHandle.getStateFlow<String?>(Argument.SPOT_CODE, null)
combine(flowLat, flowLng, flowSpotCode) { lat, lng, spotCode ->
prepareData(lat?.toDoubleOrNull(), lng?.toDoubleOrNull(), spotCode)
}.collect()
}
private suspend fun prepareData(lat: Double?, lng: Double?, spotCode: String?) {
useCase.prepareData(lat, lng, spotCode)
.flowOn(Dispatchers.IO)
.collect { state ->
when (state) {
MapUseCase.MapPrepareDto.NoPermissions -> setState { copy(requestLocationPermission = true, lat = lat, lng = lng, spotCode = spotCode) }
is MapUseCase.MapPrepareDto.Location -> setState { copy(requestLocationPermission = false, lat = state.lat, lng = state.lng) }
}
}
}
private fun onCameraMoved(lat: Double, lng: Double, radius: Int) {
_findSpotsJob?.cancel()
_findSpotsJob = viewModelScope.launch {
useCase.findSpots(lat, lng, radius)
.flowOn(Dispatchers.IO)
.catchTimber { }
.collect { res ->
when (res) {
FlowResult.Loading -> {}
is FlowResult.Success -> locationsLoaded(res.result)
}
}
}
}
private fun clickOnMarker(markerId: String?) {
val spots = _markers[markerId]
setState { copy(spotsInfoList = spots) }
}
private suspend fun selectSpot(id: String) {
useCase.selectSpot(_data[id])
.flowOn(Dispatchers.IO)
.collect { setEffect { MapContract.Effect.Navigation.ToSelectContent } }
}
private suspend fun moveToMyLocation() {
useCase.myLocation().flowOn(Dispatchers.IO).collect {
setState { copy(lat = it.first, lng = it.second) }
}
}
// callbacks
private suspend fun locationsLoaded(result: Map<LatLng2, List<RichSpot>>) {
result.forEach { entry ->
for (item in entry.value)
_data[item.spot.id] = item
}
_markers.clear()
val markers = result.map {
val id = UUID.randomUUID().toString()
_markers[id] = it.value
it.key.transformToMapMarker(id)
}
_spotResultStateFlow.emit(markers)
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.map._components
import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import com.isidroid.c23.R
import com.isidroid.c23.ui.screen.map.MapContract
import rememberPermissionState
@Composable
internal fun RequestLocationPermissionComponent(state: State<MapContract.State>, onGranted: () -> Unit) {
if (!state.value.requestLocationPermission)
return
val context = LocalContext.current
val permission = Manifest.permission.ACCESS_FINE_LOCATION
val hasPermission = ContextCompat.checkSelfPermission(context, permission) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (hasPermission) return
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) onGranted() }
val permissionState = rememberPermissionState(permission, launcher)
if (permissionState.hasPermission) return
if (permissionState.shouldShowRationale)
LocationRationalDialog { permissionState.launchPermissionRequest() }
else {
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
}
@Composable
private fun LocationRationalDialog(onGrant: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(id = R.string.label_permission_rationale)) },
text = { Text(text = stringResource(id = R.string.permission_rationale_message)) },
onDismissRequest = { },
confirmButton = {
TextButton(onClick = onGrant) {
Text(stringResource(id = R.string.action_grant_permissions))
}
}
)
}
@Composable
@Preview
fun LocationRationalDialogPreview() {
Surface {
LocationRationalDialog {}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.map._components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.ui.maps.MapsComponent
import kotlinx.coroutines.flow.StateFlow
@Composable
fun TPMapComponent(
onEventSent: (event: MapContract.Event) -> Unit,
state: State<MapContract.State>,
mapMarkersStateFlow: StateFlow<List<MapMarker>>,
modifier: Modifier = Modifier
) {
val lat = state.value.lat
val lng = state.value.lng
if (lat == null || lng == null)
return
MapsComponent(
lat = lat,
lng = lng,
mapMarkersStateFlow = mapMarkersStateFlow,
onCameraMove = { lat1, lng1, radius -> onEventSent(MapContract.Event.CameraMove(lat1, lng1, radius)) },
clickOnMarker = { onEventSent(MapContract.Event.ClickOnMarker(it)) },
moveCameraToMyLocation = { onEventSent(MapContract.Event.MoveCameraToMyLocation) },
modifier = modifier
)
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.IntSize
import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.spot.model.PrintProfile
import com.isidroid.spot.model.RichSpot
class RenderContract {
sealed interface Event : ViewEvent {
data object Click : Event
data object OpenCopiesSettings : Event
data object OpenPrintSizeSettings : Event
data object OpenOrientationSettings : Event
data object ToBack : Event
data object OpenMap : Event
data object OpenListProfiles : Event
data class OnPageOpened(val page: Int, val containerSize: IntSize?) : Event
data class UpdateRenderSettings(
@PrintSize val size: Int? = null,
@PrintOrientation val orientation: Int? = null,
val increaseCopy: Boolean? = null
) : Event
data class ChangePrintProfile(val profileId: String?) : Event
}
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
data object ToMap : Navigation
data object ToBack : Navigation
}
}
@Stable
data class State(
val renderSettings: RenderSettingsV2 = RenderSettingsV2(),
val uris: List<Uri> = emptyList(),
val printProfile: PrintProfile? = null,
val renderSeed: Int = 0,
@PrintSize val openPrintSize: Int? = null,
@PrintOrientation val openOrientation: Int? = null,
val openCopies: Int? = null,
var richSpot: RichSpot? = null,
val spotHasNoPrintProfiles: Boolean = false,
val printProfilesSelector: List<PrintProfile>? = null
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview
import android.util.SparseArray
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.rendering.model.RenderResult
import com.isidroid.c23.ui.screen.render_preview._component.PagerPreviewComponent
import com.isidroid.c23.ui.screen.render_preview._component.PaperInfoComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintCopiesModalComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintOptionsComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintOrientationModalComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintProfileListSelectorComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintSizeModalComponent
import com.isidroid.c23.ui.screen.render_preview._component.SpotHasNotPrintProfilesComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.spot.model.PrintProfile
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RenderPreviewScreen(
state: State<RenderContract.State>,
renderResultsFlow: StateFlow<SparseArray<RenderResult>?>?,
onEventSent: (event: RenderContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: RenderContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
effectFlow: Flow<RenderContract.Effect>?
) {
val openPrintSize = state.value.openPrintSize
val openOrientation = state.value.openOrientation
val copies = state.value.openCopies
val spotHasNoPrintProfiles = state.value.spotHasNoPrintProfiles
val printProfileSelector = state.value.printProfilesSelector
val bottomColor = MaterialTheme.colorScheme.secondaryContainer
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is RenderContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
BackHandler {
onEventSent(RenderContract.Event.ToBack)
}
when {
spotHasNoPrintProfiles -> {
SpotHasNotPrintProfilesComponent(
onConfirmClick = { onEventSent(RenderContract.Event.OpenMap) },
onDismissRequest = { onEventSent(RenderContract.Event.ToBack) }
)
return
}
printProfileSelector != null -> PrintProfileListSelectorComponent(
list = printProfileSelector,
current = state.value.printProfile,
onChange = { onEventSent(RenderContract.Event.ChangePrintProfile(it)) }
)
openPrintSize != null -> PrintSizeModalComponent(
currentPrintSize = openPrintSize,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(size = it)) }
)
openOrientation != null -> PrintOrientationModalComponent(
currentOrientation = openOrientation,
onSelect = { onEventSent(RenderContract.Event.UpdateRenderSettings(orientation = it)) }
)
copies != null -> PrintCopiesModalComponent(
copies = copies,
onChange = { increaseCopy -> onEventSent(RenderContract.Event.UpdateRenderSettings(increaseCopy = increaseCopy)) },
)
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.appbar_print_preview),
alpha = .65f,
onNavigationClick = { onEventSent(RenderContract.Event.ToBack) }
)
}
) { paddingValues ->
val bottomPaddings = paddingValues.calculateBottomPadding()
val topPadding = paddingValues.calculateTopPadding() + 6.dp
ConstraintLayout(
modifier = modifier
.padding(top = topPadding)
.consumeWindowInsets(paddingValues)
.fillMaxSize()
) {
val (footer, spotView, printOptions, paperInfo, pager) = createRefs()
PagerPreviewComponent(
state = state,
renderResultsFlow = renderResultsFlow,
modifier = Modifier
.fillMaxWidth()
.constrainAs(pager) {
linkTo(top = parent.top, bottom = paperInfo.top)
height = Dimension.fillToConstraints
},
onPageOpened = { page, containerSize -> onEventSent(RenderContract.Event.OnPageOpened(page, containerSize)) }
)
PaperInfoComponent(
state = state,
modifier = Modifier
.fillMaxWidth()
.clickable { onEventSent(RenderContract.Event.OpenListProfiles) }
.padding(bottom = 12.dp)
.padding(horizontal = 16.dp)
.constrainAs(paperInfo) { bottom.linkTo(printOptions.top) }
)
PrintOptionsComponent(
renderSettingsV2 = state.value.renderSettings,
modifier = Modifier.constrainAs(printOptions) { bottom.linkTo(spotView.top) },
clickOnCopies = { onEventSent(RenderContract.Event.OpenCopiesSettings) },
clickOnPrintSize = { onEventSent(RenderContract.Event.OpenPrintSizeSettings) },
clickOnOrientation = { onEventSent(RenderContract.Event.OpenOrientationSettings) },
)
ShortOfficeInfoComponent(
officeName = "officeName",
bottomColor = bottomColor,
modifier = Modifier
.fillMaxWidth()
.constrainAs(spotView) { bottom.linkTo(footer.top) },
containerClick = {},
titleRes = R.string.preview_footer_title,
buttonTextRes = R.string.action_print
)
Box(modifier = Modifier
.fillMaxWidth()
.height(bottomPaddings)
.background(bottomColor)
.constrainAs(footer) { bottom.linkTo(parent.bottom) })
}
}
}
@Preview(
device = Devices.PIXEL_4,
showSystemUi = true
)
@Composable
private fun RenderPreviewScreenPreview() {
Surface(Modifier.fillMaxSize()) {
RenderPreviewScreen(
state = remember {
mutableStateOf(
RenderContract.State(
printProfile = PrintProfile(name = "Test")
)
)
},
renderResultsFlow = null,
onEventSent = {},
onNavigationRequested = {},
effectFlow = null,
)
}
}
package com.isidroid.c23.ui.screen.render_preview
import android.util.SparseArray
import androidx.compose.ui.unit.IntSize
import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.isidroid.c23.constant.Argument
import com.isidroid.c23.domain.use_case.RenderUseCase
import com.isidroid.rendering.model.RenderResult
import com.isidroid.c23.ext.isDebug
import com.isidroid.c23.ext.renderPreviewDefaultSettings
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
import com.isidroid.spot.model.RichSpot
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.random.Random
@HiltViewModel
class RenderViewModel @Inject constructor(
private val useCase: RenderUseCase,
savedState: SavedStateHandle
) : BaseViewModel<RenderContract.Event, RenderContract.State, RenderContract.Effect>() {
private var _containerSize: IntSize? = null
private var _richSpot: RichSpot? = null
private val _renderResults = SparseArray<RenderResult>()
private val _renderResultsFlow = MutableStateFlow<SparseArray<RenderResult>?>(null)
val renderResultsFlow = _renderResultsFlow.asStateFlow()
init {
viewModelScope.launch {
savedState.getStateFlow<String?>(Argument.URI, null)
.filterNotNull()
.collect { uris -> load(uris) }
}
}
override val isDebug: Boolean = isDebug()
override fun setInitialState(): RenderContract.State = RenderContract.State()
override suspend fun handleEvents(event: RenderContract.Event) {
when (event) {
RenderContract.Event.Click -> RenderContract.State()
RenderContract.Event.OpenCopiesSettings -> setState { copy(openCopies = viewState.value.renderSettings.copies) }
RenderContract.Event.OpenOrientationSettings -> setState { copy(openOrientation = viewState.value.renderSettings.orientation) }
RenderContract.Event.OpenPrintSizeSettings -> setState { copy(openPrintSize = viewState.value.renderSettings.printSize) }
RenderContract.Event.ToBack -> goBack()
RenderContract.Event.OpenMap -> openMap()
RenderContract.Event.OpenListProfiles -> setState { copy(printProfilesSelector = _richSpot?.printProfiles) }
is RenderContract.Event.OnPageOpened -> onPageOpened(event.page, event.containerSize)
is RenderContract.Event.UpdateRenderSettings -> updateRenderSettings(size = event.size, orientation = event.orientation, increaseCopy = event.increaseCopy)
is RenderContract.Event.ChangePrintProfile -> changePrintProfile(event.profileId)
}
}
// events
private suspend fun load(uris: String) {
useCase.loadSpot()
.flowOn(Dispatchers.IO)
.catchTimber { setState { copy(spotHasNoPrintProfiles = true) } }
.collect { richSpot ->
_richSpot = richSpot
val printProfile = richSpot.printProfiles.first()
setState {
copy(
richSpot = richSpot,
uris = uris.split(",").map { it.trim().toUri() },
renderSettings = renderPreviewDefaultSettings(greyscale = printProfile.grayscale),
printProfile = printProfile
)
}
}
}
private suspend fun onPageOpened(page: Int, containerSize: IntSize? = null) {
if (containerSize != null)
_containerSize = containerSize
val size = _containerSize
val cachedResult = _renderResults.get(page)
if (cachedResult != null) {
onRenderPage(cachedResult, page)
return
}
val state = viewState.value
useCase.render(state.uris.getOrNull(page), size, state.printProfile, state.renderSettings)
.flowOn(Dispatchers.IO)
.catchTimber { Timber.e(it) }
.collect { flowResult ->
when (flowResult) {
FlowResult.Loading -> {}
is FlowResult.Success -> onRenderPage(flowResult.result, page)
}
}
}
private suspend fun updateRenderSettings(@PrintSize size: Int?, @PrintOrientation orientation: Int?, increaseCopy: Boolean?, forceRefresh: Boolean = false) {
val refreshRender = (increaseCopy == null && viewState.value.openCopies == null) || forceRefresh
updateRenderResults(refreshRender)
val current = viewState.value.renderSettings
val copiesCount = getNumberCopies(current.copies, increaseCopy)
val openCopies = if (increaseCopy != null) copiesCount else null
val renderSettings = current.copy(
printSize = size ?: current.printSize,
orientation = orientation ?: current.orientation,
copies = copiesCount,
greyscale = viewState.value.printProfile?.grayscale == true
)
setState {
copy(
openPrintSize = null,
openOrientation = null,
openCopies = openCopies,
renderSettings = renderSettings,
)
}
if (refreshRender) {
onPageOpened(0)
}
}
private fun openMap() {
setState { copy(spotHasNoPrintProfiles = false) }
setEffect { RenderContract.Effect.Navigation.ToMap }
}
private fun goBack() {
setState { copy(spotHasNoPrintProfiles = false) }
setEffect { RenderContract.Effect.Navigation.ToBack }
}
private suspend fun changePrintProfile(profileId: String?) {
val printProfile = profileId?.let { _richSpot?.printProfiles?.find { it.id == profileId } } ?: viewState.value.printProfile
setState { copy(printProfilesSelector = null, printProfile = printProfile) }
if (profileId != null)
updateRenderSettings(increaseCopy = null, orientation = null, size = null)
}
// callbacks
private suspend fun onRenderPage(result: RenderResult, page: Int) {
_renderResults[page] = result
_renderResultsFlow.emit(_renderResults)
setState { copy(renderSeed = Random.nextInt()) }
}
private fun getNumberCopies(current: Int, increaseCopy: Boolean?): Int {
if (increaseCopy == null) return current
return when {
increaseCopy -> current + 1
else -> current.let { if (it == 1) 1 else it - 1 }
}
}
private fun updateRenderResults(isReset: Boolean) {
if (isReset)
_renderResults.clear()
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview._component
import android.util.SparseArray
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.rendering.model.RenderResult
import com.isidroid.c23.ui.screen.render_preview.RenderContract
import com.isidroid.render_preview.RenderPlaceholderComponent
import com.isidroid.render_preview.RenderPreviewComponent
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun PagerPreviewComponent(
state: State<RenderContract.State>,
renderResultsFlow: StateFlow<SparseArray<RenderResult>?>?,
modifier: Modifier,
onPageOpened: (page: Int, containerSize: IntSize?) -> Unit,
) {
var containerSize by remember { mutableStateOf<IntSize?>(null) }
if (containerSize == null)
RenderPlaceholderComponent(
onSize = { containerSize = it },
modifier = modifier.fillMaxSize()
)
else
PagerContent(
state = state,
renderResultsFlow = renderResultsFlow,
modifier = modifier,
onPageOpened = { onPageOpened(it, containerSize) }
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PagerContent(
state: State<RenderContract.State>,
renderResultsFlow: StateFlow<SparseArray<RenderResult>?>?,
modifier: Modifier,
onPageOpened: (Int) -> Unit,
) {
val pageCount = state.value.uris.size
val renderResults = renderResultsFlow?.collectAsState(initial = null)
var currentPage by remember { mutableIntStateOf(1) }
val pagerState = rememberPagerState(
pageCount = { pageCount },
)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.distinctUntilChanged()
.collect { page ->
currentPage = page + 1
onPageOpened(page)
}
}
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
val currentRender = renderResults?.value?.get(it)
if (currentRender != null) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
RenderPreviewComponent(
widthPx = currentRender.width,
heightPx = currentRender.height,
filePath = currentRender.filePath,
modifier = Modifier
)
}
} else {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Loading")
}
}
}
PagerIndicatorComponent(
currentPage = currentPage,
totalPage = pageCount,
modifier = Modifier.padding(top = 6.dp)
)
}
}
@Composable
private fun PagerIndicatorComponent(
currentPage: Int,
totalPage: Int,
modifier: Modifier = Modifier
) {
Text(
text = "Page $currentPage of $totalPage",
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Light,
letterSpacing = .48.sp,
modifier = modifier
.background(Color.Black.copy(alpha = .45f), RoundedCornerShape(24.dp))
.padding(horizontal = 24.dp, vertical = 12.dp)
)
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview._component
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
import com.isidroid.c23.ui.screen.render_preview.RenderContract
import com.isidroid.spot.model.PrintProfile
import com.isidroid.utils.asCost
@Composable
fun PaperInfoComponent(
state: State<RenderContract.State>,
modifier: Modifier = Modifier,
) {
val printProfile = state.value.printProfile ?: return
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = printProfile.name,
fontWeight = FontWeight.Bold,
lineHeight = 32.sp,
fontSize = 18.sp
)
Text(
text = printProfileSizeName(printProfile),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .75f),
lineHeight = 24.sp,
fontSize = 16.sp
)
Text(
text = stringResource(id = R.string.print_profile_cost_info, printProfile.cost.asCost()),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .75f),
lineHeight = 24.sp,
fontSize = 16.sp
)
}
}
private fun printProfileSizeName(printProfile: PrintProfile) = buildString {
append(printProfile.name)
append(" ")
append("(${printProfile.width / 100}x${printProfile.height / 100}) mm")
}
@Preview
@Composable
private fun PaperInfoComponentPreview() {
Surface {
PaperInfoComponent(
state = remember {
mutableStateOf(
RenderContract.State(
printProfile = PrintProfile(
name = "Test Profile",
width = 215,
height = 279,
cost = 12f
)
)
)
}
)
}
}
package com.isidroid.c23.ui.screen.render_preview._component
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
import com.isidroid.c23.ui.screen.render_preview.ext.getOrientationIcon
import com.isidroid.c23.ui.screen.render_preview.ext.getOrientationTitle
import com.isidroid.c23.ui.screen.render_preview.ext.getPrintSizeIcon
import com.isidroid.c23.ui.screen.render_preview.ext.getPrintSizeTitle
import com.isidroid.rendering.model.RenderSettingsV2
@Composable
fun PrintOptionsComponent(
renderSettingsV2: RenderSettingsV2,
clickOnCopies: () -> Unit,
clickOnPrintSize: () -> Unit,
clickOnOrientation: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = stringResource(id = R.string.printing_options),
modifier = Modifier
.background(MaterialTheme.colorScheme.tertiaryContainer)
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 24.dp),
fontSize = 16.sp,
fontFamily = FontFamily.Serif,
fontWeight = FontWeight.Bold
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.padding(top = 24.dp, bottom = 12.dp)
) {
PrintOptionItemComponent(
modifier = Modifier
.weight(1f)
.clickable { clickOnCopies() },
icon = R.drawable.ic_option_copies,
counter = renderSettingsV2.copies,
titleRes = R.string.copies,
)
PrintOptionItemComponent(
modifier = Modifier
.weight(1f)
.clickable { clickOnPrintSize() },
icon = getPrintSizeIcon(renderSettingsV2.printSize),
titleRes = getPrintSizeTitle(renderSettingsV2.printSize)
)
PrintOptionItemComponent(
modifier = Modifier
.weight(1f)
.clickable { clickOnOrientation() },
icon = getOrientationIcon(renderSettingsV2.orientation),
titleRes = getOrientationTitle(renderSettingsV2.orientation)
)
}
}
}
@Composable
private fun PrintOptionItemComponent(
modifier: Modifier,
@StringRes titleRes: Int,
@DrawableRes icon: Int,
counter: Int? = null,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.padding(bottom = 16.dp)
.size(48.dp),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
if (counter != null)
Text(text = "$counter", fontSize = 20.sp)
}
Text(
text = stringResource(id = titleRes),
fontSize = 16.sp
)
}
}
@Composable
@Preview
private fun PrintOptionsComponentPreview() {
Surface {
PrintOptionsComponent(
renderSettingsV2 = RenderSettingsV2(),
clickOnCopies = {},
clickOnPrintSize = {},
clickOnOrientation = {},
)
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview._component
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.isidroid.c23.R
import com.isidroid.compose_components.DefaultAlertDialog
@Composable
internal fun SpotHasNotPrintProfilesComponent(
onConfirmClick: () -> Unit = {},
onDismissRequest: () -> Unit = {}
) {
DefaultAlertDialog(
message = stringResource(id = R.string.error_spot_has_no_printing_profiles),
confirmButtonText = stringResource(id = R.string.action_find_spot),
onConfirmClick = onConfirmClick,
onDismissRequest = onDismissRequest
)
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview.ext
import com.isidroid.c23.R
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
internal fun getPrintSizeIcon(@PrintSize printSize: Int) = when (printSize) {
PrintSize.FIT_TO_PAPER -> R.drawable.ic_option_printsize_fill_to_paper
PrintSize.FIT_TO_PRINTABLE_AREA -> R.drawable.ic_option_printsize_fit_to_printable
PrintSize.PAPER_ORIGINAL_PAGE -> R.drawable.ic_option_printsize_actual
PrintSize.FILL_PAGE -> R.drawable.ic_option_printsize_fill_to_paper
else -> R.drawable.ic_option_print_size
}
internal fun getOrientationIcon(@PrintOrientation orientation: Int) = when (orientation) {
PrintOrientation.AUTO -> R.drawable.ic_option_auto_orientation
PrintOrientation.PORTRAIT -> R.drawable.ic_option_portrait
PrintOrientation.LANDSCAPE -> R.drawable.ic_option_landscape
else -> R.drawable.ic_option_auto_orientation
}
internal fun getPrintSizeTitle(@PrintSize printSize: Int) = when (printSize) {
PrintSize.FIT_TO_PAPER -> R.string.fit_to_paper
PrintSize.FIT_TO_PRINTABLE_AREA -> R.string.fit_to_printable_area
PrintSize.PAPER_ORIGINAL_PAGE -> R.string.original_page
PrintSize.FILL_PAGE -> R.string.fill_page
else -> R.string.fit_to_paper
}
internal fun getOrientationTitle(@PrintOrientation orientation: Int) = when (orientation) {
PrintOrientation.AUTO -> R.string.orientation_auto
PrintOrientation.PORTRAIT -> R.string.portrait
PrintOrientation.LANDSCAPE -> R.string.landscape
else -> R.string.orientation_auto
}
package com.isidroid.c23.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF002f6c)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
val md_theme_light_secondary = Color(0xFF1457FF)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xffF1F1F1)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
val md_theme_light_tertiary = Color(0xFF7D5260)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFE4E1E1)
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
val md_theme_light_error = Color(0xFFB3261E)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
val md_theme_light_outline = Color(0xFF79747E)
val md_theme_light_background = Color(0xFFFFFFFF)
val md_theme_light_onBackground = Color(0xFF1D1B20)
val md_theme_light_surface = Color(0xFFFFFFFF)
val md_theme_light_onSurface = Color(0xFF1D1B20)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_inverseSurface = Color(0xFF322F35)
val md_theme_light_inverseOnSurface = Color(0xFFF5EFF7)
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
val md_theme_light_surfaceTint = Color(0xFF6750A4)
val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFD0BCFF)
val md_theme_dark_onPrimary = Color(0xFF381E72)
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
val md_theme_dark_secondary = Color(0xFFCCC2DC)
val md_theme_dark_onSecondary = Color(0xFF332D41)
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
val md_theme_dark_onTertiary = Color(0xFF492532)
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
val md_theme_dark_error = Color(0xFFF2B8B5)
val md_theme_dark_onError = Color(0xFF601410)
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
val md_theme_dark_outline = Color(0xFF938F99)
val md_theme_dark_background = Color(0xFF141218)
val md_theme_dark_onBackground = Color(0xFFE6E0E9)
val md_theme_dark_surface = Color(0xFF141218)
val md_theme_dark_onSurface = Color(0xFFE6E0E9)
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
val md_theme_dark_inverseSurface = Color(0xFFE6E0E9)
val md_theme_dark_inverseOnSurface = Color(0xFF322F35)
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
val md_theme_dark_outlineVariant = Color(0xFF49454F)
val md_theme_dark_scrim = Color(0xFF000000)
\ No newline at end of file
package com.isidroid.c23.ui.theme
import android.app.Activity
import android.os.Build
import android.view.WindowManager
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
private val LightColorScheme = lightColorScheme(
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,
onError = md_theme_light_onError,
errorContainer = md_theme_light_errorContainer,
onErrorContainer = md_theme_light_onErrorContainer,
outline = md_theme_light_outline,
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,
inverseSurface = md_theme_light_inverseSurface,
inverseOnSurface = md_theme_light_inverseOnSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColorScheme = darkColorScheme(
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,
onError = md_theme_dark_onError,
errorContainer = md_theme_dark_errorContainer,
onErrorContainer = md_theme_dark_onErrorContainer,
outline = md_theme_dark_outline,
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,
inverseSurface = md_theme_dark_inverseSurface,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
// val context = LocalContext.current
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
// }
//
// darkTheme -> LightColorScheme
else -> LightColorScheme
}
// val view = LocalView.current
// if (!view.isInEditMode) {
// SideEffect {
// val window = (view.context as Activity).window
// val windowInsetsController = WindowCompat.getInsetsController(window, view)
// val transparentColor = ContextCompat.getColor(view.context, android.R.color.transparent)
//
// window.statusBarColor = transparentColor
// window.navigationBarColor = colorScheme.primary.toArgb()
//
// windowInsetsController.isAppearanceLightStatusBars = true
// windowInsetsController.isAppearanceLightNavigationBars = true
// window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
// }
// }
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
package com.isidroid.c23.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
private val rubikSansFamily = FontFamily(
Font(R.font.rubik_light, FontWeight.Light),
Font(R.font.rubik_regular, FontWeight.Normal),
Font(R.font.rubik_medium, FontWeight.Medium),
Font(R.font.rubik_semibold, FontWeight.SemiBold),
Font(R.font.rubik_bold, FontWeight.Bold)
)
// Set of Material typography styles to start with
val Typography = Typography(
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = rubikSansFamily,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
letterSpacing = 0.5.sp
),
bodyLarge = TextStyle(
fontFamily = rubikSansFamily,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
letterSpacing = 0.5.sp
),
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
package com.isidroid.c23.utils
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.lang.reflect.Type
class DateDeserializer : JsonDeserializer<Date?> {
override fun deserialize(
jsonElement: JsonElement, typeOF: Type,
context: JsonDeserializationContext
): Date? {
val formats = arrayOf(
"yyyy-MM-dd'T'HH:mm:ssZZ",
"yyyy-MM-dd",
"MMM dd, yyyy HH:mm:ss",
"MMM dd, yyyy",
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ssZZZZ",
)
for (format in formats) {
try {
return SimpleDateFormat(format, Locale.getDefault()).parse(jsonElement.asString)
} catch (_: Throwable) {
}
}
return null
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="84dp"
android:height="108dp"
android:viewportWidth="84"
android:viewportHeight="108">
<path
android:pathData="M0.5,1.2c-0.3,0.7 -0.4,24.9 -0.3,53.8l0.3,52.5 40.8,0.3 40.7,0.2 0,-45.9 0,-45.8 -10.6,-8.2 -10.7,-8.1 -29.9,-0c-23,-0 -30,0.3 -30.3,1.2zM57,11.9c0,9.2 -0.1,9.1 10.1,9.1l8.9,-0 0,40.5 0,40.5 -35,-0 -35,-0 0,-48 0,-48 25.5,-0 25.5,-0 0,5.9z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
<path
android:pathData="M35.9,44.7c-0.6,1.6 -3,8 -5.4,14.3 -2.4,6.3 -4.8,12.7 -5.4,14.2 -1,2.8 -1,2.8 2.8,2.8 3.4,-0 3.9,-0.3 4.7,-3.3 0.9,-3.1 1.1,-3.2 7.2,-3.5l6.3,-0.3 1.4,3.6c1.3,3.1 2,3.5 5.1,3.5 2,-0 3.4,-0.4 3.2,-0.8 -0.1,-0.5 -3,-8.2 -6.3,-17l-6,-16.2 -3.3,-0c-2.6,-0 -3.5,0.5 -4.3,2.7zM41.9,56.1c0.7,2.3 1.5,4.7 1.8,5.5 0.4,1.1 -0.5,1.4 -3.7,1.4 -3.2,-0 -4.1,-0.3 -3.7,-1.4 0.3,-0.8 1.1,-3.2 1.8,-5.5 0.7,-2.3 1.5,-4.1 1.9,-4.1 0.4,-0 1.2,1.8 1.9,4.1z"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="50dp"
android:height="37dp"
android:viewportWidth="50"
android:viewportHeight="37">
<path
android:pathData="M50,0L50,36.8421L0,36.8421L0,0L50,0ZM2.0058,2.0142L2.0058,13.1742L7.2251,18.432L2.0058,23.6899L2.0058,34.8498L48.2003,34.8498L48.2003,2.0142L2.0058,2.0142Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="21dp"
android:viewportWidth="27"
android:viewportHeight="21">
<path
android:pathData="M4.6332,0.7105C4.4914,0.7105 4.3669,0.7914 4.2948,0.9142L0.6348,5.6474C0.6,5.6922 0.578,5.7442 0.5618,5.7982C0.5013,5.8767 0.4634,5.977 0.4634,6.0884L0.4634,19.972C0.4634,20.2196 0.6439,20.4199 0.8669,20.4199L25.8859,20.4199C26.1087,20.4199 26.2895,20.2196 26.2895,19.972L26.2895,1.162C26.2895,0.9147 26.1087,0.7141 25.8859,0.7141L4.7677,0.7141C4.7497,0.7141 4.7328,0.7174 4.7153,0.7201C4.6889,0.7138 4.6612,0.7105 4.6332,0.7105ZM24.8453,18.9063L1.9076,18.9063L1.9076,6.6751L5.0934,6.6751C5.3047,6.6751 5.4757,6.4883 5.4757,6.2582L5.4757,2.2274L24.8453,2.2274L24.8453,18.9063Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M14.0561,15.3227L7.5995,15.3227L12.1871,5.8078L15.4153,12.5034L16.9445,9.3318L19.833,15.3227L14.0561,15.3227Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="27dp"
android:viewportWidth="21"
android:viewportHeight="27">
<path
android:pathData="M20.6053,4.3594C20.6053,4.2112 20.5207,4.0809 20.3924,4.0056L15.444,0.1792C15.3971,0.1429 15.3428,0.1198 15.2863,0.1029C15.2042,0.0397 15.0994,0 14.9829,0L0.4682,0C0.2094,0 0,0.1887 0,0.4219L0,26.5781C0,26.811 0.2094,27 0.4682,27L20.1333,27C20.3918,27 20.6015,26.811 20.6015,26.5781L20.6015,4.5C20.6015,4.4812 20.5981,4.4634 20.5953,4.4452C20.6018,4.4176 20.6053,4.3886 20.6053,4.3594ZM1.5824,25.4901L1.5824,1.5099L14.3695,1.5099L14.3695,4.8405C14.3695,5.0613 14.5648,5.2401 14.8055,5.2401L19.0194,5.2401L19.0194,25.4901L1.5824,25.4901Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M11.0132,18.4737L4.2632,18.4737L9.0592,8.5263L12.4342,15.5263L14.0329,12.2105L17.0526,18.4737L11.0132,18.4737Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M21,15h2v2h-2v-2zM21,11h2v2h-2v-2zM23,19h-2v2c1,0 2,-1 2,-2zM13,3h2v2h-2L13,3zM21,7h2v2h-2L21,7zM21,3v2h2c0,-1 -1,-2 -2,-2zM1,7h2v2L1,9L1,7zM17,3h2v2h-2L17,3zM17,19h2v2h-2v-2zM3,3C2,3 1,4 1,5h2L3,3zM9,3h2v2L9,5L9,3zM5,3h2v2L5,5L5,3zM1,11v8c0,1.1 0.9,2 2,2h12L15,11L1,11zM3,19l2.5,-3.21 1.79,2.15 2.5,-3.22L13,19L3,19z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="27dp"
android:viewportWidth="36"
android:viewportHeight="27">
<path
android:pathData="M1,1L1,25C1,25.5523 1.4477,26 2,26L35,26L35,2C35,1.4477 34.5523,1 34,1L1,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="nonZero"
android:strokeColor="#000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="27dp"
android:viewportWidth="36"
android:viewportHeight="27">
<path
android:pathData="M1,1L1,25C1,25.5523 1.4477,26 2,26L35,26L35,2C35,1.4477 34.5523,1 34,1L1,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="nonZero"/>
<path
android:pathData="M4.1057,3.8949l-0,4.3548l1.3601,-1.3601l2.7983,2.7983l1.4228,-1.4228l-2.7983,-2.7983l1.4105,-1.4105l-4.3548,-0"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M32.0031,23.1698l0,-4.3548l-1.3601,1.3601l-2.7983,-2.7983l-1.4228,1.4228l2.7983,2.7983l-1.4105,1.4105l4.3548,-0"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="27dp"
android:viewportWidth="36"
android:viewportHeight="27">
<path
android:pathData="M1,1L1,25C1,25.5523 1.4477,26 2,26L35,26L35,2C35,1.4477 34.5523,1 34,1L1,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="nonZero"
android:strokeColor="#000000"/>
<path
android:pathData="M4,12h16v11h-16z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="nonZero"
android:strokeColor="#000000"/>
<path
android:pathData="M4.5,2.5L4.5,22.5"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="evenOdd"
android:strokeLineCap="square"/>
<path
android:pathData="M31.1531,4.1774l-4.3548,-0l1.3601,1.3601l-2.7983,2.7983l1.4228,1.4228l2.7983,-2.7983l1.4105,1.4105l0,-4.3548"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M5.5,22.5L33.5,22.5"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="evenOdd"
android:strokeLineCap="square"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="27dp"
android:viewportWidth="36"
android:viewportHeight="27">
<path
android:pathData="M1,1L1,25C1,25.5523 1.4477,26 2,26L35,26L35,2C35,1.4477 34.5523,1 34,1L1,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:fillType="nonZero"/>
<path
android:pathData="M30.2778,13.6667L27.3889,13.6667L27.3889,13.0001C27.3889,12.7476 27.2343,12.5166 26.9896,12.4038L24.1008,11.0705C23.8772,10.9671 23.6108,10.9783 23.3979,11.0996C23.185,11.2212 23.0553,11.4356 23.0553,11.6665L23.0553,12.9998L12.9442,12.9998L12.9442,11.6665C12.9442,11.4356 12.8147,11.2209 12.6015,11.0996C12.3889,10.978 12.1228,10.9671 11.8987,11.0705L9.0098,12.4038C8.7657,12.5169 8.6111,12.7476 8.6111,13.0001L8.6111,13.6667L5.7222,13.6667C5.3233,13.6667 5,13.9651 5,14.3334C5,14.7017 5.3233,15 5.7222,15L8.6111,15L8.6111,16.3333C8.6111,16.7016 8.9344,17 9.3333,17L12.2222,17C12.6212,17 12.9444,16.7016 12.9444,16.3333L12.9444,15.6667L23.0556,15.6667L23.0556,16.3333C23.0556,16.7016 23.3788,17 23.7778,17L26.6667,17C27.0656,17 27.3889,16.7016 27.3889,16.3333L27.3889,15L30.2778,15C30.6767,15 31,14.7017 31,14.3334C31,13.9651 30.6767,13.6667 30.2778,13.6667ZM11.5,15.6667L10.0556,15.6667L10.0556,13.4121L11.5,12.7454L11.5,15.6667ZM25.9444,15.6667L24.5,15.6667L24.5,12.7454L25.9444,13.4121L25.9444,15.6667Z"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
\ No newline at end of file
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment