diff --git a/app/.gitignore b/app/.gitignore index c3c9e64e..950eef7f 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,2 @@ /build -release \ No newline at end of file +release diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f6345f4..1c01d8c3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,6 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) - alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.plugin.compose) alias(libs.plugins.androidx.room) alias(libs.plugins.google.dagger.hilt.android) @@ -21,18 +20,18 @@ object Version { kotlin { jvmToolchain(17) compilerOptions { - freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") + freeCompilerArgs.add("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode") } } android { namespace = "ru.spbu.depnav" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "ru.spbu.depnav" - minSdk = 21 - targetSdk = 36 + minSdk = 24 // ModalNavigationDrawer crashes on 23 + targetSdk = 37 versionCode = Version.CODE versionName = Version.NAME @@ -42,8 +41,8 @@ android { signingConfigs { val keystorePropertiesFile = rootProject.file("keystore.properties") if (keystorePropertiesFile.exists()) { - val keystoreProperties = Properties().apply { - load(keystorePropertiesFile.inputStream()) + val keystoreProperties = keystorePropertiesFile.inputStream().use { + Properties().apply { load(it) } } create("release") { storeFile = file(keystoreProperties.getProperty("storeFile")) @@ -57,6 +56,7 @@ android { buildTypes { release { isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -68,14 +68,17 @@ android { buildFeatures { compose = true } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } +} + +room { + schemaDirectory("$projectDir/schemas") } dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) + implementation(libs.google.android.material) + implementation(libs.androidx.lifecycle.runtimeKtx) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.viewmodelCompose) @@ -94,8 +97,8 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.tooling) - implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) + implementation(libs.plrapps.mapcompose) testImplementation(libs.junit) @@ -104,6 +107,25 @@ dependencies { androidTestImplementation(libs.androidx.test.extJunit) } -room { - schemaDirectory("$projectDir/schemas") +val generateMapsDatabase = tasks.register("generateMapsDatabase") { + schemaDirectory = project.layout.projectDirectory + .dir("schemas/ru.spbu.depnav.data.db.AppDatabase") + dataDirectory = project.layout.projectDirectory + .dir("maps/infos") + outputFile = project.layout.buildDirectory + .file("intermediates/maps_database/maps.db") + dependsOn(tasks.named("copyRoomSchemas")) +} +androidComponents { + onVariants { variant -> + val copyMapsDatabase = tasks.register( + "copyMapsDatabaseTo${variant.name.replaceFirstChar { it.uppercase() }}Assets" + ) { + sources.from(generateMapsDatabase.map { it.outputFile }) + } + variant.sources.assets?.addGeneratedSourceDirectory( + copyMapsDatabase, + CopyFilesTask::destination + ) + } } diff --git a/data/jsons/spbu-mm.json b/app/maps/infos/spbu-mm.json similarity index 99% rename from data/jsons/spbu-mm.json rename to app/maps/infos/spbu-mm.json index d738bfcd..a4b721ae 100644 --- a/data/jsons/spbu-mm.json +++ b/app/maps/infos/spbu-mm.json @@ -7563,7 +7563,7 @@ "description": null }, "en": { - "title": "2244", + "title": "3244", "location": null, "description": null } @@ -10703,7 +10703,7 @@ "description": null }, "en": { - "title": "2248", + "title": "4248", "location": null, "description": null } diff --git a/data/jsons/spbu-pf.json b/app/maps/infos/spbu-pf.json similarity index 100% rename from data/jsons/spbu-pf.json rename to app/maps/infos/spbu-pf.json diff --git a/data/map-info-schema.json b/app/maps/map-info-schema.json similarity index 100% rename from data/map-info-schema.json rename to app/maps/map-info-schema.json diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3f1d6fb7..2dbaacce 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -12,13 +12,15 @@ # public *; #} +-dontobfuscate + # 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 +#-renamesourcefileattribute SourceFile # Remove logging (https://www.guardsquare.com/manual/configuration/examples#logging) -assumenosideeffects class android.util.Log { diff --git a/app/schemas/ru.spbu.depnav.data.db.AppDatabase/10.json b/app/schemas/ru.spbu.depnav.data.db.AppDatabase/10.json new file mode 100644 index 00000000..81d00231 --- /dev/null +++ b/app/schemas/ru.spbu.depnav.data.db.AppDatabase/10.json @@ -0,0 +1,327 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "b2f5f82a76e1409ed0ad2d4aafaef05b", + "entities": [ + { + "tableName": "map_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `internal_name` TEXT NOT NULL, `floor_width` INTEGER NOT NULL, `floor_height` INTEGER NOT NULL, `tile_size` INTEGER NOT NULL, `levels_num` INTEGER NOT NULL, `floors_num` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "internalName", + "columnName": "internal_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "floorWidth", + "columnName": "floor_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "floorHeight", + "columnName": "floor_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileSize", + "columnName": "tile_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "levelsNum", + "columnName": "levels_num", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "floorsNum", + "columnName": "floors_num", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_map_info_internal_name", + "unique": true, + "columnNames": [ + "internal_name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_map_info_internal_name` ON `${TABLE_NAME}` (`internal_name`)" + } + ] + }, + { + "tableName": "map_title", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`map_id` INTEGER NOT NULL, `language_id` TEXT NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`map_id`, `language_id`), FOREIGN KEY(`map_id`) REFERENCES `map_info`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "mapId", + "columnName": "map_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "languageId", + "columnName": "language_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "map_id", + "language_id" + ] + }, + "foreignKeys": [ + { + "table": "map_info", + "onDelete": "RESTRICT", + "onUpdate": "CASCADE", + "columns": [ + "map_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "marker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `map_id` INTEGER NOT NULL, `type` TEXT NOT NULL, `floor` INTEGER NOT NULL, `x` REAL NOT NULL, `y` REAL NOT NULL, FOREIGN KEY(`map_id`) REFERENCES `map_info`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mapId", + "columnName": "map_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "floor", + "columnName": "floor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "x", + "columnName": "x", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "y", + "columnName": "y", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_marker_map_id_floor", + "unique": false, + "columnNames": [ + "map_id", + "floor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_marker_map_id_floor` ON `${TABLE_NAME}` (`map_id`, `floor`)" + } + ], + "foreignKeys": [ + { + "table": "map_info", + "onDelete": "RESTRICT", + "onUpdate": "CASCADE", + "columns": [ + "map_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "marker_text", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`marker_id` INTEGER NOT NULL, `language_id` TEXT NOT NULL, `title` TEXT, `location` TEXT, `description` TEXT, PRIMARY KEY(`marker_id`, `language_id`), FOREIGN KEY(`marker_id`) REFERENCES `marker`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "markerId", + "columnName": "marker_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "languageId", + "columnName": "language_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "marker_id", + "language_id" + ] + }, + "foreignKeys": [ + { + "table": "marker", + "onDelete": "RESTRICT", + "onUpdate": "CASCADE", + "columns": [ + "marker_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "marker_text_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`title` TEXT, `location` TEXT, `description` TEXT, tokenize=unicode61, content=`marker_text`)", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [], + "contentTable": "marker_text", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_marker_text_fts_BEFORE_UPDATE BEFORE UPDATE ON `marker_text` BEGIN DELETE FROM `marker_text_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_marker_text_fts_BEFORE_DELETE BEFORE DELETE ON `marker_text` BEGIN DELETE FROM `marker_text_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_marker_text_fts_AFTER_UPDATE AFTER UPDATE ON `marker_text` BEGIN INSERT INTO `marker_text_fts`(`docid`, `title`, `location`, `description`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`location`, NEW.`description`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_marker_text_fts_AFTER_INSERT AFTER INSERT ON `marker_text` BEGIN INSERT INTO `marker_text_fts`(`docid`, `title`, `location`, `description`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`location`, NEW.`description`); END" + ] + }, + { + "tableName": "search_history_entry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`marker_id` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`marker_id`), FOREIGN KEY(`marker_id`) REFERENCES `marker`(`id`) ON UPDATE CASCADE ON DELETE RESTRICT )", + "fields": [ + { + "fieldPath": "markerId", + "columnName": "marker_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "marker_id" + ] + }, + "foreignKeys": [ + { + "table": "marker", + "onDelete": "RESTRICT", + "onUpdate": "CASCADE", + "columns": [ + "marker_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, 'b2f5f82a76e1409ed0ad2d4aafaef05b')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3718c613..ce6f7061 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,16 +8,15 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.DepNav"> + android:theme="@style/Theme.DepNav.Starting"> - - \ No newline at end of file + diff --git a/app/src/main/assets/maps.db b/app/src/main/assets/maps.db deleted file mode 100644 index b8669212..00000000 Binary files a/app/src/main/assets/maps.db and /dev/null differ diff --git a/app/src/main/java/ru/spbu/depnav/data/db/AppDatabase.kt b/app/src/main/java/ru/spbu/depnav/data/db/AppDatabase.kt index ad037027..4b0970e9 100644 --- a/app/src/main/java/ru/spbu/depnav/data/db/AppDatabase.kt +++ b/app/src/main/java/ru/spbu/depnav/data/db/AppDatabase.kt @@ -33,7 +33,7 @@ import ru.spbu.depnav.data.model.SearchHistoryEntry MapInfo::class, MapTitle::class, Marker::class, MarkerText::class, MarkerTextFts::class, SearchHistoryEntry::class ], - version = 9 + version = 10 ) abstract class AppDatabase : RoomDatabase() { /** DAO for the table containing information about the available maps. */ diff --git a/app/src/main/java/ru/spbu/depnav/data/db/DatabaseModule.kt b/app/src/main/java/ru/spbu/depnav/data/db/DatabaseModule.kt index 1c15d393..65f62921 100644 --- a/app/src/main/java/ru/spbu/depnav/data/db/DatabaseModule.kt +++ b/app/src/main/java/ru/spbu/depnav/data/db/DatabaseModule.kt @@ -43,7 +43,7 @@ object DatabaseModule { DB_ASSET ) .createFromAsset(DB_ASSET) - .fallbackToDestructiveMigration(false) + .fallbackToDestructiveMigration(true) .build() @Provides diff --git a/app/src/main/java/ru/spbu/depnav/data/preferences/PreferencesManager.kt b/app/src/main/java/ru/spbu/depnav/data/preferences/PreferencesManager.kt index 786a24e1..1d19a15a 100644 --- a/app/src/main/java/ru/spbu/depnav/data/preferences/PreferencesManager.kt +++ b/app/src/main/java/ru/spbu/depnav/data/preferences/PreferencesManager.kt @@ -38,7 +38,7 @@ private const val ENABLE_ROTATION_DEFAULT = false private const val SELECTED_MAP_KEY = "map" -/** Helper class to load ans save user settings. */ +/** Helper class to load and save user settings. */ @Singleton class PreferencesManager @Inject constructor(@ApplicationContext context: Context) { private val prefs = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) diff --git a/app/src/main/java/ru/spbu/depnav/ui/MainActivity.kt b/app/src/main/java/ru/spbu/depnav/ui/MainActivity.kt index 2945f713..53cb9fd8 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/MainActivity.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/MainActivity.kt @@ -18,15 +18,20 @@ package ru.spbu.depnav.ui -import android.graphics.Color +import android.app.ActivityManager +import android.content.res.Resources +import android.os.Build 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.isSystemInDarkTheme -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.toArgb +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint import ru.spbu.depnav.data.preferences.PreferencesManager @@ -34,6 +39,8 @@ import ru.spbu.depnav.data.preferences.ThemeMode import ru.spbu.depnav.ui.screen.MapScreen import ru.spbu.depnav.ui.theme.DepNavTheme import javax.inject.Inject +import android.graphics.Color as PlatformColor +import androidx.compose.ui.graphics.Color as ComposeColor @AndroidEntryPoint @Suppress("UndocumentedPublicClass") // Class name is self-explanatory @@ -43,23 +50,50 @@ class MainActivity : ComponentActivity() { lateinit var prefs: PreferencesManager override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() super.onCreate(savedInstanceState) - setContent { + // Set primary color for recents list of older Androids; it only depends on system's + // dark mode setting, not on the in-app one + DepNavTheme { + val primary = MaterialTheme.colorScheme.primary + SideEffect { setPrimaryColor(primary) } + } + val themeMode by prefs.themeModeFlow.collectAsStateWithLifecycle() val darkTheme = when (themeMode) { ThemeMode.LIGHT -> false ThemeMode.DARK -> true ThemeMode.SYSTEM -> isSystemInDarkTheme() } + SideEffect { enableEdgeToEdge { darkTheme } } + DepNavTheme(darkTheme) { MapScreen(prefs) } + } + } - LaunchedEffect(darkTheme) { - val style = - SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) { darkTheme } - enableEdgeToEdge(style, style) - } + private fun enableEdgeToEdge(detectDarkMode: (Resources) -> Boolean) { + // Scrims are the defaults from enableEdgeToEdge sources + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + lightScrim = PlatformColor.TRANSPARENT, + darkScrim = PlatformColor.TRANSPARENT, + detectDarkMode + ), + navigationBarStyle = SystemBarStyle.auto( + lightScrim = PlatformColor.argb(0xe6, 0xFF, 0xFF, 0xFF), + darkScrim = PlatformColor.argb(0x80, 0x1b, 0x1b, 0x1b), + detectDarkMode + ) + ) + } - DepNavTheme(darkTheme = darkTheme) { MapScreen(prefs) } + private fun setPrimaryColor(color: ComposeColor) { + val colorArgb = color.toArgb() + val taskDescription = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityManager.TaskDescription.Builder().setPrimaryColor(colorArgb).build() + } else { + ActivityManager.TaskDescription(null, null, colorArgb) } + setTaskDescription(taskDescription) } } diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/FloorSwitch.kt b/app/src/main/java/ru/spbu/depnav/ui/component/FloorSwitch.kt index 6cb98387..15e776e9 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/FloorSwitch.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/FloorSwitch.kt @@ -27,9 +27,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Column import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -38,11 +35,13 @@ 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.graphicsLayer +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import ru.spbu.depnav.R import ru.spbu.depnav.ui.theme.DepNavTheme -import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA +import ru.spbu.depnav.ui.theme.MAP_OVERLAY_ALPHA private const val MIN_FLOOR = 1 @@ -57,7 +56,7 @@ fun FloorSwitch( Surface( modifier = modifier, shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = ON_MAP_SURFACE_ALPHA) + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = MAP_OVERLAY_ALPHA) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { IconButton( @@ -65,7 +64,7 @@ fun FloorSwitch( enabled = floor < maxFloor ) { Icon( - Icons.Rounded.KeyboardArrowUp, + painterResource(R.drawable.ic_keyboard_arrow_up), contentDescription = stringResource(R.string.label_to_floor_above) ) } @@ -75,10 +74,10 @@ fun FloorSwitch( transitionSpec = { if (targetState > initialState) { slideInVertically { height -> -height } + fadeIn() togetherWith - slideOutVertically { height -> height } + fadeOut() + slideOutVertically { height -> height } + fadeOut() } else { slideInVertically { height -> height } + fadeIn() togetherWith - slideOutVertically { height -> -height } + fadeOut() + slideOutVertically { height -> -height } + fadeOut() } using SizeTransform(clip = false) }, label = "floor switch scroll" @@ -91,8 +90,9 @@ fun FloorSwitch( enabled = floor > MIN_FLOOR ) { Icon( - Icons.Rounded.KeyboardArrowDown, - contentDescription = stringResource(R.string.label_to_floor_below) + painterResource(R.drawable.ic_keyboard_arrow_up), + contentDescription = stringResource(R.string.label_to_floor_below), + modifier = Modifier.graphicsLayer { rotationZ = 180f } ) } } diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/MainMenuSheet.kt b/app/src/main/java/ru/spbu/depnav/ui/component/MainMenuSheet.kt index e338a1f6..6ddbcffe 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/MainMenuSheet.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/MainMenuSheet.kt @@ -26,9 +26,6 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Info -import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -40,12 +37,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ru.spbu.depnav.R import ru.spbu.depnav.ui.viewmodel.AvailableMap -import androidx.compose.ui.platform.LocalResources // https://m3.material.io/components/navigation-drawer/specs#368147de-9661-4a28-9fc1-ce2f8c9eac40 private val ITEM_HEIGHT = 56.dp @@ -77,7 +74,7 @@ fun MainMenuSheet( MiscItem( icon = { Icon( - Icons.Rounded.Settings, + painterResource(R.drawable.ic_settings), contentDescription = stringResource(R.string.label_open_settings) ) }, @@ -88,7 +85,7 @@ fun MainMenuSheet( MiscItem( icon = { Icon( - Icons.Rounded.Info, + painterResource(R.drawable.ic_info), contentDescription = stringResource(R.string.label_open_map_legend) ) }, diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/MapSearchBar.kt b/app/src/main/java/ru/spbu/depnav/ui/component/MapSearchBar.kt index 79e30ff1..084dbaf2 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/MapSearchBar.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/MapSearchBar.kt @@ -19,13 +19,12 @@ package ru.spbu.depnav.ui.component import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues @@ -33,31 +32,29 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Clear -import androidx.compose.material.icons.rounded.Menu +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text -import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import ovh.plrapps.mapcompose.utils.lerp +import androidx.compose.ui.util.lerp import ru.spbu.depnav.R -import ru.spbu.depnav.ui.theme.DEFAULT_PADDING -import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA +import ru.spbu.depnav.ui.theme.MAP_OVERLAY_ALPHA import ru.spbu.depnav.ui.viewmodel.SearchResults // These are basically copied from SearchBar implementation @@ -79,8 +76,7 @@ private val EXPANSION_EXIT_SPEC = tween( @Suppress("LongParameterList") // Considered OK for a composable @OptIn(ExperimentalMaterial3Api::class) fun MapSearchBar( - query: String, - onQueryChange: (String) -> Unit, + queryState: TextFieldState, mapTitle: String, expanded: Boolean, onExpandedChange: (Boolean) -> Unit, @@ -89,24 +85,30 @@ fun MapSearchBar( onMenuClick: () -> Unit, modifier: Modifier = Modifier ) { - val expansionAnimationProgress by animateFloatAsState( + val expansionProgress by animateFloatAsState( targetValue = if (expanded) 1f else 0f, animationSpec = if (expanded) EXPANSION_ENTER_SPEC else EXPANSION_EXIT_SPEC, label = "Map search bar activation animation progress" ) + // SearchBar always consumes the insets but adds padding only when collapsed + val horizontalInsetsPadding = + (SearchBarDefaults.windowInsets.only(WindowInsetsSides.Horizontal) * + expansionProgress).asPaddingValues() + + // TODO: on older Android versions SearchBar auto-opens after rotation for some reason, seems + // to be a Google's bug, not of just this app. + // TODO: SearchBar + ExpandedFullScreenSearchBar is now preferred but currently it has its own + // problems (after collapsing keyboard flashes and if there are horizontal insets input field + // jumps horizontally). To be revisited in the future. SearchBar( inputField = { + val focusManager = LocalFocusManager.current SearchBarDefaults.InputField( - query = query, - onQueryChange = onQueryChange, - onSearch = with (LocalFocusManager.current) { { clearFocus() } }, + state = queryState, + onSearch = { focusManager.clearFocus() }, expanded = expanded, onExpandedChange = onExpandedChange, - modifier = Modifier.windowInsetsPadding( - WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) * - expansionAnimationProgress - ), placeholder = { Text( stringResource(R.string.search_on_map, mapTitle), @@ -123,24 +125,31 @@ fun MapSearchBar( }, trailingIcon = { AnimatedTrailingIcon( - expanded, - queryEmpty = query.isEmpty(), - onClearClick = { onQueryChange("") } + queryEmpty = queryState.text.isEmpty(), + onClearClick = queryState::clearText ) - } + }, + colors = SearchBarDefaults.inputFieldColors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent + ), + modifier = Modifier + .padding(horizontalInsetsPadding) + // On older Android versions it is impossible to leave the search without this + .focusProperties { canFocus = expanded || expansionProgress == 0f } ) }, expanded = expanded, onExpandedChange = onExpandedChange, modifier = modifier, colors = SearchBarDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy( - alpha = lerp(ON_MAP_SURFACE_ALPHA, 1f, expansionAnimationProgress) + containerColor = SearchBarDefaults.colors().containerColor.copy( + alpha = lerp(MAP_OVERLAY_ALPHA, 1f, expansionProgress) ) - ), + ) ) { val keyboard = LocalSoftwareKeyboardController.current - SearchResultsView( results, onScroll = { onTop -> keyboard?.apply { if (onTop) show() else hide() } }, @@ -149,15 +158,21 @@ fun MapSearchBar( onResultClick(it) }, modifier = Modifier + .padding(horizontalInsetsPadding) .windowInsetsPadding(WindowInsets.safeDrawing) - .padding(horizontal = DEFAULT_PADDING * 1.5f) ) } + + LaunchedEffect(expanded) { + if (!expanded) { + queryState.clearText() + } + } } @Composable private operator fun WindowInsets.times(num: Float): WindowInsets { - val paddings = asPaddingValues(LocalDensity.current) + val paddings = asPaddingValues() val layoutDirection = LocalLayoutDirection.current return WindowInsets( paddings.calculateLeftPadding(layoutDirection) * num, @@ -170,26 +185,21 @@ private operator fun WindowInsets.times(num: Float): WindowInsets { @Composable private fun AnimatedLeadingIcon( searchBarActive: Boolean, - modifier: Modifier = Modifier, onMenuClick: () -> Unit, onNavigateBackClick: () -> Unit ) { - AnimatedContent( - searchBarActive, - modifier = modifier, - label = "Map search bar leading icon change" - ) { active -> + AnimatedContent(searchBarActive) { active -> if (active) { IconButton(onClick = onNavigateBackClick) { Icon( - Icons.AutoMirrored.Rounded.ArrowBack, + painterResource(R.drawable.ic_arrow_back), contentDescription = stringResource(R.string.label_navigate_back) ) } } else { IconButton(onClick = onMenuClick) { Icon( - Icons.Rounded.Menu, + painterResource(R.drawable.ic_menu), contentDescription = stringResource(R.string.label_open_main_menu) ) } @@ -198,30 +208,13 @@ private fun AnimatedLeadingIcon( } @Composable -private fun AnimatedTrailingIcon( - searchBarActive: Boolean, - queryEmpty: Boolean, - onClearClick: () -> Unit, - modifier: Modifier = Modifier -) { - AnimatedContent( - searchBarActive to queryEmpty, - modifier = modifier, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - label = "Map search bar trailing icon change" - ) { (active, emptyQuery) -> - if (!active) { - return@AnimatedContent - } - if (emptyQuery) { - Spacer(modifier = Modifier.minimumInteractiveComponentSize()) - } else { - IconButton(onClick = onClearClick) { - Icon( - Icons.Rounded.Clear, - contentDescription = stringResource(R.string.label_clear_text_field) - ) - } +private fun AnimatedTrailingIcon(queryEmpty: Boolean, onClearClick: () -> Unit) { + AnimatedVisibility(!queryEmpty, enter = fadeIn(), exit = fadeOut()) { + IconButton(onClick = onClearClick) { + Icon( + painterResource(R.drawable.ic_close), + contentDescription = stringResource(R.string.label_clear_text_field) + ) } } } diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/MarkerCluster.kt b/app/src/main/java/ru/spbu/depnav/ui/component/MarkerCluster.kt index cadb4959..d78ef1b6 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/MarkerCluster.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/MarkerCluster.kt @@ -55,12 +55,16 @@ fun MarkersCluster( Marker.MarkerType.ROOM -> RoomMarkersCluster(markerIds, modifier) Marker.MarkerType.ENTRANCE -> NonRoomMarkersCluster(painterResource(R.drawable.mrk_entrance), modifier) + Marker.MarkerType.STAIRS_UP, Marker.MarkerType.STAIRS_DOWN, Marker.MarkerType.STAIRS_BOTH -> NonRoomMarkersCluster(painterResource(R.drawable.mrk_stairs), modifier) + Marker.MarkerType.ELEVATOR -> NonRoomMarkersCluster(painterResource(R.drawable.mrk_elevator), modifier) + Marker.MarkerType.WC_MAN, Marker.MarkerType.WC_WOMAN, Marker.MarkerType.WC -> NonRoomMarkersCluster(painterResource(R.drawable.mrk_wc), modifier) + Marker.MarkerType.OTHER -> NonRoomMarkersCluster(painterResource(R.drawable.mrk_other), modifier) } @@ -83,8 +87,8 @@ private fun RoomMarkersCluster(markerIds: List, modifier: Modifier = Mod ) .then(modifier), shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.onBackground, - contentColor = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.onSurface, + contentColor = MaterialTheme.colorScheme.surface ) { Box(contentAlignment = Alignment.Center) { Text( @@ -109,8 +113,8 @@ private fun NonRoomMarkersCluster(painter: Painter, modifier: Modifier = Modifie ) .then(modifier), shape = MaterialTheme.shapes.extraSmall, - color = MaterialTheme.colorScheme.onBackground, - contentColor = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.onSurface, + contentColor = MaterialTheme.colorScheme.surface ) { Box(contentAlignment = Alignment.Center) { Icon( diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/MarkerView.kt b/app/src/main/java/ru/spbu/depnav/ui/component/MarkerView.kt index a955363b..f4f124e2 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/MarkerView.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/MarkerView.kt @@ -59,46 +59,55 @@ fun MarkerView( modifier = modifier ) } + MarkerType.ENTRANCE -> MarkerIcon( painter = painterResource(R.drawable.mrk_entrance), contentDescription = stringResource(R.string.label_entrance_icon), modifier = modifier ) + MarkerType.STAIRS_UP -> MarkerIcon( painter = painterResource(R.drawable.mrk_stairs_up), contentDescription = stringResource(R.string.label_stairs_up_icon), modifier = modifier ) + MarkerType.STAIRS_DOWN -> MarkerIcon( painter = painterResource(R.drawable.mrk_stairs_down), contentDescription = stringResource(R.string.label_stairs_down_icon), modifier = modifier ) + MarkerType.STAIRS_BOTH -> MarkerIcon( painter = painterResource(R.drawable.mrk_stairs), contentDescription = stringResource(R.string.label_stairs_both_icon), modifier = modifier ) + MarkerType.ELEVATOR -> MarkerIcon( painter = painterResource(R.drawable.mrk_elevator), contentDescription = stringResource(R.string.label_elevator_icon), modifier = modifier ) + MarkerType.WC_MAN -> MarkerIcon( painter = painterResource(R.drawable.mrk_wc_man), contentDescription = stringResource(R.string.label_wc_man_icon), modifier = modifier ) + MarkerType.WC_WOMAN -> MarkerIcon( painter = painterResource(R.drawable.mrk_wc_woman), contentDescription = stringResource(R.string.label_wc_woman_icon), modifier = modifier ) + MarkerType.WC -> MarkerIcon( painter = painterResource(R.drawable.mrk_wc), contentDescription = stringResource(R.string.label_wc_icon), modifier = modifier ) + MarkerType.OTHER -> MarkerIcon( painter = painterResource(R.drawable.mrk_other), contentDescription = stringResource(R.string.label_other_icon), @@ -124,7 +133,7 @@ private fun RoomName(name: String, modifier: Modifier) { modifier = modifier, maxLines = 1, style = LocalTextStyle.current.copy( - shadow = Shadow(color = MaterialTheme.colorScheme.background, blurRadius = 6f) + shadow = Shadow(color = MaterialTheme.colorScheme.surface, blurRadius = 6f) ) ) } diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt b/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt index 833b7b93..a4c224ad 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt @@ -57,7 +57,7 @@ import ru.spbu.depnav.utils.map.rotation import ru.spbu.depnav.utils.map.top /** - * When the pin is outside of the map area visible on the screen, shows a pointer towards the pin. + * When the pin is outside the map area visible on the screen, shows a pointer towards the pin. * * It is intended to be placed exactly over the map's composable. */ @@ -87,7 +87,7 @@ fun PinPointer(mapState: MapState, pin: Marker?) { ) { calculatePointerPose(visibleArea, pinPoint) } else { - null // There is no pin or it is visible on the screen + null // There is no pin, or it is visible on the screen } // Have to remember the latest non-null pointer pose to continue showing it while the exit @@ -127,18 +127,21 @@ private data class PinPointerPose( .toInt() .coerceIn(0, boxSize.height - pinSize) ) + Side.RIGHT -> IntOffset( x = (boxSize.width - pinSize).coerceAtLeast(0), y = (boxSize.height * sideFraction - pinSize / 2f) .toInt() .coerceIn(0, boxSize.height - pinSize) ) + Side.TOP -> IntOffset( x = (boxSize.width * sideFraction - pinSize / 2f) .toInt() .coerceIn(0, boxSize.width - pinSize), y = 0 ) + Side.BOTTOM -> IntOffset( x = (boxSize.width * sideFraction - pinSize / 2f) .toInt() diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/SearchResults.kt b/app/src/main/java/ru/spbu/depnav/ui/component/SearchResults.kt index 3d4a0529..bf6975fc 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/SearchResults.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/SearchResults.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.PaddingValues 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.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -75,7 +76,7 @@ fun SearchResultsView( LazyColumn( modifier = modifier, state = state, - contentPadding = PaddingValues(top = DEFAULT_PADDING / 2), + contentPadding = PaddingValues(vertical = DEFAULT_PADDING / 2), verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(results.items.asReversed()) { (marker, markerText) -> @@ -86,16 +87,16 @@ fun SearchResultsView( markerText = markerText, onClick = onResultClick, trailingIcon = ( - @Composable { - Icon( - painter = painterResource(R.drawable.ic_searched_for), - contentDescription = null, - modifier = Modifier - .size(26.dp) - .alpha(DISABLED_ALPHA) - ) - } - ).takeIf { results.isHistory } + @Composable { + Icon( + painter = painterResource(R.drawable.ic_searched_for), + contentDescription = null, + modifier = Modifier + .size(26.dp) + .alpha(DISABLED_ALPHA) + ) + } + ).takeIf { results.isHistory } ) } } @@ -119,6 +120,7 @@ private fun SearchResultView( Row( modifier = Modifier .clickable { onClick(marker.id) } + .padding(horizontal = DEFAULT_PADDING * 1.5f) .fillMaxWidth() .height(LocalViewConfiguration.current.minimumTouchTargetSize.height), horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/ru/spbu/depnav/ui/dialog/SettingsDialog.kt b/app/src/main/java/ru/spbu/depnav/ui/dialog/SettingsDialog.kt index fb3818ea..3404457c 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/dialog/SettingsDialog.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/dialog/SettingsDialog.kt @@ -21,13 +21,16 @@ package ru.spbu.depnav.ui.dialog import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize 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.lazy.LazyColumn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton @@ -63,47 +66,45 @@ fun SettingsDialog(prefs: PreferencesManager, onDismiss: () -> Unit) { }, title = { Text(stringResource(R.string.settings)) }, text = { - LazyColumn( - modifier = Modifier.padding( - horizontal = HORIZONTAL_PADDING, - vertical = HORIZONTAL_PADDING / 2 - ), + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding( + horizontal = HORIZONTAL_PADDING, + vertical = HORIZONTAL_PADDING / 2 + ), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) ) { - item { GroupTitle(stringResource(R.string.theme)) } + GroupTitle(stringResource(R.string.theme)) - item { - val themeMode by prefs.themeModeFlow.collectAsStateWithLifecycle() - - RadioOption( - options = listOf( - R.string.light_theme, - R.string.dark_theme, - R.string.system_theme - ).map { id -> StringWithId(stringResource(id), id) }, - selected = themeMode.titleId.let { id -> - StringWithId(stringResource(id), id) - }, - onSelected = { (_, id) -> - val selectedMode = ThemeMode.fromTitleId(id) - checkNotNull(selectedMode) { "Unknown theme mode selected (id $id)" } - prefs.updateThemeMode(selectedMode) - } - ) - } + val themeMode by prefs.themeModeFlow.collectAsStateWithLifecycle() + RadioOption( + options = listOf( + R.string.light_theme, + R.string.dark_theme, + R.string.system_theme + ).map { id -> StringWithId(stringResource(id), id) }, + selected = themeMode.titleId.let { id -> + StringWithId(stringResource(id), id) + }, + onSelected = { (_, id) -> + val selectedMode = ThemeMode.fromTitleId(id) + checkNotNull(selectedMode) { "Unknown theme mode selected (id $id)" } + prefs.updateThemeMode(selectedMode) + } + ) - item { GroupTitle(stringResource(R.string.map)) } + GroupTitle(stringResource(R.string.map)) - item { - val enableRotation by prefs.enableRotationFlow.collectAsStateWithLifecycle() - SwitchOption( - title = stringResource(R.string.rotation_gesture), - checked = enableRotation, - onChecked = prefs::updateEnableRotation - ) - } + val enableRotation by prefs.enableRotationFlow.collectAsStateWithLifecycle() + SwitchOption( + title = stringResource(R.string.rotation_gesture), + checked = enableRotation, + onChecked = prefs::updateEnableRotation + ) } - } + }, + modifier = Modifier.width(IntrinsicSize.Max) ) } diff --git a/app/src/main/java/ru/spbu/depnav/ui/screen/MapScreen.kt b/app/src/main/java/ru/spbu/depnav/ui/screen/MapScreen.kt index 0c17a1d5..b57278ab 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/screen/MapScreen.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/screen/MapScreen.kt @@ -20,7 +20,6 @@ package ru.spbu.depnav.ui.screen import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally @@ -41,14 +40,14 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.DrawerValue -import androidx.compose.material3.LocalAbsoluteTonalElevation +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Surface import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -84,11 +83,10 @@ import ru.spbu.depnav.ui.component.ZoomInHint import ru.spbu.depnav.ui.dialog.MapLegendDialog import ru.spbu.depnav.ui.dialog.SettingsDialog import ru.spbu.depnav.ui.theme.DEFAULT_PADDING -import ru.spbu.depnav.ui.theme.ON_MAP_SURFACE_ALPHA +import ru.spbu.depnav.ui.theme.MAP_OVERLAY_ALPHA import ru.spbu.depnav.ui.viewmodel.MapUiState import ru.spbu.depnav.ui.viewmodel.MapViewModel import ru.spbu.depnav.ui.viewmodel.SearchResults -import ru.spbu.depnav.ui.viewmodel.SearchUiState import ru.spbu.depnav.ui.viewmodel.SearchViewModel /** Screen containing a navigable map. */ @@ -108,7 +106,7 @@ fun MapScreen( MapLegendDialog(onDismiss = { openMapLegend = false }) } - Surface(color = MaterialTheme.colorScheme.background) { + Surface { val selectedMapId by prefs.selectedMapIdFlow.collectAsStateWithLifecycle() val mapUiState by mapVm.uiState.collectAsStateWithLifecycle() @@ -135,7 +133,7 @@ fun MapScreen( gesturesEnabled = selectedMapId != null ) { val readyMapUiState = mapUiState as? MapUiState.Ready ?: return@ModalNavigationDrawer - val searchUiState by searchVm.uiState.collectAsStateWithLifecycle() + val searchResults by searchVm.resultsFlow.collectAsStateWithLifecycle() val markerAlpha by mapVm.markerAlpha.collectAsStateWithLifecycle() val markersVisible by remember { derivedStateOf { markerAlpha > 0f } } @@ -147,8 +145,8 @@ fun MapScreen( OnMapUi( mapUiState = readyMapUiState, - searchUiState = searchUiState, - onSearchQueryChange = searchVm::search, + searchQuery = searchVm.queryState, + searchResults = searchResults, onSearchResultClick = { markerId -> mapVm.focusOnMarker(markerId) searchVm.addToSearchHistory(markerId) @@ -171,60 +169,55 @@ fun MapScreen( @Suppress("LongParameterList") // Considered OK for a composable private fun OnMapUi( mapUiState: MapUiState.Ready, - searchUiState: SearchUiState, - onSearchQueryChange: (String) -> Unit, + searchQuery: TextFieldState, + searchResults: SearchResults, onSearchResultClick: (Int) -> Unit, onMainMenuClick: () -> Unit, onFloorSwitch: (Int) -> Unit, markersVisible: Boolean ) { - CompositionLocalProvider(LocalAbsoluteTonalElevation provides 4.dp) { - Box(modifier = Modifier.fillMaxSize()) { - PinPointer(mapUiState.mapState, mapUiState.pinnedMarker?.marker) + Box(modifier = Modifier.fillMaxSize()) { + PinPointer(mapUiState.mapState, mapUiState.pinnedMarker?.marker) - AnimatedSearchBar( - visible = mapUiState.showOnMapUi, - mapTitle = mapUiState.mapTitle, - query = searchUiState.query, - onQueryChange = onSearchQueryChange, - searchResults = searchUiState.results, - onResultClick = onSearchResultClick, - onMenuClick = onMainMenuClick - ) + AnimatedSearchBar( + visible = mapUiState.showOnMapUi, + mapTitle = mapUiState.mapTitle, + query = searchQuery, + searchResults = searchResults, + onResultClick = onSearchResultClick, + onMenuClick = onMainMenuClick + ) - AnimatedFloorSwitch( - visible = mapUiState.showOnMapUi, - currentFloor = mapUiState.currentFloor, - maxFloor = mapUiState.floorsNum, - onFloorSwitch = onFloorSwitch - ) + AnimatedFloorSwitch( + visible = mapUiState.showOnMapUi, + currentFloor = mapUiState.currentFloor, + maxFloor = mapUiState.floorsNum, + onFloorSwitch = onFloorSwitch + ) - AnimatedBottom( - pinnedMarker = mapUiState.pinnedMarker, - showZoomInHint = !markersVisible - ) - } + AnimatedBottom( + pinnedMarker = mapUiState.pinnedMarker, + showZoomInHint = !markersVisible + ) } } @Composable @Suppress("LongParameterList") // Considered OK for a composable +@OptIn(ExperimentalMaterial3Api::class) private fun BoxScope.AnimatedSearchBar( visible: Boolean, mapTitle: String, - query: String, - onQueryChange: (String) -> Unit, + query: TextFieldState, searchResults: SearchResults, onResultClick: (Int) -> Unit, onMenuClick: () -> Unit ) { var searchBarExpanded by rememberSaveable { mutableStateOf(false) } - if (!visible) { - searchBarExpanded = false - } - - if (!searchBarExpanded && query.isNotEmpty()) { - onQueryChange("") + LaunchedEffect(visible) { + if (!visible) { + searchBarExpanded = false + } } AnimatedVisibility( @@ -235,21 +228,14 @@ private fun BoxScope.AnimatedSearchBar( enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(), exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut() ) { - val horizontalPadding by animateDpAsState( - if (searchBarExpanded) 0.dp else DEFAULT_PADDING, - label = "Map search bar horizontal padding" - ) - MapSearchBar( - query = query, - onQueryChange = onQueryChange, + queryState = query, mapTitle = mapTitle, expanded = searchBarExpanded, onExpandedChange = { searchBarExpanded = it }, results = searchResults, onResultClick = onResultClick, - onMenuClick = onMenuClick, - modifier = Modifier.padding(horizontal = horizontalPadding) + onMenuClick = onMenuClick ) } } @@ -303,7 +289,7 @@ private fun BoxScope.AnimatedBottom(pinnedMarker: MarkerWithText?, showZoomInHin bottomStart = CornerSize(0), bottomEnd = CornerSize(0) ), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = ON_MAP_SURFACE_ALPHA) + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = MAP_OVERLAY_ALPHA) ) { // Have to remember the latest pinned marker to continue showing it while the exit // animation is still in progress diff --git a/app/src/main/java/ru/spbu/depnav/ui/theme/Color.kt b/app/src/main/java/ru/spbu/depnav/ui/theme/Color.kt index 70ddb521..4ac247b5 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/theme/Color.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/theme/Color.kt @@ -22,64 +22,75 @@ package ru.spbu.depnav.ui.theme import androidx.compose.ui.graphics.Color -// Generated with Material Theme Builder (seed 0xFFB0BEC5) +// Generated with Material Theme Builder (ic_launcher.png as the seed + "Color match") +val primaryLight = Color(0xFF536066) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFAFBDC3) +val onPrimaryContainerLight = Color(0xFF3F4D52) +val secondaryLight = Color(0xFF5A5F61) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFDCE0E3) +val onSecondaryContainerLight = Color(0xFF5E6366) +val tertiaryLight = Color(0xFF625C6A) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFC0B7C7) +val onTertiaryContainerLight = Color(0xFF4E4855) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFFBF9F9) +val onBackgroundLight = Color(0xFF1B1C1C) +val surfaceLight = Color(0xFFFBF9F9) +val onSurfaceLight = Color(0xFF1B1C1C) +val surfaceVariantLight = Color(0xFFDFE3E5) +val onSurfaceVariantLight = Color(0xFF43474A) +val outlineLight = Color(0xFF73787A) +val outlineVariantLight = Color(0xFFC3C7C9) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF303031) +val inverseOnSurfaceLight = Color(0xFFF2F0F0) +val inversePrimaryLight = Color(0xFFBBC9CF) +val surfaceDimLight = Color(0xFFDBD9D9) +val surfaceBrightLight = Color(0xFFFBF9F9) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF5F3F3) +val surfaceContainerLight = Color(0xFFF0EDED) +val surfaceContainerHighLight = Color(0xFFEAE8E7) +val surfaceContainerHighestLight = Color(0xFFE4E2E2) -val md_theme_light_primary = Color(0xFF006781) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFFB9EAFF) -val md_theme_light_onPrimaryContainer = Color(0xFF001F29) -val md_theme_light_secondary = Color(0xFF4C626B) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFCFE6F1) -val md_theme_light_onSecondaryContainer = Color(0xFF071E26) -val md_theme_light_tertiary = Color(0xFF5B5B7E) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFE1DFFF) -val md_theme_light_onTertiaryContainer = Color(0xFF181837) -val md_theme_light_error = Color(0xFFBA1A1A) -val md_theme_light_errorContainer = Color(0xFFFFDAD6) -val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_onErrorContainer = Color(0xFF410002) -val md_theme_light_background = Color(0xFFFBFCFE) -val md_theme_light_onBackground = Color(0xFF191C1D) -val md_theme_light_surface = Color(0xFFFBFCFE) -val md_theme_light_onSurface = Color(0xFF191C1D) -val md_theme_light_surfaceVariant = Color(0xFFDCE4E8) -val md_theme_light_onSurfaceVariant = Color(0xFF40484C) -val md_theme_light_outline = Color(0xFF70787C) -val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2) -val md_theme_light_inverseSurface = Color(0xFF2E3132) -val md_theme_light_inversePrimary = Color(0xFF5FD4FD) -val md_theme_light_surfaceTint = Color(0xFF006781) -val md_theme_light_outlineVariant = Color(0xFFC0C8CC) -val md_theme_light_scrim = Color(0xFF000000) - -val md_theme_dark_primary = Color(0xFF5FD4FD) -val md_theme_dark_onPrimary = Color(0xFF003544) -val md_theme_dark_primaryContainer = Color(0xFF004D62) -val md_theme_dark_onPrimaryContainer = Color(0xFFB9EAFF) -val md_theme_dark_secondary = Color(0xFFB3CAD5) -val md_theme_dark_onSecondary = Color(0xFF1E333C) -val md_theme_dark_secondaryContainer = Color(0xFF354A53) -val md_theme_dark_onSecondaryContainer = Color(0xFFCFE6F1) -val md_theme_dark_tertiary = Color(0xFFC4C3EB) -val md_theme_dark_onTertiary = Color(0xFF2D2D4D) -val md_theme_dark_tertiaryContainer = Color(0xFF434465) -val md_theme_dark_onTertiaryContainer = Color(0xFFE1DFFF) -val md_theme_dark_error = Color(0xFFFFB4AB) -val md_theme_dark_errorContainer = Color(0xFF93000A) -val md_theme_dark_onError = Color(0xFF690005) -val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -val md_theme_dark_background = Color(0xFF191C1D) -val md_theme_dark_onBackground = Color(0xFFE1E3E4) -val md_theme_dark_surface = Color(0xFF191C1D) -val md_theme_dark_onSurface = Color(0xFFE1E3E4) -val md_theme_dark_surfaceVariant = Color(0xFF40484C) -val md_theme_dark_onSurfaceVariant = Color(0xFFC0C8CC) -val md_theme_dark_outline = Color(0xFF8A9296) -val md_theme_dark_inverseOnSurface = Color(0xFF191C1D) -val md_theme_dark_inverseSurface = Color(0xFFE1E3E4) -val md_theme_dark_inversePrimary = Color(0xFF006781) -val md_theme_dark_surfaceTint = Color(0xFF5FD4FD) -val md_theme_dark_outlineVariant = Color(0xFF40484C) -val md_theme_dark_scrim = Color(0xFF000000) +val primaryDark = Color(0xFFCBD9DF) +val onPrimaryDark = Color(0xFF253237) +val primaryContainerDark = Color(0xFFAFBDC3) +val onPrimaryContainerDark = Color(0xFF3F4D52) +val secondaryDark = Color(0xFFC3C7CA) +val onSecondaryDark = Color(0xFF2C3133) +val secondaryContainerDark = Color(0xFF454A4C) +val onSecondaryContainerDark = Color(0xFFB4B9BB) +val tertiaryDark = Color(0xFFDCD3E3) +val onTertiaryDark = Color(0xFF342E3B) +val tertiaryContainerDark = Color(0xFFC0B7C7) +val onTertiaryContainerDark = Color(0xFF4E4855) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF131314) +val onBackgroundDark = Color(0xFFE4E2E2) +val surfaceDark = Color(0xFF131314) +val onSurfaceDark = Color(0xFFE4E2E2) +val surfaceVariantDark = Color(0xFF43474A) +val onSurfaceVariantDark = Color(0xFFC3C7C9) +val outlineDark = Color(0xFF8D9194) +val outlineVariantDark = Color(0xFF43474A) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE4E2E2) +val inverseOnSurfaceDark = Color(0xFF303031) +val inversePrimaryDark = Color(0xFF536066) +val surfaceDimDark = Color(0xFF131314) +val surfaceBrightDark = Color(0xFF393939) +val surfaceContainerLowestDark = Color(0xFF0E0E0F) +val surfaceContainerLowDark = Color(0xFF1B1C1C) +val surfaceContainerDark = Color(0xFF1F2020) +val surfaceContainerHighDark = Color(0xFF2A2A2A) +val surfaceContainerHighestDark = Color(0xFF353535) diff --git a/app/src/main/java/ru/spbu/depnav/ui/theme/Theme.kt b/app/src/main/java/ru/spbu/depnav/ui/theme/Theme.kt index d95ec879..a1d03bdb 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/theme/Theme.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/theme/Theme.kt @@ -29,68 +29,80 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -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, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) -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, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) /** Padding applied where elements need to be spaced out by default. */ @@ -100,7 +112,7 @@ val DEFAULT_PADDING = 16.dp const val DISABLED_ALPHA = 0.38f /** Alpha value applied to surfaces comprising on-map UI. */ -const val ON_MAP_SURFACE_ALPHA = 0.9f +const val MAP_OVERLAY_ALPHA = 0.9f /** Theme of the application. */ @Composable @@ -114,8 +126,9 @@ fun DepNavTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme + + darkTheme -> darkScheme + else -> lightScheme } MaterialTheme( colorScheme = colorScheme, diff --git a/app/src/main/java/ru/spbu/depnav/ui/viewmodel/MapViewModel.kt b/app/src/main/java/ru/spbu/depnav/ui/viewmodel/MapViewModel.kt index 0d27059b..440eed38 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/viewmodel/MapViewModel.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/viewmodel/MapViewModel.kt @@ -120,7 +120,14 @@ class MapViewModel @Inject constructor( val floor: Floor ) { constructor(mapInfo: MapInfo, mapState: MapState, title: String, floor: Floor) : - this(mapInfo.id, mapInfo.internalName, mapState, title, mapInfo.floorsNum, floor) + this( + mapInfo.id, + mapInfo.internalName, + mapState, + title, + mapInfo.floorsNum, + floor + ) } fun toMapUiState() = if (displayedMap == null) { diff --git a/app/src/main/java/ru/spbu/depnav/ui/viewmodel/SearchViewModel.kt b/app/src/main/java/ru/spbu/depnav/ui/viewmodel/SearchViewModel.kt index b356e302..5f23b617 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/viewmodel/SearchViewModel.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/viewmodel/SearchViewModel.kt @@ -19,6 +19,8 @@ package ru.spbu.depnav.ui.viewmodel import android.util.Log +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.text.intl.Locale import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -27,15 +29,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.spbu.depnav.data.composite.MarkerWithText @@ -48,8 +45,6 @@ import ru.spbu.depnav.data.repository.SearchHistoryRepo import javax.inject.Inject private const val TAG = "SearchViewModel" - -private const val MIN_QUERY_PERIOD_MS = 300L private const val SEARCH_HISTORY_SIZE = 10 /** View model for map markers search on [MapScreen][ru.spbu.depnav.ui.screen.MapScreen]. */ @@ -60,61 +55,41 @@ class SearchViewModel @Inject constructor( private val markerRepo: MarkerWithTextRepo, prefs: PreferencesManager ) : ViewModel() { - // Updated only on the main thread - private val state = MutableStateFlow(PrivateState()) - - /** UI-visible state. */ - val uiState = state.map { it.toSearchUiState() }.stateIn( - viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = state.value.toSearchUiState() - ) + private val languageState = MutableStateFlow(Locale.current.toLanguage()) + private val _resultsFlow = MutableStateFlow(SearchResults(emptyList(), true)) - private data class PrivateState( - val mapId: Int? = null, - val query: String = "", - val language: Language = Locale.current.toLanguage(), - val results: SearchResults = SearchResults(emptyList(), true) - ) { - fun toSearchUiState() = SearchUiState(query, results) - } + val queryState = TextFieldState() + val resultsFlow = _resultsFlow.asStateFlow() init { - state - .debounce(MIN_QUERY_PERIOD_MS) - .distinctUntilChangedBy { Triple(it.mapId, it.query, it.language) } - .mapLatest { (mapId, query, language) -> - val results = withContext(Dispatchers.IO) { - if (mapId == null) { - SearchResults(emptyList(), true) - } else if (query.isNotEmpty()) { - SearchResults( - items = markerRepo.loadByQuery(mapId, query, language), - isHistory = false - ) - } else { - SearchResults( - items = searchHistoryRepo.loadByMap(mapId).map { - markerRepo.loadById(it.markerId, language) - }, - isHistory = true - ) - } - } - Log.d(TAG, "Query '$query', map $mapId, $language: ${results.items.size} items") - state.update { it.copy(results = results) } - } - .launchIn(viewModelScope) - - prefs.selectedMapIdFlow - .onEach { mapId -> state.update { it.copy(mapId = mapId, query = "") } } + combine( + prefs.selectedMapIdFlow.filterNotNull(), + snapshotFlow { queryState.text }, + languageState + ) { mapId, query, language -> + _resultsFlow.value = search(mapId, query.toString(), language) + } .launchIn(viewModelScope) } - /** Updates the search query and asynchronously runs marker search with it. */ - fun search(query: String) { - state.update { it.copy(query = query) } - } + private suspend fun search(mapId: Int, query: String, language: Language): SearchResults = + withContext(Dispatchers.IO) { + if (query.isNotEmpty()) { + SearchResults( + items = markerRepo.loadByQuery(mapId, query, language), + isHistory = false + ) + } else { + SearchResults( + items = searchHistoryRepo.loadByMap(mapId).map { + markerRepo.loadById(it.markerId, language) + }, + isHistory = true + ) + } + }.apply { + Log.d(TAG, "Query '$query', map $mapId, $language: ${items.size} items") + } /** Asynchronously adds the provided marker ID to the search history. */ fun addToSearchHistory(markerId: Int) { @@ -129,11 +104,7 @@ class SearchViewModel @Inject constructor( * and thus are not updated automatically by Android. */ fun onLocaleChange(locale: Locale) { - val language = locale.toLanguage() - if (state.value.language != language) { - Log.i(TAG, "Updating language to $language") - state.update { it.copy(language = language) } - } + languageState.value = locale.toLanguage() } } @@ -144,16 +115,3 @@ data class SearchResults( /** Whether these are the search history entries or the actual search results. */ val isHistory: Boolean ) - -/** Map markers search UI state. */ -data class SearchUiState( - /** Marker search query entered by the user. */ - val query: String = "", - /** - * Either the actual results of the query or a history of previous searches. - * - * Note that these results mey correspond not to the current query, but to some query in the - * past while the current query is being processed. - */ - val results: SearchResults = SearchResults(emptyList(), true) -) diff --git a/app/src/main/java/ru/spbu/depnav/utils/map/Clustering.kt b/app/src/main/java/ru/spbu/depnav/utils/map/Clustering.kt index 8269ca3f..ac252a46 100644 --- a/app/src/main/java/ru/spbu/depnav/utils/map/Clustering.kt +++ b/app/src/main/java/ru/spbu/depnav/utils/map/Clustering.kt @@ -89,6 +89,7 @@ fun getClustererId(markerType: Marker.MarkerType) = when (markerType) { Marker.MarkerType.ENTRANCE -> ENTRANCE_CLUSTERER_ID Marker.MarkerType.STAIRS_UP, Marker.MarkerType.STAIRS_DOWN, Marker.MarkerType.STAIRS_BOTH -> STAIRS_CLUSTERER_ID + Marker.MarkerType.ELEVATOR -> ELEVATOR_CLUSTERER_ID Marker.MarkerType.WC_MAN, Marker.MarkerType.WC_WOMAN, Marker.MarkerType.WC -> WC_CLUSTERER_ID Marker.MarkerType.OTHER -> OTHER_CLUSTERER_ID diff --git a/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt b/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt index ec0dd1eb..001da071 100644 --- a/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt +++ b/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt @@ -49,13 +49,13 @@ data class LineSegment(val p1: Point, val p2: Point) { val numerator1 = (p1.x - l.p1.x) * (l.p1.y - l.p2.y) - (p1.y - l.p1.y) * (l.p1.x - l.p2.x) val t1 = numerator1 / denominator - if (t1 < 0 || t1 > 1) { + if (t1 !in 0.0..1.0) { return null } val numerator2 = (p1.x - p2.x) * (p1.y - l.p1.y) - (p1.y - p2.y) * (p1.x - l.p1.x) val t2 = -(numerator2 / denominator) - if (t2 < 0 || t2 > 1) { + if (t2 !in 0.0..1.0) { return null } diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 015f3f04..00000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..814f759c --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..0740ec7f --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..ec28677b --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml new file mode 100644 index 00000000..5ba60b8f --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_background.xml rename to app/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..4664fb83 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml index 41665a18..709a01a2 100644 --- a/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,28 +1,21 @@ - - - - - - + android:viewportWidth="108" + android:viewportHeight="108"> + + + + diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 00000000..09b6854b --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..f1ab6fdb --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_splashscreen.xml b/app/src/main/res/drawable/ic_splashscreen.xml new file mode 100644 index 00000000..b4b8b5e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_splashscreen.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index b070c763..8fde4563 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index b070c763..8fde4563 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/raw/ru_spbu_depnav_keep.xml b/app/src/main/res/raw/ru_spbu_depnav_keep.xml new file mode 100644 index 00000000..66d7a124 --- /dev/null +++ b/app/src/main/res/raw/ru_spbu_depnav_keep.xml @@ -0,0 +1,3 @@ + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8310b126..46c87099 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -52,4 +52,4 @@ Открыть настройки Вернуться назад Очистить поле ввода - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b214b3ab..02518385 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,4 +56,4 @@ Open settings Navigate back Clear text field - \ No newline at end of file + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 61f3b0c8..54d298b1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,10 @@ - + +