diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/MockStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/MockStorage.kt deleted file mode 100644 index ee54b41..0000000 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/MockStorage.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.nullptroma.wallenc.data - -import com.github.nullptroma.wallenc.domain.models.IStorage -import com.github.nullptroma.wallenc.domain.models.IStorageAccessor -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import java.util.UUID - -class MockStorage( - override val size: StateFlow = MutableStateFlow(null), - override val numberOfFiles: StateFlow = MutableStateFlow(null), - override val uuid: UUID, - override val name: StateFlow = MutableStateFlow(""), - override val totalSpace: StateFlow = MutableStateFlow(null), - override val availableSpace: StateFlow = MutableStateFlow(null), - override val isAvailable: StateFlow = MutableStateFlow(false), - override val accessor: IStorageAccessor -) : IStorage { - - override suspend fun rename(newName: String) { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/IStorageCallbackHandler.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/IStorageCallbackHandler.kt new file mode 100644 index 0000000..578e033 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/IStorageCallbackHandler.kt @@ -0,0 +1,7 @@ +package com.github.nullptroma.wallenc.data.vaults + +interface IStorageCallbackHandler { + fun changeSize(delta: Int) + fun changeNumOfFiles(delta: Int) + +} \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt new file mode 100644 index 0000000..d18a5f1 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorage.kt @@ -0,0 +1,28 @@ +package com.github.nullptroma.wallenc.data.vaults.local + +import com.github.nullptroma.wallenc.domain.models.IStorage +import com.github.nullptroma.wallenc.domain.models.IStorageAccessor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.StateFlow +import java.util.UUID + + +class LocalStorage( + override val uuid: UUID, + absolutePath: String, + ioDispatcher: CoroutineDispatcher +) : IStorage { + override val size: StateFlow + get() = accessor.size + override val numberOfFiles: StateFlow + get() = accessor.numberOfFiles + override val name: StateFlow + get() = TODO("Добавить класс в Domain, который с помощью accessor будет читать и сохранять имя в скрытую папку") + override val isAvailable: StateFlow + get() = accessor.isAvailable + override val accessor: IStorageAccessor = LocalStorageAccessor(absolutePath, ioDispatcher) + + override suspend fun rename(newName: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt index 7199324..e1fb983 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalStorageAccessor.kt @@ -4,30 +4,92 @@ import com.github.nullptroma.wallenc.domain.datatypes.DataPackage import com.github.nullptroma.wallenc.domain.models.IDirectory import com.github.nullptroma.wallenc.domain.models.IFile import com.github.nullptroma.wallenc.domain.models.IStorageAccessor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File import java.io.InputStream import java.io.OutputStream -import java.net.URI +import kotlin.io.path.Path +import kotlin.io.path.fileSize -class LocalStorageAccessor : IStorageAccessor { +class LocalStorageAccessor( + private val absolutePath: String, + private val ioDispatcher: CoroutineDispatcher +) : IStorageAccessor { + private val _size = MutableStateFlow(null) + private val _numberOfFiles = MutableStateFlow(null) + private val _isAvailable = MutableStateFlow(false) + + override val size: StateFlow + get() = _size + override val numberOfFiles: StateFlow + get() = _numberOfFiles override val isAvailable: StateFlow - get() = TODO("Not yet implemented") + get() = _isAvailable override val filesUpdates: SharedFlow> get() = TODO("Not yet implemented") override val dirsUpdates: SharedFlow> get() = TODO("Not yet implemented") + init { + CoroutineScope(ioDispatcher).launch { + scanStorage() + } + } + + private fun checkAvailable(): Boolean { + _isAvailable.value = File(absolutePath).exists() + return _isAvailable.value + } + + private fun forAllFiles(dir: File, callback: (File) -> Unit) { + if (dir.exists() == false) + return + callback(dir) + + val nextDirs = dir.listFiles() + if (nextDirs != null) { + for (nextDir in nextDirs) { + forAllFiles(nextDir, callback) + } + } + } + + private suspend fun scanStorage() = withContext(ioDispatcher) { + _isAvailable.value = File(absolutePath).exists() + + var size = 0L + var numOfFiles = 0 + + forAllFiles(File(absolutePath)) { + if (it.isFile) { + numOfFiles++ + size += Path(it.path).fileSize() + } + } + _size.value = size + _numberOfFiles.value = numOfFiles + } + override suspend fun getAllFiles(): List { + if(checkAvailable() == false) + return listOf() + + val list = mutableListOf() + + } + + override suspend fun getFiles(path: String): List { TODO("Not yet implemented") } - override suspend fun getFiles(path: URI): List { - TODO("Not yet implemented") - } - - override fun getFilesFlow(path: URI): Flow> { + override fun getFilesFlow(path: String): Flow> { TODO("Not yet implemented") } @@ -35,43 +97,43 @@ class LocalStorageAccessor : IStorageAccessor { TODO("Not yet implemented") } - override suspend fun getDirs(path: URI): List { + override suspend fun getDirs(path: String): List { TODO("Not yet implemented") } - override fun getDirsFlow(path: URI): Flow> { + override fun getDirsFlow(path: String): Flow> { TODO("Not yet implemented") } - override suspend fun touchFile(path: URI) { + override suspend fun touchFile(path: String) { TODO("Not yet implemented") } - override suspend fun touchDir(path: URI) { + override suspend fun touchDir(path: String) { TODO("Not yet implemented") } - override suspend fun delete(path: URI) { + override suspend fun delete(path: String) { TODO("Not yet implemented") } - override suspend fun getFileInfo(path: URI) { + override suspend fun getFileInfo(path: String) { TODO("Not yet implemented") } - override suspend fun getDirInfo(path: URI) { + override suspend fun getDirInfo(path: String) { TODO("Not yet implemented") } - override suspend fun openWrite(path: URI): InputStream { + override suspend fun openWrite(path: String): InputStream { TODO("Not yet implemented") } - override suspend fun openRead(path: URI): OutputStream { + override suspend fun openRead(path: String): OutputStream { TODO("Not yet implemented") } - override suspend fun moveToTrash(path: URI) { + override suspend fun moveToTrash(path: String) { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt index 98b9c85..9be1cc6 100644 --- a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/LocalVault.kt @@ -1,7 +1,6 @@ package com.github.nullptroma.wallenc.data.vaults.local import android.content.Context -import com.github.nullptroma.wallenc.data.MockStorage import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import com.github.nullptroma.wallenc.domain.enums.VaultType import com.github.nullptroma.wallenc.domain.models.IStorage @@ -12,42 +11,64 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File import java.util.UUID import kotlin.io.path.Path import kotlin.io.path.createDirectory +import kotlin.io.path.pathString class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context) : IVault { - private val path = context.getExternalFilesDir("LocalVault") + private val _path = MutableStateFlow(null) private val _storages = MutableStateFlow(listOf()) + private val _totalSpace = MutableStateFlow(null) + private val _availableSpace = MutableStateFlow(null) + private val _isAvailable = MutableStateFlow(false) + init { CoroutineScope(ioDispatcher).launch { - if(path == null) - return@launch - - val dirs = path.listFiles()?.filter { it.isDirectory } - if(dirs != null) - _storages.value = dirs.map { - MockStorage(uuid = UUID.fromString(it.name), accessor = LocalStorageAccessor()) - } - val next = Path(path.path, UUID.randomUUID().toString()) - next.createDirectory() + _path.value = context.getExternalFilesDir("LocalVault") + _isAvailable.value = _path.value != null + readStorages() } } - override suspend fun createStorage(name: String): IStorage = withContext(ioDispatcher) { - TODO("Not yet implemented") + private fun readStorages() { + val path = _path.value + if (path == null || _isAvailable.value == false) + return + + val dirs = path.listFiles()?.filter { it.isDirectory } + if (dirs != null) { + _storages.value = dirs.map { + val uuid = UUID.fromString(it.name) + LocalStorage(uuid, it.path, ioDispatcher) + } + } + } + + override suspend fun createStorage(): IStorage = withContext(ioDispatcher) { + val path = _path.value + if (path == null || _isAvailable.value == false) + throw Exception("Not available") + + val uuid = UUID.randomUUID() + val next = Path(path.path, uuid.toString()) + next.createDirectory() + val newStorage = LocalStorage(uuid, next.pathString, ioDispatcher) + _storages.value = _storages.value.toMutableList().apply { + add(newStorage) + } + return@withContext newStorage } override suspend fun createStorage( - name: String, key: EncryptKey ): IStorage = withContext(ioDispatcher) { TODO("Not yet implemented") } override suspend fun createStorage( - name: String, key: EncryptKey, uuid: UUID ): IStorage = withContext(ioDispatcher) { @@ -64,6 +85,9 @@ class LocalVault(private val ioDispatcher: CoroutineDispatcher, context: Context override val storages: StateFlow> get() = _storages override val isAvailable: StateFlow - get() = TODO("Not yet implemented") - + get() = _isAvailable + override val totalSpace: StateFlow + get() = _totalSpace + override val availableSpace: StateFlow + get() = _availableSpace } \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt new file mode 100644 index 0000000..9ed3f46 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalDirectory.kt @@ -0,0 +1,8 @@ +package com.github.nullptroma.wallenc.data.vaults.local.entity + +import com.github.nullptroma.wallenc.domain.models.IDirectory + +class LocalDirectory( + override val metaInfo: LocalMetaInfo, + override val elementsCount: Int +) : IDirectory \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt new file mode 100644 index 0000000..ccbead2 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalFile.kt @@ -0,0 +1,5 @@ +package com.github.nullptroma.wallenc.data.vaults.local.entity + +import com.github.nullptroma.wallenc.domain.models.IFile + +class LocalFile(override val metaInfo: LocalMetaInfo) : IFile \ No newline at end of file diff --git a/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt new file mode 100644 index 0000000..ead8162 --- /dev/null +++ b/data/src/main/java/com/github/nullptroma/wallenc/data/vaults/local/entity/LocalMetaInfo.kt @@ -0,0 +1,23 @@ +package com.github.nullptroma.wallenc.data.vaults.local.entity + +import com.github.nullptroma.wallenc.domain.models.IMetaInfo +import java.time.LocalDateTime + +class LocalMetaInfo : IMetaInfo { + init { + + } + + override val name: String + get() = TODO("Not yet implemented") + override val size: Int + get() = TODO("Not yet implemented") + override val isDeleted: Boolean + get() = TODO("Not yet implemented") + override val isHidden: Boolean + get() = TODO("Not yet implemented") + override val lastModified: LocalDateTime + get() = TODO("Not yet implemented") + override val path: String + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt index 423837b..926be06 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IMetaInfo.kt @@ -1,6 +1,5 @@ package com.github.nullptroma.wallenc.domain.models -import java.net.URI import java.time.LocalDateTime @@ -10,5 +9,5 @@ interface IMetaInfo { val isDeleted: Boolean val isHidden: Boolean val lastModified: LocalDateTime - val path: URI + val path: String } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt index d1eac37..5a92576 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorage.kt @@ -4,12 +4,10 @@ import kotlinx.coroutines.flow.StateFlow import java.util.UUID interface IStorage { - val size: StateFlow + val size: StateFlow val numberOfFiles: StateFlow val uuid: UUID val name: StateFlow - val totalSpace: StateFlow - val availableSpace: StateFlow val isAvailable: StateFlow val accessor: IStorageAccessor diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt index b62dae8..174a8ce 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageAccessor.kt @@ -6,37 +6,38 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import java.io.InputStream import java.io.OutputStream -import java.net.URI interface IStorageAccessor { + val size: StateFlow + val numberOfFiles: StateFlow val isAvailable: StateFlow val filesUpdates: SharedFlow> val dirsUpdates: SharedFlow> suspend fun getAllFiles(): List - suspend fun getFiles(path: URI): List + suspend fun getFiles(path: String): List /** * Получение списка файлов в директории * @param path Путь к директории * @return Поток файлов */ - fun getFilesFlow(path: URI): Flow> + fun getFilesFlow(path: String): Flow> suspend fun getAllDirs(): List - suspend fun getDirs(path: URI): List + suspend fun getDirs(path: String): List /** * Получение списка директорий в директории * @param path Путь к директории * @return Поток директорий */ - fun getDirsFlow(path: URI): Flow> + fun getDirsFlow(path: String): Flow> - suspend fun touchFile(path: URI) - suspend fun touchDir(path: URI) - suspend fun delete(path: URI) - suspend fun getFileInfo(path: URI) - suspend fun getDirInfo(path: URI) - suspend fun openWrite(path: URI): InputStream - suspend fun openRead(path: URI): OutputStream - suspend fun moveToTrash(path: URI) + suspend fun touchFile(path: String) + suspend fun touchDir(path: String) + suspend fun delete(path: String) + suspend fun getFileInfo(path: String) + suspend fun getDirInfo(path: String) + suspend fun openWrite(path: String): InputStream + suspend fun openRead(path: String): OutputStream + suspend fun moveToTrash(path: String) } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt index 5346e9e..9e28361 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IStorageExplorer.kt @@ -1,10 +1,9 @@ package com.github.nullptroma.wallenc.domain.models import kotlinx.coroutines.flow.StateFlow -import java.net.URL interface IStorageExplorer { - val currentPath: StateFlow + val currentPath: StateFlow // TODO // пока бесполезный интерфейс diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt index daa999a..79e7329 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVault.kt @@ -4,8 +4,8 @@ import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey import java.util.UUID interface IVault : IVaultInfo { - suspend fun createStorage(name: String): IStorage - suspend fun createStorage(name: String, key: EncryptKey): IStorage - suspend fun createStorage(name: String, key: EncryptKey, uuid: UUID): IStorage + suspend fun createStorage(): IStorage + suspend fun createStorage(key: EncryptKey): IStorage + suspend fun createStorage(key: EncryptKey, uuid: UUID): IStorage suspend fun remove(storage: IStorage) } \ No newline at end of file diff --git a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt index 944d939..a5b7105 100644 --- a/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt +++ b/domain/src/main/java/com/github/nullptroma/wallenc/domain/models/IVaultInfo.kt @@ -9,4 +9,6 @@ interface IVaultInfo { val uuid: UUID val storages: StateFlow> val isAvailable: StateFlow + val totalSpace: StateFlow + val availableSpace: StateFlow } \ No newline at end of file diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 87c75ac..ee33a63 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -47,6 +47,9 @@ android { } dependencies { + // Timber + implementation(libs.timber) + implementation(libs.navigation) implementation(libs.navigation.hilt.compose) diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt index 1dfc394..c1a8103 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultScreen.kt @@ -1,6 +1,9 @@ package com.github.nullptroma.wallenc.presentation.screens.main.screens.local.vault import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -9,16 +12,22 @@ import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle - @OptIn(ExperimentalMaterial3Api::class) @Composable fun LocalVaultScreen(modifier: Modifier = Modifier, viewModel: LocalVaultViewModel = hiltViewModel()) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Column { - for(storage in uiState.storagesList) { - Text(storage.uuid.toString()) + val uiState by viewModel.state.collectAsStateWithLifecycle() + LazyColumn(modifier = modifier) { + items(uiState.storagesList) { + Card { + Column { + Text(it.uuid.toString()) + Text("IsAvailable: ${it.isAvailable.value}") + Text("Files: ${it.numberOfFiles.value}") + Text("Size: ${it.size.value}") + } + } } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt index 61f2510..79e84eb 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/screens/main/screens/local/vault/LocalVaultViewModel.kt @@ -13,9 +13,10 @@ class LocalVaultViewModel @Inject constructor(private val getAllRawStoragesUseCa init { viewModelScope.launch { getAllRawStoragesUseCase.localStorage.storages.collect { - mutableUiState.value = mutableUiState.value.copy( + val newState = state.value.copy( storagesList = it ) + updateState(newState) } } } diff --git a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/viewmodel/ViewModelBase.kt b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/viewmodel/ViewModelBase.kt index defe203..e2c35e8 100644 --- a/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/viewmodel/ViewModelBase.kt +++ b/presentation/src/main/java/com/github/nullptroma/wallenc/presentation/viewmodel/ViewModelBase.kt @@ -4,11 +4,19 @@ package com.github.nullptroma.wallenc.presentation.viewmodel import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber abstract class ViewModelBase(initState: TState) : ViewModel() { - protected val mutableUiState = MutableStateFlow(initState) + private val _state = MutableStateFlow(initState) - val uiState: StateFlow - get() = mutableUiState.asStateFlow() + init { + Timber.d("Init ViewModel ${this.javaClass.name}") + } + + val state: StateFlow + get() = _state + + protected fun updateState(newState: TState) { + _state.value = newState + } } \ No newline at end of file