diff --git a/.gitignore b/.gitignore index 4227450..7c823c7 100644 --- a/.gitignore +++ b/.gitignore @@ -269,3 +269,4 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin,windows /keystore.properties +/app/release/ diff --git a/app/build.gradle b/app/build.gradle index ea24768..d8bdc9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,6 +26,9 @@ android { // Google Client ID buildConfigField "String", "GOOGLE_CLIENT_ID", keystoreProperties["GOOGLE_CLIENT_ID"] + // MasterKey for encrypted SharedPreference + buildConfigField "String", "SECURE_PREFS_MASTER_KEY", keystoreProperties["SECURE_PREFS_MASTER_KEY"] + // URL for main API buildConfigField "String", "DARK_API_URL", keystoreProperties["DARK_API_URL"] @@ -54,6 +57,9 @@ android { freeCompilerArgs += "-Xcontext-receivers" } applicationVariants.all { variant -> + variant.outputs.all { + outputFileName = "DarkApp.apk" + } variant.sourceSets.java.each { it.srcDirs += "build/generated/ksp/${variant.name}/kotlin" } @@ -134,6 +140,17 @@ dependencies { // Desugaring (Time-related features support for API 21+) coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + // Images with Glide + def glide_version = "4.13.0" + implementation "com.github.bumptech.glide:glide:$glide_version" + annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" + implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0" + implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { + // Excludes the support library because it's already included by Glide. + transitive = false + } + kapt "com.github.bumptech.glide:compiler:$glide_version" + // Tests testImplementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' diff --git a/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt b/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt new file mode 100644 index 0000000..cf27cb8 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/model/AuthCredentials.kt @@ -0,0 +1,14 @@ +package lab.maxb.dark.domain.model + +data class AuthCredentials( + val login: String, + val password: String? = null, + val initial: Boolean = false, +) + +data class ReceivedAuthCredentials( + val token: String, + val id: String, + val role: Role, + val request: AuthCredentials, +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/domain/model/Image.kt b/app/src/main/java/lab/maxb/dark/domain/model/Image.kt deleted file mode 100644 index 63cb384..0000000 --- a/app/src/main/java/lab/maxb/dark/domain/model/Image.kt +++ /dev/null @@ -1,8 +0,0 @@ -package lab.maxb.dark.domain.model - -import lab.maxb.dark.domain.operations.randomUUID - -open class Image( - open var path: String = "", - open var id: String = randomUUID, -) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt index 98fec0a..df7096e 100644 --- a/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/domain/model/RecognitionTask.kt @@ -4,7 +4,7 @@ import lab.maxb.dark.domain.operations.randomUUID open class RecognitionTask( open var names: Set? = null, - open var images: List? = null, + open var images: List? = null, open var owner: User? = null, open var reviewed: Boolean = false, open var id: String = randomUUID, diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt b/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt new file mode 100644 index 0000000..d4494d2 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/operations/Credentials.kt @@ -0,0 +1,14 @@ +package lab.maxb.dark.domain.operations + +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.ReceivedAuthCredentials +import lab.maxb.dark.domain.model.User + +inline fun ReceivedAuthCredentials.toProfile( + user: (String) -> User? = { null }, +) = Profile( + request.login, + user(id), + token, + role = role +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt b/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt index cdfd364..66b8fc2 100644 --- a/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt +++ b/app/src/main/java/lab/maxb/dark/domain/operations/RecognitionTaskOperations.kt @@ -1,6 +1,5 @@ package lab.maxb.dark.domain.operations -import lab.maxb.dark.domain.model.Image import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.domain.model.User @@ -18,7 +17,7 @@ fun createRecognitionTask( return RecognitionTask( namesSet, - images.map { Image(it) }, + images, owner, ) } diff --git a/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt b/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt new file mode 100644 index 0000000..48310ab --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/domain/operations/Source.kt @@ -0,0 +1,3 @@ +package lab.maxb.dark.domain.operations + +typealias ValSource = (String) -> T? diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt index 655b645..350941a 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/ImageHelper.kt @@ -2,84 +2,31 @@ package lab.maxb.dark.presentation.extra import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.net.Uri +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.ResponseBody import org.koin.core.annotation.Single -private fun getContentResolver(context: Context) = context.applicationContext.contentResolver -private fun bitmapFromUri(uri: Uri, - context: Context, - opts: BitmapFactory.Options): Bitmap? - = BitmapFactory.decodeFileDescriptor( - getContentResolver(context).openFileDescriptor( - uri, "r" - )?.fileDescriptor, - null, - opts -) +@GlideModule +class MyAppGlideModule : AppGlideModule() fun Uri.takePersistablePermission(context: Context) { - getContentResolver(context).takePersistableUriPermission(this, + context.applicationContext.contentResolver.takePersistableUriPermission(this, Intent.FLAG_GRANT_READ_URI_PERMISSION) } -fun Uri.toBitmap( - context: Context, - reqWidth: Int, - reqHeight: Int -): Bitmap? - // First decode with inJustDecodeBounds=true to check dimensions - = try {BitmapFactory.Options().run { - inJustDecodeBounds = true - bitmapFromUri(this@toBitmap, context, this) - - // Calculate inSampleSize - inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) - - // Decode bitmap with inSampleSize set - inJustDecodeBounds = false - - bitmapFromUri(this@toBitmap, context, this) ?: return null - }} catch (exception: Throwable) { null } - -private fun calculateInSampleSize(options: BitmapFactory.Options, - reqWidth: Int, - reqHeight: Int): Int { - // Raw height and width of image - val (height: Int, width: Int) = options.run { - outHeight to outWidth - } - var inSampleSize = 1 - - if (height > reqHeight || width > reqWidth) { - - val halfHeight: Int = height / 2 - val halfWidth: Int = width / 2 - - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { - inSampleSize *= 2 - } - } - - return inSampleSize -} - @Single class ImageLoader(context: Context) { private var context = context.applicationContext suspend fun fromUri(uri: Uri): MultipartBody.Part = withContext(Dispatchers.IO) { - val contentResolver = getContentResolver(context) + val contentResolver = context.applicationContext.contentResolver val content = contentResolver.openInputStream(uri)!!.readBytes() return@withContext MultipartBody.Part.createFormData( "file", @@ -90,30 +37,4 @@ class ImageLoader(context: Context) { ) ) } - - suspend fun fromResponse(body: ResponseBody?, filename: String): String { - body ?: return "" - - val path = "image_$filename" - - return withContext(Dispatchers.IO) { - try { - body.byteStream().use { input -> - context.openFileOutput(path, Context.MODE_PRIVATE).use { output -> - val buffer = ByteArray(4 * 1024) - var read: Int - while (input.read(buffer).also { read = it } != -1) - output.write(buffer, 0, read) - output.flush() - } - } - return@withContext Uri.fromFile( - context.getFileStreamPath(path) - ).toString() - } catch (e: Exception){ - e.printStackTrace() - } - "" - } - } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt index 0738975..c412eb5 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/Observe.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt index 3e9f9c4..ac9f015 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettings.kt @@ -1,23 +1,6 @@ package lab.maxb.dark.presentation.extra -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import lab.maxb.dark.presentation.extra.delegates.property -import org.koin.core.annotation.Single - -@Single -class UserSettings(context: Context) { - private val pref = PreferenceManager.getDefaultSharedPreferences(context) - private val securePref: SharedPreferences = EncryptedSharedPreferences.create( - context, - "secure_dark_preferences", - MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - var token: String by securePref.property() - var login: String by pref.property() -} \ No newline at end of file +interface UserSettings { + var token: String + var login: String +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt new file mode 100644 index 0000000..a0a1488 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/extra/UserSettingsImpl.kt @@ -0,0 +1,25 @@ +package lab.maxb.dark.presentation.extra + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import lab.maxb.dark.BuildConfig +import lab.maxb.dark.presentation.extra.delegates.property +import org.koin.core.annotation.Single + +@Single +class UserSettingsImpl(context: Context) : UserSettings { + private val pref = PreferenceManager.getDefaultSharedPreferences(context) + private val securePref: SharedPreferences = EncryptedSharedPreferences.create( + context, + "secure_dark.preferences", + MasterKey.Builder(context, BuildConfig.SECURE_PREFS_MASTER_KEY) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + override var token: String by securePref.property() + override var login: String by pref.property() +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt deleted file mode 100644 index 29c19c5..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ImagesRepositoryImpl.kt +++ /dev/null @@ -1,53 +0,0 @@ -package lab.maxb.dark.presentation.repository.implementation - -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.mapLatest -import lab.maxb.dark.domain.model.Image -import lab.maxb.dark.presentation.extra.ImageLoader -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository -import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.model.toImage -import lab.maxb.dark.presentation.repository.room.model.toImageDTO -import lab.maxb.dark.presentation.repository.utils.StaticResource -import org.koin.core.annotation.Single - -@Single -class ImagesRepositoryImpl( - db: LocalDatabase, - private val mDarkService: DarkService, - private val imageLoader: ImageLoader, -) : ImagesRepository { - private val mImageDao = db.imageDao() - - @OptIn(ExperimentalCoroutinesApi::class) - private val imageResource = StaticResource().apply { - fetchRemote = { - try { - Image(imageLoader.fromResponse( - mDarkService.downloadImage(it), - it - ), it) - } catch (e: NullPointerException) { - null - } - } - localStore = { - mImageDao.save(it.toImageDTO()) - } - clearLocalStore = { - mImageDao.delete(it) - } - fetchLocal = { - mImageDao.getById(it).mapLatest { image -> - image?.toImage() - } - } - } - - override suspend fun save(image: Image) = mImageDao.save(image.toImageDTO()) - - override suspend fun getById(id: String) = imageResource.query(id) - - override suspend fun delete(id: String) = mImageDao.delete(id) -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt index e318209..0dac312 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/ProfileRepositoryImpl.kt @@ -1,53 +1,78 @@ package lab.maxb.dark.presentation.repository.implementation import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.operations.toProfile import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest +import lab.maxb.dark.presentation.repository.network.dark.model.toDomain +import lab.maxb.dark.presentation.repository.network.dark.model.toNetworkDTO import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.toDomain +import lab.maxb.dark.presentation.repository.utils.RefreshControllerImpl +import lab.maxb.dark.presentation.repository.utils.Resource import org.koin.core.annotation.Single +import java.time.Duration @Single class ProfileRepositoryImpl( db: LocalDatabase, - private val darkService: DarkService, + private val networkDataSource: DarkService, private val userSettings: UserSettings, private val usersRepository: UsersRepository, ) : ProfileRepository { - private val profileDAO = db.profileDao() - private val _login = MutableStateFlow(userSettings.login) + private val localDataSource = db.profiles() + private val _credentials = MutableStateFlow(AuthCredentials(userSettings.login)) - @OptIn(ExperimentalCoroutinesApi::class) - override val profile = _login.flatMapLatest { login -> - profileDAO.getByLogin(login).distinctUntilChanged().mapLatest { fullProfile -> - fullProfile?.toProfile() + init { + networkDataSource.onAuthRequired = { + localDataSource.clear() } } - override suspend fun sendCredentials(login: String, password: String, initial: Boolean) { - val request = AuthRequest(login, password) - val response = if (initial) - darkService.signup(request) - else - darkService.login(request) - userSettings.token = response.token - userSettings.login = login - save( - Profile( - login, - usersRepository.getUser(response.id).firstOrNull()!!, - response.token, - role = response.role - ) - ) - _login.value = login + @OptIn(ExperimentalCoroutinesApi::class) + override val profile = _credentials.flatMapLatest { + profileResource.query(it, true) } - override suspend fun save(profile: Profile) = profileDAO.save(ProfileDTO(profile)) + @OptIn(ExperimentalCoroutinesApi::class) + private val profileResource = Resource( + RefreshControllerImpl(Duration.ofHours(12).toMillis()) + ).apply { + fetchLocal = { + localDataSource.getByLogin(it.login).mapLatest { x -> x?.toDomain() } + } + fetchRemote = remote@ { + it.password ?: run { + localDataSource.getUserIdByLogin(it.login)?.let { id -> + usersRepository.refresh(id) + } + return@remote null + } + + val request = it.toNetworkDTO() + val response = (if (it.initial) + networkDataSource.signup(request) + else + networkDataSource.login(request)).toDomain(it) + userSettings.token = response.token + userSettings.login = it.login + response.toProfile { id -> + usersRepository.getUser(id).firstOrNull() ?: return@remote null + } + } + localStore = { localDataSource.save(it.toLocalDTO()) } + } + + override fun sendCredentials(credentials: AuthCredentials) { + _credentials.value = credentials + } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt index fcc5bed..b4a24fc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/RecognitionTasksRepositoryImpl.kt @@ -5,23 +5,20 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.map -import androidx.room.withTransaction import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.extra.ImageLoader -import lab.maxb.dark.presentation.repository.interfaces.ImagesRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO +import lab.maxb.dark.presentation.repository.network.dark.model.toDomain +import lab.maxb.dark.presentation.repository.network.dark.model.toNetworkDTO import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.dao.RecognitionTaskDAO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskImageCrossref -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.toDomain import lab.maxb.dark.presentation.repository.utils.Resource import lab.maxb.dark.presentation.repository.utils.pagination.Page import lab.maxb.dark.presentation.repository.utils.pagination.RecognitionTaskMediator @@ -29,160 +26,116 @@ import org.koin.core.annotation.Single @Single class RecognitionTasksRepositoryImpl( - private val db: LocalDatabase, - private val mDarkService: DarkService, + db: LocalDatabase, + private val networkDataSource: DarkService, private val usersRepository: UsersRepository, - private val imagesRepository: ImagesRepository, private val imageLoader: ImageLoader, ) : RecognitionTasksRepository { - private val mRecognitionTaskDao: RecognitionTaskDAO = db.recognitionTaskDao() + private val localDataSource = db.recognitionTasks() @OptIn(ExperimentalCoroutinesApi::class) private val tasksResource = Resource>().apply { fetchLocal = { page -> - mRecognitionTaskDao.getAllRecognitionTasks().mapLatest { - data -> data?.map { it.toRecognitionTask() } + localDataSource.getAll().mapLatest { + data -> data?.map { it.toDomain() } } } fetchRemote = { page -> - mDarkService.getAllTasks(page.page, page.size)?.map { - RecognitionTask( - setOf(), - imagesRepository.getById(it.image!!).firstOrNull()?.let { listOf(it) }, - usersRepository.getUser(it.owner_id).firstOrNull()!!, - it.reviewed, - it.id, - ) + networkDataSource.getAllTasks(page.page, page.size)?.map { + it.toDomain { getUser(it.owner_id) } } } - localStore = { - db.withTransaction { - it.forEach { task -> - mRecognitionTaskDao.addRecognitionTask( - RecognitionTaskDTO(task) - ) - mRecognitionTaskDao.addRecognitionTaskImages( - listOf(RecognitionTaskImageCrossref( - task.id, - task.images?.get(0)?.id ?: return@forEach, - )) - ) - } + isEmptyResponse = { it.isNullOrEmpty() } + isEmptyCache = { it.isNullOrEmpty() } + localStore = { tasks -> + tasks.map { + it.toLocalDTO() + }.toTypedArray().let { + localDataSource.saveOnly(*it) } } clearLocalStore = { page -> if (page.page == 0) - mRecognitionTaskDao.clear() + localDataSource.clear() } } @OptIn(ExperimentalPagingApi::class) private val pager = Pager( - config = PagingConfig(pageSize = 5), - remoteMediator = RecognitionTaskMediator(tasksResource, db.remoteKeysDao()), + config = PagingConfig(pageSize = 50), + remoteMediator = RecognitionTaskMediator(tasksResource, db.remoteKeys()), ) { - mRecognitionTaskDao.getAllRecognitionTasksPaged() + localDataSource.getAllPaged() }.flow.map { page -> - page.map { - it.toRecognitionTask() - } + page.map { it.toDomain() } } override fun getAllRecognitionTasks() = pager override suspend fun addRecognitionTask(task: RecognitionTask) { - val taskLocal = RecognitionTaskDTO(task) - mDarkService.addTask( - RecognitionTaskCreationDTO( - task.names!! - ) + val taskLocal = task.toLocalDTO() + networkDataSource.addTask( + task.toNetworkDTO() )?.also { taskLocal.id = it } - val images = task.images!!.map { - it.id = mDarkService.addImage( + taskLocal.images = task.images!!.map { + networkDataSource.addImage( taskLocal.id, - imageLoader.fromUri(it.path.toUri()) + imageLoader.fromUri(it.toUri()) )!! - imagesRepository.save(it) - RecognitionTaskImageCrossref( - taskLocal.id, it.id, - ) } - mRecognitionTaskDao.addRecognitionTask( - taskLocal, - task.names!!.map { - RecognitionTaskName(taskLocal.id, it) - }, - images - ) + localDataSource.save(taskLocal) } - override suspend fun markRecognitionTask(task: RecognitionTask) { - mRecognitionTaskDao.updateRecognitionTask(task as RecognitionTaskDTO) - try { - if (mDarkService.markTask(task.id, task.reviewed)) - getRecognitionTask(task.id, true).firstOrNull() + override suspend fun markRecognitionTask(task: RecognitionTask) + = try { + networkDataSource.markTask(task.id, task.reviewed).also { + if (it) refresh(task.id) + } } catch (e: Throwable) { e.printStackTrace() + false } - } override suspend fun solveRecognitionTask(id: String, answer: String) = try { - mDarkService.solveTask(id, answer) + networkDataSource.solveTask(id, answer) } catch (e: Throwable) { e.printStackTrace() false } - override suspend fun deleteRecognitionTask(task: RecognitionTask) { - mRecognitionTaskDao.deleteRecognitionTask( - task as RecognitionTaskDTO - ) - } - @OptIn(ExperimentalCoroutinesApi::class) private val taskResource = Resource().apply { fetchRemote = { id -> - mDarkService.getTask(id)?.let { task -> - RecognitionTask( - task.names, - task.images?.map { image -> - imagesRepository.getById(image).firstOrNull()!! - }, - usersRepository.getUser( - task.owner_id - ).firstOrNull()!!, - task.reviewed, - task.id, - ) + networkDataSource.getTask(id)?.let { + it.toDomain { getUser(it.owner_id) } } } fetchLocal = { id -> - mRecognitionTaskDao.getRecognitionTask(id).mapLatest { - it?.toRecognitionTask() + localDataSource.get(id).mapLatest { + it?.toDomain() } } localStore = { task -> - mRecognitionTaskDao.addRecognitionTask( - RecognitionTaskDTO(task), - task.names!!.map { name -> - RecognitionTaskName(task.id, name) - }, - task.images!!.map { image -> - RecognitionTaskImageCrossref( - task.id, - image.id, - ) - }, - ) + localDataSource.save(task.toLocalDTO()) } clearLocalStore = { - mRecognitionTaskDao.deleteRecognitionTask(it) + localDataSource.delete(it) } } override suspend fun getRecognitionTask(id: String, forceUpdate: Boolean) - = taskResource.query(id, forceUpdate) + = taskResource.query(id, forceUpdate, true) + + override suspend fun refresh(id: String) { + taskResource.refresh(id) + } + + override fun getRecognitionTaskImage(path: String) + = networkDataSource.getImageSource(path) + + private suspend fun getUser(id: String) + = usersRepository.getUser(id).firstOrNull()!! } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt index 33c9a53..addd178 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/implementation/UsersRepositoryImpl.kt @@ -1,28 +1,37 @@ package lab.maxb.dark.presentation.repository.implementation +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.repository.interfaces.UsersRepository import lab.maxb.dark.presentation.repository.network.dark.DarkService import lab.maxb.dark.presentation.repository.room.LocalDatabase -import lab.maxb.dark.presentation.repository.room.dao.UserDAO -import lab.maxb.dark.presentation.repository.room.model.UserDTO +import lab.maxb.dark.presentation.repository.room.dao.UsersDAO +import lab.maxb.dark.presentation.repository.room.model.toDomain +import lab.maxb.dark.presentation.repository.room.model.toLocalDTO import lab.maxb.dark.presentation.repository.utils.Resource import org.koin.core.annotation.Single @Single class UsersRepositoryImpl( db: LocalDatabase, - private val darkService: DarkService + private val networkDataSource: DarkService ) : UsersRepository { - private val mUserDao: UserDAO = db.userDao() + private val localDataSource: UsersDAO = db.users() + + @OptIn(ExperimentalCoroutinesApi::class) private val userResource = Resource().apply { - fetchLocal = { mUserDao.getUser(it) } - fetchRemote = { darkService.getUser(it) } - localStore = { mUserDao.save(UserDTO(it)) } - clearLocalStore = { mUserDao.deleteUser(it) } + fetchLocal = { localDataSource.get(it).mapLatest { x -> x?.toDomain() } } + fetchRemote = { networkDataSource.getUser(it) } + localStore = { localDataSource.save(it.toLocalDTO()) } + clearLocalStore = { localDataSource.delete(it) } } override suspend fun getUser(id: String, fresh: Boolean): Flow - = userResource.query(id, fresh) + = userResource.query(id, fresh, true) + + override suspend fun refresh(id: String) { + userResource.refresh(id) + } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt deleted file mode 100644 index 8317f45..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ImagesRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package lab.maxb.dark.presentation.repository.interfaces - -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.domain.model.Image - -interface ImagesRepository { - suspend fun save(image: Image) - suspend fun getById(id: String): Flow - suspend fun delete(id: String) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt index d07cd61..ef5dbe9 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/ProfileRepository.kt @@ -1,10 +1,10 @@ package lab.maxb.dark.presentation.repository.interfaces import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile interface ProfileRepository { - suspend fun sendCredentials(login: String, password: String, initial: Boolean = false) + fun sendCredentials(credentials: AuthCredentials) val profile: Flow - suspend fun save(profile: Profile) } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt index 0896c9f..0739851 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/RecognitionTasksRepository.kt @@ -1,14 +1,16 @@ package lab.maxb.dark.presentation.repository.interfaces import androidx.paging.PagingData +import com.bumptech.glide.load.model.GlideUrl import kotlinx.coroutines.flow.Flow import lab.maxb.dark.domain.model.RecognitionTask interface RecognitionTasksRepository { fun getAllRecognitionTasks(): Flow> suspend fun getRecognitionTask(id: String, forceUpdate: Boolean = false): Flow + suspend fun refresh(id: String) + fun getRecognitionTaskImage(path: String): GlideUrl suspend fun addRecognitionTask(task: RecognitionTask) - suspend fun markRecognitionTask(task: RecognitionTask) + suspend fun markRecognitionTask(task: RecognitionTask): Boolean suspend fun solveRecognitionTask(id: String, answer: String): Boolean - suspend fun deleteRecognitionTask(task: RecognitionTask) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt index 9710e9e..0b21ccd 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/interfaces/UsersRepository.kt @@ -5,4 +5,5 @@ import lab.maxb.dark.domain.model.User interface UsersRepository { suspend fun getUser(id: String, fresh: Boolean = false): Flow + suspend fun refresh(id: String) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt index 7c70379..88d2819 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/AuthInterceptor.kt @@ -9,9 +9,12 @@ import org.koin.core.annotation.Single class AuthInterceptor( private val userSettings: UserSettings ): Interceptor { + val header = "Authorization" + val value get() = "Bearer ${userSettings.token}" + override fun intercept(chain: Interceptor.Chain): Response = with(chain.request().newBuilder()) { - addHeader("Authorization", "Bearer ${userSettings.token}") + addHeader(header, value) chain.proceed(build()) } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt index 0ab9be4..f1935cb 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkService.kt @@ -1,20 +1,22 @@ package lab.maxb.dark.presentation.repository.network.dark +import com.bumptech.glide.load.model.GlideUrl import lab.maxb.dark.domain.model.User import lab.maxb.dark.presentation.repository.network.dark.model.* import okhttp3.MultipartBody -import okhttp3.ResponseBody interface DarkService { - suspend fun getAllTasks(page: Int, size: Int): List? - suspend fun getTask(id: String): RecognitionTaskFullViewDTO? - suspend fun addTask(task: RecognitionTaskCreationDTO): String? + suspend fun getAllTasks(page: Int, size: Int): List? + suspend fun getTask(id: String): RecognitionTaskFullViewNetworkDTO? + suspend fun addTask(task: RecognitionTaskCreationNetworkDTO): String? suspend fun markTask(id: String, isAllowed: Boolean): Boolean suspend fun solveTask(id: String, answer: String): Boolean suspend fun addImage(id: String, filePart: MultipartBody.Part): String? - suspend fun downloadImage(path: String): ResponseBody? + fun getImageSource(path: String): GlideUrl suspend fun getUser(id: String): User? suspend fun login(request: AuthRequest): AuthResponse suspend fun signup(request: AuthRequest): AuthResponse + + var onAuthRequired: (suspend () -> Unit)? } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt index 49ef73b..41fd878 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/DarkServiceImpl.kt @@ -1,9 +1,11 @@ package lab.maxb.dark.presentation.repository.network.dark +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders import com.google.gson.GsonBuilder import lab.maxb.dark.BuildConfig import lab.maxb.dark.presentation.repository.network.dark.model.AuthRequest -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO import lab.maxb.dark.presentation.repository.network.logger import okhttp3.MultipartBody import okhttp3.OkHttpClient @@ -11,12 +13,16 @@ import org.koin.core.annotation.Single import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.EOFException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException @Single class DarkServiceImpl( - private val authInterceptor: AuthInterceptor, + private val authInterceptor: AuthInterceptor ) : DarkService { private val api = buildDarkService() + override var onAuthRequired: (suspend () -> Unit)? = null override suspend fun getAllTasks(page: Int, size: Int) = catchAll { api.getAllTasks(page, size) @@ -26,7 +32,7 @@ class DarkServiceImpl( api.getTask(id) } - override suspend fun addTask(task: RecognitionTaskCreationDTO) = catchAll { + override suspend fun addTask(task: RecognitionTaskCreationNetworkDTO) = catchAll { api.addTask(task) } @@ -42,9 +48,12 @@ class DarkServiceImpl( api.addImage(id, filePart) } - override suspend fun downloadImage(path: String) = catchAll { - api.downloadImage(path) - } + override fun getImageSource(path: String) = GlideUrl( + "${BuildConfig.DARK_API_URL}/task/image/$path", + LazyHeaders.Builder() + .addHeader(authInterceptor.header, authInterceptor.value) + .build() + ) override suspend fun getUser(id: String) = catchAll { api.getUser(id) @@ -59,16 +68,41 @@ class DarkServiceImpl( } private suspend inline fun catchAll( - crossinline block: suspend () -> T + crossinline block: suspend () -> T, ): T = try { - block() + retry(block) } catch (e: EOFException) { null as T + } catch (e: UnableToObtainResource) { + throw e + } catch (e: UnknownHostException) { + throw UnableToObtainResource() + } catch (e: ConnectException) { + throw UnableToObtainResource() + } catch (e: retrofit2.HttpException) { + when (e.code()) { + 401, 403 -> onAuthRequired?.invoke() + else -> e.printStackTrace() + } + throw UnableToObtainResource() } catch (e: Throwable) { e.printStackTrace() throw e } + private suspend inline fun retry( + crossinline block: suspend () -> T, + ): T { + repeat(MAX_RETRY_COUNT) { + try { + return block() + } catch (e: SocketTimeoutException) { + // Ignore (delay included in timeout) + } + } + throw UnableToObtainResource() + } + // Initialization private fun buildDarkService() = Retrofit.Builder() @@ -88,4 +122,11 @@ class DarkServiceImpl( }.create().run { GsonConverterFactory.create(this) } -} \ No newline at end of file + + companion object { + const val MAX_RETRY_COUNT = 6 + } +} + + +class UnableToObtainResource: Exception() \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt index ee925e7..daf3337 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/Auth.kt @@ -1,5 +1,7 @@ package lab.maxb.dark.presentation.repository.network.dark.model +import lab.maxb.dark.domain.model.AuthCredentials +import lab.maxb.dark.domain.model.ReceivedAuthCredentials import lab.maxb.dark.domain.model.Role class AuthRequest( @@ -12,3 +14,12 @@ class AuthResponse( var id: String, var role: Role, ) + +fun AuthCredentials.toNetworkDTO() = AuthRequest( + login, + password!!, +) + +fun AuthResponse.toDomain(request: AuthCredentials) = ReceivedAuthCredentials( + token, id, role, request, +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt index 5cdd1b0..541fcd8 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/model/RecognitionTask.kt @@ -1,13 +1,17 @@ package lab.maxb.dark.presentation.repository.network.dark.model -class RecognitionTaskListViewDTO ( +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.model.User + + +class RecognitionTaskListViewNetworkDTO ( var image: String?, val owner_id: String, val reviewed: Boolean, var id: String, ) -class RecognitionTaskFullViewDTO( +class RecognitionTaskFullViewNetworkDTO( var names: Set?, var images: List?, val owner_id: String, @@ -15,6 +19,30 @@ class RecognitionTaskFullViewDTO( var id: String, ) -class RecognitionTaskCreationDTO( +class RecognitionTaskCreationNetworkDTO( var names: Set +) + +fun RecognitionTask.toNetworkDTO() = RecognitionTaskCreationNetworkDTO( + names!! +) + +inline fun RecognitionTaskListViewNetworkDTO.toDomain( + user: () -> User? = { null }, +) = RecognitionTask( + setOf(), + image?.let { listOf(it) }, + user(), + reviewed, + id, +) + +inline fun RecognitionTaskFullViewNetworkDTO.toDomain( + user: () -> User? = { null } +) = RecognitionTask( + names, + images, + user(), + reviewed, + id, ) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt index 2a14cbc..00b03b6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/dark/routes/RecognitionTask.kt @@ -1,10 +1,9 @@ package lab.maxb.dark.presentation.repository.network.dark.routes -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationDTO -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskFullViewDTO -import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskListViewDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskCreationNetworkDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskFullViewNetworkDTO +import lab.maxb.dark.presentation.repository.network.dark.model.RecognitionTaskListViewNetworkDTO import okhttp3.MultipartBody -import okhttp3.ResponseBody import retrofit2.http.* interface RecognitionTask { @@ -12,13 +11,13 @@ interface RecognitionTask { suspend fun getAllTasks( @Query("page") page: Int = 0, @Query("size") size: Int = 2, - ): List? + ): List? @GET("$path/{id}") - suspend fun getTask(@Path("id") id: String): RecognitionTaskFullViewDTO? + suspend fun getTask(@Path("id") id: String): RecognitionTaskFullViewNetworkDTO? @POST("$path/add") - suspend fun addTask(@Body task: RecognitionTaskCreationDTO): String? + suspend fun addTask(@Body task: RecognitionTaskCreationNetworkDTO): String? @PATCH("$path/mark/{id}/{isAllowed}") suspend fun markTask( @@ -39,12 +38,6 @@ interface RecognitionTask { @Part filePart: MultipartBody.Part ): String? - @Streaming - @GET("$path/image/{path}") - suspend fun downloadImage( - @Path("path") path: String - ): ResponseBody? - companion object { const val path = "/task" } diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt index 891717d..935ee62 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/network/synonymizer/SynonymFounder.kt @@ -9,12 +9,16 @@ import retrofit2.converter.gson.GsonConverterFactory @Single class SynonymFounder : SynonymsRepository { private val api: RusTxtAPI - private suspend fun getSynonym(text: String): String? { - return api.getSynonym(MultipartBody.Builder().setType(MultipartBody.FORM) + private suspend fun getSynonym(text: String) + = try { + api.getSynonym(MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("method","getSynText") .addFormDataPart("text", text) .build() )?.modified_text?.let { fixText(it) } + } catch (e: Throwable) { + e.printStackTrace() + null } private fun fixText(text: String) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt index 8482915..7a64c6e 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/LocalDatabase.kt @@ -4,32 +4,34 @@ import android.app.Application import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.dao.* -import lab.maxb.dark.presentation.repository.room.model.* +import androidx.room.TypeConverters +import lab.maxb.dark.presentation.repository.room.converters.CollectionsConverter +import lab.maxb.dark.presentation.repository.room.dao.ProfilesDAO +import lab.maxb.dark.presentation.repository.room.dao.RecognitionTasksDAO +import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO +import lab.maxb.dark.presentation.repository.room.dao.UsersDAO +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.model.RemoteKey +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO @Database(entities = [ - UserDTO::class, - RecognitionTaskDTO::class, - RecognitionTaskName::class, - ImageDTO::class, - RecognitionTaskImageCrossref::class, - ProfileDTO::class, - RemoteKeys::class, - ], version = 5, exportSchema = false) + UserLocalDTO::class, + RecognitionTaskLocalDTO::class, + ProfileLocalDTO::class, + RemoteKey::class, + ], version = 7, exportSchema = false) +@TypeConverters(CollectionsConverter::class) abstract class LocalDatabase : RoomDatabase() { - abstract fun recognitionTaskDao(): RecognitionTaskDAO - abstract fun userDao(): UserDAO - abstract fun profileDao(): ProfileDAO - abstract fun imageDao(): ImageDAO - abstract fun remoteKeysDao(): RemoteKeysDAO + abstract fun recognitionTasks(): RecognitionTasksDAO + abstract fun users(): UsersDAO + abstract fun profiles(): ProfilesDAO + abstract fun remoteKeys(): RemoteKeysDAO companion object { - internal fun build(app: Application) - = Room.databaseBuilder( - app.applicationContext, - LocalDatabase::class.java, "dark_database" - ).fallbackToDestructiveMigration() - .build() + internal fun build(app: Application) = Room.databaseBuilder( + app.applicationContext, + LocalDatabase::class.java, "dark_database" + ).fallbackToDestructiveMigration().build() } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt new file mode 100644 index 0000000..fd8a83b --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/converters/CollectionsConverter.kt @@ -0,0 +1,35 @@ +package lab.maxb.dark.presentation.repository.room.converters + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import java.lang.reflect.Type + + +class CollectionsConverter { + @TypeConverter + fun fromList(list: List?) = list?.let { + gson.toJson(list, typeList) + } + + @TypeConverter + fun toList(list: String?) = list?.let { + gson.fromJson>(list, typeList) + } + + @TypeConverter + fun fromSet(list: Set?) = list?.let { + gson.toJson(list, typeSet) + } + + @TypeConverter + fun toSet(list: String?) = list?.let { + gson.fromJson>(list, typeSet) + } + + companion object { + private val gson = Gson() + private val typeList: Type = object : TypeToken?>() {}.type + private val typeSet: Type = object : TypeToken?>() {}.type + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt new file mode 100644 index 0000000..6e6a940 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/AdvancedDAO.kt @@ -0,0 +1,47 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.* +import androidx.room.OnConflictStrategy.IGNORE +import androidx.sqlite.db.SimpleSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQuery +import lab.maxb.dark.presentation.repository.room.model.BaseLocalDTO + +interface BaseDAO { + @Insert(onConflict = IGNORE) + suspend fun add(value: DTO): Long + + @Update + suspend fun update(value: DTO) + + @Delete + suspend fun delete(vararg value: DTO) +} + +abstract class AdvancedDAO( + private val tableName: String, +) : BaseDAO { + suspend fun clear() = run(SimpleSQLiteQuery("DELETE FROM $tableName")) + + suspend fun getById(id: String) = runForResult(SimpleSQLiteQuery( + "SELECT * FROM $tableName WHERE id = :id", + arrayOf(id), + )) + + suspend fun delete(id: String) = run(SimpleSQLiteQuery( + "DELETE FROM $tableName WHERE id = :id", + arrayOf(id), + )) + + @Transaction + open suspend fun save(vararg value: DTO) { + for (it in value) + if (add(it) == -1L) + update(it) + } + + @RawQuery + protected abstract suspend fun run(query: SupportSQLiteQuery): Long + + @RawQuery + protected abstract suspend fun runForResult(query: SupportSQLiteQuery): DTO? +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt deleted file mode 100644 index 9278c6f..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ImageDAO.kt +++ /dev/null @@ -1,20 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.ImageDTO - -@Dao -interface ImageDAO { - @Insert(onConflict = REPLACE) - suspend fun save(image: ImageDTO) - - @Query("SELECT * FROM image WHERE imageId = :id") - fun getById(id: String): Flow - - @Query("DELETE FROM image WHERE imageId = :id") - suspend fun delete(id: String) -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt deleted file mode 100644 index 655b0af..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfileDAO.kt +++ /dev/null @@ -1,18 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.relations.FullProfile - -@Dao -interface ProfileDAO { - @Insert(onConflict = REPLACE) - suspend fun save(profile: ProfileDTO) - - @Query("SELECT * FROM profile WHERE login = :login") - fun getByLogin(login: String): Flow -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt new file mode 100644 index 0000000..884ca2e --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/ProfilesDAO.kt @@ -0,0 +1,16 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.FullProfileLocalDTO + +@Dao +abstract class ProfilesDAO: AdvancedDAO("profile") { + @Query("SELECT * FROM profile WHERE id = :login") + abstract fun getByLogin(login: String): Flow + + @Query("SELECT user_id FROM profile WHERE id = :login") + abstract suspend fun getUserIdByLogin(login: String): String? +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt deleted file mode 100644 index 67f6bfb..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTaskDAO.kt +++ /dev/null @@ -1,59 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.paging.PagingSource -import androidx.room.* -import androidx.room.OnConflictStrategy.IGNORE -import androidx.room.OnConflictStrategy.REPLACE -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskDTO -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskImageCrossref -import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskName -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithNamesAndImages -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwnerAndImage - -@Dao -interface RecognitionTaskDAO { - @Insert(onConflict = REPLACE) - suspend fun addRecognitionTask(task: RecognitionTaskDTO) - - @Insert(onConflict = REPLACE) - suspend fun addRecognitionTaskNames(names: List) - - @Insert(onConflict = IGNORE) - suspend fun addRecognitionTaskImages(images: List) - - @Transaction - suspend fun addRecognitionTask(task: RecognitionTaskDTO, - names: List, - images: List) { - deleteRecognitionTask(task) - addRecognitionTask(task) - addRecognitionTaskNames(names) - addRecognitionTaskImages(images) - } - - @Update - suspend fun updateRecognitionTask(task: RecognitionTaskDTO) - - @Delete - suspend fun deleteRecognitionTask(task: RecognitionTaskDTO) - - @Query("DELETE FROM recognition_task WHERE id=:id") - suspend fun deleteRecognitionTask(id: String) - - @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasks(): Flow?> - - @Transaction - @Query("SELECT * FROM recognition_task ORDER BY reviewed") - fun getAllRecognitionTasksPaged(): PagingSource - - @Transaction - @Query("SELECT * FROM recognition_task WHERE id = :id") - fun getRecognitionTask(id: String): Flow - - @Transaction - @Query("DELETE FROM recognition_task") - suspend fun clear() -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt new file mode 100644 index 0000000..2ba98eb --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RecognitionTasksDAO.kt @@ -0,0 +1,34 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.relations.FullRecognitionTaskDTO + +@Dao +abstract class RecognitionTasksDAO: AdvancedDAO( + "recognition_task" +) { + @Transaction + @Query("SELECT * FROM recognition_task ORDER BY reviewed ASC, id ASC") + abstract fun getAll(): Flow?> + + @Transaction + @Query("SELECT * FROM recognition_task ORDER BY reviewed ASC, id ASC") + abstract fun getAllPaged(): PagingSource + + @Query("SELECT * FROM recognition_task WHERE id = :id") + abstract fun get(id: String): Flow + + @Query("DELETE FROM recognition_task WHERE NOT (id in (:id))") + abstract fun deleteOther(id: List) + + @Transaction + open suspend fun saveOnly(vararg value: RecognitionTaskLocalDTO) { + save(*value) + deleteOther(value.map { it.id }) + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt index a0492b2..143bd00 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/RemoteKeysDAO.kt @@ -1,19 +1,11 @@ package lab.maxb.dark.presentation.repository.room.dao import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy import androidx.room.Query -import lab.maxb.dark.presentation.repository.room.model.RemoteKeys +import lab.maxb.dark.presentation.repository.room.model.RemoteKey @Dao -interface RemoteKeysDAO { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(remoteKey: List) - - @Query("SELECT * FROM remotekeys WHERE id = :id") - suspend fun getById(id: String): RemoteKeys? - - @Query("DELETE FROM remotekeys") - suspend fun clear() +abstract class RemoteKeysDAO: AdvancedDAO("remote_keys") { + @Query("SELECT EXISTS(SELECT 1 FROM remote_keys)") + abstract suspend fun hasContent(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt deleted file mode 100644 index 295bc54..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UserDAO.kt +++ /dev/null @@ -1,27 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.dao - -import androidx.room.* -import androidx.room.OnConflictStrategy.* -import kotlinx.coroutines.flow.Flow -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -@Dao -interface UserDAO { - @Insert(onConflict = IGNORE) - suspend fun addUser(user: UserDTO): Long - - @Update - suspend fun updateUser(user: UserDTO) - - @Transaction - suspend fun save(user: UserDTO) { - if (addUser(user) == -1L) - updateUser(user) - } - - @Query("DELETE FROM user WHERE id = :id") - suspend fun deleteUser(id: String) - - @Query("SELECT * FROM user WHERE id = :id") - fun getUser(id: String): Flow -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt new file mode 100644 index 0000000..cadc5fa --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/dao/UsersDAO.kt @@ -0,0 +1,13 @@ +package lab.maxb.dark.presentation.repository.room.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + + +@Dao +abstract class UsersDAO : AdvancedDAO("user") { + @Query("SELECT * FROM user WHERE id = :id") + abstract fun get(id: String): Flow +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt new file mode 100644 index 0000000..d766ca3 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/BaseLocalDTO.kt @@ -0,0 +1,5 @@ +package lab.maxb.dark.presentation.repository.room.model + +abstract class BaseLocalDTO { + abstract val id: String +} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt deleted file mode 100644 index 0c36fbe..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ImageDTO.kt +++ /dev/null @@ -1,21 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.Image -import lab.maxb.dark.domain.operations.randomUUID - -@Entity(tableName = "image") -data class ImageDTO( - val path: String, - @PrimaryKey - val imageId: String = randomUUID, -) - -fun ImageDTO.toImage() = Image( - path, imageId -) - -fun Image.toImageDTO() = ImageDTO( - path, id -) - diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt deleted file mode 100644 index 4a23d41..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileDTO.kt +++ /dev/null @@ -1,36 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.Server.Model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.Role -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -@Entity(tableName = "profile", - ignoredColumns = ["user"], - foreignKeys = [ - ForeignKey( - entity = UserDTO::class, - parentColumns = ["id"], - childColumns = ["user_id"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class ProfileDTO( - @PrimaryKey - override var login: String, - var user_id: String, - override var token: String, - override var type: AuthType, - override var role: Role, -): Profile(login, null, token, type, role) { - constructor(profile: Profile) : this( - profile.login, - profile.user!!.id, - profile.token, - profile.type, - profile.role - ) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt new file mode 100644 index 0000000..70780d7 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/ProfileLocalDTO.kt @@ -0,0 +1,36 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.Role +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "profile", + foreignKeys = [ + ForeignKey( + entity = UserLocalDTO::class, + parentColumns = ["id"], + childColumns = ["user_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ProfileLocalDTO( + var user_id: String, + var token: String, + var type: Profile.AuthType, + var role: Role, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun Profile.toLocalDTO() = ProfileLocalDTO( + user!!.id, + token, + type, + role, + login, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt deleted file mode 100644 index 2467987..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskDTO.kt +++ /dev/null @@ -1,30 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.RecognitionTask - -@Entity(tableName = "recognition_task", - ignoredColumns = ["owner", "names", "images"], - foreignKeys = [ - ForeignKey( - entity = UserDTO::class, - parentColumns = ["id"], - childColumns = ["owner_id"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class RecognitionTaskDTO( - @PrimaryKey - override var id: String, - val owner_id: String, - override var reviewed: Boolean = false, -): RecognitionTask(id=id) { - constructor(task: RecognitionTask) : this( - task.id, - task.owner!!.id, - task.reviewed, - ) -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt deleted file mode 100644 index 7452b42..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskImageCrossref.kt +++ /dev/null @@ -1,29 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE - -@Entity(tableName = "recognition_task_image", - primaryKeys = ["id", "imageId"], - foreignKeys = [ - ForeignKey( - entity = RecognitionTaskDTO::class, - parentColumns = ["id"], - childColumns = ["id"], - onDelete = CASCADE, - onUpdate = CASCADE, - deferred = true - ), - ForeignKey( - entity = ImageDTO::class, - parentColumns = ["imageId"], - childColumns = ["imageId"], - onDelete = CASCADE, - deferred = true - ) - ], -) -data class RecognitionTaskImageCrossref( - var id: String, - var imageId: String, -) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt new file mode 100644 index 0000000..95a0632 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskLocalDTO.kt @@ -0,0 +1,43 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "recognition_task", + foreignKeys = [ + ForeignKey( + entity = UserLocalDTO::class, + parentColumns = ["id"], + childColumns = ["owner_id"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class RecognitionTaskLocalDTO( + var names: Set?, + var images: List?, + val owner_id: String, + var reviewed: Boolean = false, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun RecognitionTask.toLocalDTO() = RecognitionTaskLocalDTO( + names, + images, + owner!!.id, + reviewed, + id, +) + +fun RecognitionTaskLocalDTO.toDomain() = RecognitionTask( + names, + images, + null, + reviewed, + id, +) \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt deleted file mode 100644 index 8fe0bb9..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RecognitionTaskName.kt +++ /dev/null @@ -1,22 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE - -@Entity(tableName = "recognition_task_name", - foreignKeys = [ - ForeignKey( - entity = RecognitionTaskDTO::class, - parentColumns = ["id"], - childColumns = ["recognition_task"], - onDelete = CASCADE, - onUpdate = CASCADE, - deferred = true - ) - ], - primaryKeys = ["recognition_task", "name"] -) -data class RecognitionTaskName( - val recognition_task: String, - val name: String, -) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt similarity index 57% rename from app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt rename to app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt index e12e255..33a957b 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKeys.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/RemoteKey.kt @@ -3,9 +3,10 @@ package lab.maxb.dark.presentation.repository.room.model import androidx.room.Entity import androidx.room.PrimaryKey -@Entity -data class RemoteKeys( - @PrimaryKey val id: String, +@Entity(tableName = "remote_keys") +data class RemoteKey( + @PrimaryKey + override var id: String, val prevKey: Int?, val nextKey: Int? -) \ No newline at end of file +): BaseLocalDTO() \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt deleted file mode 100644 index cfde4aa..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserDTO.kt +++ /dev/null @@ -1,19 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import lab.maxb.dark.domain.model.User - -@Entity(tableName = "user") -data class UserDTO( - @PrimaryKey - override var id: String, - override var name: String, - override var rating: Int, -): User(name=name, rating=rating) { - constructor(user: User) : this( - user.id, - user.name, - user.rating, - ) -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt new file mode 100644 index 0000000..3f0d389 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/model/UserLocalDTO.kt @@ -0,0 +1,27 @@ +package lab.maxb.dark.presentation.repository.room.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.domain.operations.randomUUID + +@Entity(tableName = "user") +data class UserLocalDTO( + var name: String, + var rating: Int, + + @PrimaryKey + override var id: String = randomUUID, +): BaseLocalDTO() + +fun User.toLocalDTO() = UserLocalDTO( + name, + rating, + id, +) + +fun UserLocalDTO.toDomain() = User( + name, + rating, + id, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt deleted file mode 100644 index b65cf34..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfile.kt +++ /dev/null @@ -1,22 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations - -import androidx.room.Embedded -import androidx.room.Relation -import lab.maxb.dark.domain.model.Profile -import lab.maxb.dark.domain.model.User -import lab.maxb.dark.presentation.repository.room.Server.Model.ProfileDTO -import lab.maxb.dark.presentation.repository.room.model.UserDTO - -data class FullProfile( - @Embedded val profile: ProfileDTO, - @Relation( - parentColumn = "user_id", - entityColumn = "id", - entity = UserDTO::class - ) - val user: User? -) { - fun toProfile() = profile.also { - it.user = user - } as Profile -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt new file mode 100644 index 0000000..a36e9ee --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullProfileLocalDTO.kt @@ -0,0 +1,26 @@ +package lab.maxb.dark.presentation.repository.room.relations + +import androidx.room.Embedded +import androidx.room.Relation +import lab.maxb.dark.domain.model.Profile +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.presentation.repository.room.model.ProfileLocalDTO +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + +data class FullProfileLocalDTO( + @Embedded val profile: ProfileLocalDTO, + @Relation( + parentColumn = "user_id", + entityColumn = "id", + entity = UserLocalDTO::class + ) + val user: User? +) + +fun FullProfileLocalDTO.toDomain() = Profile( + profile.id, + user, + profile.token, + profile.type, + profile.role, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt new file mode 100644 index 0000000..aa214a4 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/FullRecognitionTaskDTO.kt @@ -0,0 +1,26 @@ +package lab.maxb.dark.presentation.repository.room.relations + +import androidx.room.Embedded +import androidx.room.Relation +import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.domain.model.User +import lab.maxb.dark.presentation.repository.room.model.RecognitionTaskLocalDTO +import lab.maxb.dark.presentation.repository.room.model.UserLocalDTO + +data class FullRecognitionTaskDTO( + @Embedded val recognition_task: RecognitionTaskLocalDTO, + @Relation( + parentColumn = "owner_id", + entityColumn = "id", + entity = UserLocalDTO::class + ) + val owner: User? +) + +fun FullRecognitionTaskDTO.toDomain() = RecognitionTask( + recognition_task.names, + recognition_task.images, + owner, + recognition_task.reviewed, + recognition_task.id, +) diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt deleted file mode 100644 index d010f90..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithNamesAndImages.kt +++ /dev/null @@ -1,26 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation -import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.presentation.repository.room.model.* - -data class RecognitionTaskWithNamesAndImages( - @Embedded val recognition_task: RecognitionTaskDTO, - @Relation( - parentColumn = "id", - entityColumn = "recognition_task" - ) - val names: List, - @Relation( - parentColumn = "id", - entityColumn = "imageId", - associateBy = Junction(RecognitionTaskImageCrossref::class) - ) - val images: List -) { - fun toRecognitionTask() = recognition_task.also { task -> - task.names = names.map { it.name }.toSet() - task.images = images.map { it.toImage() } - } as RecognitionTask -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt deleted file mode 100644 index 8a1a7c8..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/room/relations/RecognitionTaskWithOwnerAndImage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package lab.maxb.dark.presentation.repository.room.relations - -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation -import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.domain.model.User -import lab.maxb.dark.presentation.repository.room.model.* - -data class RecognitionTaskWithOwnerAndImage( - @Embedded val recognition_task: RecognitionTaskDTO, - @Relation( - parentColumn = "owner_id", - entityColumn = "id", - entity = UserDTO::class - ) - val owner: User?, - @Relation( - parentColumn = "id", - entityColumn = "imageId", - associateBy = Junction(RecognitionTaskImageCrossref::class) - ) - val image: ImageDTO? -) { - fun toRecognitionTask() = recognition_task.also { task -> - task.owner = owner - task.images = image?.let { listOf(it.toImage()) } ?: listOf() - } as RecognitionTask -} diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt deleted file mode 100644 index 597a2ce..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/BaseResource.kt +++ /dev/null @@ -1,26 +0,0 @@ -package lab.maxb.dark.presentation.repository.utils - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow - -open class BaseResource { - open lateinit var fetchLocal: suspend (Input) -> Flow - open lateinit var fetchRemote: suspend (Input) -> Output? - open lateinit var isFresh: (suspend (Input) -> Boolean) - open var onRefresh: (suspend () -> Unit)? = null - open var localStore: (suspend (Output) -> Unit)? = null - open var clearLocalStore: (suspend (Input) -> Unit)? = null - - protected open suspend fun getCache(args: Input) = fetchLocal(args) - - suspend fun query(args: Input, force: Boolean = false) = flow { - if (force || !isFresh(args)) { - fetchRemote(args)?.run { - localStore?.invoke(this) - } ?: clearLocalStore?.invoke(args) - onRefresh?.invoke() - } - emitAll(getCache(args)) - } -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt index 45589d5..edac698 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/Resource.kt @@ -1,17 +1,54 @@ package lab.maxb.dark.presentation.repository.utils +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flow +import lab.maxb.dark.presentation.repository.network.dark.UnableToObtainResource -class Resource( +open class Resource( var refreshController: RefreshController = RefreshControllerImpl() -) : BaseResource() { +) { + open lateinit var fetchLocal: suspend (Input) -> Flow + open var fetchLocalSnapshot: suspend (Input) -> Output? = { + fetchLocal(it).firstOrNull() + } + open lateinit var fetchRemote: suspend (Input) -> Output? + open var isFresh: (suspend (Input, Output?) -> Boolean) = { it, cache -> + !refreshController.isExpired() && !isEmptyCache(cache) + } + open var isEmptyResponse: (suspend (Output?) -> Boolean) = { it == null } + open var isEmptyCache: (suspend (Output?) -> Boolean) = { it == null } + open var onRefresh: (suspend () -> Unit)? = { + refreshController.refresh() + } + open var localStore: (suspend (Output) -> Unit)? = null + open var clearLocalStore: (suspend (Input) -> Unit)? = null - override var isFresh: suspend (Input) -> Boolean = { - !refreshController.isExpired() && getCache(it).firstOrNull() != null + suspend fun query(args: Input, force: Boolean = false, useCache: Boolean = false) = flow { + val cache = fetchLocalSnapshot(args) + + if (useCache && !isEmptyCache(cache)) + emit(cache) + + if (force || !isFresh(args, cache)) + refresh(args) + + emitAll(fetchLocal(args)) } - override var onRefresh: (suspend () -> Unit)? = { - refreshController.refresh() + suspend fun refresh(args: Input) { + try { + fetchRemote(args).also { + if (isEmptyResponse(it)) + clearLocalStore?.invoke(args) + else + localStore?.invoke(it!!) + } + onRefresh?.invoke() + } catch (e: UnableToObtainResource) {} } -} + suspend fun checkIsFresh(args: Input) + = isFresh(args, fetchLocalSnapshot(args)) +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt deleted file mode 100644 index 5c1ef5e..0000000 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/StaticResource.kt +++ /dev/null @@ -1,9 +0,0 @@ -package lab.maxb.dark.presentation.repository.utils - -import kotlinx.coroutines.flow.firstOrNull - -open class StaticResource : BaseResource() { - override var isFresh: suspend (Input) -> Boolean = { - getCache(it).firstOrNull() != null - } -} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt index d3dddc4..4b7e902 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/repository/utils/pagination/RecognitionTasksMediator.kt @@ -7,9 +7,9 @@ import androidx.paging.RemoteMediator import kotlinx.coroutines.flow.firstOrNull import lab.maxb.dark.domain.model.RecognitionTask import lab.maxb.dark.presentation.repository.room.dao.RemoteKeysDAO -import lab.maxb.dark.presentation.repository.room.model.RemoteKeys -import lab.maxb.dark.presentation.repository.room.relations.RecognitionTaskWithOwnerAndImage -import lab.maxb.dark.presentation.repository.utils.BaseResource +import lab.maxb.dark.presentation.repository.room.model.RemoteKey +import lab.maxb.dark.presentation.repository.room.relations.FullRecognitionTaskDTO +import lab.maxb.dark.presentation.repository.utils.Resource import retrofit2.HttpException import java.io.IOException import java.io.InvalidObjectException @@ -17,38 +17,42 @@ import java.io.InvalidObjectException @OptIn(ExperimentalPagingApi::class) class RecognitionTaskMediator( - private val resource: BaseResource>, + private val resource: Resource>, private val remoteKeys: RemoteKeysDAO, -) : RemoteMediator() { +) : RemoteMediator() { - override suspend fun initialize() = if (resource.isFresh(Page(0, 1))) + override suspend fun initialize() = if ( + resource.checkIsFresh(Page(0, 1)) + && remoteKeys.hasContent() + ) InitializeAction.SKIP_INITIAL_REFRESH else InitializeAction.LAUNCH_INITIAL_REFRESH override suspend fun load( loadType: LoadType, - state: PagingState + state: PagingState ): MediatorResult { return try { - val pageKeyData = getKeyPageData(loadType, state) + val pageKeyData = getKeyPageData(if (remoteKeys.hasContent()) + loadType else LoadType.REFRESH, state) val page = when (pageKeyData) { is MediatorResult.Success -> return pageKeyData else -> pageKeyData as Int } val response = resource.query(Page(page, state.config.pageSize), true).firstOrNull() - val isEndOfList = response?.isEmpty() ?: true + val isEndOfList = response.isNullOrEmpty() if (loadType == LoadType.REFRESH) remoteKeys.clear() response?.map { - RemoteKeys( + RemoteKey( it.id, if (page == 0) null else page - 1, if (isEndOfList) null else page + 1 ) - }?.let { - remoteKeys.save(it) + }?.toTypedArray()?.let { + remoteKeys.save(*it) } MediatorResult.Success( @@ -61,21 +65,21 @@ class RecognitionTaskMediator( } } - private suspend fun getFirstRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getFirstRemoteKey(state: PagingState): RemoteKey? { return state.pages .firstOrNull { it.data.isNotEmpty() } ?.data?.firstOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getLastRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getLastRemoteKey(state: PagingState): RemoteKey? { return state.pages .lastOrNull { it.data.isNotEmpty() } ?.data?.lastOrNull() ?.let { doggo -> remoteKeys.getById(doggo.recognition_task.id) } } - private suspend fun getClosestRemoteKey(state: PagingState): RemoteKeys? { + private suspend fun getClosestRemoteKey(state: PagingState): RemoteKey? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.recognition_task?.id?.let { id -> remoteKeys.getById(id) @@ -83,7 +87,7 @@ class RecognitionTaskMediator( } } - private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { + private suspend fun getKeyPageData(loadType: LoadType, state: PagingState): Any? { return when (loadType) { LoadType.REFRESH -> { val remoteKeys = getClosestRemoteKey(state) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt index 3691b8d..feabdca 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AddRecognitionTaskFragment.kt @@ -11,27 +11,29 @@ import android.widget.AutoCompleteTextView import android.widget.Toast import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import com.bumptech.glide.RequestManager import lab.maxb.dark.R import lab.maxb.dark.databinding.AddRecognitionTaskFragmentBinding import lab.maxb.dark.domain.operations.unicname +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.delegates.viewBinding import lab.maxb.dark.presentation.extra.goBack import lab.maxb.dark.presentation.extra.launch import lab.maxb.dark.presentation.extra.observe -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.view.adapter.InputListAdapter import lab.maxb.dark.presentation.viewModel.AddRecognitionTaskViewModel -import lab.maxb.dark.presentation.viewModel.utils.map import org.koin.androidx.viewmodel.ext.android.sharedViewModel class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragment) { private val mViewModel: AddRecognitionTaskViewModel by sharedViewModel() private val mBinding: AddRecognitionTaskFragmentBinding by viewBinding() private var mInputsAdapter: InputListAdapter by autoCleaned() + private var mGlide: RequestManager by autoCleaned() private var mImagesAdapter: ImageSliderAdapter by autoCleaned() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -45,23 +47,15 @@ class AddRecognitionTaskFragment : Fragment(R.layout.add_recognition_task_fragme } private fun setupImageUploadPanel() = with (mBinding) { - mImagesAdapter = ImageSliderAdapter() + mGlide = GlideApp.with(this@AddRecognitionTaskFragment) + mImagesAdapter = ImageSliderAdapter { + mGlide.load(it.toUri()) + .error(R.drawable.ic_error) + } imageSlider.adapter = mImagesAdapter mViewModel.images observe { uris -> - uris.mapNotNull { - it.value.toBitmap( - requireContext(), - imageSlider.layoutParams.width, - imageSlider.layoutParams.height, - )?.let { image -> - it.map { uri -> - uri.toString() to image - } - } - }.also { - mImagesAdapter.submitList(it) - } + mImagesAdapter.submitList(uris) val hasUris = uris.isNotEmpty() addImageButtonAlternative.isVisible = !hasUris imageSlider.isVisible = hasUris diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt index 6ac7eb5..784d882 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AuthFragment.kt @@ -13,13 +13,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.wada811.databinding.dataBinding -import kotlinx.coroutines.flow.collectLatest import lab.maxb.dark.NavGraphDirections import lab.maxb.dark.R import lab.maxb.dark.databinding.AuthFragmentBinding import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.domain.operations.unicname -import lab.maxb.dark.presentation.extra.launchRepeatingOnLifecycle import lab.maxb.dark.presentation.extra.navigate import lab.maxb.dark.presentation.extra.observe import lab.maxb.dark.presentation.extra.setPasswordVisibility @@ -73,20 +71,21 @@ class AuthFragment : Fragment(R.layout.auth_fragment) { }?.let { return@setOnClickListener } mViewModel.isLoading.value = true - launchRepeatingOnLifecycle { - mViewModel.authorize() - mViewModel.profile.collectLatest { state -> - state.ifLoaded { - val message = if (mViewModel.isAccountNew.value) - R.string.auth_message_signup_incorrectCredentials - else - R.string.auth_message_login_incorrectCredentials - handleResult(message, it) - } - } - } + mViewModel.authorize() } + mViewModel.profile observe { state -> + if (!mViewModel.isLoading.value) + return@observe + + state.ifLoaded { + val message = if (mViewModel.isAccountNew.value) + R.string.auth_message_signup_incorrectCredentials + else + R.string.auth_message_login_incorrectCredentials + handleResult(message, it) + } + } // mBinding.googleSignIn.setOnClickListener { // mGoogleSignInPage.open(mGoogleSignInLogic.signInIntent) // } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt index 0867a30..c1965ed 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/AuthHandleFragment.kt @@ -21,9 +21,10 @@ class AuthHandleFragment : Fragment(R.layout.auth_handle_fragment) { super.onViewCreated(view, savedInstanceState) mViewModel.profile observe { it.ifLoaded { profile -> - if (profile == null) + if (profile == null) { + mViewModel.handleNotAuthorizedYet() NavGraphDirections.navToAuthFragment().navigate() - else + } else NavGraphDirections.navToMainFragment().navigate() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt index 071e60e..c0deab0 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/MainActivity.kt @@ -77,8 +77,8 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), binding.navView.setNavigationItemSelectedListener(this) // Logic - navController.addOnDestinationChangedListener { _, destionation, _ -> - val inAuthZone = destionation.id in authDestinations + navController.addOnDestinationChangedListener { _, destination, _ -> + val inAuthZone = destination.id in authDestinations binding.drawerLayout.setDrawerLockMode( if (!inAuthZone) DrawerLayout.LOCK_MODE_UNLOCKED else DrawerLayout.LOCK_MODE_LOCKED_CLOSED @@ -90,14 +90,12 @@ class MainActivity : AppCompatActivity(R.layout.main_activity), it.ifLoaded { profile -> if (profile == null && navController.currentDestination?.id != R.id.nav_auth_fragment) navController.navigate(NavGraphDirections.navToAuthFragment()) + + authViewModel.handleAuthorizedStateChanges() } } } - override fun onSupportNavigateUp(): Boolean { - return super.onSupportNavigateUp() - } - override fun onBackPressed(): Unit = with(binding.drawerLayout) { if (isDrawerOpen(GravityCompat.START)) closeDrawer(GravityCompat.START) diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt index aae852b..b05ab3c 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/RecognitionTaskListFragment.kt @@ -1,13 +1,16 @@ package lab.maxb.dark.presentation.view +import android.graphics.drawable.AnimatedVectorDrawable import android.os.Bundle import android.view.View +import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import lab.maxb.dark.R import lab.maxb.dark.databinding.RecognitionTaskListFragmentBinding import lab.maxb.dark.domain.model.RecognitionTask +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.delegates.viewBinding import lab.maxb.dark.presentation.extra.navigate @@ -21,18 +24,31 @@ class RecognitionTaskListFragment : Fragment(R.layout.recognition_task_list_frag private val mViewModel: RecognitionTaskListViewModel by sharedViewModel() private val mBinding: RecognitionTaskListFragmentBinding by viewBinding() private var mAdapter: RecognitionTaskListAdapter by autoCleaned() + private var mPlaceholder: AnimatedVectorDrawable by autoCleaned() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) mViewModel.isTaskCreationAllowed observe { mBinding.fab.isVisible = it } - mBinding.fab.setOnClickListener { v -> + mBinding.fab.setOnClickListener { RecognitionTaskListFragmentDirections.navToAddTaskFragment().navigate() } - - mAdapter = RecognitionTaskListAdapter() + mPlaceholder = AppCompatResources.getDrawable( + requireContext(), + R.drawable.loading_vector + ) as AnimatedVectorDrawable + mPlaceholder.start() + mAdapter = RecognitionTaskListAdapter(GlideApp.with(this), + !mViewModel.isTaskCreationAllowed.value + ) { + load(mViewModel.getImage(it)) + .transition(withCrossFade()) + .placeholder(mPlaceholder) + .error(R.drawable.ic_error) + } mBinding.recognitionTaskListRecycler.adapter = mAdapter + mBinding.recognitionTaskListRecycler.addOnScrollListener(mAdapter.preloader.raw) mAdapter.onElementClickListener = { _: View, task: RecognitionTask -> RecognitionTaskListFragmentDirections.navToSolveTaskFragment( task.id diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt index 100e458..5d76bfb 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/SolveRecognitionTaskFragment.kt @@ -1,40 +1,52 @@ package lab.maxb.dark.presentation.view import android.content.Intent +import android.graphics.drawable.AnimatedVectorDrawable import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.widget.Toast -import androidx.core.net.toUri +import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs +import com.bumptech.glide.RequestManager import com.wada811.databinding.dataBinding import lab.maxb.dark.R import lab.maxb.dark.databinding.SolveRecognitionTaskFragmentBinding +import lab.maxb.dark.presentation.extra.GlideApp import lab.maxb.dark.presentation.extra.delegates.autoCleaned import lab.maxb.dark.presentation.extra.goBack import lab.maxb.dark.presentation.extra.launchRepeatingOnLifecycle import lab.maxb.dark.presentation.extra.observe -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.ImageSliderAdapter import lab.maxb.dark.presentation.viewModel.SolveRecognitionTaskViewModel import lab.maxb.dark.presentation.viewModel.utils.ItemHolder import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import java.util.* class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fragment) { private val mViewModel: SolveRecognitionTaskViewModel by sharedViewModel() private val mBinding: SolveRecognitionTaskFragmentBinding by dataBinding() private var mAdapter: ImageSliderAdapter by autoCleaned() + private var mGlide: RequestManager by autoCleaned() + private var mPlaceholder: AnimatedVectorDrawable by autoCleaned() private val args: SolveRecognitionTaskFragmentArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(mBinding) { super.onViewCreated(view, savedInstanceState) mViewModel.init(args.id) data = mViewModel - mAdapter = ImageSliderAdapter() + mGlide = GlideApp.with(this@SolveRecognitionTaskFragment) + mPlaceholder = getDrawable(requireContext(), R.drawable.loading_vector) as AnimatedVectorDrawable + mPlaceholder.start() + mAdapter = ImageSliderAdapter { + mGlide.load(mViewModel.getImage(it)) + .placeholder(mPlaceholder) + .error(R.drawable.ic_error) + } imageSlider.adapter = mAdapter checkAnswer.setOnClickListener { launchRepeatingOnLifecycle { @@ -50,14 +62,9 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr goBack() return@ifLoaded } - (task.images ?: listOf()).mapNotNull { image -> - image.path.toUri().toBitmap( - requireContext(), - imageSlider.layoutParams.width, - imageSlider.layoutParams.height, - )?.let { content -> - ItemHolder(image.path to content) - } + + (task.images ?: listOf()).map { image -> + ItemHolder(image, image) }.run { mAdapter.submitList(this) } mViewModel.isReviewMode observe { @@ -83,8 +90,8 @@ class SolveRecognitionTaskFragment : Fragment(R.layout.solve_recognition_task_fr } private fun mark(isAllowed: Boolean) = launchRepeatingOnLifecycle { - mViewModel.mark(isAllowed) - goBack() + if (mViewModel.mark(isAllowed)) + goBack() } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt new file mode 100644 index 0000000..394b6d3 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImagePreloader.kt @@ -0,0 +1,36 @@ +package lab.maxb.dark.presentation.view.adapter + +import com.bumptech.glide.ListPreloader +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager +import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader +import com.bumptech.glide.util.ViewPreloadSizeProvider +import java.util.* + +class ImagePreloader( + private val manager: RequestManager, + private val getItem: (Int) -> String?, + private val getImageLoader: RequestManager.(String) -> RequestBuilder<*>, + maxPreload: Int = 10, +) { + private val sizeProvider = ViewPreloadSizeProvider() + private val modelProvider = MyPreloadModelProvider() + val raw = RecyclerViewPreloader( + manager, + modelProvider, + sizeProvider, + maxPreload, + ) + + private inner class MyPreloadModelProvider : ListPreloader.PreloadModelProvider { + override fun getPreloadItems(position: Int): MutableList { + val url = getItem(position) + return if (url.isNullOrEmpty()) { + Collections.emptyList() + } else Collections.singletonList(url) + } + + override fun getPreloadRequestBuilder(item: String): RequestBuilder<*> + = getImageLoader(manager, item) + } +} \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt index bdfc5b9..6a80884 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/ImageSliderAdapter.kt @@ -1,19 +1,19 @@ package lab.maxb.dark.presentation.view.adapter -import android.graphics.Bitmap import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.RequestBuilder import lab.maxb.dark.databinding.ImageElementBinding import lab.maxb.dark.presentation.viewModel.utils.ItemHolder -typealias ComparableImage = Pair -open class ImageSliderAdapter : ListAdapter, - ImageSliderAdapter.ImageSliderViewHolder>(DiffCallback) { +open class ImageSliderAdapter( + private val getImageLoader: (String) -> RequestBuilder<*>, +) : ListAdapter, + ImageSliderAdapter.ImageSliderViewHolder>(stringHolderDiffCallback) { + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -28,20 +28,9 @@ open class ImageSliderAdapter : ListAdapter, holder: ImageSliderAdapter.ImageSliderViewHolder, position: Int ) { - val item = getItem(position) - holder.binding.imageContent.setImageBitmap(item.value.second) + getImageLoader(getItem(position).value).into(holder.binding.imageContent) } inner class ImageSliderViewHolder(var binding: ImageElementBinding) : RecyclerView.ViewHolder(binding.root) - - companion object { - private val DiffCallback = object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) - = oldItem.value.first == newItem.value.first - }.let { AsyncDifferConfig.Builder(it).build() } - } } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt index 75acdfd..70542a6 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/InputListAdapter.kt @@ -4,8 +4,6 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.EditText import androidx.core.widget.doOnTextChanged -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -14,7 +12,7 @@ import lab.maxb.dark.presentation.viewModel.utils.ItemHolder class InputListAdapter : - ListAdapter, InputListAdapter.ViewHolder>(DiffCallback) { + ListAdapter, InputListAdapter.ViewHolder>(stringHolderDiffCallback) { var onItemTextChangedListener: ((input: EditText, position: Int, newValue: String?) -> Unit)? = null @@ -34,26 +32,24 @@ class InputListAdapter : with (holder.binding.input){ setText(item.value) doOnTextChanged { text, _, _, _ -> - if (holder.adapterPosition != NO_POSITION) - onItemTextChangedListener?.invoke(this, holder.adapterPosition, text?.toString()) + if (holder.absoluteAdapterPosition != NO_POSITION) + onItemTextChangedListener?.invoke( + this, + holder.absoluteAdapterPosition, + text?.toString() + ) } setOnFocusChangeListener { _, hasFocus -> - if (holder.adapterPosition != NO_POSITION) - onItemFocusedListener?.invoke(this, holder.adapterPosition, hasFocus) + if (holder.absoluteAdapterPosition != NO_POSITION) + onItemFocusedListener?.invoke( + this, + holder.absoluteAdapterPosition, + hasFocus + ) } } } inner class ViewHolder(var binding: InputListItemBinding): RecyclerView.ViewHolder(binding.root) - - companion object { - private val DiffCallback = object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) = - oldItem.value == newItem.value - }.let { AsyncDifferConfig.Builder(it).build() } - } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt index b3225df..ae71936 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/RecognitionTaskListAdapter.kt @@ -1,19 +1,30 @@ package lab.maxb.dark.presentation.view.adapter -import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.RequestManager import lab.maxb.dark.databinding.RecognitionTaskListElementBinding import lab.maxb.dark.domain.model.RecognitionTask -import lab.maxb.dark.presentation.extra.toBitmap import lab.maxb.dark.presentation.view.adapter.RecognitionTaskListAdapter.TaskViewHolder -class RecognitionTaskListAdapter: - PagingDataAdapter(COMPARATOR) { +class RecognitionTaskListAdapter( + private val manager: RequestManager, + private val accentNonReviewed: Boolean, + private val getImageLoader: RequestManager.(String) -> RequestBuilder<*>, +): PagingDataAdapter(COMPARATOR) { + + val preloader = ImagePreloader( + manager, + getItem = { + getItem(it)?.images?.firstOrNull() + }, + getImageLoader = getImageLoader + ) companion object { private val COMPARATOR = object : DiffUtil.ItemCallback() { @@ -21,10 +32,9 @@ class RecognitionTaskListAdapter: oldItem.id == newItem.id override fun areContentsTheSame(oldItem: RecognitionTask, newItem: RecognitionTask): Boolean = - oldItem.id == newItem.id && oldItem.owner?.id == newItem.owner?.id && oldItem.owner?.name == newItem.owner?.name && - oldItem.images?.firstOrNull()?.id == newItem.images?.firstOrNull()?.id + oldItem.images?.firstOrNull() == newItem.images?.firstOrNull() } } @@ -40,20 +50,16 @@ class RecognitionTaskListAdapter: var onElementClickListener: ((view: View, item: RecognitionTask) -> Unit)? = null - override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { - val item = getItem(position) ?: return - holder.binding.taskOwnerName.text = item.owner?.name - try { - holder.binding.taskImage.setImageBitmap( - Uri.parse(item.images?.get(0)?.path).toBitmap( - holder.itemView.context, - holder.binding.taskImage.layoutParams.width, - holder.binding.taskImage.layoutParams.height, - ) - ) - } catch (ignored: Throwable) {} - holder.itemView.setOnClickListener { v -> - if (position != RecyclerView.NO_POSITION) + override fun onBindViewHolder(holder: TaskViewHolder, position: Int) = with(holder.binding) { + val item = getItem(position) + taskOwnerName.text = item?.owner?.name ?: "" + if (accentNonReviewed) + root.alpha = if (item?.reviewed != true) 1f else 0.75f + item?.images?.firstOrNull()?.let { + getImageLoader(manager, it).into(taskImage) + } ?: manager.clear(taskImage) + root.setOnClickListener { v -> + if (position != RecyclerView.NO_POSITION && item != null) onElementClickListener?.invoke(v, item) } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt new file mode 100644 index 0000000..29d5560 --- /dev/null +++ b/app/src/main/java/lab/maxb/dark/presentation/view/adapter/StringHolderDiffchecker.kt @@ -0,0 +1,13 @@ +package lab.maxb.dark.presentation.view.adapter + +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import lab.maxb.dark.presentation.viewModel.utils.ItemHolder + +val stringHolderDiffCallback = object : DiffUtil.ItemCallback>() { + override fun areItemsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + = oldItem.id == newItem.id + + override fun areContentsTheSame(oldItem: ItemHolder, newItem: ItemHolder) + = oldItem.value == newItem.value +}.let { AsyncDifferConfig.Builder(it).build() } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt index 5fc0600..0f1978f 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AddRecognitionTaskViewModel.kt @@ -15,8 +15,8 @@ import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.repository.interfaces.SynonymsRepository import lab.maxb.dark.presentation.viewModel.utils.ItemHolder -import lab.maxb.dark.presentation.viewModel.utils.asItemHolder import lab.maxb.dark.presentation.viewModel.utils.firstNotNull +import lab.maxb.dark.presentation.viewModel.utils.map import lab.maxb.dark.presentation.viewModel.utils.stateIn import org.koin.android.annotation.KoinViewModel @@ -30,7 +30,7 @@ class AddRecognitionTaskViewModel( private val _namesRaw = mutableListOf(ItemHolder("")) private val _names = MutableStateFlow(_namesRaw.toList()) val names = _names.asStateFlow() - private var _imagesRaw = mutableListOf>() + private var _imagesRaw = mutableListOf>() private val _images = MutableStateFlow(_imagesRaw.toList()) val images = _images.asStateFlow() @@ -50,7 +50,7 @@ class AddRecognitionTaskViewModel( val user = profile.firstNotNull().user!! val task = createRecognitionTask( _namesRaw.map { it.value }.filter { it.isNotBlank() }, - _imagesRaw.map { it.value.toString() }, + _imagesRaw.map { it.value }, user)!! recognitionTasksRepository.addRecognitionTask(task) true @@ -108,14 +108,14 @@ class AddRecognitionTaskViewModel( if (!allowImageAddition) return@forEach it.takePersistablePermission(getApplication()) - _imagesRaw.add(it.asItemHolder()) + _imagesRaw.add(ItemHolder(it.toString())) } updateImages() } fun updateImage(position: Int, uri: Uri) { uri.takePersistablePermission(getApplication()) - _imagesRaw[position].value = uri + _imagesRaw[position] = _imagesRaw[position].map(uri.toString()) updateImages() } } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt index ce0873e..0dcf312 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/AuthViewModel.kt @@ -3,9 +3,8 @@ package lab.maxb.dark.presentation.viewModel import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withTimeout +import lab.maxb.dark.domain.model.AuthCredentials import lab.maxb.dark.domain.model.Profile import lab.maxb.dark.presentation.extra.UserSettings import lab.maxb.dark.presentation.extra.launch @@ -16,7 +15,6 @@ import lab.maxb.dark.presentation.viewModel.utils.UiState import lab.maxb.dark.presentation.viewModel.utils.stateIn import lab.maxb.dark.presentation.viewModel.utils.valueOrNull import org.koin.android.annotation.KoinViewModel -import java.time.Duration @OptIn(ExperimentalCoroutinesApi::class) @@ -28,6 +26,7 @@ class AuthViewModel( private val userSettings: UserSettings, // private val mGoogleSignInLogic: GoogleSignInLogic, ) : ViewModel() { + private var _wasAuthorized = false private val _profile = MutableStateFlow(UiState.Loading as UiState) val profile = _profile.stateIn(UiState.Loading) @@ -56,18 +55,29 @@ class AuthViewModel( } } - suspend fun authorize() { - try { - _profile.value = UiState.Loading - withTimeout(Duration.ofSeconds(10).toMillis()) { - profileRepository.sendCredentials(login.value, password.value, isAccountNew.value) - } - } catch (e: Throwable) { - _profile.value = UiState.Error(e) - println(e) + fun handleNotAuthorizedYet() { + _profile.value = UiState.Loading + } + + fun handleAuthorizedStateChanges() = _profile.value.ifLoaded { + if (it != null) + _wasAuthorized = true + else if (_wasAuthorized) { + _wasAuthorized = false + signOut() } } + fun authorize() { + _profile.value = UiState.Loading + profileRepository.sendCredentials(AuthCredentials( + login.value, + password.value, + isAccountNew.value, + )) + } + + @Suppress("unused", "UNUSED_PARAMETER") fun authorizeByOAUTHProvider(login: String, name: String, authCode: String) { TODO() } diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt index 8b0fa05..5fe6817 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/RecognitionTaskListViewModel.kt @@ -5,9 +5,11 @@ import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn import androidx.paging.filter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapLatest import lab.maxb.dark.domain.model.isUser +import lab.maxb.dark.presentation.extra.launch import lab.maxb.dark.presentation.repository.interfaces.ProfileRepository import lab.maxb.dark.presentation.repository.interfaces.RecognitionTasksRepository import lab.maxb.dark.presentation.viewModel.utils.stateIn @@ -15,11 +17,17 @@ import org.koin.android.annotation.KoinViewModel @KoinViewModel class RecognitionTaskListViewModel( - recognitionTasksRepository: RecognitionTasksRepository, + private val recognitionTasksRepository: RecognitionTasksRepository, profileRepository: ProfileRepository, ) : ViewModel() { private val profile = profileRepository.profileState + init { + launch { + profile.buffer() + } + } + @OptIn(ExperimentalCoroutinesApi::class) val recognitionTaskList = recognitionTasksRepository .getAllRecognitionTasks().mapLatest { page -> @@ -32,4 +40,6 @@ class RecognitionTaskListViewModel( val isTaskCreationAllowed = profile.mapLatest { it?.role?.isUser ?: false }.stateIn(false) + + fun getImage(path: String) = recognitionTasksRepository.getRecognitionTaskImage(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt index 828f6d2..3c60fcc 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/SolveRecognitionTaskViewModel.kt @@ -47,22 +47,22 @@ class SolveRecognitionTaskViewModel( } }.stateIn(false) - suspend fun mark(isAllowed: Boolean) { - (getCurrentTask() ?: return).apply { - reviewed = isAllowed - }.also { + suspend fun mark(isAllowed: Boolean) = + getCurrentTask()?.let { + it.reviewed = isAllowed recognitionTasksRepository.markRecognitionTask(it) - } - } + } ?: false suspend fun solveRecognitionTask() = getCurrentTask()?.let { recognitionTasksRepository.solveRecognitionTask( it.id, answer.firstOrNull() ?: "" ).also { result -> if (result) { - usersRepository.getUser(profile.firstOrNull()!!.user!!.id, true).firstOrNull() - recognitionTasksRepository.getRecognitionTask(it.id, true).firstOrNull() + usersRepository.refresh(profile.firstOrNull()!!.user!!.id) + recognitionTasksRepository.refresh(it.id) } } } ?: false + + fun getImage(path: String) = recognitionTasksRepository.getRecognitionTaskImage(path) } \ No newline at end of file diff --git a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt index bd30c31..5b1def4 100644 --- a/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt +++ b/app/src/main/java/lab/maxb/dark/presentation/viewModel/utils/ItemHolder.kt @@ -1,17 +1,13 @@ package lab.maxb.dark.presentation.viewModel.utils +import lab.maxb.dark.domain.operations.randomUUID -import java.util.* - -data class ItemHolder( +class ItemHolder( var value: T, - val id: UUID = UUID.randomUUID(), + val id: String = randomUUID, ) -fun T.asItemHolder() - = ItemHolder(this) - fun ItemHolder.map(value: R) = ItemHolder(value, id) inline fun ItemHolder.map(block: (T) -> R) - = map(block(value)) \ No newline at end of file + = map(block(value)) diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 0000000..cab3486 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/loading_vector.xml b/app/src/main/res/drawable/loading_vector.xml new file mode 100644 index 0000000..96fea0c --- /dev/null +++ b/app/src/main/res/drawable/loading_vector.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index c7c77ee..c2aec14 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -31,5 +31,4 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/welcome_label" /> - \ No newline at end of file diff --git a/app/src/main/res/values/string_darwable_consts.xml b/app/src/main/res/values/string_darwable_consts.xml new file mode 100644 index 0000000..f6091c8 --- /dev/null +++ b/app/src/main/res/values/string_darwable_consts.xml @@ -0,0 +1,4 @@ + + + M0,0h12v12h-12z + \ No newline at end of file diff --git a/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt b/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt index 7078400..ce15a72 100644 --- a/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt +++ b/app/src/test/java/lab/maxb/dark/domain/operations/RecognitionTaskOperationsKtTest.kt @@ -1,7 +1,8 @@ package lab.maxb.dark.domain.operations import lab.maxb.dark.domain.model.User -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test