From c1eab96db1a22d10f1a0bdcb95ba4bab00bb14ef Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 26 Feb 2025 12:56:43 +0100 Subject: [PATCH 01/23] Setup event channel foundation --- flutter_module/channel/contract.dart | 7 +++++++ flutter_module/lib/generated/data_model.g.dart | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 25d3234..ede5aa4 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -160,6 +160,13 @@ abstract class ChannelModelEventApi { ModelEventData channelEvent(); } +@EventChannelApi() +abstract class ChannelModelEventApi { + ModelStateData channelState(); + + ModelEventData channelEvent(); +} + @HostApi() abstract class ChannelMainModel { void resetEvent(); diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 256e6f0..9a423d8 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -647,7 +647,7 @@ Stream channelState( {String instanceName = ''}) { return event as ModelStateData; }); } - + Stream channelEvent( {String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; @@ -658,7 +658,7 @@ Stream channelEvent( {String instanceName = ''}) { return event as ModelEventData; }); } - + class ChannelMainModel { /// Constructor for [ChannelMainModel]. The [binaryMessenger] named argument is From d1ec5c5e0b88319fdedf9cbe70d083d4726d0699 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 26 Feb 2025 13:16:24 +0100 Subject: [PATCH 02/23] Fixed missing dependency --- .../dev/snipme/snipmeapp/di/ChannelModule.kt | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt deleted file mode 100644 index e5e49a7..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.snipme.snipmeapp.di - -import dev.snipme.snipmeapp.channel.FlowChannelEventStreamHandler -import dev.snipme.snipmeapp.channel.FlowChannelStateStreamHandler -import dev.snipme.snipmeapp.channel.details.DetailsModel -import dev.snipme.snipmeapp.channel.login.LoginModel -import dev.snipme.snipmeapp.channel.main.MainModel -import dev.snipme.snipmeapp.channel.session.SessionModel -import org.koin.dsl.module - -internal val channelModule = module { - single { FlowChannelStateStreamHandler() } - single { FlowChannelEventStreamHandler() } - single { SessionModel(get()) } - single { LoginModel(get(), get(), get()) } - single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } - single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } -} \ No newline at end of file From d03ff0a9b4f655ad8b2a5113d465c6ce4d0f2964 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 26 Feb 2025 14:55:39 +0100 Subject: [PATCH 03/23] Added event api --- .../channel/EventStreamHandlerPlugin.kt | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt deleted file mode 100644 index a0230cf..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.snipme.snipmeapp.channel - -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding -import io.flutter.plugin.common.BinaryMessenger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class FlowChannelEventStreamHandler : ChannelEventStreamHandler() { - private val scope = CoroutineScope(Dispatchers.Main) - private val sinkFlow = MutableSharedFlow( - replay = 3, - onBufferOverflow = DROP_OLDEST - ) - - fun onSetup(messenger: BinaryMessenger) { - register(messenger, this) - } - - override fun onListen(p0: Any?, sink: PigeonEventSink) { - sinkFlow.onEach { sink.success(it) }.launchIn(scope) - } - - override fun onCancel(p0: Any?) { - sinkFlow.resetReplayCache() - } - - fun zip(flow: Flow) { - flow.onEach { sinkFlow.emit(it) }.launchIn(scope) - } -} - -class EventStreamHandlerPlugin : FlutterPlugin, KoinComponent { - private val eventStream by inject() - - override fun onAttachedToEngine(binding: FlutterPluginBinding) { - eventStream.onSetup(binding.binaryMessenger) - } - - override fun onDetachedFromEngine(binding: FlutterPluginBinding) {} -} \ No newline at end of file From d0be5962c82cbdaab0d8e2b8fc6b16e6687eec0a Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 26 Feb 2025 17:05:19 +0100 Subject: [PATCH 04/23] Corrected naming --- .../channel/details/DetailsModelPlugin.kt | 87 ------------------- 1 file changed, 87 deletions(-) delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt 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 deleted file mode 100644 index 6c1eac4..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.snipme.snipmeapp.channel.details - -import dev.snipme.snipmeapp.channel.FlowChannelEventStreamHandler -import dev.snipme.snipmeapp.channel.FlowChannelStateStreamHandler -import dev.snipme.snipmeapp.channel.ModelPlugin -import dev.snipme.snipmeapp.channel.toModelData -import dev.snipme.snipmeapp.channel.ChannelDetailsModel -import io.flutter.plugin.common.BinaryMessenger -import kotlinx.coroutines.flow.map -import org.koin.core.component.inject -import dev.snipme.snipmeapp.channel.DetailsModelEvent as ChannelDetailsModelEvent -import dev.snipme.snipmeapp.channel.DetailsModelEventData as ChannelDetailsModelEventData -import dev.snipme.snipmeapp.channel.DetailsModelStateData as ChannelDetailsModelStateData -import dev.snipme.snipmeapp.channel.ModelState as ChannelModelState - -class DetailsModelPlugin : ModelPlugin(), ChannelDetailsModel { - private val model: DetailsModel by inject() - private val channelStateFlow by inject() - private val channelEventFlow by inject() - - override fun onSetup(messenger: BinaryMessenger, channelModel: ChannelDetailsModel?) { - ChannelDetailsModel.setUp(messenger, channelModel) - channelStateFlow.zip(model.state.map { getModelState(it) }) - channelEventFlow.zip(model.event.map { getModelEvent(it) }) - } - - override fun resetEvent() { - model.event.value = Idle - } - - override fun load(uuid: String) { - model.load(uuid) - } - - override fun like() { - model.like() - } - - override fun dislike() { - model.dislike() - } - - override fun save() { - model.save() - } - - override fun copyToClipboard() { - model.copyToClipboard() - } - - override fun share() { - model.share() - } - - override fun delete() { - model.delete() - } - - private fun getModelState(viewState: DetailsViewState): ChannelDetailsModelStateData { - return ChannelDetailsModelStateData( - state = viewState.toModelState(), - isLoading = viewState is Loading, - data = (viewState as? Loaded)?.snippet?.toModelData(), - ) - } - - private fun getModelEvent(event: DetailsEvent): ChannelDetailsModelEventData { - return ChannelDetailsModelEventData( - event = event.toModelEvent(), - value = (event as? Saved)?.snippetId.toString(), - ) - } - - private fun DetailsViewState.toModelState() = - when (this) { - Loading -> ChannelModelState.LOADING - is Loaded -> ChannelModelState.LOADED - else -> ChannelModelState.ERROR - } - - private fun DetailsEvent.toModelEvent() = - when (this) { - is Saved -> ChannelDetailsModelEvent.SAVED - is Deleted -> ChannelDetailsModelEvent.DELETED - else -> ChannelDetailsModelEvent.NONE - } -} \ No newline at end of file From 4215a06a8a723b7db135f5d8c228437eb7b70306 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Thu, 27 Feb 2025 12:39:02 +0100 Subject: [PATCH 05/23] Removed loading state sending --- .../snipmeapp/channel/login/LoginModel.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt index ed1cc70..4906e3c 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt @@ -19,7 +19,6 @@ import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber -import java.util.concurrent.TimeUnit class LoginModel( private val errorMessages: ErrorMessages, @@ -51,18 +50,18 @@ class LoginModel( } fun init() { - initialLogin() - .delay(3, TimeUnit.SECONDS) - .subscribeOn(Schedulers.io()) - .doOnEvent { setState(Loaded) } - .subscribeBy( - onComplete = { setEvent(Logged) }, - onError = { - if (it !is NotAuthorizedException) { - Timber.e("Couldn't get token or user, error = $it") - } - } - ).also { disposables += it } + setState(Loading) +// initialLogin() +// .delay(3, TimeUnit.SECONDS) +// .subscribeOn(Schedulers.io()) +// .subscribeBy( +// onComplete = { setEvent(Logged) }, +// onError = { +// if (it !is NotAuthorizedException) { +// Timber.e("Couldn't get token or user, error = $it") +// } +// } +// ).also { disposables += it } } fun loginOrRegister(email: String, password: String) { From 1e07cea8173258cadc36e2407dec3aab6291c84a Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Mon, 3 Mar 2025 08:24:27 +0100 Subject: [PATCH 06/23] Corrected streams listening --- .../snipmeapp/channel/login/LoginModel.kt | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt index 4906e3c..ed1cc70 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/login/LoginModel.kt @@ -19,6 +19,7 @@ import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber +import java.util.concurrent.TimeUnit class LoginModel( private val errorMessages: ErrorMessages, @@ -50,18 +51,18 @@ class LoginModel( } fun init() { - setState(Loading) -// initialLogin() -// .delay(3, TimeUnit.SECONDS) -// .subscribeOn(Schedulers.io()) -// .subscribeBy( -// onComplete = { setEvent(Logged) }, -// onError = { -// if (it !is NotAuthorizedException) { -// Timber.e("Couldn't get token or user, error = $it") -// } -// } -// ).also { disposables += it } + initialLogin() + .delay(3, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .doOnEvent { setState(Loaded) } + .subscribeBy( + onComplete = { setEvent(Logged) }, + onError = { + if (it !is NotAuthorizedException) { + Timber.e("Couldn't get token or user, error = $it") + } + } + ).also { disposables += it } } fun loginOrRegister(email: String, password: String) { From 816bf64c6b58ba95e88b24ab182a5e547b699e3c Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Mon, 3 Mar 2025 09:59:47 +0100 Subject: [PATCH 07/23] Setup creating image from widget --- .../snipme/snipmeapp/channel/DataModel.g.kt | 45 +++++++----------- .../assets/images/icons/reaction_dislike.jpeg | Bin 998 -> 0 bytes .../assets/images/icons/reaction_like.jpeg | Bin 932 -> 0 bytes .../images/icons/reaction_undefined.jpeg | Bin 1050 -> 0 bytes flutter_module/channel/contract.dart | 8 ++-- flutter_module/lib/generated/assets.dart | 3 -- .../lib/generated/data_model.g.dart | 38 ++++----------- .../presentation/screens/details_screen.dart | 41 +++++++++++++--- .../widgets/snippet_action_bar.dart | 23 ++++----- .../widgets/snippet_details_bar.dart | 18 ++----- flutter_module/pubspec.lock | 10 +++- flutter_module/pubspec.yaml | 4 +- 12 files changed, 82 insertions(+), 108 deletions(-) delete mode 100644 flutter_module/assets/images/icons/reaction_dislike.jpeg delete mode 100644 flutter_module/assets/images/icons/reaction_like.jpeg delete mode 100644 flutter_module/assets/images/icons/reaction_undefined.jpeg 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 5aae457..0a6b323 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 @@ -9,8 +9,8 @@ import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer @@ -848,11 +848,10 @@ interface ChannelMainModel { interface ChannelDetailsModel { fun resetEvent() fun load(uuid: String) - fun like() - fun dislike() - fun save() + fun favorite() + fun saveImage(image: ByteArray) fun copyToClipboard() - fun share() + fun shareImage(image: ByteArray) fun delete() companion object { @@ -899,27 +898,11 @@ interface ChannelDetailsModel { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.like$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.like() - 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.dislike$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.favorite$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.dislike() + api.favorite() listOf(null) } catch (exception: Throwable) { wrapError(exception) @@ -931,11 +914,13 @@ interface ChannelDetailsModel { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.save$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.saveImage$separatedMessageChannelSuffix", codec) if (api != null) { - channel.setMessageHandler { _, reply -> + channel.setMessageHandler { message, reply -> + val args = message as List + val imageArg = args[0] as ByteArray val wrapped: List = try { - api.save() + api.saveImage(imageArg) listOf(null) } catch (exception: Throwable) { wrapError(exception) @@ -963,11 +948,13 @@ interface ChannelDetailsModel { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.share$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.shareImage$separatedMessageChannelSuffix", codec) if (api != null) { - channel.setMessageHandler { _, reply -> + channel.setMessageHandler { message, reply -> + val args = message as List + val imageArg = args[0] as ByteArray val wrapped: List = try { - api.share() + api.shareImage(imageArg) listOf(null) } catch (exception: Throwable) { wrapError(exception) diff --git a/flutter_module/assets/images/icons/reaction_dislike.jpeg b/flutter_module/assets/images/icons/reaction_dislike.jpeg deleted file mode 100644 index dbc7ebaba3812ec89895f1229c3fb16911ffb99a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 998 zcmex=DK+5#AqGJX1`P%sW=16jCP7AK zLB{__7<3pIm>Ge90RdRqn3x&aQG~#f|8FtyFf%eR2`~#VFfi5j>=aqODyX1qPnWLr z$`cR$Eai^YdhjtQECYJ@2m=#LH3KtHv!Y>-$>b9-sgxlPe2NPFz`B%GlXHF*xh;nW&TV1Wsw@nw*Gx$u99T(!A)p z_>tld#T~!(0k@%+Jaicc@yD*0@_E49UF*X&RuvrP`iOzoq79Xx5qXfO5a zc#E!gaY3WWw<^&sk3P!9`zOw?SayZ|8FzNFa-yOh^G3gw7I`fyER70mprBF|0LDKk z^56l(DCo!_pkSD&7+ARQ!$(-;*((Zi{FM)pC`jYiw`;XcTt7)>X2t894u%!kH#{Qu zdX*s!eRJCx zde;>6tk_+#{Bh-h1q&3IdO>cH1GDK+5#AqGJX1`P%sW=16jCP7AK zLB{__7<7ORWMT$75&>A*7+KhvL9Ryy|8Fty097&xFbgm+Fzu95Q(p4W+e#($t-$1z z4KmjkXH2!`sBZQQFfxk? zh=`gBE4TsmaR9AmWM_Kt`JL?p3jvj|?ef#q&SWPZVGt9#e6n+a^49FhN&eA7RUZ$& zuD!hXkp1Exr*hBif4t&-LI0!^9nERymiTvAoIcUM_RDO+7B|n$Ii9bR{3Cm0FFJO| zV>nIVgTKt@b{mk>Lsj#SI(+8+$IqaEEx~neL^=CA%>b=dRl^Q}2hcNT<`*A(ul?91Qy(G^Pli1o~1z02m$2tZ*-Y60xA6K;j1lMaRI#g|G;+S6n`WyZwOZd_9+=yA1t# z6sBJY&~MOPKi^r3`D{oqM{VAPjc+bITJpf|(Zv4L^uOCTANaCp)-=X5uQ_VpT-b3u zF5;-G$i36zGo5_x!L|Y;9cV2qRRC=jWDrnPFmyBsOk4;HDMovNjTdy-rl?sl+)BD- z<`R56^M&~Xrh~y3KZ;)X&(Orp`I&!TvP)GakKl5vDK+5#AqGJX1_K5IW=16jCP7AK zLB{__7<3pIn3zE>MF3VNW;PZ^R56h3|62?^%!~|70?YzHO_#OzUpzSXA_LR3lR1ia z3MQ|;G;sst+>1;Vmo5N}Wr8VYU$E1(^M9KA+b~211baDmYgN<)*7M zc4Vy7tq2J#{?;xYbm_Ur^9Psgfng#q08AV}0E%%4U=(y@P*gA!2uv(o2#Z*I#TSfz z_YXu*Ty#5RMutVpuKx@v$=8?FINyB3S=tb|rYcRSC^#MrI;*1P}iiQe-i2{yrM+qn-+5OsAxUN=s@}v6GXRYpgSuBs1i@O|I zBdfER=~>011}Db&f2SA9E6#fM=QX#-ck}0wQ6Cc4|7S?O(A8JomBI3av0A@*$zndu ZYsaT=u?HCuaZn~d(AvrZWODugn*b!fP1gVb diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index ede5aa4..07c4c16 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -190,15 +190,13 @@ abstract class ChannelDetailsModel { void load(String uuid); - void like(); + void favorite(); - void dislike(); - - void save(); + void saveImage(Uint8List image); void copyToClipboard(); - void share(); + void shareImage(Uint8List image); void delete(); } diff --git a/flutter_module/lib/generated/assets.dart b/flutter_module/lib/generated/assets.dart index b0ec512..fd8188a 100644 --- a/flutter_module/lib/generated/assets.dart +++ b/flutter_module/lib/generated/assets.dart @@ -3,8 +3,5 @@ class Assets { Assets._(); static const String appLogo = 'assets/images/illustrations/app_logo.png'; - static const String reactionDislike = 'assets/images/icons/reaction_dislike.jpeg'; - static const String reactionLike = 'assets/images/icons/reaction_like.jpeg'; - static const String reactionUndefined = 'assets/images/icons/reaction_undefined.jpeg'; } diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 9a423d8..05d3036 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -841,8 +841,8 @@ class ChannelDetailsModel { } } - Future like() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.like$pigeonVar_messageChannelSuffix'; + Future favorite() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.favorite$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -863,37 +863,15 @@ class ChannelDetailsModel { } } - Future dislike() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.dislike$pigeonVar_messageChannelSuffix'; + Future saveImage(Uint8List image) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.saveImage$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) 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 save() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.save$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_channel.send([image]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -929,15 +907,15 @@ class ChannelDetailsModel { } } - Future share() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.share$pigeonVar_messageChannelSuffix'; + Future shareImage(Uint8List image) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.shareImage$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_channel.send([image]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index f7643ee..1aa834c 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -15,6 +15,7 @@ import 'package:flutter_module/presentation/widgets/view_state_wrapper.dart'; import 'package:flutter_module/utils/hooks/use_navigator.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:screenshot/screenshot.dart'; class DetailsScreen extends NamedScreen { DetailsScreen({ @@ -47,6 +48,7 @@ class _DetailsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final captureController = ScreenshotController(); useNavigator([navigator]); final stateNotification = ref.watch(detailsPageStateProvider); @@ -75,6 +77,20 @@ class _DetailsPage extends HookConsumerWidget { } }); + void saveImage() { + captureController.capture().then((image) { + if (image == null) return; + model.saveImage(image); + }); + } + + void shareImage() { + captureController.capture().then((image) { + if (image == null) return; + model.shareImage(image); + }); + } + useEffect(() { model.load(navigator.snippetId ?? ''); return null; @@ -103,6 +119,9 @@ class _DetailsPage extends HookConsumerWidget { builder: (_, snippet) => _DetailPageData( model: model, snippet: snippet, + captureController: captureController, + saveImage: saveImage, + shareImage: shareImage, ), ), ); @@ -118,10 +137,16 @@ class _DetailPageData extends StatelessWidget { const _DetailPageData({ required this.model, required this.snippet, + required this.captureController, + required this.saveImage, + required this.shareImage, }); final ChannelDetailsModel model; final Snippet? snippet; + final ScreenshotController captureController; + final VoidCallback saveImage; + final VoidCallback shareImage; @override Widget build(BuildContext context) { @@ -136,9 +161,12 @@ class _DetailPageData extends StatelessWidget { color: ColorStyles.codeBackground(), child: NoOverscrollSingleChildScrollView( padding: const EdgeInsets.all(Dimens.l), - child: CodeTextView( - code: snippet!.code!.raw!, - tokens: snippet!.code?.tokens, + child: Screenshot( + controller: captureController, + child: CodeTextView( + code: snippet!.code!.raw!, + tokens: snippet!.code?.tokens, + ), ), ), ), @@ -147,11 +175,10 @@ class _DetailPageData extends StatelessWidget { Center( child: SnippetActionBar( snippet: snippet!, - onLikeTap: model.like, - onDislikeTap: model.dislike, - onSaveTap: model.save, + onFavoriteTap: model.favorite, + onSaveTap: saveImage, onCopyTap: model.copyToClipboard, - onShareTap: model.share, + onShareTap: shareImage, onDeleteTap: model.delete, ), ), diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 9c0e0cd..5e0c3d0 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -6,19 +6,18 @@ import 'package:flutter_module/presentation/widgets/state_icon.dart'; class SnippetActionBar extends StatelessWidget { const SnippetActionBar({ - super.key, required this.snippet, - this.onLikeTap, - this.onDislikeTap, + this.onFavoriteTap, this.onSaveTap, this.onCopyTap, this.onShareTap, + // TODO Add archive action this.onDeleteTap, + super.key, }); final Snippet snippet; - final GestureTapCallback? onLikeTap; - final GestureTapCallback? onDislikeTap; + final GestureTapCallback? onFavoriteTap; final GestureTapCallback? onSaveTap; final GestureTapCallback? onCopyTap; final GestureTapCallback? onShareTap; @@ -31,15 +30,14 @@ class SnippetActionBar extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ StateIcon( - icon: Icons.thumb_up_alt_outlined, + icon: Icons.favorite, active: snippet.isLiked, - onTap: snippet.isLiked == false ? null : onLikeTap, + onTap: onFavoriteTap, ), const SizedBox(width: Dimens.l), StateIcon( - icon: Icons.thumb_down_alt_outlined, - active: snippet.isDisliked, - onTap: snippet.isDisliked == false ? null : onDislikeTap, + icon: Icons.share, + onTap: onShareTap, ), const SizedBox(width: Dimens.l), StateIcon( @@ -53,11 +51,6 @@ class SnippetActionBar extends StatelessWidget { onTap: onCopyTap, ), const SizedBox(width: Dimens.l), - StateIcon( - icon: Icons.share, - onTap: onShareTap, - ), - const SizedBox(width: Dimens.l), StateIcon( onTap: snippet.isToDelete == true ? onDeleteTap : null, active: snippet.isToDelete == true ? null : false, diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index 9f35679..5de306b 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -43,6 +43,7 @@ class SnippetDetailsBar extends StatelessWidget { ); } + // TODO Remove String _getVoteCountText(int? voteResult) { const defaultValue = '+0'; if (voteResult == null) return defaultValue; @@ -62,20 +63,7 @@ class _UserReactionIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - if (reaction == UserReaction.like) { - return Image.asset( - Assets.reactionLike, - scale: _scale, - ); - } - - if (reaction == UserReaction.dislike) { - return Image.asset( - Assets.reactionDislike, - scale: _scale, - ); - } - - return Image.asset(Assets.reactionUndefined, scale: _scale); + // TODO Implement favorite + return SizedBox.shrink(); } } diff --git a/flutter_module/pubspec.lock b/flutter_module/pubspec.lock index 48fc5c6..db6abd1 100644 --- a/flutter_module/pubspec.lock +++ b/flutter_module/pubspec.lock @@ -435,6 +435,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + screenshot: + dependency: "direct main" + description: + name: screenshot + sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" + url: "https://pub.dev" + source: hosted + version: "3.0.0" shelf: dependency: transitive description: @@ -650,4 +658,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.22.0" diff --git a/flutter_module/pubspec.yaml b/flutter_module/pubspec.yaml index 4557956..27e3ae8 100644 --- a/flutter_module/pubspec.yaml +++ b/flutter_module/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: logger: ^2.4.0 flutter_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1 + screenshot: ^3.0.0 dev_dependencies: build_runner: ^2.2.1 flutter_lints: ^4.0.0 @@ -42,9 +43,6 @@ flutter: uses-material-design: true assets: - assets/images/illustrations/app_logo.png - - assets/images/icons/reaction_undefined.jpeg - - assets/images/icons/reaction_like.jpeg - - assets/images/icons/reaction_dislike.jpeg fonts: - family: Kanit fonts: From 7e84d426e7589ead9214ab5b58c2b652e78c7646 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Mon, 3 Mar 2025 11:32:33 +0100 Subject: [PATCH 08/23] Setup native image share --- app/src/main/AndroidManifest.xml | 30 ++++++++--- .../snipmeapp/channel/details/DetailsModel.kt | 16 +++--- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 4 +- .../domain/share/ShareSnippetCodeUseCase.kt | 24 --------- .../domain/share/ShareSnippetUseCase.kt | 50 +++++++++++++++++++ app/src/main/res/xml/file_paths.xml | 6 +++ flutter_module/channel/contract.dart | 2 +- 7 files changed, 87 insertions(+), 45 deletions(-) delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt create mode 100644 app/src/main/res/xml/file_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e321917..58b4508 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + + + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true"> + android:exported="true" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> - + + + + + \ No newline at end of file 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 079d1cd..4ff4b9c 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 @@ -14,7 +14,7 @@ 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.ShareSnippetCodeUseCase +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 @@ -33,7 +33,7 @@ class DetailsModel( private val getTargetReaction: GetTargetUserReactionUseCase, private val setUserReaction: SetUserReactionUseCase, private val saveSnippet: SaveSnippetUseCase, - private val shareSnippet: ShareSnippetCodeUseCase, + private val shareSnippet: ShareSnippetUseCase, private val deleteSnippet: DeleteSnippetUseCase, private val session: SessionModel ) : ErrorParsable { @@ -71,12 +71,8 @@ class DetailsModel( ).also { disposables += it } } - fun like() { - changeReaction(UserReaction.LIKE) - } - - fun dislike() { - changeReaction(UserReaction.DISLIKE) + fun toggleFavorite() { + // TODO Implement } fun copyToClipboard() { @@ -103,9 +99,9 @@ class DetailsModel( } } - fun share() { + fun share(image: ByteArray) { getSnippet()?.let { - shareSnippet(it) + shareSnippet(it, image) } } 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 3d2f1b6..bec68fc 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -8,7 +8,7 @@ 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.ShareSnippetCodeUseCase +import dev.snipme.snipmeapp.domain.share.ShareSnippetUseCase import dev.snipme.snipmeapp.domain.snippet.* import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByLanguageUseCase import dev.snipme.snipmeapp.domain.filter.FilterSnippetsByScopeUseCase @@ -44,7 +44,7 @@ internal val useCaseModule = module { // Language factory { GetLanguagesUseCase(get(), get(), get()) } // Share - factory { ShareSnippetCodeUseCase(get()) } + factory { ShareSnippetUseCase(get()) } // Clipboard single { AddToClipboardUseCase(get()) } factory { GetFromClipboardUseCase(get()) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt deleted file mode 100644 index d7d1cf5..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetCodeUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.snipme.snipmeapp.domain.share - -import android.content.Context -import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import androidx.core.content.ContextCompat.startActivity -import dev.snipme.snipmeapp.domain.snippets.Snippet - -class ShareSnippetCodeUseCase( - private val context: Context -) { - - operator fun invoke(snippet: Snippet) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, snippet.code.raw) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, snippet.title) - shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) - startActivity(context, shareIntent, null) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt new file mode 100644 index 0000000..f3dfe07 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt @@ -0,0 +1,50 @@ +package dev.snipme.snipmeapp.domain.share + +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import androidx.core.content.ContextCompat.startActivity +import androidx.core.content.FileProvider +import dev.snipme.snipmeapp.domain.snippets.Snippet +import java.io.File + +class ShareSnippetUseCase( + private val context: Context +) { + + operator fun invoke(snippet: Snippet, image: ByteArray) { + val imageFile = File(context.cacheDir, "code.png") + val imageUri = FileProvider.getUriForFile( + context, + "dev.snipme.snipmeapp.fileprovider", + imageFile, + ) + context.grantUriPermission( + "dev.snipme.snipmeapp.fileprovider", + imageUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + imageFile.writeBytes(image) + + println("Image: ${image.size}") + println("ImageFile: $imageFile") + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TITLE, snippet.title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + putExtra(Intent.EXTRA_TEXT, snippet.code.raw) + putExtra(Intent.EXTRA_SUBJECT, snippet.language.raw) + putExtra(Intent.EXTRA_STREAM, imageUri) + type = "image/*" + setDataAndType(imageUri, context.getContentResolver().getType(imageUri)); + clipData = ClipData.newRawUri(snippet.title, imageUri) + } + + val shareIntent = Intent.createChooser(sendIntent, snippet.title) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(context, shareIntent, null) + } +} \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..3de0b33 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 07c4c16..2399d29 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -190,7 +190,7 @@ abstract class ChannelDetailsModel { void load(String uuid); - void favorite(); + void toggleFavorite(); void saveImage(Uint8List image); From 92c04d93bd038ee4cfe8eeae9226dfacddadcd69 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 5 Mar 2025 12:08:29 +0100 Subject: [PATCH 09/23] Setup saving file to media store --- .../java/dev/snipme/snipmeapp/AppService.kt | 109 ++++++++++++++++++ .../snipmeapp/channel/details/DetailsModel.kt | 28 +++-- .../dev/snipme/snipmeapp/di/UtilModule.kt | 8 +- .../domain/share/ShareSnippetUseCase.kt | 45 +------- .../domain/snippet/SaveSnippetUseCase.kt | 19 ++- app/src/main/res/xml/file_paths.xml | 3 + .../widgets/snippet_action_bar.dart | 12 +- 7 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/AppService.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/AppService.kt b/app/src/main/java/dev/snipme/snipmeapp/AppService.kt new file mode 100644 index 0000000..405e8f0 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/AppService.kt @@ -0,0 +1,109 @@ +package dev.snipme.snipmeapp + +import android.content.ClipData +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import androidx.core.content.ContextCompat.startActivity +import androidx.core.content.FileProvider +import dev.snipme.snipmeapp.domain.snippets.Snippet +import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.text.DateFormat +import java.util.Date + +class AppService(private val context: Context) { + private var imageUri: Uri? = null + + fun getCurrentDateFormatted(): String { + return DateFormat.getDateInstance().format(Date()) + } + + fun storeFile(image: ByteArray, fileName: String, temp: Boolean = false) { + val directoryFile = if (temp) context.cacheDir else context.getExternalFilesDir( + Environment.DIRECTORY_PICTURES + ) + + if (directoryFile == null) throw IllegalStateException("Storage not available") + if (!directoryFile.exists()) { + directoryFile.mkdirs() + } + + val imageFile = File(directoryFile, fileName) + + val imageUri = FileProvider.getUriForFile( + context, + "dev.snipme.snipmeapp.fileprovider", + imageFile, + ) + context.grantUriPermission( + "dev.snipme.snipmeapp.fileprovider", + imageUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + imageFile.exists() + imageFile.writeBytes(image) + + Timber.d("After save ${imageFile.length()}") + this.imageUri = imageUri + } + + fun launchShareIntent(snippet: Snippet) { + if (imageUri == null) + throw IllegalStateException("Image path is not set. Store image first!") + + val uri = imageUri!! // Store temporary to avoid var change + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + type = "image/*" + putExtra(Intent.EXTRA_TITLE, snippet.title) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + putExtra(Intent.EXTRA_TEXT, snippet.code.raw) + putExtra(Intent.EXTRA_SUBJECT, snippet.language.raw) + putExtra(Intent.EXTRA_STREAM, uri) + setDataAndType(uri, context.contentResolver.getType(uri)); + clipData = ClipData.newRawUri(snippet.title, uri) + } + + val shareIntent = Intent.createChooser(sendIntent, null) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(context, shareIntent, null) + } + + fun storeMediaFile(image: ByteArray, name: String) { + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, name) + put(MediaStore.Images.Media.MIME_TYPE, "image/png") + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/SnipMeApp") + } + + val resolver = context.contentResolver + val existingUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon() + .appendQueryParameter(MediaStore.Images.Media.DISPLAY_NAME, name) + .appendQueryParameter(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/SnipMeApp") + .build() + + val cursor = resolver.query(existingUri, arrayOf(MediaStore.Images.Media._ID), null, null, null) + val uri: Uri? = if (cursor != null && cursor.moveToFirst()) { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) + Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString()) + } else { + resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + } + cursor?.close() + + if (uri != null) { + resolver.openOutputStream(uri)?.use { outputStream -> + outputStream.write(image) + } + } else { + throw FileNotFoundException("Failed to create or update MediaStore record.") + } + } +} \ No newline at end of file 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 4ff4b9c..2c985ed 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 @@ -81,21 +81,19 @@ class DetailsModel( } } - fun save() { - getSnippet()?.let { - setState(Loading) - saveSnippet(it) - .subscribeOn(Schedulers.io()) - .subscribeBy( - onSuccess = { saved -> - setState(Loaded(it)) - mutableEvent.value = Saved(saved.uuid) - }, - onError = { error -> - Timber.e("Couldn't save snippet, error = $error") - parseError(error) - } - ).also { disposables += it } + fun save(image: ByteArray) { + try { + getSnippet()?.let { + setState(Loading) + saveSnippet(image, it) + } + } catch (e: Exception) { + Timber.e("Couldn't save snippet, error = $e") + mutableEvent.value = Alert(errorMessages.generic) + } finally { + getSnippet()?.let { + mutableEvent.value = Saved(it.uuid) + } } } diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt index 1ca58e7..89d5bd9 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UtilModule.kt @@ -2,18 +2,20 @@ package dev.snipme.snipmeapp.di import android.content.ClipboardManager import android.content.Context.CLIPBOARD_SERVICE -import org.koin.android.ext.koin.androidApplication -import org.koin.dsl.module +import dev.snipme.snipmeapp.AppService import dev.snipme.snipmeapp.BuildConfig import dev.snipme.snipmeapp.domain.error.DebugErrorHandler import dev.snipme.snipmeapp.domain.error.SafeErrorHandler import dev.snipme.snipmeapp.domain.message.ErrorMessages -import dev.snipme.snipmeapp.domain.message.ValidationMessages import dev.snipme.snipmeapp.domain.message.RealValidationMessages +import dev.snipme.snipmeapp.domain.message.ValidationMessages +import org.koin.android.ext.koin.androidApplication +import org.koin.dsl.module internal val utilModule = module { factory { if (BuildConfig.DEBUG) DebugErrorHandler() else SafeErrorHandler() } factory { ErrorMessages(get()) } factory { RealValidationMessages(get()) } single { androidApplication().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager } + single { AppService(androidApplication()) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt index f3dfe07..1368b33 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt @@ -1,50 +1,15 @@ package dev.snipme.snipmeapp.domain.share -import android.content.ClipData -import android.content.Context -import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import androidx.core.content.ContextCompat.startActivity -import androidx.core.content.FileProvider +import dev.snipme.snipmeapp.AppService import dev.snipme.snipmeapp.domain.snippets.Snippet -import java.io.File class ShareSnippetUseCase( - private val context: Context + private val appService: AppService ) { operator fun invoke(snippet: Snippet, image: ByteArray) { - val imageFile = File(context.cacheDir, "code.png") - val imageUri = FileProvider.getUriForFile( - context, - "dev.snipme.snipmeapp.fileprovider", - imageFile, - ) - context.grantUriPermission( - "dev.snipme.snipmeapp.fileprovider", - imageUri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - - imageFile.writeBytes(image) - - println("Image: ${image.size}") - println("ImageFile: $imageFile") - - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TITLE, snippet.title) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - putExtra(Intent.EXTRA_TEXT, snippet.code.raw) - putExtra(Intent.EXTRA_SUBJECT, snippet.language.raw) - putExtra(Intent.EXTRA_STREAM, imageUri) - type = "image/*" - setDataAndType(imageUri, context.getContentResolver().getType(imageUri)); - clipData = ClipData.newRawUri(snippet.title, imageUri) - } - - val shareIntent = Intent.createChooser(sendIntent, snippet.title) - shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) - startActivity(context, shareIntent, null) + val name = "${appService.getCurrentDateFormatted()}.png" + appService.storeFile(image, name, temp = true) + appService.launchShareIntent(snippet) } } \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt index de98bec..b1ed75a 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/SaveSnippetUseCase.kt @@ -1,20 +1,15 @@ package dev.snipme.snipmeapp.domain.snippet -import io.reactivex.Single +import dev.snipme.snipmeapp.AppService import dev.snipme.snipmeapp.domain.snippets.Snippet -import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility -import java.util.concurrent.TimeUnit class SaveSnippetUseCase( - private val createSnippet: CreateSnippetUseCase + private val appService: AppService ) { - operator fun invoke(snippet: Snippet): Single { - if (snippet.isOwner) return Single.just(snippet) - return createSnippet( - snippet.title, - snippet.code.raw, - snippet.language.raw, - visibility = SnippetVisibility.PRIVATE, - ) + operator fun invoke(image: ByteArray, snippet: Snippet): String { + val name = "${snippet.title.replace(" ", "_")}.png" + appService.storeFile(image, name, temp = false) + appService.storeMediaFile(image, name) + return name } } \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 3de0b33..87bfdc8 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -3,4 +3,7 @@ + \ 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 5e0c3d0..7e0b274 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -42,8 +42,7 @@ class SnippetActionBar extends StatelessWidget { const SizedBox(width: Dimens.l), StateIcon( icon: Icons.save_alt_outlined, - active: snippet.isSaved, - onTap: getSaveCallback(snippet.isSaved, onSaveTap), + onTap: onSaveTap, ), const SizedBox(width: Dimens.l), StateIcon( @@ -61,13 +60,4 @@ class SnippetActionBar extends StatelessWidget { ), ); } - - GestureTapCallback? getSaveCallback( - bool? isSaved, - GestureTapCallback? onSaveTap, - ) { - if (isSaved == false) return null; - if (isSaved == true) return null; - return onSaveTap; - } } From 857849e36762a58a8ffcc2ed692907f3c4d7394f Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 5 Mar 2025 13:07:26 +0100 Subject: [PATCH 10/23] Corrected events --- .../java/dev/snipme/snipmeapp/AppService.kt | 28 +++++++------- .../snipme/snipmeapp/channel/DataModel.g.kt | 8 ++-- .../snipmeapp/channel/details/DetailsModel.kt | 19 ++++++---- .../domain/share/ShareSnippetUseCase.kt | 2 +- flutter_module/channel/contract.dart | 2 +- .../lib/generated/data_model.g.dart | 6 +-- .../presentation/screens/details_screen.dart | 38 +++++++++---------- 7 files changed, 51 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/dev/snipme/snipmeapp/AppService.kt b/app/src/main/java/dev/snipme/snipmeapp/AppService.kt index 405e8f0..4d1aa21 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/AppService.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/AppService.kt @@ -11,12 +11,13 @@ import android.provider.MediaStore import androidx.core.content.ContextCompat.startActivity import androidx.core.content.FileProvider import dev.snipme.snipmeapp.domain.snippets.Snippet -import timber.log.Timber import java.io.File import java.io.FileNotFoundException import java.text.DateFormat import java.util.Date +const val imageMime = "image/png" + class AppService(private val context: Context) { private var imageUri: Uri? = null @@ -35,22 +36,19 @@ class AppService(private val context: Context) { } val imageFile = File(directoryFile, fileName) - val imageUri = FileProvider.getUriForFile( context, "dev.snipme.snipmeapp.fileprovider", imageFile, ) + context.grantUriPermission( "dev.snipme.snipmeapp.fileprovider", imageUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION ) - imageFile.exists() imageFile.writeBytes(image) - - Timber.d("After save ${imageFile.length()}") this.imageUri = imageUri } @@ -61,14 +59,12 @@ class AppService(private val context: Context) { val uri = imageUri!! // Store temporary to avoid var change val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND - type = "image/*" - putExtra(Intent.EXTRA_TITLE, snippet.title) + type = imageMime + clipData = ClipData.newRawUri(snippet.title, uri) + setDataAndType(uri, context.contentResolver.getType(uri)); addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - putExtra(Intent.EXTRA_TEXT, snippet.code.raw) - putExtra(Intent.EXTRA_SUBJECT, snippet.language.raw) + putExtra(Intent.EXTRA_TITLE, snippet.title) putExtra(Intent.EXTRA_STREAM, uri) - setDataAndType(uri, context.contentResolver.getType(uri)); - clipData = ClipData.newRawUri(snippet.title, uri) } val shareIntent = Intent.createChooser(sendIntent, null) @@ -77,19 +73,21 @@ class AppService(private val context: Context) { } fun storeMediaFile(image: ByteArray, name: String) { + val directory = "Pictures/SnipMeApp" val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, name) - put(MediaStore.Images.Media.MIME_TYPE, "image/png") - put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/SnipMeApp") + put(MediaStore.Images.Media.RELATIVE_PATH, directory) + put(MediaStore.Images.Media.MIME_TYPE, imageMime) } val resolver = context.contentResolver val existingUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon() .appendQueryParameter(MediaStore.Images.Media.DISPLAY_NAME, name) - .appendQueryParameter(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/SnipMeApp") + .appendQueryParameter(MediaStore.Images.Media.RELATIVE_PATH, directory) .build() - val cursor = resolver.query(existingUri, arrayOf(MediaStore.Images.Media._ID), null, null, null) + val cursor = + resolver.query(existingUri, arrayOf(MediaStore.Images.Media._ID), null, null, null) val uri: Uri? = if (cursor != null && cursor.moveToFirst()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id.toString()) 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 0a6b323..1003358 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 @@ -150,7 +150,7 @@ enum class MainModelEvent(val raw: Int) { enum class DetailsModelEvent(val raw: Int) { NONE(0), - SAVED(1), + ALERT(1), DELETED(2); companion object { @@ -848,7 +848,7 @@ interface ChannelMainModel { interface ChannelDetailsModel { fun resetEvent() fun load(uuid: String) - fun favorite() + fun toggleFavorite() fun saveImage(image: ByteArray) fun copyToClipboard() fun shareImage(image: ByteArray) @@ -898,11 +898,11 @@ interface ChannelDetailsModel { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.favorite$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_module.ChannelDetailsModel.toggleFavorite$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.favorite() + api.toggleFavorite() listOf(null) } catch (exception: Throwable) { wrapError(exception) 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 2c985ed..c97cb3f 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 @@ -82,24 +82,28 @@ class DetailsModel( } fun save(image: ByteArray) { + Timber.d("Saving snippet image ${image.size}") try { getSnippet()?.let { - setState(Loading) saveSnippet(image, it) + Timber.d("Snippet ${it.title} saved") } + mutableEvent.value = Alert("Snippet saved") } catch (e: Exception) { Timber.e("Couldn't save snippet, error = $e") mutableEvent.value = Alert(errorMessages.generic) - } finally { - getSnippet()?.let { - mutableEvent.value = Saved(it.uuid) - } } } fun share(image: ByteArray) { - getSnippet()?.let { - shareSnippet(it, image) + try { + getSnippet()?.let { + shareSnippet(image, it) + } + mutableEvent.value = Alert("Snippet shared") + } catch (e: Exception) { + Timber.e("Couldn't share snippet, error = $e") + mutableEvent.value = Alert(errorMessages.generic) } } @@ -160,5 +164,4 @@ sealed class DetailsEvent data object Idle : DetailsEvent() data object Deleted : DetailsEvent() data class Alert(val message: String) : DetailsEvent() -data class Saved(val snippetId: String) : DetailsEvent() data object Logout : DetailsEvent() \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt index 1368b33..71dde64 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/share/ShareSnippetUseCase.kt @@ -7,7 +7,7 @@ class ShareSnippetUseCase( private val appService: AppService ) { - operator fun invoke(snippet: Snippet, image: ByteArray) { + operator fun invoke(image: ByteArray, snippet: Snippet) { val name = "${appService.getCurrentDateFormatted()}.png" appService.storeFile(image, name, temp = true) appService.launchShareIntent(snippet) diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 2399d29..30ccc08 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -111,7 +111,7 @@ enum ModelState { loading, loaded, error } enum MainModelEvent { none, alert, logout } -enum DetailsModelEvent { none, saved, deleted } +enum DetailsModelEvent { none, alert, deleted } enum LoginModelEvent { none, logged } diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 05d3036..c92bd8a 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -89,7 +89,7 @@ enum MainModelEvent { enum DetailsModelEvent { none, - saved, + alert, deleted, } @@ -841,8 +841,8 @@ class ChannelDetailsModel { } } - Future favorite() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.favorite$pigeonVar_messageChannelSuffix'; + Future toggleFavorite() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_module.ChannelDetailsModel.toggleFavorite$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index 1aa834c..1dbf09e 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -57,24 +57,16 @@ class _DetailsPage extends HookConsumerWidget { final event = eventNotification.event; WidgetsBinding.instance.addPostFrameCallback((_) { - if (event == DetailsModelEvent.saved) { - final snippetId = eventNotification.value; - if (snippetId == null) { + switch (event) { + case DetailsModelEvent.alert: + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(eventNotification.value ?? ""))); + case DetailsModelEvent.deleted: _exit(); - return; - } - - _exit(); - WidgetsBinding.instance.addPostFrameCallback((_) { - navigator.goToDetails(context, snippetId); - }); - } - }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (event == DetailsModelEvent.deleted) { - _exit(); + default: + print('Event $event'); } + model.resetEvent(); }); void saveImage() { @@ -163,9 +155,15 @@ class _DetailPageData extends StatelessWidget { padding: const EdgeInsets.all(Dimens.l), child: Screenshot( controller: captureController, - child: CodeTextView( - code: snippet!.code!.raw!, - tokens: snippet!.code?.tokens, + child: ColoredBox( + color: ColorStyles.codeBackground(), + child: Padding( + padding: const EdgeInsets.all(Dimens.m), + child: CodeTextView( + code: snippet!.code!.raw!, + tokens: snippet!.code?.tokens, + ), + ), ), ), ), @@ -175,7 +173,7 @@ class _DetailPageData extends StatelessWidget { Center( child: SnippetActionBar( snippet: snippet!, - onFavoriteTap: model.favorite, + onFavoriteTap: model.toggleFavorite, onSaveTap: saveImage, onCopyTap: model.copyToClipboard, onShareTap: shareImage, From 94bd731082a0a7604fb7cf34a5e71b01925b51a7 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Thu, 6 Mar 2025 12:00:54 +0100 Subject: [PATCH 11/23] Corrected merge errors --- .../channel/EventStreamHandlerPlugin.kt | 48 +++++++++++ .../channel/details/DetailsModelPlugin.kt | 83 +++++++++++++++++++ .../dev/snipme/snipmeapp/di/ChannelModule.kt | 18 ++++ flutter_module/channel/contract.dart | 7 -- .../lib/generated/data_model.g.dart | 4 +- 5 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt diff --git a/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt new file mode 100644 index 0000000..a0230cf --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/EventStreamHandlerPlugin.kt @@ -0,0 +1,48 @@ +package dev.snipme.snipmeapp.channel + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.common.BinaryMessenger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class FlowChannelEventStreamHandler : ChannelEventStreamHandler() { + private val scope = CoroutineScope(Dispatchers.Main) + private val sinkFlow = MutableSharedFlow( + replay = 3, + onBufferOverflow = DROP_OLDEST + ) + + fun onSetup(messenger: BinaryMessenger) { + register(messenger, this) + } + + override fun onListen(p0: Any?, sink: PigeonEventSink) { + sinkFlow.onEach { sink.success(it) }.launchIn(scope) + } + + override fun onCancel(p0: Any?) { + sinkFlow.resetReplayCache() + } + + fun zip(flow: Flow) { + flow.onEach { sinkFlow.emit(it) }.launchIn(scope) + } +} + +class EventStreamHandlerPlugin : FlutterPlugin, KoinComponent { + private val eventStream by inject() + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + eventStream.onSetup(binding.binaryMessenger) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) {} +} \ No newline at end of file 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 new file mode 100644 index 0000000..55496d6 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModelPlugin.kt @@ -0,0 +1,83 @@ +package dev.snipme.snipmeapp.channel.details + +import dev.snipme.snipmeapp.channel.ChannelDetailsModel +import dev.snipme.snipmeapp.channel.FlowChannelEventStreamHandler +import dev.snipme.snipmeapp.channel.FlowChannelStateStreamHandler +import dev.snipme.snipmeapp.channel.ModelPlugin +import dev.snipme.snipmeapp.channel.toModelData +import io.flutter.plugin.common.BinaryMessenger +import kotlinx.coroutines.flow.map +import org.koin.core.component.inject +import dev.snipme.snipmeapp.channel.DetailsModelEvent as ChannelDetailsModelEvent +import dev.snipme.snipmeapp.channel.DetailsModelEventData as ChannelDetailsModelEventData +import dev.snipme.snipmeapp.channel.DetailsModelStateData as ChannelDetailsModelStateData +import dev.snipme.snipmeapp.channel.ModelState as ChannelModelState + +class DetailsModelPlugin : ModelPlugin(), ChannelDetailsModel { + private val model: DetailsModel by inject() + private val channelStateFlow by inject() + private val channelEventFlow by inject() + + override fun onSetup(messenger: BinaryMessenger, channelModel: ChannelDetailsModel?) { + ChannelDetailsModel.setUp(messenger, channelModel) + channelStateFlow.zip(model.state.map { getModelState(it) }) + channelEventFlow.zip(model.event.map { getModelEvent(it) }) + } + + override fun resetEvent() { + model.event.value = Idle + } + + override fun load(uuid: String) { + model.load(uuid) + } + + override fun toggleFavorite() { + model.toggleFavorite() + } + + override fun saveImage(image: ByteArray) { + model.save(image) + } + + override fun copyToClipboard() { + model.copyToClipboard() + } + + override fun shareImage(image: ByteArray) { + model.share(image) + } + + override fun delete() { + model.delete() + } + + private fun getModelState(viewState: DetailsViewState): ChannelDetailsModelStateData { + return ChannelDetailsModelStateData( + state = viewState.toModelState(), + isLoading = viewState is Loading, + data = (viewState as? Loaded)?.snippet?.toModelData(), + ) + } + + private fun getModelEvent(event: DetailsEvent): ChannelDetailsModelEventData { + return ChannelDetailsModelEventData( + event = event.toModelEvent(), + value = (event as? Alert)?.message.orEmpty() + ) + } + + private fun DetailsViewState.toModelState() = + when (this) { + Loading -> ChannelModelState.LOADING + is Loaded -> ChannelModelState.LOADED + else -> ChannelModelState.ERROR + } + + private fun DetailsEvent.toModelEvent() = + when (this) { + is Deleted -> ChannelDetailsModelEvent.DELETED + is Alert -> ChannelDetailsModelEvent.ALERT + else -> ChannelDetailsModelEvent.NONE + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt new file mode 100644 index 0000000..e5e49a7 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt @@ -0,0 +1,18 @@ +package dev.snipme.snipmeapp.di + +import dev.snipme.snipmeapp.channel.FlowChannelEventStreamHandler +import dev.snipme.snipmeapp.channel.FlowChannelStateStreamHandler +import dev.snipme.snipmeapp.channel.details.DetailsModel +import dev.snipme.snipmeapp.channel.login.LoginModel +import dev.snipme.snipmeapp.channel.main.MainModel +import dev.snipme.snipmeapp.channel.session.SessionModel +import org.koin.dsl.module + +internal val channelModule = module { + single { FlowChannelStateStreamHandler() } + single { FlowChannelEventStreamHandler() } + single { SessionModel(get()) } + single { LoginModel(get(), get(), get()) } + single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } +} \ No newline at end of file diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 30ccc08..9449d2a 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -160,13 +160,6 @@ abstract class ChannelModelEventApi { ModelEventData channelEvent(); } -@EventChannelApi() -abstract class ChannelModelEventApi { - ModelStateData channelState(); - - ModelEventData channelEvent(); -} - @HostApi() abstract class ChannelMainModel { void resetEvent(); diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index c92bd8a..39faa95 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -647,7 +647,7 @@ Stream channelState( {String instanceName = ''}) { return event as ModelStateData; }); } - + Stream channelEvent( {String instanceName = ''}) { if (instanceName.isNotEmpty) { instanceName = '.$instanceName'; @@ -658,7 +658,7 @@ Stream channelEvent( {String instanceName = ''}) { return event as ModelEventData; }); } - + class ChannelMainModel { /// Constructor for [ChannelMainModel]. The [binaryMessenger] named argument is From d513ff702cb87d97597b5cc8f08eb8d3be934cd3 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Thu, 6 Mar 2025 12:15:15 +0100 Subject: [PATCH 12/23] Fixed controller instance --- .../presentation/screens/details_screen.dart | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index 1dbf09e..84da026 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -48,8 +48,8 @@ class _DetailsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final captureController = ScreenshotController(); useNavigator([navigator]); + final captureController = useRef(ScreenshotController()); final stateNotification = ref.watch(detailsPageStateProvider); final state = stateNotification.state; @@ -69,18 +69,24 @@ class _DetailsPage extends HookConsumerWidget { model.resetEvent(); }); - void saveImage() { - captureController.capture().then((image) { + Future saveImage() async { + try { + final image = await captureController.value.capture(); if (image == null) return; model.saveImage(image); - }); + } catch (e, s) { + print('Could not invoke save image, error: $e, stack: $s'); + } } - void shareImage() { - captureController.capture().then((image) { + Future shareImage() async { + try { + final image = await captureController.value.capture(); if (image == null) return; model.shareImage(image); - }); + } catch (e, s) { + print('Could not invoke share image, error: $e, stack: $s'); + } } useEffect(() { @@ -111,7 +117,7 @@ class _DetailsPage extends HookConsumerWidget { builder: (_, snippet) => _DetailPageData( model: model, snippet: snippet, - captureController: captureController, + captureController: captureController.value, saveImage: saveImage, shareImage: shareImage, ), From a94cf4fe2fad30b68bd15f5f84c9c064e09ec107 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Fri, 7 Mar 2025 08:17:06 +0100 Subject: [PATCH 13/23] Changed reactions to favorite --- .../snipme/snipmeapp/channel/DataModel.g.kt | 103 +++++++----------- .../snipme/snipmeapp/channel/ModelPlugin.kt | 23 +--- .../snipmeapp/channel/details/DetailsModel.kt | 49 ++++----- .../dev/snipme/snipmeapp/di/ChannelModule.kt | 2 +- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 33 ++++-- .../domain/favorite/SetFavoriteSnippet.kt | 20 ++++ .../reaction/GetTargetUserReactionUseCase.kt | 11 -- .../domain/reaction/SetUserReactionUseCase.kt | 25 ----- .../snipmeapp/domain/reaction/UserReaction.kt | 7 -- .../repository/snippet/SnippetRepository.kt | 14 +-- .../snippet/SnippetRepositoryReal.kt | 18 ++- .../domain/snippet/CreateSnippetUseCase.kt | 9 +- .../domain/snippet/EditInteractor.kt | 9 +- .../domain/snippet/UpdateSnippetUseCase.kt | 6 +- .../snipmeapp/domain/snippets/Snippet.kt | 11 +- .../domain/snippets/SnippetResponseMapper.kt | 19 +--- .../infrastructure/local/AppDatabase.kt | 5 +- .../infrastructure/local/SnippetDao.kt | 24 ++-- .../infrastructure/local/SnippetEntity.kt | 14 +-- flutter_module/channel/contract.dart | 6 +- .../lib/generated/data_model.g.dart | 98 +++++++---------- .../widgets/snippet_action_bar.dart | 2 +- .../widgets/snippet_details_bar.dart | 32 +----- flutter_module/lib/utils/mock/mocks.dart | 4 +- 24 files changed, 193 insertions(+), 351 deletions(-) create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/GetTargetUserReactionUseCase.kt delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/SetUserReactionUseCase.kt delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/reaction/UserReaction.kt 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..50aa5f7 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), @@ -181,10 +169,8 @@ data class Snippet ( 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 isFavorite: Boolean? = null, val isSaved: Boolean? = null, val isToDelete: Boolean? = null ) @@ -199,13 +185,11 @@ data class Snippet ( 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 isPrivate = pigeonVar_list[8] as Boolean? + val isFavorite = pigeonVar_list[9] as Boolean? + val isSaved = pigeonVar_list[10] as Boolean? + val isToDelete = pigeonVar_list[11] as Boolean? + return Snippet(uuid, title, code, language, owner, isOwner, timeAgo, voteResult, isPrivate, isFavorite, isSaved, isToDelete) } } fun toList(): List { @@ -218,10 +202,8 @@ data class Snippet ( isOwner, timeAgo, voteResult, - userReaction, isPrivate, - isLiked, - isDisliked, + isFavorite, isSaved, isToDelete, ) @@ -503,86 +485,81 @@ 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() -> { + 139.toByte() -> { return (readValue(buffer) as? List)?.let { Owner.fromList(it) } } - 141.toByte() -> { + 140.toByte() -> { return (readValue(buffer) as? List)?.let { SnippetFilter.fromList(it) } } - 142.toByte() -> { + 141.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelStateData.fromList(it) } } - 143.toByte() -> { + 142.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelEventData.fromList(it) } } - 144.toByte() -> { + 143.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelStateData.fromList(it) } } - 145.toByte() -> { + 144.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelEventData.fromList(it) } } - 146.toByte() -> { + 145.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelStateData.fromList(it) } } - 147.toByte() -> { + 146.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelEventData.fromList(it) } @@ -600,72 +577,68 @@ 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) + stream.write(138) writeValue(stream, value.toList()) } is Owner -> { - stream.write(140) + stream.write(139) writeValue(stream, value.toList()) } is SnippetFilter -> { - stream.write(141) + stream.write(140) writeValue(stream, value.toList()) } is MainModelStateData -> { - stream.write(142) + stream.write(141) writeValue(stream, value.toList()) } is MainModelEventData -> { - stream.write(143) + stream.write(142) writeValue(stream, value.toList()) } is DetailsModelStateData -> { - stream.write(144) + stream.write(143) writeValue(stream, value.toList()) } is DetailsModelEventData -> { - stream.write(145) + stream.write(144) writeValue(stream, value.toList()) } is LoginModelStateData -> { - stream.write(146) + stream.write(145) writeValue(stream, value.toList()) } is LoginModelEventData -> { - stream.write(147) + stream.write(146) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) 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..9dafcd6 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,17 @@ 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.Owner as ChannelOwner 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 { @@ -39,10 +37,7 @@ fun Snippet.toModelData(): ChannelSnippet = 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), + isFavorite = favorite, isPrivate = visibility == SnippetVisibility.PRIVATE, isSaved = calculateSavedState(isOwner, visibility), isToDelete = isOwner, @@ -69,16 +64,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 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..2775381 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,10 +10,8 @@ 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 @@ -24,14 +22,14 @@ import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import timber.log.Timber 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, @@ -72,7 +70,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 +94,6 @@ class DetailsModel( } fun save(image: ByteArray) { - Timber.d("Saving snippet image ${image.size}") try { getSnippet()?.let { saveSnippet(image, it) @@ -97,10 +108,7 @@ 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) @@ -122,25 +130,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/di/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt index e5e49a7..e5e3f63 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt @@ -14,5 +14,5 @@ internal val channelModule = module { single { SessionModel(get()) } single { LoginModel(get(), get(), get()) } single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } - single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get()) } } \ 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..2cc46e6 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,34 @@ 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.user.GetSingleUserUseCase +import org.koin.dsl.module internal val useCaseModule = module { // Base @@ -38,8 +50,7 @@ internal val useCaseModule = module { 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()) } // Language factory { GetLanguagesUseCase(get(), get(), get()) } @@ -59,5 +70,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..2e52402 --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt @@ -0,0 +1,20 @@ +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, + userId = snippet.owner.id, + favorite = favorite, + ) +} \ 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..145fd27 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,12 +1,10 @@ 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 { @@ -21,7 +19,8 @@ interface SnippetRepository { code: String, language: String, visibility: SnippetVisibility, - userId: Int + userId: Int, + favorite: Boolean ): Single fun update( @@ -30,12 +29,11 @@ interface SnippetRepository { code: String, language: String, visibility: SnippetVisibility, - userId: Int + 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..3f1b78b 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,11 +1,9 @@ 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.extension.mapError @@ -15,9 +13,7 @@ import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import java.util.Date -const val PAGE_START = 0 const val SNIPPET_PAGE_SIZE = 10 -const val ONE_SNIPPET = 1 class SnippetRepositoryReal( private val errorHandler: ErrorHandler, @@ -40,7 +36,8 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int + userId: Int, + favorite: Boolean ): Single { return service.create( SnippetEntry( @@ -50,7 +47,9 @@ class SnippetRepositoryReal( modifiedAt = Date().toString(), visibility = visibility.name, ownerId = userId, + language = language, + favorite = favorite ) ) .mapError { errorHandler.handle(it) } @@ -67,9 +66,10 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int + 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) @@ -84,8 +84,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..7cdc3d0 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.PUBLIC, + favorite: Boolean = false, ): Single = auth() .andThen(getSingleUser()) .flatMap { user -> @@ -26,7 +26,8 @@ class CreateSnippetUseCase( code = code, language = language, visibility = visibility, - userId = user.id + 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/UpdateSnippetUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippet/UpdateSnippetUseCase.kt index ed9c6af..c234901 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, it.id, favorite)} .doOnSuccess() { repository.updateListener.onNext(it) Single.just(it) 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..4359ead 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, @@ -13,9 +12,7 @@ data class Snippet( 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( @@ -27,9 +24,7 @@ data class Snippet( isOwner = false, owner = Owner(0, ""), 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..0cd6814 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,31 @@ 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, + isOwner = true, owner = Owner(ownerId , response.ownerName), modifiedAt = modifiedAt.toDate(), - numberOfLikes = response.numberOfLikes, - numberOfDislikes = response.numberOfDislikes, - userReaction = getUserReaction(response.userReaction) + 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) 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..d32191d 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,10 +1,11 @@ package dev.snipme.snipmeapp.infrastructure.local + +import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -import android.content.Context -@Database(entities = [UserEntry::class, SnippetEntry::class, ReactionEntry::class], version = 1) +@Database(entities = [UserEntry::class, SnippetEntry::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun snippetDao(): SnippetDao 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..ad402fa 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 @@ -12,31 +12,23 @@ 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 + CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner 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 + 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 + CASE WHEN s.ownerId = :userId THEN 1 ELSE 0 END as isOwner 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 """ ) - fun snippets(userId: Int): Single> + fun snippets(userId: Int): Single> @Query("SELECT COUNT(*) FROM snippets") fun count(): Single @@ -51,7 +43,8 @@ interface SnippetDao { code = :code, modifiedAt = current_timestamp, visibility = :visibility, - language = :language + language = :language, + favorite = :favorite WHERE id = :uuid """ ) @@ -61,12 +54,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..404a621 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,10 +1,8 @@ package dev.snipme.snipmeapp.infrastructure.local -import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey - @Entity(tableName = "snippets") data class SnippetEntry( @PrimaryKey(true) val id: Long = 0, @@ -14,15 +12,7 @@ data class SnippetEntry( val modifiedAt: String, 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 language: String, + val favorite: Boolean, ) diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 9449d2a..6d1f97a 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -19,10 +19,8 @@ class Snippet { bool? isOwner; String? timeAgo; int? voteResult; - UserReaction? userReaction; bool? isPrivate; - bool? isLiked; - bool? isDisliked; + bool? isFavorite; bool? isSaved; bool? isToDelete; } @@ -105,8 +103,6 @@ class SnippetFilter { String? selectedScope; } -enum UserReaction { none, like, dislike } - enum ModelState { loading, loaded, error } enum MainModelEvent { none, alert, logout } diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 39faa95..f67cdf6 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, @@ -108,10 +102,8 @@ class Snippet { this.isOwner, this.timeAgo, this.voteResult, - this.userReaction, this.isPrivate, - this.isLiked, - this.isDisliked, + this.isFavorite, this.isSaved, this.isToDelete, }); @@ -132,13 +124,9 @@ class Snippet { int? voteResult; - UserReaction? userReaction; - bool? isPrivate; - bool? isLiked; - - bool? isDisliked; + bool? isFavorite; bool? isSaved; @@ -154,10 +142,8 @@ class Snippet { isOwner, timeAgo, voteResult, - userReaction, isPrivate, - isLiked, - isDisliked, + isFavorite, isSaved, isToDelete, ]; @@ -174,12 +160,10 @@ class Snippet { 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?, + isPrivate: result[8] as bool?, + isFavorite: result[9] as bool?, + isSaved: result[10] as bool?, + isToDelete: result[11] as bool?, ); } } @@ -525,56 +509,53 @@ 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); + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is Owner) { - buffer.putUint8(140); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is SnippetFilter) { - buffer.putUint8(141); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is MainModelStateData) { - buffer.putUint8(142); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is MainModelEventData) { - buffer.putUint8(143); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is DetailsModelStateData) { - buffer.putUint8(144); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else if (value is DetailsModelEventData) { - buffer.putUint8(145); + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is LoginModelStateData) { - buffer.putUint8(146); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else if (value is LoginModelEventData) { - buffer.putUint8(147); + buffer.putUint8(146); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -591,43 +572,40 @@ 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: + case 139: return Owner.decode(readValue(buffer)!); - case 141: + case 140: return SnippetFilter.decode(readValue(buffer)!); - case 142: + case 141: return MainModelStateData.decode(readValue(buffer)!); - case 143: + case 142: return MainModelEventData.decode(readValue(buffer)!); - case 144: + case 143: return DetailsModelStateData.decode(readValue(buffer)!); - case 145: + case 144: return DetailsModelEventData.decode(readValue(buffer)!); - case 146: + case 145: return LoginModelStateData.decode(readValue(buffer)!); - case 147: + case 146: return LoginModelEventData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 7e0b274..7de68ea 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -31,7 +31,7 @@ class SnippetActionBar extends StatelessWidget { children: [ StateIcon( icon: Icons.favorite, - active: snippet.isLiked, + active: snippet.isFavorite, onTap: onFavoriteTap, ), const SizedBox(width: Dimens.l), diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index 5de306b..643a48e 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -32,38 +32,16 @@ class SnippetDetailsBar extends StatelessWidget { ], ), ), - _UserReactionIndicator(reaction: snippet.userReaction), - const SizedBox(width: Dimens.l), SurfaceStyles.rateBox( TextStyles.title( - _getVoteCountText(snippet.voteResult), + TextSpan( + children: [ + WidgetSpan(child: Icon(Icons.visibility)), + ] + ).text! ), ) ], ); } - - // 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 UserReaction? reaction; - final _scale = 2.0; - - @override - Widget build(BuildContext context) { - // TODO Implement favorite - return SizedBox.shrink(); - } } diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart index 7da7de7..a5525ed 100644 --- a/flutter_module/lib/utils/mock/mocks.dart +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -8,9 +8,7 @@ class Mocks { owner: Owner(id: 0, login: 'Snippet owner'), timeAgo: '2 days ago', voteResult: 32, - userReaction: UserReaction.like, - isLiked: true, - isDisliked: false, + isFavorite: false, isPrivate: true, language: SnippetLanguage( raw: 'Python', From 89899831b8d481d2971cdca3d8364226198b8de0 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Fri, 7 Mar 2025 08:32:33 +0100 Subject: [PATCH 14/23] Removed owner --- .../snipme/snipmeapp/channel/ModelPlugin.kt | 12 ---------- .../dev/snipme/snipmeapp/di/ServiceModule.kt | 8 +++++-- .../domain/favorite/SetFavoriteSnippet.kt | 1 - .../filter/FilterSnippetsByScopeUseCase.kt | 2 +- .../repository/snippet/SnippetRepository.kt | 5 ++-- .../snippet/SnippetRepositoryReal.kt | 15 +++++------- .../domain/snippet/GetSingleSnippetUseCase.kt | 5 ++-- .../ObserveUpdatedSnippetPageUseCase.kt | 11 ++++----- .../domain/snippet/UpdateSnippetUseCase.kt | 2 +- .../domain/snippets/GetSnippetsUseCase.kt | 8 +++---- .../snipmeapp/domain/snippets/Snippet.kt | 4 ---- .../domain/snippets/SnippetResponseMapper.kt | 2 -- .../infrastructure/local/AppDatabase.kt | 19 --------------- .../infrastructure/local/SnippetDao.kt | 23 ++++++------------- .../infrastructure/local/SnippetEntity.kt | 2 -- 15 files changed, 33 insertions(+), 86 deletions(-) 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 9dafcd6..10e3350 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt @@ -35,12 +35,8 @@ fun Snippet.toModelData(): ChannelSnippet = title = title, code = code.toModelSnippetCode(), language = language.toModelSnippetLanguage(), - owner = owner.toModelOwner(), - isOwner = isOwner, isFavorite = favorite, isPrivate = visibility == SnippetVisibility.PRIVATE, - isSaved = calculateSavedState(isOwner, visibility), - isToDelete = isOwner, timeAgo = DateUtils.getRelativeTimeSpanString( modifiedAt.time, Date().time, @@ -64,14 +60,6 @@ private fun SnippetLanguage.toModelSnippetLanguage() = type = ChannelSnippetLanguageType.valueOf(type.name), ) -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/di/ServiceModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt index b7c9db7..872a14e 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ServiceModule.kt @@ -4,8 +4,12 @@ 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 { @@ -19,7 +23,7 @@ internal val serviceModule = module { single { Room.databaseBuilder( get(), AppDatabase::class.java, "app_database" - ).createFromAsset("app_database.db").build() + ).build() } single { get().userDao() } 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 index 2e52402..bf3358d 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/favorite/SetFavoriteSnippet.kt @@ -14,7 +14,6 @@ class SetFavoriteSnippet( code = snippet.code.raw, language = snippet.language.raw, visibility = snippet.visibility, - userId = snippet.owner.id, 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/repository/snippet/SnippetRepository.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/repository/snippet/SnippetRepository.kt index 145fd27..d189615 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 @@ -10,9 +10,9 @@ interface SnippetRepository { val updateListener: BehaviorSubject - fun snippets(userId: Int): Single> + fun snippets(): Single> - fun snippet(uuid: String, userId: Int): Single + fun snippet(uuid: String): Single fun create( title: String, @@ -29,7 +29,6 @@ interface SnippetRepository { code: String, language: String, visibility: SnippetVisibility, - userId: Int, favorite: Boolean ): Single 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 3f1b78b..620ba6e 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 @@ -22,13 +22,13 @@ class SnippetRepositoryReal( ) : SnippetRepository { override val updateListener = BehaviorSubject.create() - override fun snippets(userId: Int): Single> = - service.snippets(userId) + 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( @@ -46,15 +46,13 @@ class SnippetRepositoryReal( createdAt = Date().toString(), modifiedAt = Date().toString(), 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) } } @@ -66,13 +64,12 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int, favorite: Boolean ): Single = 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) } ) 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 c234901..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 @@ -20,7 +20,7 @@ class UpdateSnippetUseCase( favorite: Boolean ) = auth() .andThen(getSingleUser()) - .flatMap{repository.update(uuid, title, code, language, visibility, it.id, favorite)} + .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..3712aec 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,10 +1,9 @@ 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.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase +import io.reactivex.Single class GetSnippetsUseCase( private val auth: AuthorizationUseCase, @@ -14,9 +13,8 @@ class GetSnippetsUseCase( operator fun invoke(scope: SnippetScope, page: Int): Single> = auth() .andThen(getSingleUser()) - .flatMap { - user -> - repository.snippets(user.id) + .flatMap { user -> + repository.snippets() .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/Snippet.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt index 4359ead..36ce39e 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 @@ -9,8 +9,6 @@ data class Snippet( val code: SnippetCode, val language: SnippetLanguage, val visibility: SnippetVisibility, - val isOwner: Boolean, - val owner: Owner, val modifiedAt: Date, val favorite: Boolean, ) { @@ -21,8 +19,6 @@ data class Snippet( code = SnippetCode("", SpannableString("")), language = SnippetLanguage("", SnippetLanguageType.UNKNOWN), visibility = SnippetVisibility.PRIVATE, - isOwner = false, - owner = Owner(0, ""), modifiedAt = Date(), 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 0cd6814..1c2de38 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 @@ -19,8 +19,6 @@ class SnippetResponseMapper { code = getCode(code), language = getLanguage(language), visibility = getVisibility(visibility), - isOwner = true, - owner = Owner(ownerId , response.ownerName), modifiedAt = modifiedAt.toDate(), favorite = favorite, ) 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 d32191d..c339b62 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,29 +1,10 @@ package dev.snipme.snipmeapp.infrastructure.local -import android.content.Context import androidx.room.Database -import androidx.room.Room import androidx.room.RoomDatabase @Database(entities = [UserEntry::class, SnippetEntry::class], version = 1) 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 ad402fa..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,24 +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 - FROM snippets as s - INNER JOIN users as u ON s.ownerId = u.id - 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 - FROM snippets as s - INNER JOIN users as u ON s.ownerId = u.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 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 404a621..c1f8ed2 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 @@ -11,8 +11,6 @@ data class SnippetEntry( val createdAt: String, val modifiedAt: String, val visibility: String, - val ownerId: Int, - val ownerName: String, val language: String, val favorite: Boolean, ) From efcf36c3492c688ef908fc9b6e6117eed983c9ca Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Sun, 9 Mar 2025 12:03:33 +0100 Subject: [PATCH 15/23] Setup demo data on start --- ...otlin-compiler-11651766980853507312.salive | 0 .../snipmeapp/channel/details/DetailsModel.kt | 1 - .../snipmeapp/channel/main/MainModel.kt | 51 ++++++-- .../dev/snipme/snipmeapp/di/ChannelModule.kt | 2 +- .../dev/snipme/snipmeapp/di/KoinConfig.kt | 2 +- .../snipme/snipmeapp/di/PreferenceModule.kt | 11 -- .../snipme/snipmeapp/di/RepositoryModule.kt | 4 +- .../dev/snipme/snipmeapp/di/ServiceModule.kt | 15 --- .../dev/snipme/snipmeapp/di/StorageModule.kt | 22 ++++ .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 4 + .../repository/snippet/SnippetRepository.kt | 2 + .../snippet/SnippetRepositoryReal.kt | 6 + .../GetDemoSnippetsSetupStatusUseCase.kt | 12 ++ .../snippets/SetupDemoSnippetsUseCase.kt | 116 ++++++++++++++++++ 14 files changed, 208 insertions(+), 40 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-11651766980853507312.salive delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/PreferenceModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/di/StorageModule.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt create mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt diff --git a/.kotlin/sessions/kotlin-compiler-11651766980853507312.salive b/.kotlin/sessions/kotlin-compiler-11651766980853507312.salive new file mode 100644 index 0000000..e69de29 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 2775381..fbf0bd6 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 @@ -22,7 +22,6 @@ import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import timber.log.Timber class DetailsModel( 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..81cd864 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,25 +1,43 @@ package dev.snipme.snipmeapp.channel.main -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.error.ErrorParsable 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.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.* +import dev.snipme.snipmeapp.domain.snippets.GetDemoSnippetsSetupStatusUseCase +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 dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase import dev.snipme.snipmeapp.domain.user.User -import dev.snipme.snipmeapp.channel.error.ErrorParsable +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 timber.log.Timber private const val ONE_PAGE = 1 class MainModel( private val errorMessages: ErrorMessages, + private val getDemoSetupStatus: GetDemoSnippetsSetupStatusUseCase, + private val setupDemoSnippets: SetupDemoSnippetsUseCase, private val getUser: GetSingleUserUseCase, private val getSnippets: GetSnippetsUseCase, private val observeUpdates: ObserveSnippetUpdatesUseCase, @@ -47,10 +65,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 } @@ -76,6 +97,18 @@ class MainModel( selectedScope = "All" ) + // TODO Get demo status + + setupDemoSnippets() + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { println("Setup demo data") }, + onError = { + Timber.e("Couldn't setup demo snippets, error = $it") + parseError(it) + } + ).also { disposables += it } + getUser() .subscribeOn(Schedulers.io()) .subscribeBy( diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt index e5e3f63..50f7ede 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt @@ -13,6 +13,6 @@ internal val channelModule = module { single { FlowChannelEventStreamHandler() } single { SessionModel(get()) } single { LoginModel(get(), get(), get()) } - single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get()) } } \ No newline at end of file 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 872a14e..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,9 +1,5 @@ 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 @@ -18,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" - ).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 2cc46e6..01b961f 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -25,8 +25,10 @@ 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.GetDemoSnippetsSetupStatusUseCase 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 @@ -52,6 +54,8 @@ internal val useCaseModule = module { factory { ObserveSnippetUpdatesUseCase(get()) } factory { SetFavoriteSnippet(get()) } factory { DeleteSnippetUseCase(get()) } + factory { GetDemoSnippetsSetupStatusUseCase(get()) } + factory { SetupDemoSnippetsUseCase(get()) } // Language factory { GetLanguagesUseCase(get(), get(), get()) } // Share 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 d189615..7bb06b8 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 @@ -10,6 +10,8 @@ interface SnippetRepository { val updateListener: BehaviorSubject + fun getDemoSetupStatus(): Boolean + fun snippets(): Single> fun snippet(uuid: String): Single 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 620ba6e..ad9b179 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 @@ -6,6 +6,7 @@ import dev.snipme.snipmeapp.domain.snippets.SnippetResponseMapper import dev.snipme.snipmeapp.domain.snippets.SnippetVisibility 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 @@ -13,15 +14,20 @@ import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import java.util.Date +const val KEY_DEMO_SETUP_STATUS = "KEY_INITIALIZATION_STATUS" const val SNIPPET_PAGE_SIZE = 10 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 getDemoSetupStatus(): Boolean = + preferencesUtil.get(KEY_DEMO_SETUP_STATUS) ?: false + override fun snippets(): Single> = service.snippets() .mapError { errorHandler.handle(it) } diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt new file mode 100644 index 0000000..f15307e --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt @@ -0,0 +1,12 @@ +package dev.snipme.snipmeapp.domain.snippets + +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository + +class GetDemoSnippetsSetupStatusUseCase( + private val snippetRepository: SnippetRepository +) { + + operator fun invoke(): Boolean { + return snippetRepository.getDemoSetupStatus() + } +} \ 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..b01425b --- /dev/null +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -0,0 +1,116 @@ +package dev.snipme.snipmeapp.domain.snippets + +import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository + +class SetupDemoSnippetsUseCase( + private val snippetRepository: SnippetRepository +) { + + operator fun invoke() = + snippetRepository.create( + title = "Your first snippet", + code = KOTLIN_SAMPLE, + language = SnippetLanguageType.KOTLIN.name, + visibility = SnippetVisibility.PUBLIC, + userId = 1, + favorite = false + ) + +// snippetRepository.create( +// title = "Hello World", +// code = "console.log('Hello, World!')", +// language = "JavaScript", +// visibility = SnippetVisibility.PUBLIC, +// userId = 1, +// favorite = false +// ) + +// snippetRepository.create( +// title = "Hello World", +// code = "print('Hello, World!')", +// language = "Python", +// visibility = SnippetVisibility.PUBLIC, +// userId = 1, +// favorite = false +// ) + +// snippetRepository.create( +// title = "Hello World", +// code = "System.out.println(\"Hello, World!\");", +// language = "Java", +// visibility = SnippetVisibility.PUBLIC, +// userId = 1, +// favorite = false +// ) + } +//} + +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) + } + } +} +""" \ No newline at end of file From a1b2e6062bda34f4550e801cf9186610a7eb2817 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Mon, 10 Mar 2025 08:02:51 +0100 Subject: [PATCH 16/23] Fixed date storing --- .../kotlin-compiler-11651766980853507312.salive | 0 .../snipme/snipmeapp/channel/main/MainModel.kt | 17 +++-------------- .../repository/snippet/SnippetRepositoryReal.kt | 4 ++-- .../domain/snippets/SetupDemoSnippetsUseCase.kt | 5 +++-- .../domain/snippets/SnippetResponseMapper.kt | 3 +-- .../infrastructure/local/AppDatabase.kt | 16 ++++++++++++++++ .../infrastructure/local/SnippetEntity.kt | 5 +++-- .../snipmeapp/util/extension/TextExtensions.kt | 14 +++++--------- 8 files changed, 33 insertions(+), 31 deletions(-) delete mode 100644 .kotlin/sessions/kotlin-compiler-11651766980853507312.salive diff --git a/.kotlin/sessions/kotlin-compiler-11651766980853507312.salive b/.kotlin/sessions/kotlin-compiler-11651766980853507312.salive deleted file mode 100644 index e69de29..0000000 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 81cd864..5995027 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 @@ -102,22 +102,12 @@ class MainModel( setupDemoSnippets() .subscribeOn(Schedulers.io()) .subscribeBy( - onSuccess = { println("Setup demo data") }, + onSuccess = { loadSnippets() }, onError = { Timber.e("Couldn't setup demo snippets, error = $it") parseError(it) } ).also { disposables += it } - - getUser() - .subscribeOn(Schedulers.io()) - .subscribeBy( - onSuccess = { user -> loadSnippets(user) }, - onError = { - Timber.e("Couldn't load user, error = $it") - parseError(it) - } - ).also { disposables += it } } fun filterLanguage(language: String, isSelected: Boolean) { @@ -153,7 +143,7 @@ class MainModel( .subscribeBy( onSuccess = { hasMore -> if (hasMore) { - loadSnippets(state.user, pages = state.pages + ONE_PAGE) + loadSnippets(pages = state.pages + ONE_PAGE) } }, onError = { @@ -165,7 +155,6 @@ class MainModel( } private fun loadSnippets( - user: User, pages: Int = 1, scope: SnippetScope = SnippetScope.ALL ) { @@ -178,7 +167,7 @@ class MainModel( val updatedFilters = getLanguageFilters(cachedSnippets) filterState = filterState.copy(languages = updatedFilters) mutableState.value = Loaded( - user, + User(0, "login", "email", ""), // TODO Remove it, pages, filterState 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 ad9b179..2ed506a 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 @@ -49,8 +49,8 @@ class SnippetRepositoryReal( SnippetEntry( title = title, code = code, - createdAt = Date().toString(), - modifiedAt = Date().toString(), + createdAt = Date(), + modifiedAt = Date(), visibility = visibility.name, language = language, favorite = favorite 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 index b01425b..58122ca 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -1,6 +1,7 @@ package dev.snipme.snipmeapp.domain.snippets import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository +import dev.snipme.snipmeapp.util.extension.titleCase class SetupDemoSnippetsUseCase( private val snippetRepository: SnippetRepository @@ -10,7 +11,7 @@ class SetupDemoSnippetsUseCase( snippetRepository.create( title = "Your first snippet", code = KOTLIN_SAMPLE, - language = SnippetLanguageType.KOTLIN.name, + language = SnippetLanguageType.KOTLIN.name.titleCase(), visibility = SnippetVisibility.PUBLIC, userId = 1, favorite = false @@ -45,7 +46,7 @@ class SetupDemoSnippetsUseCase( } //} -val KOTLIN_SAMPLE = """ +const val KOTLIN_SAMPLE = """ // Data class data class User(val id: Int, val name: String, val email: String) 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 1c2de38..232d9c2 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 @@ -5,7 +5,6 @@ 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 const val PREVIEW_COUNT = 5 @@ -19,7 +18,7 @@ class SnippetResponseMapper { code = getCode(code), language = getLanguage(language), visibility = getVisibility(visibility), - modifiedAt = modifiedAt.toDate(), + modifiedAt = modifiedAt, favorite = favorite, ) } 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 c339b62..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 @@ -2,8 +2,24 @@ package dev.snipme.snipmeapp.infrastructure.local import androidx.room.Database import androidx.room.RoomDatabase +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) } + } + + @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 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 c1f8ed2..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 @@ -2,14 +2,15 @@ package dev.snipme.snipmeapp.infrastructure.local 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 language: String, 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() } From 655e44a6f1b76701700abec4694f10099ee9c987 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Mon, 10 Mar 2025 08:31:33 +0100 Subject: [PATCH 17/23] Fixed demo setup --- .../snipme/snipmeapp/channel/DataModel.g.kt | 76 +++++------------- .../snipme/snipmeapp/channel/ModelPlugin.kt | 3 - .../snipmeapp/channel/main/MainModel.kt | 8 +- .../dev/snipme/snipmeapp/di/ChannelModule.kt | 2 +- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 2 - .../snippet/SnippetRepositoryReal.kt | 2 +- .../GetDemoSnippetsSetupStatusUseCase.kt | 12 --- .../snippets/SetupDemoSnippetsUseCase.kt | 67 ++++++++-------- flutter_module/channel/contract.dart | 7 -- .../lib/generated/data_model.g.dart | 79 +++++-------------- .../widgets/snippet_details_bar.dart | 8 +- flutter_module/lib/utils/mock/mocks.dart | 1 - 12 files changed, 76 insertions(+), 191 deletions(-) delete mode 100644 app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt 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 50aa5f7..cb6ae21 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 @@ -165,8 +165,6 @@ 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 isPrivate: Boolean? = null, @@ -181,15 +179,13 @@ 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 isPrivate = pigeonVar_list[8] as Boolean? - val isFavorite = pigeonVar_list[9] as Boolean? - val isSaved = pigeonVar_list[10] as Boolean? - val isToDelete = pigeonVar_list[11] as Boolean? - return Snippet(uuid, title, code, language, owner, isOwner, timeAgo, voteResult, isPrivate, isFavorite, isSaved, isToDelete) + val timeAgo = pigeonVar_list[4] as String? + val voteResult = pigeonVar_list[5] as Long? + val isPrivate = pigeonVar_list[6] as Boolean? + val isFavorite = pigeonVar_list[7] as Boolean? + val isSaved = pigeonVar_list[8] as Boolean? + val isToDelete = pigeonVar_list[9] as Boolean? + return Snippet(uuid, title, code, language, timeAgo, voteResult, isPrivate, isFavorite, isSaved, isToDelete) } } fun toList(): List { @@ -198,8 +194,6 @@ data class Snippet ( title, code, language, - owner, - isOwner, timeAgo, voteResult, isPrivate, @@ -276,27 +270,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, @@ -525,41 +498,36 @@ private open class DataModelPigeonCodec : StandardMessageCodec() { } } 139.toByte() -> { - return (readValue(buffer) as? List)?.let { - Owner.fromList(it) - } - } - 140.toByte() -> { return (readValue(buffer) as? List)?.let { SnippetFilter.fromList(it) } } - 141.toByte() -> { + 140.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelStateData.fromList(it) } } - 142.toByte() -> { + 141.toByte() -> { return (readValue(buffer) as? List)?.let { MainModelEventData.fromList(it) } } - 143.toByte() -> { + 142.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelStateData.fromList(it) } } - 144.toByte() -> { + 143.toByte() -> { return (readValue(buffer) as? List)?.let { DetailsModelEventData.fromList(it) } } - 145.toByte() -> { + 144.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelStateData.fromList(it) } } - 146.toByte() -> { + 145.toByte() -> { return (readValue(buffer) as? List)?.let { LoginModelEventData.fromList(it) } @@ -609,36 +577,32 @@ private open class DataModelPigeonCodec : StandardMessageCodec() { stream.write(138) writeValue(stream, value.toList()) } - is Owner -> { - stream.write(139) - writeValue(stream, value.toList()) - } is SnippetFilter -> { - stream.write(140) + stream.write(139) writeValue(stream, value.toList()) } is MainModelStateData -> { - stream.write(141) + stream.write(140) writeValue(stream, value.toList()) } is MainModelEventData -> { - stream.write(142) + stream.write(141) writeValue(stream, value.toList()) } is DetailsModelStateData -> { - stream.write(143) + stream.write(142) writeValue(stream, value.toList()) } is DetailsModelEventData -> { - stream.write(144) + stream.write(143) writeValue(stream, value.toList()) } is LoginModelStateData -> { - stream.write(145) + stream.write(144) writeValue(stream, value.toList()) } is LoginModelEventData -> { - stream.write(146) + stream.write(145) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) 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 10e3350..f683be1 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt @@ -9,7 +9,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import org.koin.core.component.KoinComponent import java.util.Date -import dev.snipme.snipmeapp.channel.Owner as ChannelOwner import dev.snipme.snipmeapp.channel.Snippet as ChannelSnippet import dev.snipme.snipmeapp.channel.SnippetCode as ChannelSnippetCode import dev.snipme.snipmeapp.channel.SnippetLanguage as ChannelSnippetLanguage @@ -44,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, 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 5995027..a549f7f 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 @@ -16,14 +16,12 @@ 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.GetDemoSnippetsSetupStatusUseCase 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 dev.snipme.snipmeapp.domain.user.GetSingleUserUseCase import dev.snipme.snipmeapp.domain.user.User import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign @@ -36,9 +34,7 @@ private const val ONE_PAGE = 1 class MainModel( private val errorMessages: ErrorMessages, - private val getDemoSetupStatus: GetDemoSnippetsSetupStatusUseCase, private val setupDemoSnippets: SetupDemoSnippetsUseCase, - private val getUser: GetSingleUserUseCase, private val getSnippets: GetSnippetsUseCase, private val observeUpdates: ObserveSnippetUpdatesUseCase, private val hasMore: HasMoreSnippetPagesUseCase, @@ -97,12 +93,10 @@ class MainModel( selectedScope = "All" ) - // TODO Get demo status - setupDemoSnippets() .subscribeOn(Schedulers.io()) .subscribeBy( - onSuccess = { loadSnippets() }, + onComplete = { loadSnippets() }, onError = { Timber.e("Couldn't setup demo snippets, error = $it") parseError(it) diff --git a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt index 50f7ede..e5e3f63 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt @@ -13,6 +13,6 @@ internal val channelModule = module { single { FlowChannelEventStreamHandler() } single { SessionModel(get()) } single { LoginModel(get(), get(), get()) } - single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get()) } } \ 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 01b961f..0679ef3 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -25,7 +25,6 @@ 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.GetDemoSnippetsSetupStatusUseCase import dev.snipme.snipmeapp.domain.snippets.GetSnippetsUseCase import dev.snipme.snipmeapp.domain.snippets.HasMoreSnippetPagesUseCase import dev.snipme.snipmeapp.domain.snippets.SetupDemoSnippetsUseCase @@ -54,7 +53,6 @@ internal val useCaseModule = module { factory { ObserveSnippetUpdatesUseCase(get()) } factory { SetFavoriteSnippet(get()) } factory { DeleteSnippetUseCase(get()) } - factory { GetDemoSnippetsSetupStatusUseCase(get()) } factory { SetupDemoSnippetsUseCase(get()) } // Language factory { GetLanguagesUseCase(get(), get(), get()) } 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 2ed506a..fa34078 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 @@ -14,7 +14,7 @@ import io.reactivex.Single import io.reactivex.subjects.BehaviorSubject import java.util.Date -const val KEY_DEMO_SETUP_STATUS = "KEY_INITIALIZATION_STATUS" +const val KEY_DEMO_SETUP_STATUS = "KEY_DEMO_SETUP_STATUS" const val SNIPPET_PAGE_SIZE = 10 class SnippetRepositoryReal( diff --git a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt deleted file mode 100644 index f15307e..0000000 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/GetDemoSnippetsSetupStatusUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.snipme.snipmeapp.domain.snippets - -import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository - -class GetDemoSnippetsSetupStatusUseCase( - private val snippetRepository: SnippetRepository -) { - - operator fun invoke(): Boolean { - return snippetRepository.getDemoSetupStatus() - } -} \ 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 index 58122ca..9225417 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -2,49 +2,44 @@ package dev.snipme.snipmeapp.domain.snippets import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository import dev.snipme.snipmeapp.util.extension.titleCase +import io.reactivex.Completable +import io.reactivex.Single class SetupDemoSnippetsUseCase( private val snippetRepository: SnippetRepository ) { - operator fun invoke() = - snippetRepository.create( - title = "Your first snippet", - code = KOTLIN_SAMPLE, - language = SnippetLanguageType.KOTLIN.name.titleCase(), - visibility = SnippetVisibility.PUBLIC, - userId = 1, - favorite = false - ) + operator fun invoke(): Completable { + val demoSetup = snippetRepository.getDemoSetupStatus() -// snippetRepository.create( -// title = "Hello World", -// code = "console.log('Hello, World!')", -// language = "JavaScript", -// visibility = SnippetVisibility.PUBLIC, -// userId = 1, -// favorite = false -// ) - -// snippetRepository.create( -// title = "Hello World", -// code = "print('Hello, World!')", -// language = "Python", -// visibility = SnippetVisibility.PUBLIC, -// userId = 1, -// favorite = false -// ) - -// snippetRepository.create( -// title = "Hello World", -// code = "System.out.println(\"Hello, World!\");", -// language = "Java", -// visibility = SnippetVisibility.PUBLIC, -// userId = 1, -// favorite = false -// ) + return if (!demoSetup) { + setupDemoSnippets() + } else { + Completable.complete() + } } -//} + + private fun setupDemoSnippets() = Completable.fromPublisher( + Single.merge( + snippetRepository.create( + title = "Your first snippet", + code = KOTLIN_SAMPLE, + language = SnippetLanguageType.KOTLIN.name.titleCase(), + visibility = SnippetVisibility.PUBLIC, + userId = 1, + favorite = false + ), + snippetRepository.create( + title = "Hello World", + code = "console.log('Hello, World!')", + language = SnippetLanguageType.JAVASCRIPT.name.titleCase(), + visibility = SnippetVisibility.PUBLIC, + userId = 1, + favorite = false + ), + ), + ) +} const val KOTLIN_SAMPLE = """ // Data class diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 6d1f97a..0a70bf1 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -15,8 +15,6 @@ class Snippet { String? title; SnippetCode? code; SnippetLanguage? language; - Owner? owner; - bool? isOwner; String? timeAgo; int? voteResult; bool? isPrivate; @@ -41,11 +39,6 @@ class SnippetLanguage { SnippetLanguageType? type; } -class Owner { - int? id; - String? login; -} - enum SnippetLanguageType { c, cpp, diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index f67cdf6..5a82389 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -98,8 +98,6 @@ class Snippet { this.title, this.code, this.language, - this.owner, - this.isOwner, this.timeAgo, this.voteResult, this.isPrivate, @@ -116,10 +114,6 @@ class Snippet { SnippetLanguage? language; - Owner? owner; - - bool? isOwner; - String? timeAgo; int? voteResult; @@ -138,8 +132,6 @@ class Snippet { title, code, language, - owner, - isOwner, timeAgo, voteResult, isPrivate, @@ -156,14 +148,12 @@ 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?, - isPrivate: result[8] as bool?, - isFavorite: result[9] as bool?, - isSaved: result[10] as bool?, - isToDelete: result[11] as bool?, + timeAgo: result[4] as String?, + voteResult: result[5] as int?, + isPrivate: result[6] as bool?, + isFavorite: result[7] as bool?, + isSaved: result[8] as bool?, + isToDelete: result[9] as bool?, ); } } @@ -251,32 +241,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, @@ -533,29 +497,26 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SnippetLanguage) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is Owner) { - buffer.putUint8(139); - writeValue(buffer, value.encode()); } else if (value is SnippetFilter) { - buffer.putUint8(140); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is MainModelStateData) { - buffer.putUint8(141); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is MainModelEventData) { - buffer.putUint8(142); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is DetailsModelStateData) { - buffer.putUint8(143); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is DetailsModelEventData) { - buffer.putUint8(144); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else if (value is LoginModelStateData) { - buffer.putUint8(145); + buffer.putUint8(144); writeValue(buffer, value.encode()); } else if (value is LoginModelEventData) { - buffer.putUint8(146); + buffer.putUint8(145); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -592,20 +553,18 @@ class _PigeonCodec extends StandardMessageCodec { case 138: return SnippetLanguage.decode(readValue(buffer)!); case 139: - return Owner.decode(readValue(buffer)!); - case 140: return SnippetFilter.decode(readValue(buffer)!); - case 141: + case 140: return MainModelStateData.decode(readValue(buffer)!); - case 142: + case 141: return MainModelEventData.decode(readValue(buffer)!); - case 143: + case 142: return DetailsModelStateData.decode(readValue(buffer)!); - case 144: + case 143: return DetailsModelEventData.decode(readValue(buffer)!); - case 145: + case 144: return LoginModelStateData.decode(readValue(buffer)!); - case 146: + case 145: return LoginModelEventData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index 643a48e..0cd9fc5 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,8 +6,8 @@ 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; @@ -24,9 +23,8 @@ class SnippetDetailsBar extends StatelessWidget { 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 ?? ""), + // TODO FIX + const TextStyles.secondary("!!!Show archive status!!!"), const SizedBox(height: Dimens.s), TextStyles.helper(snippet.timeAgo ?? "") ], diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart index a5525ed..dc519e6 100644 --- a/flutter_module/lib/utils/mock/mocks.dart +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -5,7 +5,6 @@ class Mocks { static final snippet = Snippet( uuid: '', title: 'New snippet', - owner: Owner(id: 0, login: 'Snippet owner'), timeAgo: '2 days ago', voteResult: 32, isFavorite: false, From 7c38ee9ec1bc01488305aa174b9278beee187966 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Tue, 11 Mar 2025 07:42:10 +0100 Subject: [PATCH 18/23] Changed properties of snippet --- .../snipme/snipmeapp/channel/DataModel.g.kt | 21 +++----- flutter_module/channel/contract.dart | 5 +- .../lib/generated/data_model.g.dart | 25 ++-------- .../presentation/screens/details_screen.dart | 4 +- .../widgets/snippet_action_bar.dart | 12 +++-- .../widgets/snippet_details_bar.dart | 49 +++++++++---------- flutter_module/lib/utils/mock/mocks.dart | 3 +- 7 files changed, 47 insertions(+), 72 deletions(-) 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 cb6ae21..8803a52 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 @@ -166,11 +166,8 @@ data class Snippet ( val code: SnippetCode? = null, val language: SnippetLanguage? = null, val timeAgo: String? = null, - val voteResult: Long? = null, - val isPrivate: Boolean? = null, - val isFavorite: Boolean? = null, - val isSaved: Boolean? = null, - val isToDelete: Boolean? = null + val isHidden: Boolean? = null, + val isFavorite: Boolean? = null ) { companion object { @@ -180,12 +177,9 @@ data class Snippet ( val code = pigeonVar_list[2] as SnippetCode? val language = pigeonVar_list[3] as SnippetLanguage? val timeAgo = pigeonVar_list[4] as String? - val voteResult = pigeonVar_list[5] as Long? - val isPrivate = pigeonVar_list[6] as Boolean? - val isFavorite = pigeonVar_list[7] as Boolean? - val isSaved = pigeonVar_list[8] as Boolean? - val isToDelete = pigeonVar_list[9] as Boolean? - return Snippet(uuid, title, code, language, timeAgo, voteResult, isPrivate, isFavorite, isSaved, isToDelete) + 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 { @@ -195,11 +189,8 @@ data class Snippet ( code, language, timeAgo, - voteResult, - isPrivate, + isHidden, isFavorite, - isSaved, - isToDelete, ) } } diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 0a70bf1..41f999e 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -16,11 +16,8 @@ class Snippet { SnippetCode? code; SnippetLanguage? language; String? timeAgo; - int? voteResult; - bool? isPrivate; + bool? isHidden; bool? isFavorite; - bool? isSaved; - bool? isToDelete; } class SnippetCode { diff --git a/flutter_module/lib/generated/data_model.g.dart b/flutter_module/lib/generated/data_model.g.dart index 5a82389..f4e1489 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -99,11 +99,8 @@ class Snippet { this.code, this.language, this.timeAgo, - this.voteResult, - this.isPrivate, + this.isHidden, this.isFavorite, - this.isSaved, - this.isToDelete, }); String? uuid; @@ -116,16 +113,10 @@ class Snippet { String? timeAgo; - int? voteResult; - - bool? isPrivate; + bool? isHidden; bool? isFavorite; - bool? isSaved; - - bool? isToDelete; - Object encode() { return [ uuid, @@ -133,11 +124,8 @@ class Snippet { code, language, timeAgo, - voteResult, - isPrivate, + isHidden, isFavorite, - isSaved, - isToDelete, ]; } @@ -149,11 +137,8 @@ class Snippet { code: result[2] as SnippetCode?, language: result[3] as SnippetLanguage?, timeAgo: result[4] as String?, - voteResult: result[5] as int?, - isPrivate: result[6] as bool?, - isFavorite: result[7] as bool?, - isSaved: result[8] as bool?, - isToDelete: result[9] as bool?, + isHidden: result[5] as bool?, + isFavorite: result[6] as bool?, ); } } diff --git a/flutter_module/lib/presentation/screens/details_screen.dart b/flutter_module/lib/presentation/screens/details_screen.dart index 84da026..bf71641 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( diff --git a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 7de68ea..88c014f 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 @@ -51,8 +52,13 @@ 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, + active: true, 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 0cd9fc5..f757d58 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -14,32 +14,29 @@ class SnippetDetailsBar extends StatelessWidget { @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), - // TODO FIX - const TextStyles.secondary("!!!Show archive status!!!"), - const SizedBox(height: Dimens.s), - TextStyles.helper(snippet.timeAgo ?? "") - ], - ), + final visibilityIcon = snippet.isHidden == true + ? Icons.visibility_off_outlined + : Icons.visibility_outlined; + + final visibilityText = snippet.isHidden == true ? "Hidden" : "Visible"; + + 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( - TextStyles.title( - TextSpan( - children: [ - WidgetSpan(child: Icon(Icons.visibility)), - ] - ).text! - ), - ) - ], - ); + ), + SurfaceStyles.rateBox(TextStyles.title( + TextSpan(children: [WidgetSpan(child: Icon(visibilityIcon))]).text!, + )), + ]); } } diff --git a/flutter_module/lib/utils/mock/mocks.dart b/flutter_module/lib/utils/mock/mocks.dart index dc519e6..194da2a 100644 --- a/flutter_module/lib/utils/mock/mocks.dart +++ b/flutter_module/lib/utils/mock/mocks.dart @@ -6,9 +6,8 @@ class Mocks { uuid: '', title: 'New snippet', timeAgo: '2 days ago', - voteResult: 32, isFavorite: false, - isPrivate: true, + isHidden: true, language: SnippetLanguage( raw: 'Python', type: SnippetLanguageType.python, From 1817b353c04ccd1e8c79a17619f616376801c85d Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Tue, 11 Mar 2025 07:51:21 +0100 Subject: [PATCH 19/23] Fixed displaying details --- app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt | 2 +- .../main/java/dev/snipme/snipmeapp/channel/main/MainModel.kt | 4 ++-- .../snipme/snipmeapp/domain/snippet/CreateSnippetUseCase.kt | 2 +- .../snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt | 4 ++-- .../main/java/dev/snipme/snipmeapp/domain/snippets/Snippet.kt | 2 +- .../snipme/snipmeapp/domain/snippets/SnippetResponseMapper.kt | 2 +- .../dev/snipme/snipmeapp/domain/snippets/SnippetVisibility.kt | 2 +- .../lib/presentation/widgets/snippet_details_bar.dart | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) 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 f683be1..183de5b 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/channel/ModelPlugin.kt @@ -35,7 +35,7 @@ fun Snippet.toModelData(): ChannelSnippet = code = code.toModelSnippetCode(), language = language.toModelSnippetLanguage(), isFavorite = favorite, - isPrivate = visibility == SnippetVisibility.PRIVATE, + isHidden = visibility == SnippetVisibility.HIDDEN, timeAgo = DateUtils.getRelativeTimeSpanString( modifiedAt.time, Date().time, 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 a549f7f..4e7d3b6 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 @@ -89,8 +89,8 @@ class MainModel( filterState = SnippetFilters( languages = listOf(SNIPPET_FILTER_ALL), selectedLanguages = listOf(SNIPPET_FILTER_ALL), - scopes = listOf("All", "Private", "Public"), - selectedScope = "All" + scopes = listOf("All", "Visible", "Hidden"), + selectedScope = "Visible" ) setupDemoSnippets() 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 7cdc3d0..f7beb1c 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 @@ -16,7 +16,7 @@ class CreateSnippetUseCase( title: String, code: String, language: String, - visibility: SnippetVisibility = SnippetVisibility.PUBLIC, + visibility: SnippetVisibility = SnippetVisibility.VISIBLE, favorite: Boolean = false, ): Single = auth() .andThen(getSingleUser()) 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 index 9225417..4ad3ac6 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -25,7 +25,7 @@ class SetupDemoSnippetsUseCase( title = "Your first snippet", code = KOTLIN_SAMPLE, language = SnippetLanguageType.KOTLIN.name.titleCase(), - visibility = SnippetVisibility.PUBLIC, + visibility = SnippetVisibility.VISIBLE, userId = 1, favorite = false ), @@ -33,7 +33,7 @@ class SetupDemoSnippetsUseCase( title = "Hello World", code = "console.log('Hello, World!')", language = SnippetLanguageType.JAVASCRIPT.name.titleCase(), - visibility = SnippetVisibility.PUBLIC, + visibility = SnippetVisibility.VISIBLE, userId = 1, favorite = false ), 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 36ce39e..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 @@ -18,7 +18,7 @@ data class Snippet( title = "", code = SnippetCode("", SpannableString("")), language = SnippetLanguage("", SnippetLanguageType.UNKNOWN), - visibility = SnippetVisibility.PRIVATE, + visibility = SnippetVisibility.HIDDEN, modifiedAt = Date(), 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 232d9c2..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 @@ -39,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/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/flutter_module/lib/presentation/widgets/snippet_details_bar.dart b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart index f757d58..343b366 100644 --- a/flutter_module/lib/presentation/widgets/snippet_details_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_details_bar.dart @@ -34,8 +34,8 @@ class SnippetDetailsBar extends StatelessWidget { ], ), ), - SurfaceStyles.rateBox(TextStyles.title( - TextSpan(children: [WidgetSpan(child: Icon(visibilityIcon))]).text!, + SurfaceStyles.rateBox(Text.rich( + TextSpan(children: [WidgetSpan(child: Icon(visibilityIcon))]), )), ]); } From a34b2eefe3c7eaa9b8c98bd697c0206b47fdd7d4 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Tue, 11 Mar 2025 08:07:06 +0100 Subject: [PATCH 20/23] Fixed filter view --- .../lib/presentation/screens/main_screen.dart | 132 ++++++++++-------- .../lib/presentation/styles/dimens.dart | 2 +- .../widgets/snippet_list_item.dart | 13 +- 3 files changed, 83 insertions(+), 64 deletions(-) diff --git a/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index cd3318d..1a9e47e 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,71 @@ 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); + }, ), ), - ], - ), + ), + const SizedBox(width: Dimens.xl), + const Icon( + Icons.favorite_border, + color: Colors.black, + ), + ], ), - 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_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(), From 1b8d947022ab145ff0eb859696b11ec4d271db0f Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 12 Mar 2025 07:49:43 +0100 Subject: [PATCH 21/23] Fixed prepopulate --- .../repository/snippet/SnippetRepository.kt | 3 +- .../snippet/SnippetRepositoryReal.kt | 4 +- .../domain/snippet/CreateSnippetUseCase.kt | 1 - .../snippets/SetupDemoSnippetsUseCase.kt | 104 ++++++++++++++++-- .../lib/presentation/screens/main_screen.dart | 5 - 5 files changed, 100 insertions(+), 17 deletions(-) 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 7bb06b8..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 @@ -12,6 +12,8 @@ interface SnippetRepository { fun getDemoSetupStatus(): Boolean + fun setDemoSetupStatus(status: Boolean): Completable + fun snippets(): Single> fun snippet(uuid: String): Single @@ -21,7 +23,6 @@ interface SnippetRepository { code: String, language: String, visibility: SnippetVisibility, - userId: Int, favorite: Boolean ): Single 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 fa34078..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 @@ -28,6 +28,9 @@ class SnippetRepositoryReal( 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) } @@ -42,7 +45,6 @@ class SnippetRepositoryReal( code: String, language: String, visibility: SnippetVisibility, - userId: Int, favorite: Boolean ): Single { return service.create( 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 f7beb1c..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 @@ -26,7 +26,6 @@ class CreateSnippetUseCase( code = code, language = language, visibility = visibility, - userId = user.id, favorite = favorite ) } 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 index 4ad3ac6..2df9a19 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/domain/snippets/SetupDemoSnippetsUseCase.kt @@ -1,7 +1,6 @@ package dev.snipme.snipmeapp.domain.snippets import dev.snipme.snipmeapp.domain.repository.snippet.SnippetRepository -import dev.snipme.snipmeapp.util.extension.titleCase import io.reactivex.Completable import io.reactivex.Single @@ -13,7 +12,9 @@ class SetupDemoSnippetsUseCase( val demoSetup = snippetRepository.getDemoSetupStatus() return if (!demoSetup) { - setupDemoSnippets() + setupDemoSnippets().andThen( + snippetRepository.setDemoSetupStatus(true) + ) } else { Completable.complete() } @@ -24,24 +25,36 @@ class SetupDemoSnippetsUseCase( snippetRepository.create( title = "Your first snippet", code = KOTLIN_SAMPLE, - language = SnippetLanguageType.KOTLIN.name.titleCase(), + language = SnippetLanguageType.KOTLIN.name, visibility = SnippetVisibility.VISIBLE, - userId = 1, favorite = false ), snippetRepository.create( - title = "Hello World", - code = "console.log('Hello, World!')", - language = SnippetLanguageType.JAVASCRIPT.name.titleCase(), + title = "Your favorite code", + code = JAVASCRIPT_SAMPLE, + language = SnippetLanguageType.JAVASCRIPT.name, visibility = SnippetVisibility.VISIBLE, - userId = 1, + 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 + ) ), ) } -const val KOTLIN_SAMPLE = """ +private const val KOTLIN_SAMPLE = """ // Data class data class User(val id: Int, val name: String, val email: String) @@ -109,4 +122,77 @@ suspend fun fetchUserData(): Result> { } } } +""" + +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/flutter_module/lib/presentation/screens/main_screen.dart b/flutter_module/lib/presentation/screens/main_screen.dart index 1a9e47e..1c57d36 100644 --- a/flutter_module/lib/presentation/screens/main_screen.dart +++ b/flutter_module/lib/presentation/screens/main_screen.dart @@ -203,11 +203,6 @@ class _MainPageData extends HookWidget { ), ), ), - const SizedBox(width: Dimens.xl), - const Icon( - Icons.favorite_border, - color: Colors.black, - ), ], ), ), From b6ba5e5c6d9ee80567264304b94555e2b798ca50 Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 12 Mar 2025 08:04:54 +0100 Subject: [PATCH 22/23] Improved scope filter --- .../snipmeapp/channel/main/MainModel.kt | 6 ++--- .../dev/snipme/snipmeapp/di/UseCaseModule.kt | 2 +- .../domain/snippets/GetSnippetsUseCase.kt | 22 ++++++++++--------- .../snipmeapp/domain/snippets/SnippetScope.kt | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) 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 4e7d3b6..9389136 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 @@ -89,8 +89,8 @@ class MainModel( filterState = SnippetFilters( languages = listOf(SNIPPET_FILTER_ALL), selectedLanguages = listOf(SNIPPET_FILTER_ALL), - scopes = listOf("All", "Visible", "Hidden"), - selectedScope = "Visible" + scopes = SnippetScope.entries.map { it.visibleName }, + selectedScope = SnippetScope.ALL.visibleName ) setupDemoSnippets() @@ -152,7 +152,7 @@ class MainModel( pages: Int = 1, scope: SnippetScope = SnippetScope.ALL ) { - getSnippets(scope, pages) + getSnippets(scope) .subscribeOn(Schedulers.io()) .subscribeBy( onSuccess = { 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 0679ef3..7909d3d 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/UseCaseModule.kt @@ -44,7 +44,7 @@ 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()) } 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 3712aec..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,20 +1,22 @@ package dev.snipme.snipmeapp.domain.snippets -import dev.snipme.snipmeapp.domain.auth.AuthorizationUseCase +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() - .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/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 From 65303bb7b97c3a66c8ed31e40862feb8790752bc Mon Sep 17 00:00:00 2001 From: tmaxxdd Date: Wed, 12 Mar 2025 08:27:49 +0100 Subject: [PATCH 23/23] Setup changing visibility --- .../snipme/snipmeapp/channel/DataModel.g.kt | 19 +++++++++++++ .../snipmeapp/channel/details/DetailsModel.kt | 27 +++++++++++++++++++ .../channel/details/DetailsModelPlugin.kt | 4 +++ .../snipmeapp/channel/main/MainModel.kt | 9 +------ .../dev/snipme/snipmeapp/di/ChannelModule.kt | 2 +- flutter_module/channel/contract.dart | 2 ++ .../lib/generated/data_model.g.dart | 22 +++++++++++++++ .../presentation/screens/details_screen.dart | 2 ++ .../widgets/snippet_action_bar.dart | 1 - 9 files changed, 78 insertions(+), 10 deletions(-) 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 8803a52..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 @@ -780,6 +780,7 @@ interface ChannelDetailsModel { fun saveImage(image: ByteArray) fun copyToClipboard() fun shareImage(image: ByteArray) + fun changeVisibility(isHidden: Boolean) fun delete() companion object { @@ -893,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/details/DetailsModel.kt b/app/src/main/java/dev/snipme/snipmeapp/channel/details/DetailsModel.kt index fbf0bd6..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 @@ -16,7 +16,9 @@ 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 @@ -32,6 +34,7 @@ class DetailsModel( 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() @@ -114,6 +117,30 @@ class DetailsModel( } } + 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) 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 9389136..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 @@ -22,7 +22,6 @@ 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 dev.snipme.snipmeapp.domain.user.User import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.subscribeBy @@ -160,12 +159,7 @@ class MainModel( scopedSnippets = cachedSnippets val updatedFilters = getLanguageFilters(cachedSnippets) filterState = filterState.copy(languages = updatedFilters) - mutableState.value = Loaded( - User(0, "login", "email", ""), // TODO Remove - it, - pages, - filterState - ) + mutableState.value = Loaded(it, pages, filterState) loadNextPage() }, onError = { @@ -181,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/ChannelModule.kt b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt index e5e3f63..e5e49a7 100644 --- a/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt +++ b/app/src/main/java/dev/snipme/snipmeapp/di/ChannelModule.kt @@ -14,5 +14,5 @@ internal val channelModule = module { single { SessionModel(get()) } single { LoginModel(get(), get(), get()) } single { MainModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } - single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get()) } + single { DetailsModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } } \ No newline at end of file diff --git a/flutter_module/channel/contract.dart b/flutter_module/channel/contract.dart index 41f999e..9d34a7e 100644 --- a/flutter_module/channel/contract.dart +++ b/flutter_module/channel/contract.dart @@ -177,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 f4e1489..1fe91a3 100644 --- a/flutter_module/lib/generated/data_model.g.dart +++ b/flutter_module/lib/generated/data_model.g.dart @@ -851,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 bf71641..39bb9ff 100644 --- a/flutter_module/lib/presentation/screens/details_screen.dart +++ b/flutter_module/lib/presentation/screens/details_screen.dart @@ -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/widgets/snippet_action_bar.dart b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart index 88c014f..a64b6e6 100644 --- a/flutter_module/lib/presentation/widgets/snippet_action_bar.dart +++ b/flutter_module/lib/presentation/widgets/snippet_action_bar.dart @@ -58,7 +58,6 @@ class SnippetActionBar extends StatelessWidget { const SizedBox(width: Dimens.l), StateIcon( onTap: onDeleteTap, - active: true, activeColor: Colors.redAccent, icon: Icons.delete_outline_outlined, ),