diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/DataModel.g.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/DataModel.g.kt index 1003358..46e7e5c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/DataModel.g.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/DataModel.g.kt @@ -112,18 +112,6 @@ enum class SnippetFilterType(val raw: Int) { } } -enum class UserReaction(val raw: Int) { - NONE(0), - LIKE(1), - DISLIKE(2); - - companion object { - fun ofRaw(raw: Int): UserReaction? { - return values().firstOrNull { it.raw == raw } - } - } -} - enum class ModelState(val raw: Int) { LOADING(0), LOADED(1), @@ -177,16 +165,9 @@ data class Snippet ( val title: String? = null, val code: SnippetCode? = null, val language: SnippetLanguage? = null, - val owner: Owner? = null, - val isOwner: Boolean? = null, val timeAgo: String? = null, - val voteResult: Long? = null, - val userReaction: UserReaction? = null, - val isPrivate: Boolean? = null, - val isLiked: Boolean? = null, - val isDisliked: Boolean? = null, - val isSaved: Boolean? = null, - val isToDelete: Boolean? = null + val isHidden: Boolean? = null, + val isFavorite: Boolean? = null ) { companion object { @@ -195,17 +176,10 @@ data class Snippet ( val title = pigeonVar_list[1] as String? val code = pigeonVar_list[2] as SnippetCode? val language = pigeonVar_list[3] as SnippetLanguage? - val owner = pigeonVar_list[4] as Owner? - val isOwner = pigeonVar_list[5] as Boolean? - val timeAgo = pigeonVar_list[6] as String? - val voteResult = pigeonVar_list[7] as Long? - val userReaction = pigeonVar_list[8] as UserReaction? - val isPrivate = pigeonVar_list[9] as Boolean? - val isLiked = pigeonVar_list[10] as Boolean? - val isDisliked = pigeonVar_list[11] as Boolean? - val isSaved = pigeonVar_list[12] as Boolean? - val isToDelete = pigeonVar_list[13] as Boolean? - return Snippet(uuid, title, code, language, owner, isOwner, timeAgo, voteResult, userReaction, isPrivate, isLiked, isDisliked, isSaved, isToDelete) + val timeAgo = pigeonVar_list[4] as String? + val isHidden = pigeonVar_list[5] as Boolean? + val isFavorite = pigeonVar_list[6] as Boolean? + return Snippet(uuid, title, code, language, timeAgo, isHidden, isFavorite) } } fun toList(): List { @@ -214,16 +188,9 @@ data class Snippet ( title, code, language, - owner, - isOwner, timeAgo, - voteResult, - userReaction, - isPrivate, - isLiked, - isDisliked, - isSaved, - isToDelete, + isHidden, + isFavorite, ) } } @@ -294,27 +261,6 @@ data class SnippetLanguage ( } } -/** Generated class from Pigeon that represents data sent in messages. */ -data class Owner ( - val id: Long? = null, - val login: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): Owner { - val id = pigeonVar_list[0] as Long? - val login = pigeonVar_list[1] as String? - return Owner(id, login) - } - } - fun toList(): List { - return listOf( - id, - login, - ) - } -} - /** Generated class from Pigeon that represents data sent in messages. */ data class SnippetFilter ( val languages: List? = null, @@ -503,86 +449,76 @@ private open class DataModelPigeonCodec : StandardMessageCodec() { } } 131.toByte() -> { - return (readValue(buffer) as Long?)?.let { - UserReaction.ofRaw(it.toInt()) - } - } - 132.toByte() -> { return (readValue(buffer) as Long?)?.let { ModelState.ofRaw(it.toInt()) } } - 133.toByte() -> { + 132.toByte() -> { return (readValue(buffer) as Long?)?.let { MainModelEvent.ofRaw(it.toInt()) } } - 134.toByte() -> { + 133.toByte() -> { return (readValue(buffer) as Long?)?.let { DetailsModelEvent.ofRaw(it.toInt()) } } - 135.toByte() -> { + 134.toByte() -> { return (readValue(buffer) as Long?)?.let { LoginModelEvent.ofRaw(it.toInt()) } } - 136.toByte() -> { + 135.toByte() -> { return (readValue(buffer) as? List)?.let { Snippet.fromList(it) } } - 137.toByte() -> { + 136.toByte() -> { return (readValue(buffer) as? List)?.let { SnippetCode.fromList(it) } } - 138.toByte() -> { + 137.toByte() -> { return (readValue(buffer) as? List)?.let { SyntaxToken.fromList(it) } } - 139.toByte() -> { + 138.toByte() -> { return (readValue(buffer) as? List)?.let { SnippetLanguage.fromList(it) } } - 140.toByte() -> { - return (readValue(buffer) as? List)?.let { - Owner.fromList(it) - } - } - 141.toByte() -> { + 139.toByte() -> { return (readValue(buffer) as? List)?.let { SnippetFilter.fromList(it) } } - 142.toByte() -> { + 140.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelStateData.fromList(it) } } - 143.toByte() -> { + 141.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelEventData.fromList(it) } } - 144.toByte() -> { + 142.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelStateData.fromList(it) } } - 145.toByte() -> { + 143.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelEventData.fromList(it) } } - 146.toByte() -> { + 144.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelStateData.fromList(it) } } - 147.toByte() -> { + 145.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelEventData.fromList(it) } @@ -600,72 +536,64 @@ private open class DataModelPigeonCodec : StandardMessageCodec() { stream.write(130) writeValue(stream, value.raw) } - is UserReaction -> { - stream.write(131) - writeValue(stream, value.raw) - } is ModelState -> { - stream.write(132) + stream.write(131) writeValue(stream, value.raw) } is MainModelEvent -> { - stream.write(133) + stream.write(132) writeValue(stream, value.raw) } is DetailsModelEvent -> { - stream.write(134) + stream.write(133) writeValue(stream, value.raw) } is LoginModelEvent -> { - stream.write(135) + stream.write(134) writeValue(stream, value.raw) } is Snippet -> { - stream.write(136) + stream.write(135) writeValue(stream, value.toList()) } is SnippetCode -> { - stream.write(137) + stream.write(136) writeValue(stream, value.toList()) } is SyntaxToken -> { - stream.write(138) + stream.write(137) writeValue(stream, value.toList()) } is SnippetLanguage -> { - stream.write(139) - writeValue(stream, value.toList()) - } - is Owner -> { - stream.write(140) + stream.write(138) writeValue(stream, value.toList()) } is SnippetFilter -> { - stream.write(141) + stream.write(139) writeValue(stream, value.toList()) } is MainModelStateData -> { - stream.write(142) + stream.write(140) writeValue(stream, value.toList()) } is MainModelEventData -> { - stream.write(143) + stream.write(141) writeValue(stream, value.toList()) } is DetailsModelStateData -> { - stream.write(144) + stream.write(142) writeValue(stream, value.toList()) } is DetailsModelEventData -> { - stream.write(145) + stream.write(143) writeValue(stream, value.toList()) } is LoginModelStateData -> { - stream.write(146) + stream.write(144) writeValue(stream, value.toList()) } is LoginModelEventData -> { - stream.write(147) + stream.write(145) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) @@ -852,6 +780,7 @@ interface ChannelDetailsModel { fun saveImage(image: ByteArray) fun copyToClipboard() fun shareImage(image: ByteArray) + fun changeVisibility(isHidden: Boolean) fun delete() companion object { @@ -965,6 +894,24 @@ interface ChannelDetailsModel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.changeVisibility$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val isHiddenArg = args[0] as Boolean + val wrapped: List = try { + api.changeVisibility(isHiddenArg) + listOf(null) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.delete$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt index 82ae9e5..183de5b 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt @@ -4,19 +4,16 @@ import android.text.Spanned import android.text.format.DateUtils import android.text.style.ForegroundColorSpan import androidx.core.text.getSpans +import dev.snipme.snipmeapp.domain.snippets.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import org.koin.core.component.KoinComponent -import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.domain.snippets.* -import java.util.* +import java.util.Date import dev.snipme.snipmeapp.channel.Snippet as ChannelSnippet import dev.snipme.snipmeapp.channel.SnippetCode as ChannelSnippetCode import dev.snipme.snipmeapp.channel.SnippetLanguage as ChannelSnippetLanguage import dev.snipme.snipmeapp.channel.SnippetLanguageType as ChannelSnippetLanguageType -import dev.snipme.snipmeapp.channel.UserReaction as ChannelUserReaction import dev.snipme.snipmeapp.channel.SyntaxToken as ChannelSyntaxToken -import dev.snipme.snipmeapp.channel.Owner as ChannelOwner abstract class ModelPlugin : FlutterPlugin, KoinComponent { @@ -37,15 +34,8 @@ fun Snippet.toModelData(): ChannelSnippet = title = title, code = code.toModelSnippetCode(), language = language.toModelSnippetLanguage(), - owner = owner.toModelOwner(), - isOwner = isOwner, - voteResult = (numberOfLikes - numberOfDislikes).toLong(), - userReaction = userReaction.toModelUserReaction(), - isLiked = userReaction.toModelReactionState(UserReaction.LIKE), - isDisliked = userReaction.toModelReactionState(UserReaction.DISLIKE), - isPrivate = visibility == SnippetVisibility.PRIVATE, - isSaved = calculateSavedState(isOwner, visibility), - isToDelete = isOwner, + isFavorite = favorite, + isHidden = visibility == SnippetVisibility.HIDDEN, timeAgo = DateUtils.getRelativeTimeSpanString( modifiedAt.time, Date().time, @@ -53,8 +43,6 @@ fun Snippet.toModelData(): ChannelSnippet = ).toString() ) -private fun Owner.toModelOwner() = ChannelOwner(id = id.toLong(), login = login) - private fun SnippetCode.toModelSnippetCode() = ChannelSnippetCode( raw = raw, @@ -69,24 +57,6 @@ private fun SnippetLanguage.toModelSnippetLanguage() = type = ChannelSnippetLanguageType.valueOf(type.name), ) -private fun UserReaction.toModelUserReaction(): ChannelUserReaction = - when (this) { - UserReaction.LIKE -> ChannelUserReaction.LIKE - UserReaction.DISLIKE -> ChannelUserReaction.DISLIKE - else -> ChannelUserReaction.NONE - } - -private fun UserReaction.toModelReactionState(reaction: UserReaction) = - if (this == UserReaction.NONE) null else this == reaction - -private fun calculateSavedState( - isOwner: Boolean, - visibility: SnippetVisibility -): Boolean? { - if (isOwner.not()) return null - return visibility == SnippetVisibility.PRIVATE -} - private fun ForegroundColorSpan.toSyntaxToken(spannable: Spanned) = ChannelSyntaxToken( start = spannable.getSpanStart(this).toLong(), diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModel.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModel.kt index c97cb3f..a15e897 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModel.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModel.kt @@ -10,15 +10,15 @@ import dev.snipme.snipmeapp.domain.error.exception.NetworkNotAvailableException import dev.snipme.snipmeapp.domain.error.exception.NotAuthorizedException import dev.snipme.snipmeapp.domain.error.exception.RemoteException import dev.snipme.snipmeapp.domain.error.exception.SessionExpiredException +import dev.snipme.snipmeapp.domain.favorite.SetFavoriteSnippet import dev.snipme.snipmeapp.domain.message.ErrorMessages -import dev.snipme.snipmeapp.domain.reaction.GetTargetUserReactionUseCase -import dev.snipme.snipmeapp.domain.reaction.SetUserReactionUseCase -import dev.snipme.snipmeapp.domain.reaction.UserReaction import dev.snipme.snipmeapp.domain.share.ShareSnippetUseCase import dev.snipme.snipmeapp.domain.snippet.DeleteSnippetUseCase import dev.snipme.snipmeapp.domain.snippet.GetSingleSnippetUseCase import dev.snipme.snipmeapp.domain.snippet.SaveSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.UpdateSnippetUseCase import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.subscribeBy @@ -30,11 +30,11 @@ class DetailsModel( private val errorMessages: ErrorMessages, private val getSnippet: GetSingleSnippetUseCase, private val clipboard: AddToClipboardUseCase, - private val getTargetReaction: GetTargetUserReactionUseCase, - private val setUserReaction: SetUserReactionUseCase, + private val setFavorite: SetFavoriteSnippet, private val saveSnippet: SaveSnippetUseCase, private val shareSnippet: ShareSnippetUseCase, private val deleteSnippet: DeleteSnippetUseCase, + private val updateSnippet: UpdateSnippetUseCase, private val session: SessionModel ) : ErrorParsable { private val disposables = CompositeDisposable() @@ -72,7 +72,21 @@ class DetailsModel( } fun toggleFavorite() { - // TODO Implement + getSnippet()?.let { + // Show immediate change in UI + val snippetWithUpdate = (state.value as Loaded).snippet.copy(favorite = !it.favorite) + mutableState.value = (state.value as Loaded).copy(snippet = snippetWithUpdate) + // Update field value in the background + setFavorite(it, !it.favorite) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { setState(Loaded(it)) }, + onError = { + Timber.e("Couldn't toggle favorite, error = $it") + parseError(it) + } + ).also { disposables += it } + } } fun copyToClipboard() { @@ -82,7 +96,6 @@ class DetailsModel( } fun save(image: ByteArray) { - Timber.d("Saving snippet image ${image.size}") try { getSnippet()?.let { saveSnippet(image, it) @@ -97,16 +110,37 @@ class DetailsModel( fun share(image: ByteArray) { try { - getSnippet()?.let { - shareSnippet(image, it) - } - mutableEvent.value = Alert("Snippet shared") + getSnippet()?.let { shareSnippet(image, it) } } catch (e: Exception) { Timber.e("Couldn't share snippet, error = $e") mutableEvent.value = Alert(errorMessages.generic) } } + fun changeVisibility(isHidden: Boolean) { + getSnippet()?.let { + val visibility = if (isHidden) SnippetVisibility.HIDDEN else SnippetVisibility.VISIBLE + val snippetWithUpdate = it.copy(visibility = visibility) + mutableState.value = (state.value as Loaded).copy(snippet = snippetWithUpdate) + updateSnippet( + snippetWithUpdate.uuid, + snippetWithUpdate.title, + snippetWithUpdate.code.raw, + snippetWithUpdate.language.raw, + snippetWithUpdate.visibility, + snippetWithUpdate.favorite + ) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { setState(Loaded(it)) }, + onError = { + Timber.e("Couldn't change visibility, error = $it") + parseError(it) + } + ).also { disposables += it } + } + } + fun delete() { getSnippet()?.let { setState(Loading) @@ -122,25 +156,6 @@ class DetailsModel( } } - private fun changeReaction(newReaction: UserReaction) { - // Immediately show change to user - val previousState = getLoaded() ?: return - val targetReaction = getTargetReaction(previousState.snippet, newReaction) - setState(previousState.run { copy(snippet = snippet.copy(userReaction = targetReaction)) }) - - setUserReaction(previousState.snippet, newReaction) - .subscribeOn(Schedulers.io()) - .subscribeBy( - onSuccess = { snippet -> mutableState.value = Loaded(snippet) }, - onError = { - // Revert changes - Timber.e("Couldn't change user reaction, error = $it") - mutableEvent.value = Alert(errorMessages.generic) - setState(previousState) - } - ).also { disposables += it } - } - private fun getSnippet(): Snippet? = getLoaded()?.snippet private fun getLoaded() = diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt index 55496d6..76f6e73 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt @@ -48,6 +48,10 @@ class DetailsModelPlugin : ModelPlugin(), ChannelDetailsMod model.share(image) } + override fun changeVisibility(isHidden: Boolean) { + model.changeVisibility(isHidden) + } + override fun delete() { model.delete() } diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/main/MainModel.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/main/MainModel.kt index d8263a8..f4b72c5 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/main/MainModel.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/main/MainModel.kt @@ -1,26 +1,39 @@ package dev.snipme.snipmeapp.channel.main +import dev.snipme.snipmeapp.channel.error.ErrorParsable +import dev.snipme.snipmeapp.channel.session.SessionModel +import dev.snipme.snipmeapp.domain.error.exception.ConnectionException +import dev.snipme.snipmeapp.domain.error.exception.ContentNotFoundException +import dev.snipme.snipmeapp.domain.error.exception.ForbiddenActionException +import dev.snipme.snipmeapp.domain.error.exception.NetworkNotAvailableException +import dev.snipme.snipmeapp.domain.error.exception.NotAuthorizedException +import dev.snipme.snipmeapp.domain.error.exception.RemoteException +import dev.snipme.snipmeapp.domain.error.exception.SessionExpiredException +import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByLanguageUseCase +import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByScopeUseCase +import dev.snipme.snipmeapp.domain.filter.GetLanguageFiltersUseCase +import dev.snipme.snipmeapp.domain.filter.SNIPPET_FILTER_ALL +import dev.snipme.snipmeapp.domain.filter.UpdateSnippetFiltersLanguageUseCase +import dev.snipme.snipmeapp.domain.message.ErrorMessages +import dev.snipme.snipmeapp.domain.snippet.ObserveSnippetUpdatesUseCase +import dev.snipme.snipmeapp.domain.snippets.GetSnippetsUseCase +import dev.snipme.snipmeapp.domain.snippets.HasMoreSnippetPagesUseCase +import dev.snipme.snipmeapp.domain.snippets.SetupDemoSnippetsUseCase +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetFilters +import dev.snipme.snipmeapp.domain.snippets.SnippetScope import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow -import dev.snipme.snipmeapp.channel.session.SessionModel -import dev.snipme.snipmeapp.domain.error.exception.* -import dev.snipme.snipmeapp.domain.filter.* -import dev.snipme.snipmeapp.domain.message.ErrorMessages -import dev.snipme.snipmeapp.domain.snippet.ObserveSnippetUpdatesUseCase -import dev.snipme.snipmeapp.domain.snippets.* -import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase -import dev.snipme.snipmeapp.domain.user.User -import dev.snipme.snipmeapp.channel.error.ErrorParsable import timber.log.Timber private const val ONE_PAGE = 1 class MainModel( private val errorMessages: ErrorMessages, - private val getUser: GetSingleUserUseCase, + private val setupDemoSnippets: SetupDemoSnippetsUseCase, private val getSnippets: GetSnippetsUseCase, private val observeUpdates: ObserveSnippetUpdatesUseCase, private val hasMore: HasMoreSnippetPagesUseCase, @@ -47,10 +60,13 @@ class MainModel( is ConnectionException -> mutableState.value = Error(errorMessages.parse(throwable)) is ContentNotFoundException -> mutableState.value = Error(errorMessages.parse(throwable)) + is ForbiddenActionException -> mutableState.value = Error(errorMessages.parse(throwable)) + is NetworkNotAvailableException -> mutableState.value = Error(errorMessages.parse(throwable)) + is NotAuthorizedException -> session.logOut { mutableEvent.value = Logout } is RemoteException -> mutableState.value = Error(errorMessages.parse(throwable)) is SessionExpiredException -> session.logOut { mutableEvent.value = Logout } @@ -72,16 +88,16 @@ class MainModel( filterState = SnippetFilters( languages = listOf(SNIPPET_FILTER_ALL), selectedLanguages = listOf(SNIPPET_FILTER_ALL), - scopes = listOf("All", "Private", "Public"), - selectedScope = "All" + scopes = SnippetScope.entries.map { it.visibleName }, + selectedScope = SnippetScope.ALL.visibleName ) - getUser() + setupDemoSnippets() .subscribeOn(Schedulers.io()) .subscribeBy( - onSuccess = { user -> loadSnippets(user) }, + onComplete = { loadSnippets() }, onError = { - Timber.e("Couldn't load user, error = $it") + Timber.e("Couldn't setup demo snippets, error = $it") parseError(it) } ).also { disposables += it } @@ -120,7 +136,7 @@ class MainModel( .subscribeBy( onSuccess = { hasMore -> if (hasMore) { - loadSnippets(state.user, pages = state.pages + ONE_PAGE) + loadSnippets(pages = state.pages + ONE_PAGE) } }, onError = { @@ -132,11 +148,10 @@ class MainModel( } private fun loadSnippets( - user: User, pages: Int = 1, scope: SnippetScope = SnippetScope.ALL ) { - getSnippets(scope, pages) + getSnippets(scope) .subscribeOn(Schedulers.io()) .subscribeBy( onSuccess = { @@ -144,12 +159,7 @@ class MainModel( scopedSnippets = cachedSnippets val updatedFilters = getLanguageFilters(cachedSnippets) filterState = filterState.copy(languages = updatedFilters) - mutableState.value = Loaded( - user, - it, - pages, - filterState - ) + mutableState.value = Loaded(it, pages, filterState) loadNextPage() }, onError = { @@ -165,7 +175,6 @@ class MainModel( sealed class MainViewState data object Loading : MainViewState() data class Loaded( - val user: User, val snippets: List, val pages: Int, val filters: SnippetFilters diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt b/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt index 001117a..f50c391 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/KoinConfig.kt @@ -2,7 +2,7 @@ package dev.snipme.snipmeapp.di val koinModules = listOf( mapperFilterModule, - preferenceModule, + storageModule, networkModule, serviceModule, repositoryModule, diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt deleted file mode 100644 index 5a63f08..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.snipme.snipmeapp.di - -import org.koin.android.ext.koin.androidContext -import org.koin.dsl.module -import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences -import dev.snipme.snipmeapp.util.PreferencesUtil - -val preferenceModule = module { - single { PreferencesUtil(androidContext()) } - single { AuthPreferences(get()) } -} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt index 5a8e567..1eb8097 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/RepositoryModule.kt @@ -1,6 +1,5 @@ package dev.snipme.snipmeapp.di -import org.koin.dsl.module import dev.snipme.snipmeapp.domain.repository.auth.AuthRepository import dev.snipme.snipmeapp.domain.repository.auth.AuthRepositoryReal import dev.snipme.snipmeapp.domain.repository.language.LanguageRepository @@ -11,11 +10,12 @@ import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepositoryReal import dev.snipme.snipmeapp.domain.repository.user.UserRepository import dev.snipme.snipmeapp.domain.repository.user.UserRepositoryReal +import org.koin.dsl.module internal val repositoryModule = module { single { NetworkStateRepositoryReal() } single { AuthRepositoryReal(get(), get(), get()) } single { UserRepositoryReal(get(), get()) } - single { SnippetRepositoryReal(get(), get(), get()) } + single { SnippetRepositoryReal(get(), get(), get(), get()) } single { LanguageRepositoryReal(get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt index b7c9db7..ca8bc26 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt @@ -1,11 +1,11 @@ package dev.snipme.snipmeapp.di -import androidx.room.Room -import dev.snipme.snipmeapp.infrastructure.local.AppDatabase -import dev.snipme.snipmeapp.infrastructure.local.SnippetDao -import dev.snipme.snipmeapp.infrastructure.local.UserDao +import dev.snipme.snipmeapp.infrastructure.remote.AuthService +import dev.snipme.snipmeapp.infrastructure.remote.LanguageService +import dev.snipme.snipmeapp.infrastructure.remote.ShareService +import dev.snipme.snipmeapp.infrastructure.remote.SnippetService +import dev.snipme.snipmeapp.infrastructure.remote.UserService import org.koin.dsl.module -import dev.snipme.snipmeapp.infrastructure.remote.* import retrofit2.Retrofit internal val serviceModule = module { @@ -14,15 +14,4 @@ internal val serviceModule = module { single { get().create(SnippetService::class.java) } single { get().create(LanguageService::class.java) } single { get().create(ShareService::class.java) } - - - single { - Room.databaseBuilder( - get(), AppDatabase::class.java, "app_database" - ).createFromAsset("app_database.db").build() - } - - single { get().userDao() } - single { get().snippetDao() } - } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/StorageModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/StorageModule.kt new file mode 100644 index 0000000..9802208 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/StorageModule.kt @@ -0,0 +1,22 @@ +package dev.snipme.snipmeapp.di + +import androidx.room.Room +import dev.snipme.snipmeapp.infrastructure.local.AppDatabase +import dev.snipme.snipmeapp.infrastructure.local.AuthPreferences +import dev.snipme.snipmeapp.infrastructure.local.SnippetDao +import dev.snipme.snipmeapp.infrastructure.local.UserDao +import dev.snipme.snipmeapp.util.PreferencesUtil +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val storageModule = module { + single { PreferencesUtil(androidContext()) } + single { AuthPreferences(get()) } + + single { + Room.databaseBuilder(get(), AppDatabase::class.java, "app_database").build() + } + + single { get().userDao() } + single { get().snippetDao() } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt index bec68fc..7909d3d 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -1,22 +1,35 @@ package dev.snipme.snipmeapp.di -import org.koin.dsl.module -import dev.snipme.snipmeapp.domain.auth.* +import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +import dev.snipme.snipmeapp.domain.auth.IdentifyUserUseCase +import dev.snipme.snipmeapp.domain.auth.InitialLoginUseCase +import dev.snipme.snipmeapp.domain.auth.LoginInteractor +import dev.snipme.snipmeapp.domain.auth.LoginUseCase +import dev.snipme.snipmeapp.domain.auth.LogoutUserUseCase +import dev.snipme.snipmeapp.domain.auth.RegisterUseCase import dev.snipme.snipmeapp.domain.clipboard.AddToClipboardUseCase import dev.snipme.snipmeapp.domain.clipboard.GetFromClipboardUseCase -import dev.snipme.snipmeapp.domain.language.GetLanguagesUseCase -import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase -import dev.snipme.snipmeapp.domain.reaction.GetTargetUserReactionUseCase -import dev.snipme.snipmeapp.domain.reaction.SetUserReactionUseCase -import dev.snipme.snipmeapp.domain.share.ShareSnippetUseCase -import dev.snipme.snipmeapp.domain.snippet.* +import dev.snipme.snipmeapp.domain.favorite.SetFavoriteSnippet import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByLanguageUseCase import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByScopeUseCase import dev.snipme.snipmeapp.domain.filter.GetLanguageFiltersUseCase import dev.snipme.snipmeapp.domain.filter.UpdateSnippetFiltersLanguageUseCase +import dev.snipme.snipmeapp.domain.language.GetLanguagesUseCase +import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.share.ShareSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.CreateSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.DeleteSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.EditInteractor +import dev.snipme.snipmeapp.domain.snippet.GetSingleSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.ObserveSnippetUpdatesUseCase +import dev.snipme.snipmeapp.domain.snippet.ObserveUpdatedSnippetPageUseCase +import dev.snipme.snipmeapp.domain.snippet.SaveSnippetUseCase +import dev.snipme.snipmeapp.domain.snippet.UpdateSnippetUseCase import dev.snipme.snipmeapp.domain.snippets.GetSnippetsUseCase import dev.snipme.snipmeapp.domain.snippets.HasMoreSnippetPagesUseCase +import dev.snipme.snipmeapp.domain.snippets.SetupDemoSnippetsUseCase import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import org.koin.dsl.module internal val useCaseModule = module { // Base @@ -31,16 +44,16 @@ internal val useCaseModule = module { // User factory { GetSingleUserUseCase(get(), get(), get(), get()) } // Snippet - factory { GetSnippetsUseCase(get(), get(), get()) } + factory { GetSnippetsUseCase(get()) } factory { GetSingleSnippetUseCase(get(), get(), get()) } factory { HasMoreSnippetPagesUseCase(get(), get(), get()) } factory { CreateSnippetUseCase(get(), get(), get()) } factory { UpdateSnippetUseCase(get(), get(), get()) } factory { ObserveUpdatedSnippetPageUseCase(get()) } factory { ObserveSnippetUpdatesUseCase(get()) } - factory { GetTargetUserReactionUseCase() } - factory { SetUserReactionUseCase(get(), get(), get(), get()) } + factory { SetFavoriteSnippet(get()) } factory { DeleteSnippetUseCase(get()) } + factory { SetupDemoSnippetsUseCase(get()) } // Language factory { GetLanguagesUseCase(get(), get(), get()) } // Share @@ -59,5 +72,5 @@ internal val useCaseModule = module { internal val interactorModule = module { factory { LoginInteractor(get(), get(), get()) } - factory { EditInteractor(get(), get(), get(), get(), get()) } + factory { EditInteractor(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt new file mode 100644 index 0000000..bf3358d --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt @@ -0,0 +1,19 @@ +package dev.snipme.snipmeapp.domain.favorite + +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.domain.snippets.Snippet +import io.reactivex.Single + +class SetFavoriteSnippet( + private val repository: SnippetRepository, +) { + operator fun invoke(snippet: Snippet, favorite: Boolean): Single = + repository.update( + uuid = snippet.uuid, + title = snippet.title, + code = snippet.code.raw, + language = snippet.language.raw, + visibility = snippet.visibility, + favorite = favorite, + ) +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt index 762110b..e07ee93 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/filter/FilterSnippetsByScopeUseCase.kt @@ -6,6 +6,6 @@ class FilterSnippetsByScopeUseCase { operator fun invoke(snippets: List, scope: String): List { if (scope == SNIPPET_FILTER_ALL) return snippets - return snippets.filter { it.isOwner && it.visibility.name.equals(scope, ignoreCase = true) } + return snippets.filter { it.visibility.name.equals(scope, ignoreCase = true) } } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt deleted file mode 100644 index 9547c39..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.snipme.snipmeapp.domain.reaction - -import dev.snipme.snipmeapp.domain.snippets.Snippet - -class GetTargetUserReactionUseCase { - operator fun invoke(snippet: Snippet, reaction: UserReaction) = - when { - snippet.userReaction == reaction -> UserReaction.NONE - else -> reaction - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt deleted file mode 100644 index 855251e..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.snipme.snipmeapp.domain.reaction - -import io.reactivex.Single -import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase -import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository -import dev.snipme.snipmeapp.domain.snippets.Snippet -import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase - -class SetUserReactionUseCase( - private val auth: AuthorizationUseCase, - private val repository: SnippetRepository, - private val getTargetReaction: GetTargetUserReactionUseCase, - private val getSingleUser: GetSingleUserUseCase -) { - operator fun invoke(snippet: Snippet, reaction: UserReaction): Single { - val targetReaction = getTargetReaction(snippet, reaction) - return auth() - .andThen(getSingleUser()) - .flatMap { user -> - repository.reaction(snippet.uuid, user.id, targetReaction) - .andThen(repository.snippet(snippet.uuid, user.id)) - .doOnSuccess { repository.updateListener.onNext(it) } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt deleted file mode 100644 index dc53fac..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.snipme.snipmeapp.domain.reaction - -enum class UserReaction { - DISLIKE, - NONE, - LIKE -} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt index 7790765..db034a4 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt @@ -1,27 +1,29 @@ package dev.snipme.snipmeapp.domain.repository.snippet +import dev.snipme.snipmeapp.domain.snippets.Snippet +import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility import io.reactivex.Completable import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject -import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.domain.snippets.Snippet -import dev.snipme.snipmeapp.domain.snippets.SnippetScope -import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility interface SnippetRepository { val updateListener: BehaviorSubject - fun snippets(userId: Int): Single> + fun getDemoSetupStatus(): Boolean - fun snippet(uuid: String, userId: Int): Single + fun setDemoSetupStatus(status: Boolean): Completable + + fun snippets(): Single> + + fun snippet(uuid: String): Single fun create( title: String, code: String, language: String, visibility: SnippetVisibility, - userId: Int + favorite: Boolean ): Single fun update( @@ -30,12 +32,10 @@ interface SnippetRepository { code: String, language: String, visibility: SnippetVisibility, - userId: Int + favorite: Boolean ): Single fun count(): Single - fun reaction(uuid: String, userId: Int, reaction: UserReaction): Completable - fun delete(uuid: String): Completable } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt index 9d8e86c..b8746d9 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepositoryReal.kt @@ -1,13 +1,12 @@ package dev.snipme.snipmeapp.domain.repository.snippet import dev.snipme.snipmeapp.domain.error.ErrorHandler -import dev.snipme.snipmeapp.domain.reaction.UserReaction import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetResponseMapper import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility -import dev.snipme.snipmeapp.infrastructure.local.ReactionEntry import dev.snipme.snipmeapp.infrastructure.local.SnippetDao import dev.snipme.snipmeapp.infrastructure.local.SnippetEntry +import dev.snipme.snipmeapp.util.PreferencesUtil import dev.snipme.snipmeapp.util.extension.mapError import dev.snipme.snipmeapp.util.extension.mapItems import io.reactivex.Completable @@ -15,24 +14,30 @@ import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import java.util.Date -const val PAGE_START = 0 +const val KEY_DEMO_SETUP_STATUS = "KEY_DEMO_SETUP_STATUS" const val SNIPPET_PAGE_SIZE = 10 -const val ONE_SNIPPET = 1 class SnippetRepositoryReal( private val errorHandler: ErrorHandler, private val service: SnippetDao, + private val preferencesUtil: PreferencesUtil, private val mapper: SnippetResponseMapper ) : SnippetRepository { override val updateListener = BehaviorSubject.create() - override fun snippets(userId: Int): Single> = - service.snippets(userId) + override fun getDemoSetupStatus(): Boolean = + preferencesUtil.get(KEY_DEMO_SETUP_STATUS) ?: false + + override fun setDemoSetupStatus(status: Boolean): Completable = + Completable.fromAction { preferencesUtil.save(KEY_DEMO_SETUP_STATUS, status) } + + override fun snippets(): Single> = + service.snippets() .mapError { errorHandler.handle(it) } .mapItems { mapper(it) } - override fun snippet(uuid: String, userId: Int): Single = - service.snippet(uuid.toInt(), userId).map { mapper(it) } + override fun snippet(uuid: String): Single = + service.snippet(uuid.toInt()).map { mapper(it) } .mapError { errorHandler.handle(it) } override fun create( @@ -40,22 +45,22 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int + favorite: Boolean ): Single { return service.create( SnippetEntry( title = title, code = code, - createdAt = Date().toString(), - modifiedAt = Date().toString(), + createdAt = Date(), + modifiedAt = Date(), visibility = visibility.name, - ownerId = userId, language = language, + favorite = favorite ) ) .mapError { errorHandler.handle(it) } .flatMap { newId -> - service.snippet(newId.toInt(), userId) + service.snippet(newId.toInt()) .mapError { errorHandler.handle(it) } .map { mapper(it) } } @@ -67,12 +72,12 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int + favorite: Boolean ): Single = - service.update(uuid.toInt(), title, code, language, visibility.name) + service.update(uuid.toInt(), title, code, language, visibility.name, favorite) .mapError { errorHandler.handle(it) } .andThen( - service.snippet(uuid.toInt(), userId) + service.snippet(uuid.toInt()) .mapError { errorHandler.handle(it) } .map { mapper(it) } ) @@ -84,8 +89,4 @@ class SnippetRepositoryReal( service.count() .mapError { errorHandler.handle(it) } .map { it } - - override fun reaction(uuid: String, userId: Int, reaction: UserReaction): Completable = - service.reaction(ReactionEntry(snippetId = uuid.toInt(), userId = userId, reaction = reaction.ordinal.toShort())) - .mapError { errorHandler.handle(it) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt index ffc51d2..fe290fb 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt @@ -1,12 +1,11 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Single import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase -import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import io.reactivex.Single class CreateSnippetUseCase( private val auth: AuthorizationUseCase, @@ -17,7 +16,8 @@ class CreateSnippetUseCase( title: String, code: String, language: String, - visibility: SnippetVisibility = SnippetVisibility.PUBLIC + visibility: SnippetVisibility = SnippetVisibility.VISIBLE, + favorite: Boolean = false, ): Single = auth() .andThen(getSingleUser()) .flatMap { user -> @@ -26,7 +26,7 @@ class CreateSnippetUseCase( code = code, language = language, visibility = visibility, - userId = user.id + favorite = favorite ) } .doOnSuccess { snippet -> diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt index 844994d..03a54e2 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/EditInteractor.kt @@ -1,17 +1,15 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Single -import dev.snipme.snipmeapp.domain.clipboard.GetFromClipboardUseCase import dev.snipme.snipmeapp.domain.language.GetLanguagesUseCase import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility +import io.reactivex.Single class EditInteractor( private val getLanguages: GetLanguagesUseCase, private val getSnippet: GetSingleSnippetUseCase, private val createSnippet: CreateSnippetUseCase, private val updateSnippet: UpdateSnippetUseCase, - private val fromClipboard: GetFromClipboardUseCase ) { fun languages() = getLanguages() @@ -26,7 +24,6 @@ class EditInteractor( code: String, language: String, visibility: SnippetVisibility, - ): Single = updateSnippet(uuid, title, code, language, visibility) - - fun getFromClipboard(): String? = fromClipboard() + favorite: Boolean + ): Single = updateSnippet(uuid, title, code, language, visibility, favorite) } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt index 1a6d0e7..8f0e882 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/GetSingleSnippetUseCase.kt @@ -1,11 +1,10 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Single import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase -import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import io.reactivex.Single class GetSingleSnippetUseCase( private val auth: AuthorizationUseCase, @@ -16,5 +15,5 @@ class GetSingleSnippetUseCase( operator fun invoke(uuid: String): Single = auth() .andThen(getSingleUser()) - .flatMap { repository.snippet(uuid, it.id) } + .flatMap { repository.snippet(uuid) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt index 4f4c0b9..4506f71 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/ObserveUpdatedSnippetPageUseCase.kt @@ -1,10 +1,10 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Observable -import io.reactivex.Single import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.Snippet import dev.snipme.snipmeapp.domain.snippets.SnippetScope +import io.reactivex.Observable +import io.reactivex.Single private const val START_PAGE = 1 @@ -14,22 +14,21 @@ class ObserveUpdatedSnippetPageUseCase(private val repository: SnippetRepository repository.updateListener .skipWhile { it == Snippet.EMPTY } .flatMapSingle { updated -> - getPageWithUpdated(scope, updated, START_PAGE, userId) + getPageWithUpdated(scope, updated, START_PAGE) } private fun getPageWithUpdated( scope: SnippetScope, updated: Snippet, page: Int, - userId: Int - ): Single = repository.snippets(userId) + ): Single = repository.snippets() .map { snippets -> snippets.contains(updated.uuid) } .flatMap { contains -> if (contains) { Single.just(page) } else { // Be aware of recursion here - getPageWithUpdated(scope, updated, page + 1, userId) + getPageWithUpdated(scope, updated, page + 1) } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt index ed9c6af..50c480e 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt @@ -1,11 +1,10 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Single import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase -import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import io.reactivex.Single class UpdateSnippetUseCase( private val auth: AuthorizationUseCase, @@ -18,9 +17,10 @@ class UpdateSnippetUseCase( code: String, language: String, visibility: SnippetVisibility, + favorite: Boolean ) = auth() .andThen(getSingleUser()) - .flatMap{repository.update(uuid, title, code, language, visibility, it.id)} + .flatMap { repository.update(uuid, title, code, language, visibility, favorite) } .doOnSuccess() { repository.updateListener.onNext(it) Single.just(it) diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt index 20e0106..35041b3 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetSnippetsUseCase.kt @@ -1,22 +1,22 @@ package dev.snipme.snipmeapp.domain.snippets -import io.reactivex.Single -import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase -import dev.snipme.snipmeapp.domain.network.CheckNetworkAvailableUseCase +import dev.snipme.snipmeapp.domain.filter.SNIPPET_FILTER_ALL import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository -import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import io.reactivex.Single class GetSnippetsUseCase( - private val auth: AuthorizationUseCase, private val repository: SnippetRepository, - private val getSingleUser: GetSingleUserUseCase ) { - operator fun invoke(scope: SnippetScope, page: Int): Single> = - auth() - .andThen(getSingleUser()) - .flatMap { - user -> - repository.snippets(user.id) - .map { list -> list.sortedByDescending { it.modifiedAt.time } } + operator fun invoke(scope: SnippetScope): Single> = + repository.snippets() + .map { + if (scope.visibleName.equals(SNIPPET_FILTER_ALL, ignoreCase = true)) { + return@map it + } else { + it.filter { snippet -> + snippet.visibility.name.lowercase() == scope.visibleName.lowercase() + } + } } + .map { list -> list.sortedByDescending { it.modifiedAt.time } } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt new file mode 100644 index 0000000..2df9a19 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -0,0 +1,198 @@ +package dev.snipme.snipmeapp.domain.snippets + +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import io.reactivex.Completable +import io.reactivex.Single + +class SetupDemoSnippetsUseCase( + private val snippetRepository: SnippetRepository +) { + + operator fun invoke(): Completable { + val demoSetup = snippetRepository.getDemoSetupStatus() + + return if (!demoSetup) { + setupDemoSnippets().andThen( + snippetRepository.setDemoSetupStatus(true) + ) + } else { + Completable.complete() + } + } + + private fun setupDemoSnippets() = Completable.fromPublisher( + Single.merge( + snippetRepository.create( + title = "Your first snippet", + code = KOTLIN_SAMPLE, + language = SnippetLanguageType.KOTLIN.name, + visibility = SnippetVisibility.VISIBLE, + favorite = false + ), + snippetRepository.create( + title = "Your favorite code", + code = JAVASCRIPT_SAMPLE, + language = SnippetLanguageType.JAVASCRIPT.name, + visibility = SnippetVisibility.VISIBLE, + favorite = true + ), + snippetRepository.create( + title = "Hidden one", + code = PYTHON_SAMPLE, + language = SnippetLanguageType.PYTHON.name, + visibility = SnippetVisibility.HIDDEN, + favorite = false + ), + snippetRepository.create( + title = "Popular Java code", + code = JAVA_SAMPLE, + language = SnippetLanguageType.JAVA.name, + visibility = SnippetVisibility.VISIBLE, + favorite = false + ) + ), + ) +} + +private const val KOTLIN_SAMPLE = """ +// Data class +data class User(val id: Int, val name: String, val email: String) + +// Extension function +fun String.isValidEmail(): Boolean { + return this.contains("@") && this.contains(".") +} + +// Higher-order function +fun List.customFilter(predicate: (T) -> Boolean): List { + val result = mutableListOf() + for (item in this) { + if (predicate(item)) { + result.add(item) + } + } + return result +} + +// Sealed class +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() +} + +// Coroutines +import kotlinx.coroutines.* + +fun main() = runBlocking { + val users = listOf( + User(1, "Alice", "alice@example.com"), + User(2, "Bob", "bobexample.com"), // Invalid email + User(3, "Charlie", "charlie@example.com") + ) + + // Using extension function + users.forEach { user -> + println("${'$'}{user.name}'s email is valid: ${'$'}{user.email.isValidEmail()}") + } + + // Using higher-order function + val validUsers = users.customFilter { it.email.isValidEmail() } + println("Valid users: ${'$'}validUsers") + + // Using coroutines + val result = fetchUserData() + when (result) { + is Result.Success -> println("Fetched user data: ${'$'}{result.data}") + is Result.Error -> println("Error fetching user data: ${'$'}{result.exception.message}") + } +} + +// Simulate a network call using coroutines +suspend fun fetchUserData(): Result> { + return withContext(Dispatchers.IO) { + delay(1000) // Simulate network delay + try { + val data = listOf( + User(4, "Dave", "dave@example.com"), + User(5, "Eve", "eve@example.com") + ) + Result.Success(data) + } catch (e: Exception) { + Result.Error(e) + } + } +} +""" + +const val JAVASCRIPT_SAMPLE = """ +// Async function with Promise +async function fetchUserData() { + return new Promise((resolve, reject) => { + setTimeout(() => { + try { + const data = [ + new User(1, 'Alice', 'alice@example.com'), + new User(2, 'Bob', 'bobexample.com'), // Invalid email + new User(3, 'Charlie', 'charlie@example.com') + ]; + resolve(data); + } catch (error) { + reject(error); + } + }, 1000); // Simulate network delay + }); +} +""" + +const val PYTHON_SAMPLE = """ +# Class definition +class User: + def __init__(self, id, name, email): + self.id = id + self.name = name + self.email = email + + def __str__(self): + return f'User(id={self.id}, name={self.name}, email={self.email})' + + # Function with list comprehension + def is_valid_email(email): + return '@' in email and '.' in email + + # Function with generator expression + + def custom_filter(predicate, items): + return [item for item in items if predicate(item)] +""" + +const val JAVA_SAMPLE = """ +// Class definition +public class User { + private final int id; + private final String name; + private final String email; + + public User(int id, String name, String email) { + this.id = id; + this.name = name; + this.email = email; + } + + @Override + public String toString() { + return String.format("User(id=%d, name=%s, email=%s)", id, name, email); + } + + // Function with lambda expression + public static boolean isValidEmail(String email) { + return email.contains("@") && email.contains("."); + } + + // Function with method reference + public static List customFilter(Predicate predicate, List items) { + return items.stream() + .filter(predicate) + .collect(Collectors.toList()); + } +} +""" \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt index c5ade79..1d7a796 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt @@ -1,8 +1,7 @@ package dev.snipme.snipmeapp.domain.snippets import android.text.SpannableString -import dev.snipme.snipmeapp.domain.reaction.UserReaction -import java.util.* +import java.util.Date data class Snippet( val uuid: String, @@ -10,12 +9,8 @@ data class Snippet( val code: SnippetCode, val language: SnippetLanguage, val visibility: SnippetVisibility, - val isOwner: Boolean, - val owner: Owner, val modifiedAt: Date, - val numberOfLikes: Int, - val numberOfDislikes: Int, - val userReaction: UserReaction + val favorite: Boolean, ) { companion object { val EMPTY = Snippet( @@ -23,13 +18,9 @@ data class Snippet( title = "", code = SnippetCode("", SpannableString("")), language = SnippetLanguage("", SnippetLanguageType.UNKNOWN), - visibility = SnippetVisibility.PRIVATE, - isOwner = false, - owner = Owner(0, ""), + visibility = SnippetVisibility.HIDDEN, modifiedAt = Date(), - numberOfLikes = 0, - numberOfDislikes = 0, - userReaction = UserReaction.NONE + favorite = false ) } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt index f999162..6ecdd34 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt @@ -1,42 +1,28 @@ package dev.snipme.snipmeapp.domain.snippets import android.text.SpannableString -import dev.snipme.snipmeapp.domain.reaction.UserReaction -import dev.snipme.snipmeapp.infrastructure.local.SnippetExtended +import dev.snipme.snipmeapp.infrastructure.local.SnippetEntry import dev.snipme.snipmeapp.util.SyntaxHighlighter.getHighlighted import dev.snipme.snipmeapp.util.extension.lines import dev.snipme.snipmeapp.util.extension.newLineChar -import dev.snipme.snipmeapp.util.extension.toDate import dev.snipme.snipmeapp.util.extension.toSnippetLanguage -import java.util.* const val PREVIEW_COUNT = 5 class SnippetResponseMapper { - operator fun invoke(response: SnippetExtended) = with(response.snippet) { + operator fun invoke(response: SnippetEntry) = with(response) { return@with Snippet( uuid = id.toString(), title = title, code = getCode(code), language = getLanguage(language), visibility = getVisibility(visibility), - isOwner = response.isOwner, - owner = Owner(ownerId , response.ownerName), - modifiedAt = modifiedAt.toDate(), - numberOfLikes = response.numberOfLikes, - numberOfDislikes = response.numberOfDislikes, - userReaction = getUserReaction(response.userReaction) + modifiedAt = modifiedAt, + favorite = favorite, ) } - private fun getUserReaction(value: String?) = - when { - value.equals("like", ignoreCase = true) -> UserReaction.LIKE - value.equals("dislike", ignoreCase = true) -> UserReaction.DISLIKE - else -> UserReaction.NONE - } - private fun getCode(code: String) = SnippetCode( raw = code.orEmpty(), highlighted = getPreview(code) @@ -53,7 +39,7 @@ class SnippetResponseMapper { } private fun getVisibility(visibility: String?): SnippetVisibility { - if (visibility == null) return SnippetVisibility.PRIVATE + if (visibility == null) return SnippetVisibility.HIDDEN return SnippetVisibility.valueOf(visibility) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt index be5c944..e367d50 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetScope.kt @@ -1,7 +1,7 @@ package dev.snipme.snipmeapp.domain.snippets -enum class SnippetScope { - ALL, PUBLIC, OWNED, SHARED_FOR; +enum class SnippetScope(val visibleName: String) { + ALL("All"), VISIBLE("Visible"), HIDDEN("Hidden"); } fun SnippetScope.value() = this.name.toLowerCase() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt index ff41c28..dc224d0 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt @@ -1,5 +1,5 @@ package dev.snipme.snipmeapp.domain.snippets enum class SnippetVisibility { - PUBLIC, PRIVATE + VISIBLE, HIDDEN } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt index e77bf49..bbcae84 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/AppDatabase.kt @@ -1,28 +1,26 @@ package dev.snipme.snipmeapp.infrastructure.local + import androidx.room.Database -import androidx.room.Room import androidx.room.RoomDatabase -import android.content.Context +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import java.util.Date + +class DateConverter { + @TypeConverter + fun toDate(timestamp: Long?): Date? { + return timestamp?.let { Date(it) } + } -@Database(entities = [UserEntry::class, SnippetEntry::class, ReactionEntry::class], version = 1) + @TypeConverter + fun toTimestamp(date: Date?): Long? { + return date?.time + } +} + +@Database(entities = [UserEntry::class, SnippetEntry::class], version = 1) +@TypeConverters(DateConverter::class) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun snippetDao(): SnippetDao - - companion object { - @Volatile - private var INSTANCE: AppDatabase? = null - - fun getDatabase(context: Context): AppDatabase { - return INSTANCE ?: synchronized(this) { - val instance = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - "app_database" - ).build() - INSTANCE = instance - instance - } - } - } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt index 8f070fe..0cbe4bd 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetDao.kt @@ -11,32 +11,15 @@ import io.reactivex.Single interface SnippetDao { @Query( """ - SELECT s.*, u.login as ownerName, - CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, - CASE WHEN r.reaction = 0 THEN 'DISLIKE' ELSE CASE WHEN r.reaction = 2 THEN 'LIKE' ELSE 'NONE' END END as userReaction, - (Select Count(*) FROM reactions as r where r.snippetId = :uuid and reaction = 2) as numberOfLikes, - (Select Count(*) FROM reactions as r where r.snippetId = :uuid and reaction = 0) as numberOfDislikes - FROM snippets as s - INNER JOIN users as u ON s.ownerId = u.id - LEFT JOIN reactions as r ON r.userId = :userId and r.snippetId = :uuid - WHERE s.id = :uuid - """ - ) - fun snippet(uuid: Int, userId: Int): Single - - @Query( - """ - SELECT s.*, u.login as ownerName, - CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner, - CASE WHEN r.reaction = 0 THEN "DISLIKE" ELSE CASE WHEN r.reaction = 2 THEN "LIKE" ELSE "NONE" END END as userReaction, - (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 2) as numberOfLikes, - (Select Count(*) FROM reactions as r where r.snippetId = s.id and reaction = 0) as numberOfDislikes - FROM snippets as s - INNER JOIN users as u ON s.ownerId = u.id - LEFT JOIN reactions as r ON r.userId = :userId and r.snippetId = s.id + SELECT s.* + FROM snippets as s + WHERE s.id = :uuid """ ) - fun snippets(userId: Int): Single> + fun snippet(uuid: Int): Single + + @Query("""SELECT s.* FROM snippets as s""") + fun snippets(): Single> @Query("SELECT COUNT(*) FROM snippets") fun count(): Single @@ -51,7 +34,8 @@ interface SnippetDao { code = :code, modifiedAt = current_timestamp, visibility = :visibility, - language = :language + language = :language, + favorite = :favorite WHERE id = :uuid """ ) @@ -61,12 +45,9 @@ interface SnippetDao { code: String, visibility: String, language: String, + favorite: Boolean ): Completable @Query("DELETE FROM snippets WHERE id = :uuid") fun delete(uuid: Int): Completable - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun reaction(reaction: ReactionEntry): Completable - } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt index 425f7c2..bf24fab 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/infrastructure/local/SnippetEntity.kt @@ -1,28 +1,17 @@ package dev.snipme.snipmeapp.infrastructure.local -import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey - +import java.util.Date @Entity(tableName = "snippets") data class SnippetEntry( @PrimaryKey(true) val id: Long = 0, val title: String, val code: String, - val createdAt: String, - val modifiedAt: String, + val createdAt: Date, + val modifiedAt: Date, val visibility: String, - val ownerId: Int, val language: String, -) - - -data class SnippetExtended( - @Embedded val snippet: SnippetEntry, - val ownerName: String, - val isOwner: Boolean, - val userReaction: String, - val numberOfLikes: Int, - val numberOfDislikes: Int, + val favorite: Boolean, ) diff --git a/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt b/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt index 1737799..2918ac5 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/util/extension/TextExtensions.kt @@ -1,8 +1,5 @@ package dev.snipme.snipmeapp.util.extension -import java.text.SimpleDateFormat -import java.util.* - const val newLineChar = "\n" fun CharSequence.containsDefault(other: CharSequence) = @@ -26,12 +23,11 @@ fun Char.isNewLine(): Boolean { fun String.lengthToEOF(start: Int = 0): Int { if (all { it.isNewLine().not() }) return length - start var endIndex = start - while (this.getOrNull(endIndex)?.isNewLine()?.not() == true) { endIndex++ } + while (this.getOrNull(endIndex)?.isNewLine()?.not() == true) { + endIndex++ + } return endIndex - start } -fun String.toDate(): Date { - val iso8061Pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - val sdf = SimpleDateFormat(iso8061Pattern, Locale.getDefault()) - return sdf.parse(this) -} \ No newline at end of file +fun String.titleCase(): String = + this.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 9449d2a..9d34a7e 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -15,16 +15,9 @@ class Snippet { String? title; SnippetCode? code; SnippetLanguage? language; - Owner? owner; - bool? isOwner; String? timeAgo; - int? voteResult; - UserReaction? userReaction; - bool? isPrivate; - bool? isLiked; - bool? isDisliked; - bool? isSaved; - bool? isToDelete; + bool? isHidden; + bool? isFavorite; } class SnippetCode { @@ -43,11 +36,6 @@ class SnippetLanguage { SnippetLanguageType? type; } -class Owner { - int? id; - String? login; -} - enum SnippetLanguageType { c, cpp, @@ -105,8 +93,6 @@ class SnippetFilter { String? selectedScope; } -enum UserReaction { none, like, dislike } - enum ModelState { loading, loaded, error } enum MainModelEvent { none, alert, logout } @@ -191,6 +177,8 @@ abstract class ChannelDetailsModel { void shareImage(Uint8List image); + void changeVisibility(bool isHidden); + void delete(); } diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 39faa95..1fe91a3 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -69,12 +69,6 @@ enum SnippetFilterType { shared, } -enum UserReaction { - none, - like, - dislike, -} - enum ModelState { loading, loaded, @@ -104,16 +98,9 @@ class Snippet { this.title, this.code, this.language, - this.owner, - this.isOwner, this.timeAgo, - this.voteResult, - this.userReaction, - this.isPrivate, - this.isLiked, - this.isDisliked, - this.isSaved, - this.isToDelete, + this.isHidden, + this.isFavorite, }); String? uuid; @@ -124,25 +111,11 @@ class Snippet { SnippetLanguage? language; - Owner? owner; - - bool? isOwner; - String? timeAgo; - int? voteResult; - - UserReaction? userReaction; - - bool? isPrivate; + bool? isHidden; - bool? isLiked; - - bool? isDisliked; - - bool? isSaved; - - bool? isToDelete; + bool? isFavorite; Object encode() { return [ @@ -150,16 +123,9 @@ class Snippet { title, code, language, - owner, - isOwner, timeAgo, - voteResult, - userReaction, - isPrivate, - isLiked, - isDisliked, - isSaved, - isToDelete, + isHidden, + isFavorite, ]; } @@ -170,16 +136,9 @@ class Snippet { title: result[1] as String?, code: result[2] as SnippetCode?, language: result[3] as SnippetLanguage?, - owner: result[4] as Owner?, - isOwner: result[5] as bool?, - timeAgo: result[6] as String?, - voteResult: result[7] as int?, - userReaction: result[8] as UserReaction?, - isPrivate: result[9] as bool?, - isLiked: result[10] as bool?, - isDisliked: result[11] as bool?, - isSaved: result[12] as bool?, - isToDelete: result[13] as bool?, + timeAgo: result[4] as String?, + isHidden: result[5] as bool?, + isFavorite: result[6] as bool?, ); } } @@ -267,32 +226,6 @@ class SnippetLanguage { } } -class Owner { - Owner({ - this.id, - this.login, - }); - - int? id; - - String? login; - - Object encode() { - return [ - id, - login, - ]; - } - - static Owner decode(Object result) { - result as List; - return Owner( - id: result[0] as int?, - login: result[1] as String?, - ); - } -} - class SnippetFilter { SnippetFilter({ this.languages, @@ -525,56 +458,50 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SnippetFilterType) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is UserReaction) { - buffer.putUint8(131); - writeValue(buffer, value.index); } else if (value is ModelState) { - buffer.putUint8(132); + buffer.putUint8(131); writeValue(buffer, value.index); } else if (value is MainModelEvent) { - buffer.putUint8(133); + buffer.putUint8(132); writeValue(buffer, value.index); } else if (value is DetailsModelEvent) { - buffer.putUint8(134); + buffer.putUint8(133); writeValue(buffer, value.index); } else if (value is LoginModelEvent) { - buffer.putUint8(135); + buffer.putUint8(134); writeValue(buffer, value.index); } else if (value is Snippet) { - buffer.putUint8(136); + buffer.putUint8(135); writeValue(buffer, value.encode()); } else if (value is SnippetCode) { - buffer.putUint8(137); + buffer.putUint8(136); writeValue(buffer, value.encode()); } else if (value is SyntaxToken) { - buffer.putUint8(138); + buffer.putUint8(137); writeValue(buffer, value.encode()); } else if (value is SnippetLanguage) { - buffer.putUint8(139); - writeValue(buffer, value.encode()); - } else if (value is Owner) { - buffer.putUint8(140); + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is SnippetFilter) { - buffer.putUint8(141); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is MainModelStateData) { - buffer.putUint8(142); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is MainModelEventData) { - buffer.putUint8(143); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is DetailsModelStateData) { - buffer.putUint8(144); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is DetailsModelEventData) { - buffer.putUint8(145); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else if (value is LoginModelStateData) { - buffer.putUint8(146); + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is LoginModelEventData) { - buffer.putUint8(147); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -591,43 +518,38 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : SnippetFilterType.values[value]; case 131: - final int? value = readValue(buffer) as int?; - return value == null ? null : UserReaction.values[value]; - case 132: final int? value = readValue(buffer) as int?; return value == null ? null : ModelState.values[value]; - case 133: + case 132: final int? value = readValue(buffer) as int?; return value == null ? null : MainModelEvent.values[value]; - case 134: + case 133: final int? value = readValue(buffer) as int?; return value == null ? null : DetailsModelEvent.values[value]; - case 135: + case 134: final int? value = readValue(buffer) as int?; return value == null ? null : LoginModelEvent.values[value]; - case 136: + case 135: return Snippet.decode(readValue(buffer)!); - case 137: + case 136: return SnippetCode.decode(readValue(buffer)!); - case 138: + case 137: return SyntaxToken.decode(readValue(buffer)!); - case 139: + case 138: return SnippetLanguage.decode(readValue(buffer)!); - case 140: - return Owner.decode(readValue(buffer)!); - case 141: + case 139: return SnippetFilter.decode(readValue(buffer)!); - case 142: + case 140: return MainModelStateData.decode(readValue(buffer)!); - case 143: + case 141: return MainModelEventData.decode(readValue(buffer)!); - case 144: + case 142: return DetailsModelStateData.decode(readValue(buffer)!); - case 145: + case 143: return DetailsModelEventData.decode(readValue(buffer)!); - case 146: + case 144: return LoginModelStateData.decode(readValue(buffer)!); - case 147: + case 145: return LoginModelEventData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -929,6 +851,28 @@ class ChannelDetailsModel { } } + Future changeVisibility(bool isHidden) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.changeVisibility$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([isHidden]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + Future delete() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.delete$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index 84da026..39bb9ff 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -105,8 +105,8 @@ class _DetailsPage extends HookConsumerWidget { onPressed: navigator.back, color: Colors.black, ), - actions: stateNotification.data?.isPrivate == true - ? [const PaddingStyles.regular(Icon(Icons.lock_outlined))] + actions: stateNotification.data?.isHidden == true + ? [const PaddingStyles.regular(Icon(Icons.visibility_off_outlined))] : null, ), body: ViewStateWrapper( @@ -183,6 +183,8 @@ class _DetailPageData extends StatelessWidget { onSaveTap: saveImage, onCopyTap: model.copyToClipboard, onShareTap: shareImage, + onHideTap: () => + model.changeVisibility(!(snippet?.isHidden ?? false)), onDeleteTap: model.delete, ), ), diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index cd3318d..1c57d36 100644 --- a/flutter_module/lib/presentation/screens/main_screen.dart +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -77,7 +77,8 @@ class _MainPage extends HookConsumerWidget { return Scaffold( backgroundColor: ColorStyles.pageBackground(), body: ViewStateWrapper>( - isLoading: state == ModelState.loading || stateNotification.isLoading == true, + isLoading: + state == ModelState.loading || stateNotification.isLoading == true, error: stateNotification.error, data: stateNotification.data?.cast(), builder: (_, snippets) { @@ -114,14 +115,15 @@ class _MainPage extends HookConsumerWidget { typedef ExpandChangeListener = Function(bool); class _MainPageData extends HookWidget { - const _MainPageData( - {required this.navigator, - required this.model, - required this.snippets, - required this.filter, - required this.controller, - required this.expanded, - required this.onExpandChange}); + const _MainPageData({ + required this.navigator, + required this.model, + required this.snippets, + required this.filter, + required this.controller, + required this.expanded, + required this.onExpandChange, + }); final DetailsNavigator navigator; final ChannelMainModel model; @@ -165,65 +167,66 @@ class _MainPageData extends HookWidget { ], ), SliverAppBar( - floating: true, - forceElevated: true, - expandedHeight: Dimens.extendedAppBarHeight, - elevation: Dimens.s / 2, - backgroundColor: ColorStyles.surfacePrimary(), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(Dimens.l), - bottomRight: Radius.circular(Dimens.l), - ), + floating: true, + forceElevated: true, + expandedHeight: Dimens.extendedAppBarHeight, + elevation: Dimens.s / 2, + backgroundColor: ColorStyles.surfacePrimary(), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(Dimens.l), + bottomRight: Radius.circular(Dimens.l), ), - flexibleSpace: FlexibleSpaceBar( - collapseMode: CollapseMode.parallax, - background: Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.m), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - PaddingStyles.small( - Row(children: [TextStyles.bold("Scope")]), - ), - PaddingStyles.small( - Row( - children: [ - Expanded( - child: SizedBox( - height: Dimens.filterDropdownHeight, - child: FilterDropdown( - filters: filter.scopes ?? List.empty(), - selected: filter.selectedScope ?? '', - onSelected: (scope) { - model.filterScope(scope); - }, - ), + ), + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.parallax, + background: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.m), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PaddingStyles.small( + Row(children: [TextStyles.bold("Scope")]), + ), + PaddingStyles.small( + Row( + children: [ + Expanded( + child: SizedBox( + height: Dimens.filterDropdownHeight, + child: FilterDropdown( + filters: filter.scopes ?? List.empty(), + selected: filter.selectedScope ?? '', + onSelected: (scope) { + model.filterScope(scope); + }, ), ), - ], - ), + ), + ], ), - PaddingStyles.small( - Row(children: [TextStyles.bold("Language")]), - ), - SizedBox( - height: Dimens.filterListHeight, - child: FilterListView( - filters: filter.languages ?? List.empty(), - selected: filter.selectedLanguages ?? List.empty(), - onSelected: (language, isSelected) { - model.filterLanguage(language, isSelected); - }, - ), + ), + PaddingStyles.small( + Row(children: [TextStyles.bold("Language")]), + ), + SizedBox( + height: Dimens.filterListHeight, + child: FilterListView( + filters: filter.languages ?? List.empty(), + selected: filter.selectedLanguages ?? List.empty(), + onSelected: (language, isSelected) { + model.filterLanguage(language, isSelected); + }, ), - const SizedBox( - height: Dimens.m, - ) - ], - ), + ), + const SizedBox( + height: Dimens.m, + ) + ], ), - )) + ), + ), + ) ]; }, body: CustomScrollView( diff --git a/flutter_module/lib/presentation/styles/dimens.dart b/flutter_module/lib/presentation/styles/dimens.dart index b3b3966..4953a02 100644 --- a/flutter_module/lib/presentation/styles/dimens.dart +++ b/flutter_module/lib/presentation/styles/dimens.dart @@ -8,7 +8,7 @@ class Dimens { static const inputBorderWidth = 1.0; static const filterDropdownHeight = 24.0; static const filterListHeight = 48.0; - static const extendedAppBarHeight = 144.0; + static const extendedAppBarHeight = 120.0; static const logoSignetSize = 18.0; } \ No newline at end of file diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 7e0b274..a64b6e6 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -11,7 +11,7 @@ class SnippetActionBar extends StatelessWidget { this.onSaveTap, this.onCopyTap, this.onShareTap, - // TODO Add archive action + this.onHideTap, this.onDeleteTap, super.key, }); @@ -21,6 +21,7 @@ class SnippetActionBar extends StatelessWidget { final GestureTapCallback? onSaveTap; final GestureTapCallback? onCopyTap; final GestureTapCallback? onShareTap; + final GestureTapCallback? onHideTap; final GestureTapCallback? onDeleteTap; @override @@ -31,7 +32,7 @@ class SnippetActionBar extends StatelessWidget { children: [ StateIcon( icon: Icons.favorite, - active: snippet.isLiked, + active: snippet.isFavorite, onTap: onFavoriteTap, ), const SizedBox(width: Dimens.l), @@ -51,8 +52,12 @@ class SnippetActionBar extends StatelessWidget { ), const SizedBox(width: Dimens.l), StateIcon( - onTap: snippet.isToDelete == true ? onDeleteTap : null, - active: snippet.isToDelete == true ? null : false, + icon: Icons.visibility_off_outlined, + onTap: onHideTap, + ), + const SizedBox(width: Dimens.l), + StateIcon( + onTap: onDeleteTap, activeColor: Colors.redAccent, icon: Icons.delete_outline_outlined, ), diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index 5de306b..343b366 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_module/generated/assets.dart'; import 'package:flutter_module/generated/data_model.g.dart'; import 'package:flutter_module/presentation/styles/dimens.dart'; import 'package:flutter_module/presentation/styles/surface_styles.dart'; @@ -7,63 +6,37 @@ import 'package:flutter_module/presentation/styles/text_styles.dart'; class SnippetDetailsBar extends StatelessWidget { const SnippetDetailsBar({ - super.key, required this.snippet, + super.key, }); final Snippet snippet; @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextStyles.regular(snippet.language?.raw ?? "Unknown language"), - const SizedBox(height: Dimens.m), - snippet.isOwner == true - ? TextStyles.secondaryBold(snippet.owner?.login ?? "") - : TextStyles.secondary(snippet.owner?.login ?? ""), - const SizedBox(height: Dimens.s), - TextStyles.helper(snippet.timeAgo ?? "") - ], - ), - ), - _UserReactionIndicator(reaction: snippet.userReaction), - const SizedBox(width: Dimens.l), - SurfaceStyles.rateBox( - TextStyles.title( - _getVoteCountText(snippet.voteResult), - ), - ) - ], - ); - } - - // TODO Remove - String _getVoteCountText(int? voteResult) { - const defaultValue = '+0'; - if (voteResult == null) return defaultValue; - if (voteResult == 0) return defaultValue; - if (voteResult > 0) return '+$voteResult'; - return '-$voteResult'; - } -} - -class _UserReactionIndicator extends StatelessWidget { - const _UserReactionIndicator({ - this.reaction, - }); + final visibilityIcon = snippet.isHidden == true + ? Icons.visibility_off_outlined + : Icons.visibility_outlined; - final UserReaction? reaction; - final _scale = 2.0; + final visibilityText = snippet.isHidden == true ? "Hidden" : "Visible"; - @override - Widget build(BuildContext context) { - // TODO Implement favorite - return SizedBox.shrink(); + return Row(children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextStyles.regular(snippet.language?.raw ?? "Unknown language"), + const SizedBox(height: Dimens.m), + TextStyles.secondary(visibilityText), + const SizedBox(height: Dimens.s), + TextStyles.helper(snippet.timeAgo ?? "") + ], + ), + ), + SurfaceStyles.rateBox(Text.rich( + TextSpan(children: [WidgetSpan(child: Icon(visibilityIcon))]), + )), + ]); } } diff --git a/flutter_module/lib/presentation/widgets/snippet_list_item.dart b/flutter_module/lib/presentation/widgets/snippet_list_item.dart index 2150250..403e24a 100644 --- a/flutter_module/lib/presentation/widgets/snippet_list_item.dart +++ b/flutter_module/lib/presentation/widgets/snippet_list_item.dart @@ -34,7 +34,18 @@ class SnippetListTile extends HookWidget { right: Dimens.l, bottom: Dimens.m, ), - child: TextStyles.title(snippet.title ?? ""), + child: Row( + children: [ + Expanded(child: TextStyles.title(snippet.title ?? "")), + const SizedBox(width: Dimens.s), + Icon( + snippet.isFavorite == true + ? Icons.favorite + : Icons.favorite_border, + size: Dimens.l, + ), + ], + ), ), Ink( color: ColorStyles.codeBackground(), diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart index 7da7de7..194da2a 100644 --- a/flutter_module/lib/utils/mock/mocks.dart +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -5,13 +5,9 @@ class Mocks { static final snippet = Snippet( uuid: '', title: 'New snippet', - owner: Owner(id: 0, login: 'Snippet owner'), timeAgo: '2 days ago', - voteResult: 32, - userReaction: UserReaction.like, - isLiked: true, - isDisliked: false, - isPrivate: true, + isFavorite: false, + isHidden: true, language: SnippetLanguage( raw: 'Python', type: SnippetLanguageType.python,