From 51047e345fa339ade9c37aae7647a050c257402d Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 24 Dec 2022 21:37:06 +0800 Subject: [PATCH 001/121] Add Hilt --- build.gradle | 4 ++++ main/build.gradle | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/build.gradle b/build.gradle index 552fdc34..b3057865 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,10 @@ buildscript { } } +plugins { + id 'com.google.dagger.hilt.android' version '2.48' apply false +} + allprojects { repositories { google() diff --git a/main/build.gradle b/main/build.gradle index a757aef0..ae3fe467 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -6,6 +6,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.google.firebase.firebase-perf' + id 'com.google.dagger.hilt.android' } def buildParams = getGradle().getStartParameter().toString().toLowerCase() @@ -100,6 +101,10 @@ android { } } +kapt { + correctErrorTypes true +} + dependencies { implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') @@ -138,6 +143,10 @@ dependencies { // required to avoid crash on Android 12 API 31 implementation 'androidx.work:work-runtime-ktx:2.8.1' + // Hilt + implementation "com.google.dagger:hilt-android:2.48" + kapt "com.google.dagger:hilt-compiler:2.48" + // Firebase implementation platform('com.google.firebase:firebase-bom:32.6.0') implementation 'com.google.firebase:firebase-crashlytics' From 89ddfbb77727aa037170c64067cb97a90c5d5299 Mon Sep 17 00:00:00 2001 From: firemaples Date: Thu, 4 Jan 2024 15:34:41 +0900 Subject: [PATCH 002/121] Test injection with View & ViewModel --- .../firemaples/onscreenocr/CoreApplication.kt | 2 + .../floatings/ViewHolderService.kt | 24 ++- .../onscreenocr/floatings/main/MainBar.kt | 27 ++- .../floatings/main/MainBarViewModel.kt | 33 +++- ...eManager.kt => FloatingViewCoordinator.kt} | 162 +++++++++--------- .../floatings/manager/StateNavigator.kt | 117 +++++++++++++ .../floatings/result/ResultView.kt | 11 +- .../floatings/result/ResultViewModel.kt | 22 ++- .../hilt/CoroutinesDispatchersModule.kt | 43 +++++ .../onscreenocr/hilt/CoroutinesQualifiers.kt | 27 +++ .../onscreenocr/hilt/SingletonModule.kt | 16 ++ .../onscreenocr/utils/QuickTileService.kt | 16 +- 12 files changed, 383 insertions(+), 117 deletions(-) rename main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/{FloatingStateManager.kt => FloatingViewCoordinator.kt} (79%) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/CoreApplication.kt b/main/src/main/java/tw/firemaples/onscreenocr/CoreApplication.kt index 58247809..8a06ea38 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/CoreApplication.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/CoreApplication.kt @@ -1,11 +1,13 @@ package tw.firemaples.onscreenocr import android.app.Application +import dagger.hilt.android.HiltAndroidApp import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.log.UserInfoUtils import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.utils.AdManager +@HiltAndroidApp class CoreApplication : Application() { companion object { lateinit var instance: Application diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/ViewHolderService.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/ViewHolderService.kt index f749cb69..2bc2b4d9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/ViewHolderService.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/ViewHolderService.kt @@ -1,6 +1,10 @@ package tw.firemaples.onscreenocr.floatings -import android.app.* +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo @@ -9,19 +13,22 @@ import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.floatings.manager.FloatingStateManager +import tw.firemaples.onscreenocr.floatings.manager.FloatingViewCoordinator import tw.firemaples.onscreenocr.pages.launch.LaunchActivity import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.screenshot.ScreenExtractor import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.SamsungSpenInsertedReceiver +import javax.inject.Inject +@AndroidEntryPoint class ViewHolderService : Service() { companion object { private const val NOTIFICATION_CHANNEL_ID = "floating_view_notification_channel_v1" @@ -59,10 +66,13 @@ class ViewHolderService : Service() { private var floatingStateListenerJob: Job? = null + @Inject + lateinit var floatingViewCoordinator: FloatingViewCoordinator + override fun onCreate() { super.onCreate() floatingStateListenerJob = CoroutineScope(Dispatchers.Main).launch { - FloatingStateManager.showingStateChangedFlow.collect { startForeground() } + floatingViewCoordinator.showingStateChangedFlow.collect { startForeground() } } if (SettingManager.exitAppWhileSPenInserted) { SamsungSpenInsertedReceiver.start() @@ -101,14 +111,14 @@ class ViewHolderService : Service() { private fun showViews() { if (ScreenExtractor.isGranted) { - FloatingStateManager.showMainBar() + floatingViewCoordinator.showMainBar() } else { startActivity(LaunchActivity.getLaunchIntent(this)) } } private fun hideViews() { - FloatingStateManager.detachAllViews() + floatingViewCoordinator.detachAllViews() } private fun exit() { @@ -127,13 +137,13 @@ class ViewHolderService : Service() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground( ONGOING_NOTIFICATION_ID, - createNotification(!FloatingStateManager.isMainBarAttached), + createNotification(!floatingViewCoordinator.isMainBarAttached), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION, ) } else { startForeground( ONGOING_NOTIFICATION_ID, - createNotification(!FloatingStateManager.isMainBarAttached), + createNotification(!floatingViewCoordinator.isMainBarAttached), ) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt index 005cd6ec..7edf376a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt @@ -2,12 +2,13 @@ package tw.firemaples.onscreenocr.floatings.main import android.content.Context import android.graphics.Point +import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding import tw.firemaples.onscreenocr.floatings.base.MovableFloatingView import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView -import tw.firemaples.onscreenocr.floatings.manager.FloatingStateManager import tw.firemaples.onscreenocr.floatings.manager.State +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.floatings.menu.MenuView import tw.firemaples.onscreenocr.floatings.readme.ReadmeView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel @@ -15,9 +16,19 @@ import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.pages.setting.SettingActivity import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.pref.AppPref -import tw.firemaples.onscreenocr.utils.* +import tw.firemaples.onscreenocr.utils.Utils +import tw.firemaples.onscreenocr.utils.clickOnce +import tw.firemaples.onscreenocr.utils.hide +import tw.firemaples.onscreenocr.utils.show +import tw.firemaples.onscreenocr.utils.showOrHide +import javax.inject.Inject + +class MainBar @Inject constructor( + @ApplicationContext context: Context, + private val stateNavigator: StateNavigator, + private val viewModel: MainBarViewModel, +) : MovableFloatingView(context) { -class MainBar(context: Context) : MovableFloatingView(context) { override val layoutId: Int get() = R.layout.floating_main_bar @@ -34,7 +45,7 @@ class MainBar(context: Context) : MovableFloatingView(context) { override val fadeOutAfterMoved: Boolean get() = !arrayOf(State.ScreenCircling, State.ScreenCircled) - .contains(FloatingStateManager.currentState) + .contains(stateNavigator.currentState.value) && !menuView.attached && SettingManager.enableFadingOutWhileIdle override val fadeOutDelay: Long @@ -58,8 +69,6 @@ class MainBar(context: Context) : MovableFloatingView(context) { } } - private val viewModel: MainBarViewModel by lazy { MainBarViewModel(viewScope) } - init { binding.setViews() setDragView(binding.btMenu) @@ -72,16 +81,16 @@ class MainBar(context: Context) : MovableFloatingView(context) { } btSelect.clickOnce { - FloatingStateManager.startScreenCircling() + viewModel.onSelectClicked() } btTranslate.clickOnce { FirebaseEvent.logClickTranslationStartButton() - FloatingStateManager.startScreenCapturing(viewModel.selectedOCRLang) + viewModel.onTranslateClicked() } btClose.clickOnce { - FloatingStateManager.cancelScreenCircling() + viewModel.onCloseClicked() } btMenu.clickOnce { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt index c4400d76..72b9a7e0 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt @@ -9,8 +9,10 @@ import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel -import tw.firemaples.onscreenocr.floatings.manager.FloatingStateManager +import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.State +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.hilt.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.recognition.TextRecognizer import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager @@ -22,8 +24,13 @@ import tw.firemaples.onscreenocr.utils.Constants import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.SingleLiveEvent import tw.firemaples.onscreenocr.utils.Utils +import javax.inject.Inject + +class MainBarViewModel @Inject constructor( + @MainImmediateCoroutineScope viewScope: CoroutineScope, + private val stateNavigator: StateNavigator, +) : FloatingViewModel(viewScope) { -class MainBarViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) { companion object { private const val MENU_SETTING = "setting" private const val MENU_PRIVACY_POLICY = "privacy_policy" @@ -97,7 +104,7 @@ class MainBarViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) logger.debug("onAttachedToScreen()") viewScope.launch { logger.debug("register FloatingStateManager.onStateChanged") - FloatingStateManager.currentStateFlow.collect { onStateChanged(it) } + stateNavigator.currentState.collect { onStateChanged(it) } } viewScope.launch { ocrRepo.selectedOCRLangFlow.collect { onSelectedLangChanged(_ocrLang = it) } @@ -113,7 +120,7 @@ class MainBarViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) } } viewScope.launch { - setupButtons(FloatingStateManager.currentState) +// setupButtons(floatingStateManager.currentState) if (!repo.isReadmeAlreadyShown().first()) { _showReadme.value = true @@ -228,4 +235,22 @@ class MainBarViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) repo.saveLastMainBarPosition(x, y) } } + + fun onSelectClicked() { + viewScope.launch { + stateNavigator.navigate(NavigationAction.NavigateToScreenCircling) + } + } + + fun onTranslateClicked() { + viewScope.launch { + stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing(selectedOCRLang)) + } + } + + fun onCloseClicked() { + viewScope.launch { + stateNavigator.navigate(NavigationAction.CancelScreenCircling) + } + } } \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingStateManager.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt similarity index 79% rename from main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingStateManager.kt rename to main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index fd694ded..a7009af7 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingStateManager.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -3,14 +3,14 @@ package tw.firemaples.onscreenocr.floatings.manager import android.content.Context import android.graphics.Bitmap import android.graphics.Rect -import java.io.IOException -import kotlin.reflect.KClass +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import tw.firemaples.onscreenocr.R @@ -19,6 +19,7 @@ import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog import tw.firemaples.onscreenocr.floatings.main.MainBar import tw.firemaples.onscreenocr.floatings.result.ResultView import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView +import tw.firemaples.onscreenocr.hilt.MainCoroutineScope import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.pref.AppPref @@ -26,36 +27,35 @@ import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType import tw.firemaples.onscreenocr.recognition.TextRecognizer import tw.firemaples.onscreenocr.screenshot.ScreenExtractor -import tw.firemaples.onscreenocr.translator.azure.MicrosoftAzureTranslator import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.translator.TranslationResult import tw.firemaples.onscreenocr.translator.Translator +import tw.firemaples.onscreenocr.translator.azure.MicrosoftAzureTranslator import tw.firemaples.onscreenocr.utils.Constants import tw.firemaples.onscreenocr.utils.Logger -import tw.firemaples.onscreenocr.utils.Utils import tw.firemaples.onscreenocr.utils.setReusable +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.reflect.KClass -object FloatingStateManager { - private val logger: Logger by lazy { Logger(FloatingStateManager::class) } - private val context: Context by lazy { Utils.context } - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) +@Singleton +class FloatingViewCoordinator @Inject constructor( + @ApplicationContext private val context: Context, + private val stateNavigator: StateNavigator, + @MainCoroutineScope private val scope: CoroutineScope, + private val mainBar: MainBar, + private val resultView: ResultView, +) { + private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } - val currentStateFlow = MutableStateFlow(State.Idle) - val currentState: State - get() = currentStateFlow.value + private val currentState: State + get() = stateNavigator.currentState.value - private val mainBar: MainBar by lazy { MainBar(context) } private val screenCirclingView: ScreenCirclingView by lazy { ScreenCirclingView(context).apply { onAreaSelected = { parent, selected -> - this@FloatingStateManager.onAreaSelected(parent, selected) - } - } - } - private val resultView: ResultView by lazy { - ResultView(context).apply { - onUserDismiss = { - this@FloatingStateManager.backToIdle() + this@FloatingViewCoordinator.onAreaSelected(parent, selected) } } } @@ -70,6 +70,53 @@ object FloatingStateManager { private var selectedRect: Rect? = null private var croppedBitmap: Bitmap? = null + init { + stateNavigator.navigationAction + .onEach { + when (it) { + NavigationAction.NavigateToIdle -> + backToIdle() + + NavigationAction.NavigateToScreenCircling -> + startScreenCircling() + + is NavigationAction.NavigateToScreenCircled -> + onAreaSelected( + parentRect = it.parentRect, + selectedRect = it.selectedRect, + ) + + + is NavigationAction.NavigateToScreenCapturing -> + startScreenCapturing(it.selectedOCRLang) + + is NavigationAction.NavigateToTextRecognition -> + startRecognition( + croppedBitmap = it.croppedBitmap, + parent = it.parent, + selected = it.selected, + ) + + is NavigationAction.NavigateToStartTranslation -> + startTranslation(it.recognitionResult) + + is NavigationAction.NavigateToTranslated -> + showResult(it.result) + + is NavigationAction.ShowError -> + showError(it.error) + + NavigationAction.CancelScreenCircling -> + cancelScreenCircling() + } + } + .launchIn(scope) + + resultView.onUserDismiss = { + this@FloatingViewCoordinator.backToIdle() + } + } + fun showMainBar() { if (isMainBarAttached) return mainBar.attachToScreen() @@ -105,7 +152,7 @@ object FloatingStateManager { } logger.debug("startScreenCircling()") - changeState(State.ScreenCircling) + stateNavigator.updateState(State.ScreenCircling) FirebaseEvent.logStartAreaSelection() screenCirclingView.attachToScreen() arrangeMainBarToTop() @@ -115,15 +162,15 @@ object FloatingStateManager { stateIn(State.ScreenCircling::class, State.ScreenCircled::class) { logger.debug("onAreaSelected(), parentRect: $parentRect, selectedRect: $selectedRect, size: ${selectedRect.width()}x${selectedRect.height()}") if (currentState != State.ScreenCircled) { - changeState(State.ScreenCircled) + stateNavigator.updateState(State.ScreenCircled) } - this@FloatingStateManager.selectedRect = selectedRect - this@FloatingStateManager.parentRect = parentRect + this@FloatingViewCoordinator.selectedRect = selectedRect + this@FloatingViewCoordinator.parentRect = parentRect } fun cancelScreenCircling() = stateIn(State.ScreenCircling::class, State.ScreenCircled::class) { logger.debug("cancelScreenCircling()") - changeState(State.Idle) + stateNavigator.updateState(State.Idle) screenCirclingView.detachFromScreen() } @@ -132,11 +179,11 @@ object FloatingStateManager { return@stateIn } - this@FloatingStateManager.selectedOCRLang = selectedOCRLang + this@FloatingViewCoordinator.selectedOCRLang = selectedOCRLang val parent = parentRect ?: return@stateIn val selected = selectedRect ?: return@stateIn logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") - changeState(State.ScreenCapturing) + stateNavigator.updateState(State.ScreenCapturing) mainBar.detachFromScreen() screenCirclingView.detachFromScreen() @@ -146,7 +193,7 @@ object FloatingStateManager { FirebaseEvent.logStartCaptureScreen() val croppedBitmap = ScreenExtractor.extractBitmapFromScreen(parentRect = parent, cropRect = selected) - this@FloatingStateManager.croppedBitmap = croppedBitmap + this@FloatingViewCoordinator.croppedBitmap = croppedBitmap FirebaseEvent.logCaptureScreenFinished() mainBar.attachToScreen() @@ -166,7 +213,7 @@ object FloatingStateManager { private fun startRecognition(croppedBitmap: Bitmap, parent: Rect, selected: Rect) = stateIn(State.ScreenCapturing::class) { - changeState(State.TextRecognizing) + stateNavigator.updateState(State.TextRecognizing) try { resultView.startRecognition() val recognizer = TextRecognizer.getRecognizer(selectedOCRProvider) @@ -210,7 +257,7 @@ object FloatingStateManager { fun startTranslation(recognitionResult: RecognitionResult) = stateIn(State.TextRecognizing::class, State.ResultDisplaying::class) { try { - changeState(State.TextTranslating) + stateNavigator.updateState(State.TextTranslating) val translator = Translator.getTranslator() @@ -295,14 +342,14 @@ object FloatingStateManager { private fun showResult(result: Result) = stateIn(State.TextTranslating::class) { logger.debug("showResult(), $result") - changeState(State.ResultDisplaying) + stateNavigator.updateState(State.ResultDisplaying) resultView.textTranslated(result) } private fun showError(error: String) { scope.launch { - changeState(State.ErrorDisplaying(error)) + stateNavigator.updateState(State.ErrorDisplaying(error)) logger.error(error) context.showErrorDialog(error) backToIdle() @@ -311,7 +358,7 @@ object FloatingStateManager { private fun backToIdle() = scope.launch { - if (currentState != State.Idle) changeState(State.Idle) + if (currentState != State.Idle) stateNavigator.updateState(State.Idle) croppedBitmap?.setReusable() resultView.backToIdle() showMainBar() @@ -325,53 +372,6 @@ object FloatingStateManager { scope.launch { block.invoke(this) } } else logger.error(t = IllegalStateException("The state should be in ${states.toList()}, current is $currentState")) } - - private fun changeState(newState: State) { - val allowedNextStates = when (currentState) { - State.Idle -> arrayOf(State.ScreenCircling::class) - State.ScreenCircling -> arrayOf(State.Idle::class, State.ScreenCircled::class) - State.ScreenCircled -> arrayOf(State.Idle::class, State.ScreenCapturing::class) - State.ScreenCapturing -> - arrayOf( - State.Idle::class, State.TextRecognizing::class, State.ErrorDisplaying::class - ) - - State.TextRecognizing -> - arrayOf( - State.Idle::class, State.TextTranslating::class, State.ErrorDisplaying::class - ) - - State.TextTranslating -> - arrayOf( - State.ResultDisplaying::class, State.ErrorDisplaying::class, State.Idle::class - ) - - State.ResultDisplaying -> arrayOf(State.Idle::class, State.TextTranslating::class) - is State.ErrorDisplaying -> arrayOf(State.Idle::class) - } - - if (allowedNextStates.contains(newState::class)) { - logger.debug("Change state $currentState > $newState") - currentStateFlow.value = newState - } else { - logger.error("Change state from $currentState to $newState is not allowed") - } - } -} - -sealed class State { - override fun toString(): String { - return this::class.simpleName ?: super.toString() - } - - object Idle : State() - object ScreenCircling : State() - object ScreenCircled : State() - object ScreenCapturing : State() - object TextRecognizing : State() - object TextTranslating : State() - object ResultDisplaying : State() - data class ErrorDisplaying(val error: String) : State() } sealed class Result( diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt new file mode 100644 index 00000000..0a19b47f --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -0,0 +1,117 @@ +package tw.firemaples.onscreenocr.floatings.manager + +import android.graphics.Bitmap +import android.graphics.Rect +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import tw.firemaples.onscreenocr.recognition.RecognitionResult +import tw.firemaples.onscreenocr.utils.Logger +import javax.inject.Inject +import javax.inject.Singleton + +interface StateNavigator { + val navigationAction: SharedFlow + val currentState: StateFlow + suspend fun navigate(action: NavigationAction) + + fun updateState(newState: State) +} + +@Singleton +class StateNavigatorImpl @Inject constructor() : StateNavigator { + private val logger: Logger by lazy { Logger(this::class) } + + override val navigationAction = MutableSharedFlow() + + override val currentState = MutableStateFlow(State.Idle) + + override suspend fun navigate(action: NavigationAction) { + navigationAction.subscriptionCount.first { it > 0 } + navigationAction.emit(action) + } + + override fun updateState(newState: State) { + val allowedNextStates = when (currentState.value) { + State.Idle -> arrayOf(State.ScreenCircling::class) + State.ScreenCircling -> arrayOf(State.Idle::class, State.ScreenCircled::class) + State.ScreenCircled -> arrayOf(State.Idle::class, State.ScreenCapturing::class) + State.ScreenCapturing -> + arrayOf( + State.Idle::class, State.TextRecognizing::class, State.ErrorDisplaying::class + ) + + State.TextRecognizing -> + arrayOf( + State.Idle::class, State.TextTranslating::class, State.ErrorDisplaying::class + ) + + State.TextTranslating -> + arrayOf( + State.ResultDisplaying::class, State.ErrorDisplaying::class, State.Idle::class + ) + + State.ResultDisplaying -> arrayOf(State.Idle::class, State.TextTranslating::class) + is State.ErrorDisplaying -> arrayOf(State.Idle::class) + } + + if (allowedNextStates.contains(newState::class)) { + logger.debug("Change state ${currentState.value} > $newState") + currentState.value = newState + } else { + logger.error("Change state from ${currentState.value} to $newState is not allowed") + } + } +} + +sealed interface NavigationAction { + data object NavigateToIdle : NavigationAction + + data object NavigateToScreenCircling : NavigationAction + + data class NavigateToScreenCircled( + val parentRect: Rect, + val selectedRect: Rect, + ) : NavigationAction + + data object CancelScreenCircling : NavigationAction + + data class NavigateToScreenCapturing( + val selectedOCRLang: String, + ) : NavigationAction + + data class NavigateToTextRecognition( + val croppedBitmap: Bitmap, + val parent: Rect, + val selected: Rect, + ) : NavigationAction + + data class NavigateToStartTranslation( + val recognitionResult: RecognitionResult, + ) : NavigationAction + + data class NavigateToTranslated( + val result: Result, + ) : NavigationAction + + data class ShowError( + val error: String, + ) : NavigationAction +} + +sealed class State { + override fun toString(): String { + return this::class.simpleName ?: super.toString() + } + + object Idle : State() + object ScreenCircling : State() + object ScreenCircled : State() + object ScreenCapturing : State() + object TextRecognizing : State() + object TextTranslating : State() + object ResultDisplaying : State() + data class ErrorDisplaying(val error: String) : State() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt index 44fbe846..354f42c2 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt @@ -9,7 +9,7 @@ import android.view.View import android.view.WindowManager import android.widget.RelativeLayout import androidx.core.content.ContextCompat -import java.util.Locale +import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.databinding.FloatingResultViewBinding import tw.firemaples.onscreenocr.databinding.ViewResultPanelBinding @@ -29,8 +29,13 @@ import tw.firemaples.onscreenocr.utils.getViewRect import tw.firemaples.onscreenocr.utils.setReusable import tw.firemaples.onscreenocr.utils.setTextOrGone import tw.firemaples.onscreenocr.utils.showOrHide +import java.util.Locale +import javax.inject.Inject -class ResultView(context: Context) : FloatingView(context) { +class ResultView @Inject constructor( + @ApplicationContext context: Context, + private val viewModel: ResultViewModel, +) : FloatingView(context) { companion object { private const val LABEL_RECOGNIZED_TEXT = "Recognized text" private const val LABEL_TRANSLATED_TEXT = "Translated text" @@ -50,8 +55,6 @@ class ResultView(context: Context) : FloatingView(context) { override val enableHomeButtonWatcher: Boolean get() = true - private val viewModel: ResultViewModel by lazy { ResultViewModel(viewScope) } - private val binding: FloatingResultViewBinding = FloatingResultViewBinding.bind(rootLayout) private val viewRoot: RelativeLayout = binding.viewRoot diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt index cf062fa2..0eb107aa 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt @@ -10,8 +10,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel -import tw.firemaples.onscreenocr.floatings.manager.FloatingStateManager +import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.Result +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.hilt.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.repo.GeneralRepository @@ -21,6 +23,7 @@ import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.SingleLiveEvent import tw.firemaples.onscreenocr.utils.Utils import java.util.Locale +import javax.inject.Inject typealias OCRText = Pair @@ -28,7 +31,10 @@ fun OCRText.text(): String = this.first fun OCRText.locale(): Locale = Locale.forLanguageTag(this.second) fun OCRText.langCode(): String = this.second -class ResultViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) { +class ResultViewModel @Inject constructor( + @MainImmediateCoroutineScope viewScope: CoroutineScope, + private val stateNavigator: StateNavigator, +) : FloatingViewModel(viewScope) { private val _displayOCROperationProgress = MutableLiveData() val displayOCROperationProgress: LiveData = _displayOCROperationProgress @@ -194,11 +200,13 @@ class ResultViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) // null // } ?: lastLangCode - FloatingStateManager.startTranslation( - RecognitionResult( - langCode = langCode, - result = text, - boundingBoxes = lastTextBoundingBoxes, + stateNavigator.navigate( + NavigationAction.NavigateToStartTranslation( + RecognitionResult( + langCode = langCode, + result = text, + boundingBoxes = lastTextBoundingBoxes, + ) ) ) } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt new file mode 100644 index 00000000..036dc104 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt @@ -0,0 +1,43 @@ +package tw.firemaples.onscreenocr.hilt + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object CoroutinesDispatchersModule { + @DefaultDispatcher + @Provides + fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default + + @IoDispatcher + @Provides + fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO + + @MainDispatcher + @Provides + fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main + + @MainImmediateDispatcher + @Provides + fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate + + @Singleton + @MainImmediateCoroutineScope + @Provides + fun provideMainImmediateCoroutineScope(@MainImmediateDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope = + CoroutineScope(SupervisorJob() + mainDispatcher) + + @Singleton + @MainCoroutineScope + @Provides + fun provideMainCoroutineScope(@MainDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope = + CoroutineScope(SupervisorJob() + mainDispatcher) +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt b/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt new file mode 100644 index 00000000..91de0de8 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt @@ -0,0 +1,27 @@ +package tw.firemaples.onscreenocr.hilt + +import javax.inject.Qualifier + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class DefaultDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class IoDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MainDispatcher + +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class MainImmediateDispatcher + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MainImmediateCoroutineScope + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MainCoroutineScope \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt new file mode 100644 index 00000000..012bcb04 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt @@ -0,0 +1,16 @@ +package tw.firemaples.onscreenocr.hilt + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.floatings.manager.StateNavigatorImpl + +@Module +@InstallIn(SingletonComponent::class) +interface SingletonModule { + + @Binds + fun bindStateNavigator(stateNavigatorImpl: StateNavigatorImpl): StateNavigator +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/QuickTileService.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/QuickTileService.kt index 62066f1a..02350353 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/QuickTileService.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/QuickTileService.kt @@ -6,15 +6,21 @@ import android.os.IBinder import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* -import tw.firemaples.onscreenocr.floatings.manager.FloatingStateManager +import tw.firemaples.onscreenocr.floatings.manager.FloatingViewCoordinator import tw.firemaples.onscreenocr.pages.launch.LaunchActivity import tw.firemaples.onscreenocr.screenshot.ScreenExtractor +import javax.inject.Inject +@AndroidEntryPoint @RequiresApi(Build.VERSION_CODES.N) class QuickTileService : TileService() { private val logger: Logger by lazy { Logger(QuickTileService::class) } + @Inject + lateinit var floatingViewCoordinator: FloatingViewCoordinator + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var listeningJob: Job? = null @@ -40,11 +46,11 @@ class QuickTileService : TileService() { super.onClick() logger.debug("onClick()") - if (FloatingStateManager.isMainBarAttached) { - FloatingStateManager.detachAllViews() + if (floatingViewCoordinator.isMainBarAttached) { + floatingViewCoordinator.detachAllViews() } else { if (ScreenExtractor.isGranted) { - FloatingStateManager.showMainBar() + floatingViewCoordinator.showMainBar() } else { startActivityAndCollapse(LaunchActivity.getLaunchIntent(this)) } @@ -56,7 +62,7 @@ class QuickTileService : TileService() { logger.debug("onStartListening()") listeningJob = scope.launch { - FloatingStateManager.showingStateChangedFlow.collect { + floatingViewCoordinator.showingStateChangedFlow.collect { updateTileState(it) } } From 86b7876f3228db65422b35739a7e41abea25c90e Mon Sep 17 00:00:00 2001 From: firemaples Date: Thu, 4 Jan 2024 21:09:39 +0900 Subject: [PATCH 003/121] Move navigation logic to StateNavigator --- .../floatings/main/MainBarViewModel.kt | 13 +- .../manager/FloatingViewCoordinator.kt | 136 ++++++++++-------- .../floatings/manager/StateNavigator.kt | 49 ++++--- .../translator/BaseAppTranslator.kt | 2 +- .../onscreenocr/translator/Translator.kt | 6 +- .../googlemlkit/GoogleMLKitTranslator.kt | 8 +- 6 files changed, 119 insertions(+), 95 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt index 72b9a7e0..653aed48 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.ViewHolderService @@ -100,12 +102,15 @@ class MainBarViewModel @Inject constructor( Constants.DEFAULT_TRANSLATION_PROVIDER private var selectedTranslationLang: String = Constants.DEFAULT_TRANSLATION_LANG + init { + logger.debug("register FloatingStateManager.onStateChanged") + stateNavigator.currentState + .onEach { onStateChanged(it) } + .launchIn(viewScope) + } + fun onAttachedToScreen() { logger.debug("onAttachedToScreen()") - viewScope.launch { - logger.debug("register FloatingStateManager.onStateChanged") - stateNavigator.currentState.collect { onStateChanged(it) } - } viewScope.launch { ocrRepo.selectedOCRLangFlow.collect { onSelectedLangChanged(_ocrLang = it) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index a7009af7..71c46dee 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -146,9 +146,9 @@ class FloatingViewCoordinator @Inject constructor( } } - fun startScreenCircling() = stateIn(State.Idle::class) { - if (!Translator.getTranslator().checkEnvironment(scope)) { - return@stateIn + private fun startScreenCircling() = checkNextState(State.ScreenCircling::class) { + if (!Translator.getTranslator().checkResources(scope)) { + return@checkNextState } logger.debug("startScreenCircling()") @@ -159,8 +159,12 @@ class FloatingViewCoordinator @Inject constructor( } private fun onAreaSelected(parentRect: Rect, selectedRect: Rect) = - stateIn(State.ScreenCircling::class, State.ScreenCircled::class) { - logger.debug("onAreaSelected(), parentRect: $parentRect, selectedRect: $selectedRect, size: ${selectedRect.width()}x${selectedRect.height()}") + checkNextState(State.ScreenCircled::class) { + logger.debug( + "onAreaSelected(), parentRect: $parentRect, " + + "selectedRect: $selectedRect, " + + "size: ${selectedRect.width()}x${selectedRect.height()}" + ) if (currentState != State.ScreenCircled) { stateNavigator.updateState(State.ScreenCircled) } @@ -168,51 +172,57 @@ class FloatingViewCoordinator @Inject constructor( this@FloatingViewCoordinator.parentRect = parentRect } - fun cancelScreenCircling() = stateIn(State.ScreenCircling::class, State.ScreenCircled::class) { + private fun cancelScreenCircling() = checkNextState(State.Idle::class) { logger.debug("cancelScreenCircling()") stateNavigator.updateState(State.Idle) screenCirclingView.detachFromScreen() } - fun startScreenCapturing(selectedOCRLang: String) = stateIn(State.ScreenCircled::class) { - if (!Translator.getTranslator().checkEnvironment(scope)) { - return@stateIn - } + private fun startScreenCapturing(selectedOCRLang: String) = + checkNextState(State.ScreenCapturing::class) { + if (!Translator.getTranslator().checkResources(scope)) { + return@checkNextState + } - this@FloatingViewCoordinator.selectedOCRLang = selectedOCRLang - val parent = parentRect ?: return@stateIn - val selected = selectedRect ?: return@stateIn - logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") - stateNavigator.updateState(State.ScreenCapturing) - mainBar.detachFromScreen() - screenCirclingView.detachFromScreen() + this@FloatingViewCoordinator.selectedOCRLang = selectedOCRLang + val parent = parentRect ?: return@checkNextState + val selected = selectedRect ?: return@checkNextState + logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") + stateNavigator.updateState(State.ScreenCapturing) + mainBar.detachFromScreen() + screenCirclingView.detachFromScreen() - delay(100L) - - try { - FirebaseEvent.logStartCaptureScreen() - val croppedBitmap = - ScreenExtractor.extractBitmapFromScreen(parentRect = parent, cropRect = selected) - this@FloatingViewCoordinator.croppedBitmap = croppedBitmap - FirebaseEvent.logCaptureScreenFinished() - - mainBar.attachToScreen() - - startRecognition(croppedBitmap, parent, selected) - } catch (t: TimeoutCancellationException) { - logger.debug(t = t) - showError(context.getString(R.string.error_capture_screen_timeout)) - FirebaseEvent.logCaptureScreenFailed(t) - } catch (t: Throwable) { - logger.debug(t = t) - showError(t.message ?: context.getString(R.string.error_unknown_error_capturing_screen)) - FirebaseEvent.logCaptureScreenFailed(t) - } + delay(100L) + + try { + FirebaseEvent.logStartCaptureScreen() + val croppedBitmap = + ScreenExtractor.extractBitmapFromScreen( + parentRect = parent, + cropRect = selected + ) + this@FloatingViewCoordinator.croppedBitmap = croppedBitmap + FirebaseEvent.logCaptureScreenFinished() + + mainBar.attachToScreen() + + startRecognition(croppedBitmap, parent, selected) + } catch (t: TimeoutCancellationException) { + logger.debug(t = t) + showError(context.getString(R.string.error_capture_screen_timeout)) + FirebaseEvent.logCaptureScreenFailed(t) + } catch (t: Throwable) { + logger.debug(t = t) + showError( + t.message ?: context.getString(R.string.error_unknown_error_capturing_screen) + ) + FirebaseEvent.logCaptureScreenFailed(t) + } // screenCirclingView.detachFromScreen() // To test circled area - } + } private fun startRecognition(croppedBitmap: Bitmap, parent: Rect, selected: Rect) = - stateIn(State.ScreenCapturing::class) { + checkNextState(State.TextRecognizing::class) { stateNavigator.updateState(State.TextRecognizing) try { resultView.startRecognition() @@ -254,8 +264,8 @@ class FloatingViewCoordinator @Inject constructor( } } - fun startTranslation(recognitionResult: RecognitionResult) = - stateIn(State.TextRecognizing::class, State.ResultDisplaying::class) { + private fun startTranslation(recognitionResult: RecognitionResult) = + checkNextState(State.TextTranslating::class) { try { stateNavigator.updateState(State.TextTranslating) @@ -340,37 +350,39 @@ class FloatingViewCoordinator @Inject constructor( } private fun showResult(result: Result) = - stateIn(State.TextTranslating::class) { + checkNextState(State.ResultDisplaying::class) { logger.debug("showResult(), $result") stateNavigator.updateState(State.ResultDisplaying) resultView.textTranslated(result) } - private fun showError(error: String) { - scope.launch { - stateNavigator.updateState(State.ErrorDisplaying(error)) - logger.error(error) - context.showErrorDialog(error) - backToIdle() - } + private fun showError(error: String) = checkNextState(State.ErrorDisplaying::class) { + stateNavigator.updateState(State.ErrorDisplaying(error)) + logger.error(error) + context.showErrorDialog(error) + backToIdle() } - private fun backToIdle() = - scope.launch { - if (currentState != State.Idle) stateNavigator.updateState(State.Idle) - croppedBitmap?.setReusable() - resultView.backToIdle() - showMainBar() - } + private fun backToIdle() = checkNextState(State.Idle::class) { + if (currentState != State.Idle) stateNavigator.updateState(State.Idle) + croppedBitmap?.setReusable() + resultView.backToIdle() + showMainBar() + } - private fun stateIn( - vararg states: KClass, - block: suspend CoroutineScope.() -> Unit + private fun checkNextState( + vararg nextStates: KClass, + block: suspend CoroutineScope.() -> Unit, ) { - if (states.contains(currentState::class)) { + val notAllowed = nextStates.filterNot { stateNavigator.allowedNextState(it) } + + if (notAllowed.isEmpty()) { scope.launch { block.invoke(this) } - } else logger.error(t = IllegalStateException("The state should be in ${states.toList()}, current is $currentState")) + } else { + val error = "Transit from $notAllowed to $currentState is not allowed" + logger.error(t = IllegalStateException(error)) + } } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 0a19b47f..7f8017a9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -11,12 +11,15 @@ import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject import javax.inject.Singleton +import kotlin.reflect.KClass interface StateNavigator { val navigationAction: SharedFlow val currentState: StateFlow suspend fun navigate(action: NavigationAction) + fun allowedNextState(nextState: KClass): Boolean + fun updateState(newState: State) } @@ -28,36 +31,36 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { override val currentState = MutableStateFlow(State.Idle) + private val nextStates: Map, Set>> = mapOf( + State.Idle::class to setOf(State.ScreenCircling::class), + State.ScreenCircling::class to setOf(State.Idle::class, State.ScreenCircled::class), + State.ScreenCircled::class to setOf(State.Idle::class, State.ScreenCapturing::class), + State.ScreenCapturing::class to setOf( + State.Idle::class, State.TextRecognizing::class, State.ErrorDisplaying::class, + ), + State.TextRecognizing::class to setOf( + State.Idle::class, State.TextTranslating::class, State.ErrorDisplaying::class, + ), + State.TextTranslating::class to setOf( + State.ResultDisplaying::class, State.ErrorDisplaying::class, State.Idle::class, + ), + State.ResultDisplaying::class to setOf(State.Idle::class, State.TextTranslating::class), + State.ErrorDisplaying::class to setOf(State.Idle::class), + ) + override suspend fun navigate(action: NavigationAction) { + logger.debug("Receive NavigationAction: $action") navigationAction.subscriptionCount.first { it > 0 } navigationAction.emit(action) } + override fun allowedNextState(nextState: KClass): Boolean = + nextStates[currentState.value::class]?.contains(nextState) == true + override fun updateState(newState: State) { - val allowedNextStates = when (currentState.value) { - State.Idle -> arrayOf(State.ScreenCircling::class) - State.ScreenCircling -> arrayOf(State.Idle::class, State.ScreenCircled::class) - State.ScreenCircled -> arrayOf(State.Idle::class, State.ScreenCapturing::class) - State.ScreenCapturing -> - arrayOf( - State.Idle::class, State.TextRecognizing::class, State.ErrorDisplaying::class - ) - - State.TextRecognizing -> - arrayOf( - State.Idle::class, State.TextTranslating::class, State.ErrorDisplaying::class - ) - - State.TextTranslating -> - arrayOf( - State.ResultDisplaying::class, State.ErrorDisplaying::class, State.Idle::class - ) - - State.ResultDisplaying -> arrayOf(State.Idle::class, State.TextTranslating::class) - is State.ErrorDisplaying -> arrayOf(State.Idle::class) - } + val allowedNextStates = nextStates[currentState.value::class] - if (allowedNextStates.contains(newState::class)) { + if (allowedNextStates?.contains(newState::class) == true) { logger.debug("Change state ${currentState.value} > $newState") currentState.value = newState } else { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/translator/BaseAppTranslator.kt b/main/src/main/java/tw/firemaples/onscreenocr/translator/BaseAppTranslator.kt index 9ae4ca2b..4ddc6409 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/translator/BaseAppTranslator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/translator/BaseAppTranslator.kt @@ -16,7 +16,7 @@ abstract class BaseAppTranslator : Translator { context.getString(type.nameRes) ) - override suspend fun checkEnvironment( + override suspend fun checkResources( coroutineScope: CoroutineScope ): Boolean = translatorUtils.checkIsInstalled() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/translator/Translator.kt b/main/src/main/java/tw/firemaples/onscreenocr/translator/Translator.kt index 5a9f0e0a..19c17669 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/translator/Translator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/translator/Translator.kt @@ -47,7 +47,11 @@ interface Translator { val defaultLanguage: String get() = Constants.DEFAULT_TRANSLATION_LANG - suspend fun checkEnvironment(coroutineScope: CoroutineScope): Boolean = true + /** + * Check the required resources is ready + * @return true if required resources are ready + */ + suspend fun checkResources(coroutineScope: CoroutineScope): Boolean = true suspend fun isLangSupport(): Boolean = supportedLanguages().any { it.code.firstPart() == AppPref.selectedOCRLang.firstPart() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/translator/googlemlkit/GoogleMLKitTranslator.kt b/main/src/main/java/tw/firemaples/onscreenocr/translator/googlemlkit/GoogleMLKitTranslator.kt index 2c2bd18a..4018102e 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/translator/googlemlkit/GoogleMLKitTranslator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/translator/googlemlkit/GoogleMLKitTranslator.kt @@ -6,9 +6,6 @@ import com.google.mlkit.nl.translate.TranslateLanguage import com.google.mlkit.nl.translate.TranslateRemoteModel import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.TranslatorOptions -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R @@ -20,6 +17,9 @@ import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.translator.TranslationResult import tw.firemaples.onscreenocr.translator.Translator import tw.firemaples.onscreenocr.utils.firstPart +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine object GoogleMLKitTranslator : Translator { private const val DOWNLOAD_SITE = "GoogleMLKit" @@ -52,7 +52,7 @@ object GoogleMLKitTranslator : Translator { } } - override suspend fun checkEnvironment(coroutineScope: CoroutineScope): Boolean = + override suspend fun checkResources(coroutineScope: CoroutineScope): Boolean = checkTranslationResources(coroutineScope) override suspend fun translate(text: String, sourceLangCode: String): TranslationResult { From fa370bce07757254231d489e39ea0e1e41676daa Mon Sep 17 00:00:00 2001 From: firemaples Date: Fri, 5 Jan 2024 00:41:18 +0900 Subject: [PATCH 004/121] Add Compose support --- build.gradle | 2 +- main/build.gradle | 19 +- .../floatings/base/ComposeFloatingView.kt | 325 ++++++++++++++++++ .../base/ComposeMovableFloatingView.kt | 181 ++++++++++ .../floatings/mainbar/MainBarContent.kt | 97 ++++++ .../floatings/mainbar/MainBarFloatingView.kt | 172 +++++++++ .../floatings/mainbar/MainBarModule.kt | 13 + .../floatings/mainbar/MainBarViewModel.kt | 46 +++ .../manager/FloatingViewCoordinator.kt | 4 +- 9 files changed, 855 insertions(+), 4 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt diff --git a/build.gradle b/build.gradle index b3057865..14e98d20 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.gms:google-services:4.4.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20" classpath 'com.google.firebase:perf-plugin:1.4.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' diff --git a/main/build.gradle b/main/build.gradle index ae3fe467..eb8790a8 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -92,7 +92,11 @@ android { jvmTarget = '17' } buildFeatures { - viewBinding true + viewBinding true //TODO remove after full migrating to compose + compose true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.4" } packagingOptions { jniLibs { @@ -143,6 +147,19 @@ dependencies { // required to avoid crash on Android 12 API 31 implementation 'androidx.work:work-runtime-ktx:2.8.1' + // Compose + def composeBom = platform('androidx.compose:compose-bom:2023.06.01') + implementation composeBom + androidTestImplementation composeBom + // Material Design 3 + implementation 'androidx.compose.material3:material3' + // Android Studio Preview support + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + // UI Tests + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + // Hilt implementation "com.google.dagger:hilt-android:2.48" kapt "com.google.dagger:hilt-compiler:2.48" diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt new file mode 100644 index 00000000..50e04b72 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt @@ -0,0 +1,325 @@ +package tw.firemaples.onscreenocr.floatings.base + +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Point +import android.os.Build +import android.os.Bundle +import android.os.Looper +import android.view.Gravity +import android.view.OrientationEventListener +import android.view.WindowManager +import androidx.annotation.CallSuper +import androidx.annotation.MainThread +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren +import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.PermissionUtil +import tw.firemaples.onscreenocr.utils.UIUtils +import tw.firemaples.onscreenocr.wigets.HomeButtonWatcher +import java.io.Closeable +import kotlin.coroutines.CoroutineContext + +abstract class ComposeFloatingView(protected val context: Context) { + + companion object { + private val attachedFloatingViews: MutableList = mutableListOf() + + fun detachAllFloatingViews() { + attachedFloatingViews.toList().forEach { it.detachFromScreen() } + } + } + + private val logger: Logger by lazy { Logger(this::class) } + + private val windowManager: WindowManager by lazy { context.getSystemService(Context.WINDOW_SERVICE) as WindowManager } + + open val initialPosition: Point = Point(0, 0) + open val layoutWidth: Int = WindowManager.LayoutParams.WRAP_CONTENT + open val layoutHeight: Int = WindowManager.LayoutParams.WRAP_CONTENT + open val layoutFocusable: Boolean = false + open val layoutCanMoveOutsideScreen: Boolean = false + open val fullscreenMode: Boolean = false + open val layoutGravity: Int = Gravity.TOP or Gravity.LEFT + open val enableHomeButtonWatcher: Boolean = false + + protected val params: WindowManager.LayoutParams by lazy { + val type = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + else WindowManager.LayoutParams.TYPE_PHONE + + var flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + if (!layoutFocusable) + flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + if (layoutCanMoveOutsideScreen) + flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + if (fullscreenMode) + flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + + WindowManager.LayoutParams(layoutWidth, layoutHeight, type, flags, PixelFormat.TRANSLUCENT) + .apply { + val initPoint = initialPosition + x = initPoint.x.fixXPosition() + y = initPoint.y.fixYPosition() + gravity = layoutGravity + } + } + + private val homeButtonWatcher: HomeButtonWatcher by lazy { + HomeButtonWatcher( + context = context, + onHomeButtonPressed = { onHomeButtonPressed() }, + onHomeButtonLongPressed = { onHomeButtonLongPressed() }, + ) + } + + private val viewModelStore = ViewModelStore() + private val viewModelStoreOwner = object : ViewModelStoreOwner { + override val viewModelStore: ViewModelStore + get() = this@ComposeFloatingView.viewModelStore + } + + @Composable + abstract fun RootContent() + + // abstract val layoutId: Int +// protected lateinit var rootLayout: View +// protected val rootView: BackButtonTrackerView by lazy { +// BackButtonTrackerView( +// context = context, +// onAttachedToWindow = { onAttachedToScreen() }, +// onDetachedFromWindow = { onDetachedFromScreen() }, +// onBackButtonPressed = { onBackButtonPressed() }, +// ).apply { +// rootLayout = ComposeView(context).apply { +// setContent { +// RootContent() +// } +// +// setViewTreeLifecycleOwner(lifecycleOwner) +// setViewTreeSavedStateRegistryOwner(lifecycleOwner) +// +// setViewTreeViewModelStoreOwner(viewModelStoreOwner) +// } +//// rootLayout = context.getThemedLayoutInflater().inflate(layoutId, null) +// addView( +// rootLayout, +// ViewGroup.LayoutParams( +// ViewGroup.LayoutParams.MATCH_PARENT, +// ViewGroup.LayoutParams.MATCH_PARENT +// ) +// ) +// } +// } + protected val rootView by lazy { + ComposeView(context).apply { + setContent { + RootContent() + } + + setViewTreeLifecycleOwner(lifecycleOwner) + setViewTreeSavedStateRegistryOwner(lifecycleOwner) + + setViewTreeViewModelStoreOwner(viewModelStoreOwner) + } + } + + private var lastScreenWidth: Int = -1 + open val enableDeviceDirectionTracker: Boolean = false + private val orientationEventListener = object : OrientationEventListener(context) { + override fun onOrientationChanged(orientation: Int) { + val screenWidth = UIUtils.screenSize[0] + if (screenWidth != lastScreenWidth) { + lastScreenWidth = screenWidth + onDeviceDirectionChanged() + } + } + } + + var attached: Boolean = false + private set + + var onAttached: (() -> Unit)? = null + var onDetached: (() -> Unit)? = null + + @MainThread + open fun attachToScreen() { + if (attached) return + if (!PermissionUtil.canDrawOverlays(context)) { + logger.warn("You should obtain the draw overlays permission first!") + return + } + if (Looper.myLooper() != Looper.getMainLooper()) { + logger.warn("attachToWindow() should be called in main thread") + return + } + + windowManager.addView(rootView, params) + + with(lifecycleOwner) { + handleLifecycleEvent(Lifecycle.Event.ON_START) + handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + attachedFloatingViews.add(this) + + if (enableDeviceDirectionTracker) + orientationEventListener.enable() + + attached = true + } + + @MainThread + open fun detachFromScreen() { + if (!attached) return + if (Looper.myLooper() != Looper.getMainLooper()) { + logger.warn("attachToWindow() should be called in main thread") + return + } + + if (enableHomeButtonWatcher) { + homeButtonWatcher.stopWatch() + } + + viewScope.coroutineContext.cancelChildren() + + windowManager.removeView(rootView) + + with(lifecycleOwner) { + handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + attachedFloatingViews.remove(this) + + if (enableDeviceDirectionTracker) + orientationEventListener.disable() + + attached = false + } + + open fun release() { + detachFromScreen() + with(lifecycleOwner) { + handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + } + + protected open fun onDeviceDirectionChanged() { + params.x = params.x.fixXPosition() + params.y = params.y.fixYPosition() + updateViewLayout() + } + + @CallSuper + protected open fun onAttachedToScreen() { + if (enableHomeButtonWatcher) { + homeButtonWatcher.startWatch() + } + + onAttached?.invoke() + } + + @CallSuper + protected open fun onDetachedFromScreen() { + onDetached?.invoke() + } + + fun changeViewPosition(x: Int, y: Int) { + params.x = x + params.y = y + updateViewLayout() + } + + private fun updateViewLayout() { + try { + windowManager.updateViewLayout(rootView, params) + } catch (e: Exception) { +// logger.warn(t = e) + } + } + + open fun onBackButtonPressed(): Boolean = false + + open fun onHomeButtonPressed() { + + } + + open fun onHomeButtonLongPressed() { + + } + + protected val lifecycleOwner: FloatingViewLifecycleOwner = + FloatingViewLifecycleOwner().apply { + performRestore(null) + handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + } + +// private val tasks = mutableListOf>() + + protected val viewScope: CoroutineScope by lazy { + FloatingViewCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).apply { +// tasks.add(WeakReference(this)) + } + } + + private class FloatingViewCoroutineScope(context: CoroutineContext) : + Closeable, CoroutineScope { + override val coroutineContext: CoroutineContext = context + + override fun close() { + coroutineContext.cancel() + } + } + + protected class FloatingViewLifecycleOwner : SavedStateRegistryOwner { + private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) + private var savedStateRegistryController: SavedStateRegistryController = + SavedStateRegistryController.create(this) + + val isInitialized: Boolean + get() = true + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + fun handleLifecycleEvent(event: Lifecycle.Event) { + lifecycleRegistry.handleLifecycleEvent(event) + } + + override val savedStateRegistry: SavedStateRegistry + get() = savedStateRegistryController.savedStateRegistry + + fun performRestore(savedState: Bundle?) { + savedStateRegistryController.performRestore(savedState) + } + + fun performSave(outBundle: Bundle) { + savedStateRegistryController.performSave(outBundle) + } + } + + protected fun Int.fixXPosition(): Int = + this.coerceAtLeast(0) + .coerceAtMost(UIUtils.screenSize[0] - rootView.width) + + protected fun Int.fixYPosition(): Int = + this.coerceAtLeast(0) + .coerceAtMost(UIUtils.screenSize[1] - rootView.height) +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt new file mode 100644 index 00000000..46c05a85 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt @@ -0,0 +1,181 @@ +package tw.firemaples.onscreenocr.floatings.base + +import android.animation.ValueAnimator +import android.content.Context +import android.view.Gravity +import android.view.animation.OvershootInterpolator +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.core.animation.addListener +import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.UIUtils +import tw.firemaples.onscreenocr.utils.dpToPx + +abstract class ComposeMovableFloatingView(context: Context) : ComposeFloatingView(context) { + companion object { + private const val moveToEdgeDuration: Long = 450 + + private const val fromAlpha: Float = 1f + private const val fadeOutAnimationDuration: Long = 800 + } + + private val logger: Logger by lazy { Logger(this::class) } + + open val moveToEdgeAfterMoved: Boolean = false + open val moveToEdgeMarginInDP: Float = 0f + private val moveToEdgeMargin: Int by lazy { moveToEdgeMarginInDP.dpToPx() } + override val layoutCanMoveOutsideScreen: Boolean + get() = moveToEdgeAfterMoved + + open val fadeOutAfterMoved: Boolean = false + open val fadeOutDelay: Long = 1000L + open val fadeOutDestinationAlpha: Float = 0.2f + + override fun onAttachedToScreen() { + super.onAttachedToScreen() + moveToEdgeOrFadeOut() + } + + override fun onDeviceDirectionChanged() { + super.onDeviceDirectionChanged() + moveToEdgeOrFadeOut() + } + + val onDragStart: (Offset) -> Unit = { _ -> + cancelFadeOut() + } + val onDragEnd: () -> Unit = { + cancelFadeOut() + moveToEdgeOrFadeOut() + } + val onDragCancel: () -> Unit = { + cancelFadeOut() + moveToEdgeOrFadeOut() + } + val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit = { change, dragAmount -> + cancelFadeOut() + + val nextX = (params.x + (if (isAlignParentLeft) dragAmount.x else -dragAmount.x)) + .toInt().fixXPosition() + val nextY = (params.y + (if (isAlignParentTop) dragAmount.y else -dragAmount.y)) + .toInt().fixYPosition() + + changeViewPosition(nextX, nextY) + } + + private val isAlignParentLeft: Boolean + get() = Gravity.getAbsoluteGravity(layoutGravity, rootView.layoutDirection) and + Gravity.HORIZONTAL_GRAVITY_MASK == Gravity.LEFT + + private val isAlignParentTop: Boolean + get() = layoutGravity and Gravity.VERTICAL_GRAVITY_MASK == Gravity.TOP + + private fun moveToEdgeOrFadeOut() { + when { + moveToEdgeAfterMoved -> moveToEdge() + fadeOutAfterMoved -> fadeOut() + else -> cancelFadeOut() + } + } + + //region Moving to edge + fun moveToEdgeIfEnabled() { + rootView.post { if (moveToEdgeAfterMoved) moveToEdge() } + } + + private fun moveToEdge() { + val params = params + + val edgePosition = getEdgePosition(params.x, params.y) + + moveTo(params.x, params.y, edgePosition[0], edgePosition[1], true) + } + + private fun getEdgePosition(currentX: Int, currentY: Int): IntArray { + val screenWidth = UIUtils.screenSize[0] + + val viewWidth = rootView.width + val viewCenterX = currentX + viewWidth / 2 + + val margin = moveToEdgeMargin + + val edgeX = + // near left + if (viewCenterX < screenWidth / 2) margin + // near right + else screenWidth - viewWidth - margin + + return intArrayOf(edgeX, currentY) + } + + private var moveEdgeAnimator: ValueAnimator? = null + + private fun moveTo( + currentX: Int, + currentY: Int, + destPositionX: Int, + destPositionY: Int, + withAnimation: Boolean + ) { + val currentParams = params + if (!withAnimation) { + if (currentParams.x != destPositionX || currentParams.y != destPositionY) { + changeViewPosition(destPositionX, destPositionY) + } + } else { + moveEdgeAnimator = ValueAnimator.ofInt(0, 100).apply { + addUpdateListener { animation -> + val progress = animation.animatedValue as Int / 100f + + val nextX = currentX + (destPositionX - currentX) * progress + val nextY = currentY + (destPositionY - currentY) * progress + + changeViewPosition(nextX.toInt(), nextY.toInt()) + } + + duration = moveToEdgeDuration + interpolator = OvershootInterpolator(1.25f) + addListener( + onEnd = { + if (fadeOutAfterMoved) fadeOut() + }, + ) + + start() + } + } + } + //endregion + + //region Fade-out + private var fadeOutAnimator: ValueAnimator? = null + + private fun fadeOut() { +// logger.debug("fadeOut()") + cancelFadeOut() + + fadeOutAnimator = ValueAnimator.ofFloat(fromAlpha, fadeOutDestinationAlpha).apply { + addUpdateListener { animation -> + rootView.alpha = animation.animatedValue as Float + } + duration = fadeOutAnimationDuration + startDelay = fadeOutDelay + + start() + } + } + + private fun cancelFadeOut() { +// logger.debug("cancelFadeOut(): fadeOutAnimator: $fadeOutAnimator") + fadeOutAnimator?.cancel() + + rootView.alpha = fromAlpha + } + + protected fun rescheduleFadeOut() { +// logger.debug("rescheduleFadeOut()") + cancelFadeOut() + if (fadeOutAfterMoved) fadeOut() + } + //endregion +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt new file mode 100644 index 00000000..5cfe2a73 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt @@ -0,0 +1,97 @@ +package tw.firemaples.onscreenocr.floatings.mainbar + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import tw.firemaples.onscreenocr.R + +@Composable +fun MainBarContent( + viewModel: MainBarViewModel, + onDragStart: (Offset) -> Unit = { }, + onDragEnd: () -> Unit = { }, + onDragCancel: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, +) { + Card( + modifier = Modifier, +// .background(colorResource(id = R.color.background)) + ) { + Row( + modifier = Modifier + .padding(4.dp) + ) { + MainBarButton(drawable = R.drawable.ic_selection) + Spacer(modifier = Modifier.size(4.dp)) + MainBarButton(drawable = R.drawable.ic_translate) + Spacer(modifier = Modifier.size(4.dp)) + MainBarButton(drawable = R.drawable.ic_close) + Spacer(modifier = Modifier.size(4.dp)) + Image( + modifier = Modifier + .size(32.dp) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onDrag = onDrag, + ) + }, + painter = painterResource(id = R.drawable.ic_menu_move), + contentDescription = "", + ) + } + } +} + +@Composable +private fun MainBarButton( + @DrawableRes + drawable: Int, +) { + Image( + modifier = Modifier + .size(32.dp) + .background(colorResource(id = R.color.md_blue_800), shape = RoundedCornerShape(4.dp)) + .padding(4.dp), + painter = painterResource(id = drawable), + contentDescription = "", + ) +} + +@Preview +@Composable +private fun MainBarContentPreview() { + val viewModel = object : MainBarViewModel { + override fun onMenuItemClicked(key: String) = Unit + override fun onSelectClicked() = Unit + override fun onTranslateClicked() = Unit + override fun onCloseClicked() = Unit + override fun onMenuButtonClicked() = Unit + override fun onAttachedToScreen() = Unit + override fun saveLastPosition(x: Int, y: Int) = Unit + } + + MainBarContent(viewModel = viewModel, + onDragStart = { offset -> }, + onDragEnd = {}, + onDragCancel = {}, + onDrag = { change, dragAmount -> }) +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt new file mode 100644 index 00000000..b088276f --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt @@ -0,0 +1,172 @@ +package tw.firemaples.onscreenocr.floatings.mainbar + +import android.content.Context +import android.graphics.Point +import androidx.compose.runtime.Composable +import dagger.hilt.android.qualifiers.ApplicationContext +import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding +import tw.firemaples.onscreenocr.floatings.base.ComposeMovableFloatingView +import tw.firemaples.onscreenocr.floatings.manager.State +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.floatings.menu.MenuView +import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel +import tw.firemaples.onscreenocr.log.FirebaseEvent +import tw.firemaples.onscreenocr.pages.setting.SettingManager +import tw.firemaples.onscreenocr.pref.AppPref +import tw.firemaples.onscreenocr.utils.clickOnce +import javax.inject.Inject + +class MainBarFloatingView @Inject constructor( + @ApplicationContext context: Context, + private val stateNavigator: StateNavigator, + private val viewModel: MainBarViewModel, +) : ComposeMovableFloatingView(context) { + +// override val layoutId: Int +// get() = R.layout.floating_main_bar + + override val initialPosition: Point + get() = + if (SettingManager.restoreMainBarPosition) AppPref.lastMainBarPosition + else Point(0, 0) + + @Composable + override fun RootContent() { + MainBarContent( + viewModel = viewModel, + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onDrag = onDrag, + ) + } + + override val enableDeviceDirectionTracker: Boolean + get() = true + + override val moveToEdgeAfterMoved: Boolean + get() = true + + override val fadeOutAfterMoved: Boolean + get() = !arrayOf(State.ScreenCircling, State.ScreenCircled) + .contains(stateNavigator.currentState.value) + && !menuView.attached + && SettingManager.enableFadingOutWhileIdle + override val fadeOutDelay: Long + get() = SettingManager.timeoutToFadeOut + override val fadeOutDestinationAlpha: Float + get() = SettingManager.opaquePercentageToFadeOut + +// private val binding: FloatingMainBarBinding = FloatingMainBarBinding.bind(rootLayout) + + private val menuView: MenuView by lazy { + MenuView(context, false).apply { +// setAnchor(binding.btMenu) + + onAttached = { rescheduleFadeOut() } + onDetached = { rescheduleFadeOut() } + onItemSelected = { view, key -> + view.detachFromScreen() + viewModel.onMenuItemClicked(key) + rescheduleFadeOut() + } + } + } + + init { +// binding.setViews() +// setDragView(binding.btMenu) + } + + private fun FloatingMainBarBinding.setViews() { + btLangSelector.clickOnce { + rescheduleFadeOut() + TranslationSelectPanel(context).attachToScreen() + } + + btSelect.clickOnce { + viewModel.onSelectClicked() + } + + btTranslate.clickOnce { + FirebaseEvent.logClickTranslationStartButton() + viewModel.onTranslateClicked() + } + + btClose.clickOnce { + viewModel.onCloseClicked() + } + + btMenu.clickOnce { + viewModel.onMenuButtonClicked() + } + +// viewModel.languageText.observe(lifecycleOwner) { +// tvLang.text = it +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayTranslatorIcon.observe(lifecycleOwner) { +// if (it == null) { +// ivGoogleTranslator.setImageDrawable(null) +// ivGoogleTranslator.hide() +// } else { +// ivGoogleTranslator.setImageResource(it) +// ivGoogleTranslator.show() +// } +// moveToEdgeIfEnabled() +// } +// +// viewModel.displaySelectButton.observe(lifecycleOwner) { +// btSelect.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayTranslateButton.observe(lifecycleOwner) { +// btTranslate.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayCloseButton.observe(lifecycleOwner) { +// btClose.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayMenuItems.observe(lifecycleOwner) { +// with(menuView) { +// updateData(it) +// attachToScreen() +// } +// } +// +// viewModel.rescheduleFadeOut.observe(lifecycleOwner) { +// rescheduleFadeOut() +// } +// +// viewModel.showSettingPage.observe(lifecycleOwner) { +// SettingActivity.start(context) +// } +// +// viewModel.openBrowser.observe(lifecycleOwner) { +// Utils.openBrowser(it) +// } +// +// viewModel.showVersionHistory.observe(lifecycleOwner) { +// VersionHistoryView(context).attachToScreen() +// } +// +// viewModel.showReadme.observe(lifecycleOwner) { +// ReadmeView(context).attachToScreen() +// } + } + + override fun onAttachedToScreen() { + super.onAttachedToScreen() + viewModel.onAttachedToScreen() + } + + override fun onDetachedFromScreen() { + super.onDetachedFromScreen() + viewModel.saveLastPosition(params.x, params.y) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt new file mode 100644 index 00000000..f4d07588 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt @@ -0,0 +1,13 @@ +package tw.firemaples.onscreenocr.floatings.mainbar + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface MainBarModule { + @Binds + fun bindMainBarViewModel(mainBarViewModelImpl: MainBarViewModelImpl): MainBarViewModel +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt new file mode 100644 index 00000000..a3ac5f56 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt @@ -0,0 +1,46 @@ +package tw.firemaples.onscreenocr.floatings.mainbar + +import javax.inject.Inject + +interface MainBarViewModel { + fun onMenuItemClicked(key: String) + fun onSelectClicked() + fun onTranslateClicked() + fun onCloseClicked() + fun onMenuButtonClicked() + fun onAttachedToScreen() + fun saveLastPosition(x: Int, y: Int) + +} + +class MainBarViewModelImpl @Inject constructor( + +): MainBarViewModel { + override fun onMenuItemClicked(key: String) { + + } + + override fun onSelectClicked() { + + } + + override fun onTranslateClicked() { + + } + + override fun onCloseClicked() { + + } + + override fun onMenuButtonClicked() { + + } + + override fun onAttachedToScreen() { + + } + + override fun saveLastPosition(x: Int, y: Int) { + + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index 71c46dee..1296e9df 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.base.FloatingView import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog -import tw.firemaples.onscreenocr.floatings.main.MainBar +import tw.firemaples.onscreenocr.floatings.mainbar.MainBarFloatingView import tw.firemaples.onscreenocr.floatings.result.ResultView import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView import tw.firemaples.onscreenocr.hilt.MainCoroutineScope @@ -44,7 +44,7 @@ class FloatingViewCoordinator @Inject constructor( @ApplicationContext private val context: Context, private val stateNavigator: StateNavigator, @MainCoroutineScope private val scope: CoroutineScope, - private val mainBar: MainBar, + private val mainBar: MainBarFloatingView, private val resultView: ResultView, ) { private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } From 365320337d153557497f663a31d72e76c8b77e4e Mon Sep 17 00:00:00 2001 From: firemaples Date: Fri, 5 Jan 2024 22:20:39 +0900 Subject: [PATCH 005/121] Implement views with states on MainBar --- main/build.gradle | 1 + .../data/repo/RecognitionRepository.kt | 22 ++ .../data/repo/TranslatorRepository.kt | 17 ++ .../GetCurrentOCRDisplayLangCodeUseCase.kt | 16 ++ .../GetCurrentTranslationLangUseCase.kt | 10 + .../GetCurrentTranslatorTypeUseCase.kt | 10 + .../CoroutinesDispatchersModule.kt | 2 +- .../{hilt => di}/CoroutinesQualifiers.kt | 2 +- .../{hilt => di}/SingletonModule.kt | 2 +- .../floatings/compose/base/AppTheme.kt | 23 ++ .../{ => compose}/base/ComposeFloatingView.kt | 9 +- .../base/ComposeMovableFloatingView.kt | 4 +- .../floatings/compose/base/ComposeUtils.kt | 18 ++ .../compose/mainbar/MainBarContent.kt | 231 ++++++++++++++++++ .../mainbar/MainBarFloatingView.kt | 18 +- .../{ => compose}/mainbar/MainBarModule.kt | 2 +- .../compose/mainbar/MainBarViewModel.kt | 178 ++++++++++++++ .../onscreenocr/floatings/main/MainBar.kt | 6 +- .../floatings/main/MainBarViewModel.kt | 16 +- .../floatings/mainbar/MainBarContent.kt | 97 -------- .../floatings/mainbar/MainBarViewModel.kt | 46 ---- .../manager/FloatingViewCoordinator.kt | 50 ++-- .../floatings/manager/StateNavigator.kt | 66 ++--- .../floatings/result/ResultViewModel.kt | 2 +- .../tw/firemaples/onscreenocr/pref/AppPref.kt | 2 +- 25 files changed, 623 insertions(+), 227 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/repo/RecognitionRepository.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/repo/TranslatorRepository.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslationLangUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslatorTypeUseCase.kt rename main/src/main/java/tw/firemaples/onscreenocr/{hilt => di}/CoroutinesDispatchersModule.kt (97%) rename main/src/main/java/tw/firemaples/onscreenocr/{hilt => di}/CoroutinesQualifiers.kt (93%) rename main/src/main/java/tw/firemaples/onscreenocr/{hilt => di}/SingletonModule.kt (91%) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt rename main/src/main/java/tw/firemaples/onscreenocr/floatings/{ => compose}/base/ComposeFloatingView.kt (97%) rename main/src/main/java/tw/firemaples/onscreenocr/floatings/{ => compose}/base/ComposeMovableFloatingView.kt (97%) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt rename main/src/main/java/tw/firemaples/onscreenocr/floatings/{ => compose}/mainbar/MainBarFloatingView.kt (89%) rename main/src/main/java/tw/firemaples/onscreenocr/floatings/{ => compose}/mainbar/MainBarModule.kt (83%) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt delete mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt delete mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt diff --git a/main/build.gradle b/main/build.gradle index eb8790a8..909ca7e4 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -119,6 +119,7 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.2' implementation "androidx.preference:preference-ktx:1.2.1" implementation 'androidx.webkit:webkit:1.7.0' //noinspection GradleDynamicVersion diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/RecognitionRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/RecognitionRepository.kt new file mode 100644 index 00000000..dd859e7b --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/RecognitionRepository.kt @@ -0,0 +1,22 @@ +package tw.firemaples.onscreenocr.data.repo + +import androidx.lifecycle.asFlow +import com.chibatching.kotpref.livedata.asLiveData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import tw.firemaples.onscreenocr.pref.AppPref +import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType +import tw.firemaples.onscreenocr.utils.Constants +import javax.inject.Inject + +class RecognitionRepository @Inject constructor() { + val ocrLanguage: Flow + get() = AppPref.asLiveData(AppPref::selectedOCRLang).asFlow() + + val ocrProvider: Flow + get() = AppPref.asLiveData(AppPref::selectedOCRProviderKey).asFlow() + .map { key -> + TextRecognitionProviderType.entries.firstOrNull { it.key == key } + ?: Constants.DEFAULT_OCR_PROVIDER + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/TranslatorRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/TranslatorRepository.kt new file mode 100644 index 00000000..d47d623a --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/TranslatorRepository.kt @@ -0,0 +1,17 @@ +package tw.firemaples.onscreenocr.data.repo + +import androidx.lifecycle.asFlow +import com.chibatching.kotpref.livedata.asLiveData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import tw.firemaples.onscreenocr.pref.AppPref +import tw.firemaples.onscreenocr.translator.TranslationProviderType +import javax.inject.Inject + +class TranslatorRepository @Inject constructor() { + val currentProviderType: Flow + get() = AppPref.asLiveData(AppPref::selectedTranslationProvider).asFlow() + .map { TranslationProviderType.fromKey(it) } + val currentTranslationLang: Flow + get() = AppPref.asLiveData(AppPref::selectedTranslationLang).asFlow() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt new file mode 100644 index 00000000..e7461d65 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt @@ -0,0 +1,16 @@ +package tw.firemaples.onscreenocr.data.usecase + +import kotlinx.coroutines.flow.combine +import tw.firemaples.onscreenocr.data.repo.RecognitionRepository +import tw.firemaples.onscreenocr.recognition.TextRecognizer +import javax.inject.Inject + +class GetCurrentOCRDisplayLangCodeUseCase @Inject constructor( + private val recognitionRepository: RecognitionRepository, +) { + operator fun invoke() = + recognitionRepository.ocrProvider + .combine(recognitionRepository.ocrLanguage) { provider, lang -> + TextRecognizer.getRecognizer(provider).parseToDisplayLangCode(lang) + } +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslationLangUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslationLangUseCase.kt new file mode 100644 index 00000000..84137687 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslationLangUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.TranslatorRepository +import javax.inject.Inject + +class GetCurrentTranslationLangUseCase @Inject constructor( + private val translatorRepository: TranslatorRepository, +) { + operator fun invoke() = translatorRepository.currentTranslationLang +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslatorTypeUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslatorTypeUseCase.kt new file mode 100644 index 00000000..3bfe5d09 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentTranslatorTypeUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.TranslatorRepository +import javax.inject.Inject + +class GetCurrentTranslatorTypeUseCase @Inject constructor( + private val translatorRepository: TranslatorRepository, +) { + operator fun invoke() = translatorRepository.currentProviderType +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt similarity index 97% rename from main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt rename to main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt index 036dc104..d672ac1d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesDispatchersModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.hilt +package tw.firemaples.onscreenocr.di import dagger.Module import dagger.Provides diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt similarity index 93% rename from main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt rename to main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt index 91de0de8..40c7eb81 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/hilt/CoroutinesQualifiers.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.hilt +package tw.firemaples.onscreenocr.di import javax.inject.Qualifier diff --git a/main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt similarity index 91% rename from main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt rename to main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt index 012bcb04..f3859e4c 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/hilt/SingletonModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.hilt +package tw.firemaples.onscreenocr.di import dagger.Binds import dagger.Module diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt new file mode 100644 index 00000000..6b009433 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt @@ -0,0 +1,23 @@ +package tw.firemaples.onscreenocr.floatings.compose.base + + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +lateinit var AppColorScheme: ColorScheme + +@Composable +fun AppTheme( + content: @Composable () -> Unit, +) { + AppColorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() + + MaterialTheme( + colorScheme = AppColorScheme, + content = content, + ) +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt similarity index 97% rename from main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt rename to main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 50e04b72..63a48f7b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.floatings.base +package tw.firemaples.onscreenocr.floatings.compose.base import android.content.Context import android.graphics.PixelFormat @@ -11,6 +11,7 @@ import android.view.OrientationEventListener import android.view.WindowManager import androidx.annotation.CallSuper import androidx.annotation.MainThread +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle @@ -129,8 +130,12 @@ abstract class ComposeFloatingView(protected val context: Context) { // } protected val rootView by lazy { ComposeView(context).apply { + setContent { - RootContent() + AppTheme { + logger.debug("is dark theme: ${isSystemInDarkTheme()}") + RootContent() + } } setViewTreeLifecycleOwner(lifecycleOwner) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt similarity index 97% rename from main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt rename to main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt index 46c05a85..1ac45706 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/base/ComposeMovableFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.floatings.base +package tw.firemaples.onscreenocr.floatings.compose.base import android.animation.ValueAnimator import android.content.Context @@ -19,7 +19,7 @@ abstract class ComposeMovableFloatingView(context: Context) : ComposeFloatingVie private const val fadeOutAnimationDuration: Long = 800 } - private val logger: Logger by lazy { Logger(this::class) } + protected val logger: Logger by lazy { Logger(this::class) } open val moveToEdgeAfterMoved: Boolean = false open val moveToEdgeMarginInDP: Float = 0f diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt new file mode 100644 index 00000000..75245c2f --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt @@ -0,0 +1,18 @@ +package tw.firemaples.onscreenocr.floatings.compose.base + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.Flow + +@Composable +fun Flow.collectOnLifecycleResumed(state: (T) -> Unit) { + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(this, lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { + this@collectOnLifecycleResumed.collect(state) + } + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt new file mode 100644 index 00000000..78ff0a13 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -0,0 +1,231 @@ +package tw.firemaples.onscreenocr.floatings.compose.mainbar + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme +import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme + +@Composable +fun MainBarContent( + viewModel: MainBarViewModel, + onDragStart: (Offset) -> Unit = { }, + onDragEnd: () -> Unit = { }, + onDragCancel: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, +) { + val state by viewModel.state.collectAsState() + + Box( + modifier = Modifier + .background( + color = AppColorScheme.background, + shape = RoundedCornerShape(8.dp), + ), + ) { + Row( + modifier = Modifier + .padding(4.dp) + ) { + LanguageBlock( + langText = state.langText, + translatorIcon = state.translatorIcon, + onClick = viewModel::onLanguageBlockClicked, + ) + if (state.displaySelectButton) { + Spacer(modifier = Modifier.size(4.dp)) + MainBarButton( + icon = R.drawable.ic_selection, + onClick = viewModel::onSelectClicked, + ) + } + if (state.displayTranslateButton) { + Spacer(modifier = Modifier.size(4.dp)) + MainBarButton( + icon = R.drawable.ic_translate, + onClick = viewModel::onTranslateClicked, + ) + } + if (state.displayCloseButton) { + Spacer(modifier = Modifier.size(4.dp)) + MainBarButton( + icon = R.drawable.ic_close, + onClick = viewModel::onCloseClicked, + ) + } + Spacer(modifier = Modifier.size(4.dp)) + MenuButton( + onClick = viewModel::onMenuButtonClicked, + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onDrag = onDrag, + ) + } + } +} + +@Composable +private fun LanguageBlock( + langText: String, + translatorIcon: Int? = null, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .height(32.dp) + .border( + width = 2.dp, + color = AppColorScheme.onBackground, + shape = RoundedCornerShape(4.dp), + ) + .clickable(onClick = onClick) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = langText, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = AppColorScheme.onBackground, + ) + if (translatorIcon != null) { + Image( + painter = painterResource(id = translatorIcon), + contentDescription = "", + colorFilter = ColorFilter.tint(AppColorScheme.onBackground), + ) + } + } +} + +@Composable +private fun MainBarButton( + @DrawableRes + icon: Int, + onClick: () -> Unit, +) { + Image( + modifier = Modifier + .size(32.dp) + .clickable(onClick = onClick) + .background(colorResource(id = R.color.md_blue_800), shape = RoundedCornerShape(4.dp)) + .padding(4.dp), + painter = painterResource(id = icon), + contentDescription = "", + ) +} + +@Composable +private fun MenuButton( + onClick: () -> Unit, + onDragStart: (Offset) -> Unit = { }, + onDragEnd: () -> Unit = { }, + onDragCancel: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, +) { + Image( + modifier = Modifier + .size(32.dp) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onDrag = onDrag, + ) + } + .clickable(onClick = onClick) + .padding(2.dp), + painter = painterResource(id = R.drawable.ic_menu_move), + colorFilter = ColorFilter.tint(AppColorScheme.onBackground), + contentDescription = "", + ) +} + +private class MainBarStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = listOf( + MainBarState( + langText = "en>", + translatorIcon = R.drawable.ic_google_translate_dark_grey, + displaySelectButton = true, + displayTranslateButton = true, + displayCloseButton = true, + ), + MainBarState( + langText = "en>tw", + translatorIcon = null, + displaySelectButton = true, + displayTranslateButton = true, + displayCloseButton = true, + ) + ).asSequence() + +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MainBarContentPreview( + @PreviewParameter(MainBarStateProvider::class) state: MainBarState, +) { + val viewModel = object : MainBarViewModel { + override val state: StateFlow + get() = MutableStateFlow(state) + override val action: SharedFlow + get() = MutableSharedFlow() + + override fun onMenuItemClicked(key: String) = Unit + override fun onSelectClicked() = Unit + override fun onTranslateClicked() = Unit + override fun onCloseClicked() = Unit + override fun onMenuButtonClicked() = Unit + override fun onAttachedToScreen() = Unit + override fun saveLastPosition(x: Int, y: Int) = Unit + override fun onLanguageBlockClicked() = Unit + } + + AppTheme { + MainBarContent(viewModel = viewModel, + onDragStart = { offset -> }, + onDragEnd = {}, + onDragCancel = {}, + onDrag = { change, dragAmount -> }) + } + +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt similarity index 89% rename from main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt rename to main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index b088276f..462390a9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -1,12 +1,13 @@ -package tw.firemaples.onscreenocr.floatings.mainbar +package tw.firemaples.onscreenocr.floatings.compose.mainbar import android.content.Context import android.graphics.Point import androidx.compose.runtime.Composable import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding -import tw.firemaples.onscreenocr.floatings.base.ComposeMovableFloatingView -import tw.firemaples.onscreenocr.floatings.manager.State +import tw.firemaples.onscreenocr.floatings.compose.base.ComposeMovableFloatingView +import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed +import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.floatings.menu.MenuView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel @@ -32,6 +33,13 @@ class MainBarFloatingView @Inject constructor( @Composable override fun RootContent() { + viewModel.action.collectOnLifecycleResumed { state -> + when (state) { + MainBarAction.RescheduleFadeOut -> + rescheduleFadeOut() + } + } + MainBarContent( viewModel = viewModel, onDragStart = onDragStart, @@ -48,8 +56,8 @@ class MainBarFloatingView @Inject constructor( get() = true override val fadeOutAfterMoved: Boolean - get() = !arrayOf(State.ScreenCircling, State.ScreenCircled) - .contains(stateNavigator.currentState.value) + get() = !arrayOf(NavState.ScreenCircling, NavState.ScreenCircled) + .contains(stateNavigator.currentNavState.value) && !menuView.attached && SettingManager.enableFadingOutWhileIdle override val fadeOutDelay: Long diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarModule.kt similarity index 83% rename from main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt rename to main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarModule.kt index f4d07588..3c607f35 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarModule.kt @@ -1,4 +1,4 @@ -package tw.firemaples.onscreenocr.floatings.mainbar +package tw.firemaples.onscreenocr.floatings.compose.mainbar import dagger.Binds import dagger.Module diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt new file mode 100644 index 00000000..385ac14c --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -0,0 +1,178 @@ +package tw.firemaples.onscreenocr.floatings.compose.mainbar + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.data.usecase.GetCurrentOCRDisplayLangCodeUseCase +import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase +import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslatorTypeUseCase +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.NavState +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.utils.Logger +import javax.inject.Inject + +interface MainBarViewModel { + val state: StateFlow + val action: SharedFlow + fun onMenuItemClicked(key: String) + fun onSelectClicked() + fun onTranslateClicked() + fun onCloseClicked() + fun onMenuButtonClicked() + fun onAttachedToScreen() + fun saveLastPosition(x: Int, y: Int) + fun onLanguageBlockClicked() +} + +data class MainBarState( + val langText: String = "", + val translatorIcon: Int? = null, + val displaySelectButton: Boolean = false, + val displayTranslateButton: Boolean = false, + val displayCloseButton: Boolean = false, +) + +sealed interface MainBarAction { + data object RescheduleFadeOut : MainBarAction +} + +class MainBarViewModelImpl @Inject constructor( + @MainImmediateCoroutineScope + private val scope: CoroutineScope, + stateNavigator: StateNavigator, + private val getCurrentOCRDisplayLangCodeUseCase: GetCurrentOCRDisplayLangCodeUseCase, + private val getCurrentTranslatorTypeUseCase: GetCurrentTranslatorTypeUseCase, + private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, +) : MainBarViewModel { + override val state = MutableStateFlow(MainBarState()) + override val action = MutableSharedFlow() + + private val logger: Logger by lazy { Logger(this::class) } + + init { + stateNavigator.currentNavState + .onEach { onNavigationStateChanges(it) } + .launchIn(scope) + subscribeLanguageStateChanges() + } + + private suspend fun onNavigationStateChanges(navState: NavState) { + state.update { + it.copy( + displaySelectButton = navState == NavState.Idle, + displayTranslateButton = navState == NavState.ScreenCircled, + displayCloseButton = + navState == NavState.ScreenCircling || navState == NavState.ScreenCircled, + ) + } + action.emit(MainBarAction.RescheduleFadeOut) + } + + private fun subscribeLanguageStateChanges() { + combine( + getCurrentOCRDisplayLangCodeUseCase.invoke(), + getCurrentTranslatorTypeUseCase.invoke(), + getCurrentTranslationLangUseCase.invoke(), + ) { ocrLang, translatorType, translationLang -> + updateLanguageStates( + ocrLang = ocrLang, + translationProviderType = translatorType, + translationLang = translationLang, + ) + }.launchIn(scope) + } + + private fun updateLanguageStates( + ocrLang: String, + translationProviderType: TranslationProviderType, + translationLang: String, + ) { + val icon = when (translationProviderType) { + TranslationProviderType.GoogleTranslateApp -> R.drawable.ic_google_translate_dark_grey + TranslationProviderType.BingTranslateApp -> R.drawable.ic_microsoft_bing + TranslationProviderType.OtherTranslateApp -> R.drawable.ic_open_in_app + TranslationProviderType.MicrosoftAzure, + TranslationProviderType.GoogleMLKit, + TranslationProviderType.MyMemory, + TranslationProviderType.PapagoTranslateApp, + TranslationProviderType.YandexTranslateApp, + TranslationProviderType.OCROnly -> null + } + + val text = when (translationProviderType) { + TranslationProviderType.GoogleTranslateApp, + TranslationProviderType.BingTranslateApp, + TranslationProviderType.OtherTranslateApp -> "$ocrLang>" + + TranslationProviderType.YandexTranslateApp -> "$ocrLang > Y" + TranslationProviderType.PapagoTranslateApp -> "$ocrLang > P" + TranslationProviderType.OCROnly -> " $ocrLang " + TranslationProviderType.MicrosoftAzure, + TranslationProviderType.GoogleMLKit, + TranslationProviderType.MyMemory -> "$ocrLang>$translationLang" + } + + state.update { + it.copy( + langText = text, + translatorIcon = icon, + ) + } + } + + override fun onMenuItemClicked(key: String) { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun onSelectClicked() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun onTranslateClicked() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun onCloseClicked() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun onMenuButtonClicked() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun onAttachedToScreen() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } + + override fun saveLastPosition(x: Int, y: Int) { + + } + + override fun onLanguageBlockClicked() { + scope.launch { + action.emit(MainBarAction.RescheduleFadeOut) + } + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt index 7edf376a..0df1377f 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt @@ -7,7 +7,7 @@ import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding import tw.firemaples.onscreenocr.floatings.base.MovableFloatingView import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView -import tw.firemaples.onscreenocr.floatings.manager.State +import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.floatings.menu.MenuView import tw.firemaples.onscreenocr.floatings.readme.ReadmeView @@ -44,8 +44,8 @@ class MainBar @Inject constructor( get() = true override val fadeOutAfterMoved: Boolean - get() = !arrayOf(State.ScreenCircling, State.ScreenCircled) - .contains(stateNavigator.currentState.value) + get() = !arrayOf(NavState.ScreenCircling, NavState.ScreenCircled) + .contains(stateNavigator.currentNavState.value) && !menuView.attached && SettingManager.enableFadingOutWhileIdle override val fadeOutDelay: Long diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt index 653aed48..2e1a990b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt @@ -9,12 +9,12 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel +import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.NavigationAction -import tw.firemaples.onscreenocr.floatings.manager.State import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.hilt.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.recognition.TextRecognizer import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager @@ -104,7 +104,7 @@ class MainBarViewModel @Inject constructor( init { logger.debug("register FloatingStateManager.onStateChanged") - stateNavigator.currentState + stateNavigator.currentNavState .onEach { onStateChanged(it) } .launchIn(viewScope) } @@ -137,19 +137,19 @@ class MainBarViewModel @Inject constructor( } } - private suspend fun onStateChanged(state: State) { + private suspend fun onStateChanged(state: NavState) { logger.debug("onStateChanged(): $state") setupButtons(state) _rescheduleFadeOut.value = true } @Suppress("RedundantSuspendModifier") - private suspend fun setupButtons(state: State) { + private suspend fun setupButtons(state: NavState) { logger.debug("setupButtons(): $state") - _displaySelectButton.value = state == State.Idle - _displayTranslateButton.value = state == State.ScreenCircled + _displaySelectButton.value = state == NavState.Idle + _displayTranslateButton.value = state == NavState.ScreenCircled _displayCloseButton.value = - state == State.ScreenCircling || state == State.ScreenCircled + state == NavState.ScreenCircling || state == NavState.ScreenCircled } @Suppress("RedundantSuspendModifier") diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt deleted file mode 100644 index 5cfe2a73..00000000 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarContent.kt +++ /dev/null @@ -1,97 +0,0 @@ -package tw.firemaples.onscreenocr.floatings.mainbar - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectDragGestures -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import tw.firemaples.onscreenocr.R - -@Composable -fun MainBarContent( - viewModel: MainBarViewModel, - onDragStart: (Offset) -> Unit = { }, - onDragEnd: () -> Unit = { }, - onDragCancel: () -> Unit = { }, - onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, -) { - Card( - modifier = Modifier, -// .background(colorResource(id = R.color.background)) - ) { - Row( - modifier = Modifier - .padding(4.dp) - ) { - MainBarButton(drawable = R.drawable.ic_selection) - Spacer(modifier = Modifier.size(4.dp)) - MainBarButton(drawable = R.drawable.ic_translate) - Spacer(modifier = Modifier.size(4.dp)) - MainBarButton(drawable = R.drawable.ic_close) - Spacer(modifier = Modifier.size(4.dp)) - Image( - modifier = Modifier - .size(32.dp) - .pointerInput(Unit) { - detectDragGestures( - onDragStart = onDragStart, - onDragEnd = onDragEnd, - onDragCancel = onDragCancel, - onDrag = onDrag, - ) - }, - painter = painterResource(id = R.drawable.ic_menu_move), - contentDescription = "", - ) - } - } -} - -@Composable -private fun MainBarButton( - @DrawableRes - drawable: Int, -) { - Image( - modifier = Modifier - .size(32.dp) - .background(colorResource(id = R.color.md_blue_800), shape = RoundedCornerShape(4.dp)) - .padding(4.dp), - painter = painterResource(id = drawable), - contentDescription = "", - ) -} - -@Preview -@Composable -private fun MainBarContentPreview() { - val viewModel = object : MainBarViewModel { - override fun onMenuItemClicked(key: String) = Unit - override fun onSelectClicked() = Unit - override fun onTranslateClicked() = Unit - override fun onCloseClicked() = Unit - override fun onMenuButtonClicked() = Unit - override fun onAttachedToScreen() = Unit - override fun saveLastPosition(x: Int, y: Int) = Unit - } - - MainBarContent(viewModel = viewModel, - onDragStart = { offset -> }, - onDragEnd = {}, - onDragCancel = {}, - onDrag = { change, dragAmount -> }) -} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt deleted file mode 100644 index a3ac5f56..00000000 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/mainbar/MainBarViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -package tw.firemaples.onscreenocr.floatings.mainbar - -import javax.inject.Inject - -interface MainBarViewModel { - fun onMenuItemClicked(key: String) - fun onSelectClicked() - fun onTranslateClicked() - fun onCloseClicked() - fun onMenuButtonClicked() - fun onAttachedToScreen() - fun saveLastPosition(x: Int, y: Int) - -} - -class MainBarViewModelImpl @Inject constructor( - -): MainBarViewModel { - override fun onMenuItemClicked(key: String) { - - } - - override fun onSelectClicked() { - - } - - override fun onTranslateClicked() { - - } - - override fun onCloseClicked() { - - } - - override fun onMenuButtonClicked() { - - } - - override fun onAttachedToScreen() { - - } - - override fun saveLastPosition(x: Int, y: Int) { - - } -} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index 1296e9df..bd0a5084 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -14,12 +14,12 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.di.MainCoroutineScope import tw.firemaples.onscreenocr.floatings.base.FloatingView +import tw.firemaples.onscreenocr.floatings.compose.mainbar.MainBarFloatingView import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog -import tw.firemaples.onscreenocr.floatings.mainbar.MainBarFloatingView import tw.firemaples.onscreenocr.floatings.result.ResultView import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView -import tw.firemaples.onscreenocr.hilt.MainCoroutineScope import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.pref.AppPref @@ -49,8 +49,8 @@ class FloatingViewCoordinator @Inject constructor( ) { private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } - private val currentState: State - get() = stateNavigator.currentState.value + private val currentNavState: NavState + get() = stateNavigator.currentNavState.value private val screenCirclingView: ScreenCirclingView by lazy { ScreenCirclingView(context).apply { @@ -146,40 +146,40 @@ class FloatingViewCoordinator @Inject constructor( } } - private fun startScreenCircling() = checkNextState(State.ScreenCircling::class) { + private fun startScreenCircling() = checkNextState(NavState.ScreenCircling::class) { if (!Translator.getTranslator().checkResources(scope)) { return@checkNextState } logger.debug("startScreenCircling()") - stateNavigator.updateState(State.ScreenCircling) + stateNavigator.updateState(NavState.ScreenCircling) FirebaseEvent.logStartAreaSelection() screenCirclingView.attachToScreen() arrangeMainBarToTop() } private fun onAreaSelected(parentRect: Rect, selectedRect: Rect) = - checkNextState(State.ScreenCircled::class) { + checkNextState(NavState.ScreenCircled::class) { logger.debug( "onAreaSelected(), parentRect: $parentRect, " + "selectedRect: $selectedRect, " + "size: ${selectedRect.width()}x${selectedRect.height()}" ) - if (currentState != State.ScreenCircled) { - stateNavigator.updateState(State.ScreenCircled) + if (currentNavState != NavState.ScreenCircled) { + stateNavigator.updateState(NavState.ScreenCircled) } this@FloatingViewCoordinator.selectedRect = selectedRect this@FloatingViewCoordinator.parentRect = parentRect } - private fun cancelScreenCircling() = checkNextState(State.Idle::class) { + private fun cancelScreenCircling() = checkNextState(NavState.Idle::class) { logger.debug("cancelScreenCircling()") - stateNavigator.updateState(State.Idle) + stateNavigator.updateState(NavState.Idle) screenCirclingView.detachFromScreen() } private fun startScreenCapturing(selectedOCRLang: String) = - checkNextState(State.ScreenCapturing::class) { + checkNextState(NavState.ScreenCapturing::class) { if (!Translator.getTranslator().checkResources(scope)) { return@checkNextState } @@ -188,7 +188,7 @@ class FloatingViewCoordinator @Inject constructor( val parent = parentRect ?: return@checkNextState val selected = selectedRect ?: return@checkNextState logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") - stateNavigator.updateState(State.ScreenCapturing) + stateNavigator.updateState(NavState.ScreenCapturing) mainBar.detachFromScreen() screenCirclingView.detachFromScreen() @@ -222,8 +222,8 @@ class FloatingViewCoordinator @Inject constructor( } private fun startRecognition(croppedBitmap: Bitmap, parent: Rect, selected: Rect) = - checkNextState(State.TextRecognizing::class) { - stateNavigator.updateState(State.TextRecognizing) + checkNextState(NavState.TextRecognizing::class) { + stateNavigator.updateState(NavState.TextRecognizing) try { resultView.startRecognition() val recognizer = TextRecognizer.getRecognizer(selectedOCRProvider) @@ -265,9 +265,9 @@ class FloatingViewCoordinator @Inject constructor( } private fun startTranslation(recognitionResult: RecognitionResult) = - checkNextState(State.TextTranslating::class) { + checkNextState(NavState.TextTranslating::class) { try { - stateNavigator.updateState(State.TextTranslating) + stateNavigator.updateState(NavState.TextTranslating) val translator = Translator.getTranslator() @@ -350,29 +350,29 @@ class FloatingViewCoordinator @Inject constructor( } private fun showResult(result: Result) = - checkNextState(State.ResultDisplaying::class) { + checkNextState(NavState.ResultDisplaying::class) { logger.debug("showResult(), $result") - stateNavigator.updateState(State.ResultDisplaying) + stateNavigator.updateState(NavState.ResultDisplaying) resultView.textTranslated(result) } - private fun showError(error: String) = checkNextState(State.ErrorDisplaying::class) { - stateNavigator.updateState(State.ErrorDisplaying(error)) + private fun showError(error: String) = checkNextState(NavState.ErrorDisplaying::class) { + stateNavigator.updateState(NavState.ErrorDisplaying(error)) logger.error(error) context.showErrorDialog(error) backToIdle() } - private fun backToIdle() = checkNextState(State.Idle::class) { - if (currentState != State.Idle) stateNavigator.updateState(State.Idle) + private fun backToIdle() = checkNextState(NavState.Idle::class) { + if (currentNavState != NavState.Idle) stateNavigator.updateState(NavState.Idle) croppedBitmap?.setReusable() resultView.backToIdle() showMainBar() } private fun checkNextState( - vararg nextStates: KClass, + vararg nextStates: KClass, block: suspend CoroutineScope.() -> Unit, ) { val notAllowed = nextStates.filterNot { stateNavigator.allowedNextState(it) } @@ -380,7 +380,7 @@ class FloatingViewCoordinator @Inject constructor( if (notAllowed.isEmpty()) { scope.launch { block.invoke(this) } } else { - val error = "Transit from $notAllowed to $currentState is not allowed" + val error = "Transit from $notAllowed to $currentNavState is not allowed" logger.error(t = IllegalStateException(error)) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 7f8017a9..9695fb49 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -15,12 +15,12 @@ import kotlin.reflect.KClass interface StateNavigator { val navigationAction: SharedFlow - val currentState: StateFlow + val currentNavState: StateFlow suspend fun navigate(action: NavigationAction) - fun allowedNextState(nextState: KClass): Boolean + fun allowedNextState(nextNavState: KClass): Boolean - fun updateState(newState: State) + fun updateState(newNavState: NavState) } @Singleton @@ -29,23 +29,23 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { override val navigationAction = MutableSharedFlow() - override val currentState = MutableStateFlow(State.Idle) + override val currentNavState = MutableStateFlow(NavState.Idle) - private val nextStates: Map, Set>> = mapOf( - State.Idle::class to setOf(State.ScreenCircling::class), - State.ScreenCircling::class to setOf(State.Idle::class, State.ScreenCircled::class), - State.ScreenCircled::class to setOf(State.Idle::class, State.ScreenCapturing::class), - State.ScreenCapturing::class to setOf( - State.Idle::class, State.TextRecognizing::class, State.ErrorDisplaying::class, + private val nextStates: Map, Set>> = mapOf( + NavState.Idle::class to setOf(NavState.ScreenCircling::class), + NavState.ScreenCircling::class to setOf(NavState.Idle::class, NavState.ScreenCircled::class), + NavState.ScreenCircled::class to setOf(NavState.Idle::class, NavState.ScreenCapturing::class), + NavState.ScreenCapturing::class to setOf( + NavState.Idle::class, NavState.TextRecognizing::class, NavState.ErrorDisplaying::class, ), - State.TextRecognizing::class to setOf( - State.Idle::class, State.TextTranslating::class, State.ErrorDisplaying::class, + NavState.TextRecognizing::class to setOf( + NavState.Idle::class, NavState.TextTranslating::class, NavState.ErrorDisplaying::class, ), - State.TextTranslating::class to setOf( - State.ResultDisplaying::class, State.ErrorDisplaying::class, State.Idle::class, + NavState.TextTranslating::class to setOf( + NavState.ResultDisplaying::class, NavState.ErrorDisplaying::class, NavState.Idle::class, ), - State.ResultDisplaying::class to setOf(State.Idle::class, State.TextTranslating::class), - State.ErrorDisplaying::class to setOf(State.Idle::class), + NavState.ResultDisplaying::class to setOf(NavState.Idle::class, NavState.TextTranslating::class), + NavState.ErrorDisplaying::class to setOf(NavState.Idle::class), ) override suspend fun navigate(action: NavigationAction) { @@ -54,17 +54,17 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { navigationAction.emit(action) } - override fun allowedNextState(nextState: KClass): Boolean = - nextStates[currentState.value::class]?.contains(nextState) == true + override fun allowedNextState(nextNavState: KClass): Boolean = + nextStates[currentNavState.value::class]?.contains(nextNavState) == true - override fun updateState(newState: State) { - val allowedNextStates = nextStates[currentState.value::class] + override fun updateState(newNavState: NavState) { + val allowedNextStates = nextStates[currentNavState.value::class] - if (allowedNextStates?.contains(newState::class) == true) { - logger.debug("Change state ${currentState.value} > $newState") - currentState.value = newState + if (allowedNextStates?.contains(newNavState::class) == true) { + logger.debug("Change state ${currentNavState.value} > $newNavState") + currentNavState.value = newNavState } else { - logger.error("Change state from ${currentState.value} to $newState is not allowed") + logger.error("Change state from ${currentNavState.value} to $newNavState is not allowed") } } } @@ -104,17 +104,17 @@ sealed interface NavigationAction { ) : NavigationAction } -sealed class State { +sealed class NavState { override fun toString(): String { return this::class.simpleName ?: super.toString() } - object Idle : State() - object ScreenCircling : State() - object ScreenCircled : State() - object ScreenCapturing : State() - object TextRecognizing : State() - object TextTranslating : State() - object ResultDisplaying : State() - data class ErrorDisplaying(val error: String) : State() + object Idle : NavState() + object ScreenCircling : NavState() + object ScreenCircled : NavState() + object ScreenCapturing : NavState() + object TextRecognizing : NavState() + object TextTranslating : NavState() + object ResultDisplaying : NavState() + data class ErrorDisplaying(val error: String) : NavState() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt index 0eb107aa..bc3e6e69 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt @@ -13,7 +13,7 @@ import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.Result import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.hilt.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.repo.GeneralRepository diff --git a/main/src/main/java/tw/firemaples/onscreenocr/pref/AppPref.kt b/main/src/main/java/tw/firemaples/onscreenocr/pref/AppPref.kt index b3a02c2f..210ce1c0 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/pref/AppPref.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/pref/AppPref.kt @@ -17,7 +17,7 @@ object AppPref : KotprefModel() { Kotpref.gson = Gson() } - private var selectedOCRProviderKey by stringPref( + var selectedOCRProviderKey by stringPref( default = Constants.DEFAULT_OCR_PROVIDER.key ) var selectedOCRProvider: TextRecognitionProviderType From 81dcd1327796613ecc6c1a5db7953c4b25200489 Mon Sep 17 00:00:00 2001 From: firemaples Date: Fri, 5 Jan 2024 22:42:58 +0900 Subject: [PATCH 006/121] Save last main bar position --- .../data/repo/PreferenceRepository.kt | 14 ++++++++++++++ .../onscreenocr/data/repo/SettingRepository.kt | 9 +++++++++ .../usecase/GetMainBarInitialPositionUseCase.kt | 16 ++++++++++++++++ .../usecase/SaveLastMainBarPositionUseCase.kt | 11 +++++++++++ .../floatings/compose/mainbar/MainBarContent.kt | 2 ++ .../compose/mainbar/MainBarFloatingView.kt | 5 +---- .../compose/mainbar/MainBarViewModel.kt | 14 +++++++++++++- 7 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetMainBarInitialPositionUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt new file mode 100644 index 00000000..3aa6f8b8 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt @@ -0,0 +1,14 @@ +package tw.firemaples.onscreenocr.data.repo + +import android.graphics.Point +import tw.firemaples.onscreenocr.pref.AppPref +import javax.inject.Inject + +class PreferenceRepository @Inject constructor() { + fun saveLastMainBarPosition(x: Int, y: Int) { + AppPref.lastMainBarPosition = Point(x, y) + } + + fun getLastMainBarPosition(): Point = + AppPref.lastMainBarPosition +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt new file mode 100644 index 00000000..2b7d0f21 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt @@ -0,0 +1,9 @@ +package tw.firemaples.onscreenocr.data.repo + +import tw.firemaples.onscreenocr.pages.setting.SettingManager +import javax.inject.Inject + +class SettingRepository @Inject constructor() { + fun shouldRestoreMainBarPosition(): Boolean = + SettingManager.restoreMainBarPosition +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetMainBarInitialPositionUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetMainBarInitialPositionUseCase.kt new file mode 100644 index 00000000..4ce09432 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetMainBarInitialPositionUseCase.kt @@ -0,0 +1,16 @@ +package tw.firemaples.onscreenocr.data.usecase + +import android.graphics.Point +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import tw.firemaples.onscreenocr.data.repo.SettingRepository +import javax.inject.Inject + +class GetMainBarInitialPositionUseCase @Inject constructor( + private val settingRepository: SettingRepository, + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke() = + if (settingRepository.shouldRestoreMainBarPosition()) + preferenceRepository.getLastMainBarPosition() + else Point(0, 0) +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt new file mode 100644 index 00000000..de439f52 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt @@ -0,0 +1,11 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class SaveLastMainBarPositionUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke(x: Int, y: Int) = + preferenceRepository.saveLastMainBarPosition(x = x, y = y) +} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 78ff0a13..7ff33551 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -1,6 +1,7 @@ package tw.firemaples.onscreenocr.floatings.compose.mainbar import android.content.res.Configuration +import android.graphics.Point import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -210,6 +211,7 @@ private fun MainBarContentPreview( override val action: SharedFlow get() = MutableSharedFlow() + override fun getInitialPosition(): Point = Point() override fun onMenuItemClicked(key: String) = Unit override fun onSelectClicked() = Unit override fun onTranslateClicked() = Unit diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index 462390a9..b6125948 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -13,7 +13,6 @@ import tw.firemaples.onscreenocr.floatings.menu.MenuView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.pages.setting.SettingManager -import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.utils.clickOnce import javax.inject.Inject @@ -27,9 +26,7 @@ class MainBarFloatingView @Inject constructor( // get() = R.layout.floating_main_bar override val initialPosition: Point - get() = - if (SettingManager.restoreMainBarPosition) AppPref.lastMainBarPosition - else Point(0, 0) + get() = viewModel.getInitialPosition() @Composable override fun RootContent() { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 385ac14c..77f20db7 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -1,5 +1,6 @@ package tw.firemaples.onscreenocr.floatings.compose.mainbar +import android.graphics.Point import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -14,6 +15,8 @@ import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.data.usecase.GetCurrentOCRDisplayLangCodeUseCase import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslatorTypeUseCase +import tw.firemaples.onscreenocr.data.usecase.GetMainBarInitialPositionUseCase +import tw.firemaples.onscreenocr.data.usecase.SaveLastMainBarPositionUseCase import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.StateNavigator @@ -24,6 +27,7 @@ import javax.inject.Inject interface MainBarViewModel { val state: StateFlow val action: SharedFlow + fun getInitialPosition(): Point fun onMenuItemClicked(key: String) fun onSelectClicked() fun onTranslateClicked() @@ -46,6 +50,7 @@ sealed interface MainBarAction { data object RescheduleFadeOut : MainBarAction } +@Suppress("LongParameterList", "TooManyFunctions") class MainBarViewModelImpl @Inject constructor( @MainImmediateCoroutineScope private val scope: CoroutineScope, @@ -53,6 +58,8 @@ class MainBarViewModelImpl @Inject constructor( private val getCurrentOCRDisplayLangCodeUseCase: GetCurrentOCRDisplayLangCodeUseCase, private val getCurrentTranslatorTypeUseCase: GetCurrentTranslatorTypeUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, + private val saveLastMainBarPositionUseCase: SaveLastMainBarPositionUseCase, + private val getMainBarInitialPositionUseCase: GetMainBarInitialPositionUseCase, ) : MainBarViewModel { override val state = MutableStateFlow(MainBarState()) override val action = MutableSharedFlow() @@ -130,6 +137,9 @@ class MainBarViewModelImpl @Inject constructor( } } + override fun getInitialPosition(): Point = + getMainBarInitialPositionUseCase.invoke() + override fun onMenuItemClicked(key: String) { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) @@ -167,7 +177,9 @@ class MainBarViewModelImpl @Inject constructor( } override fun saveLastPosition(x: Int, y: Int) { - + scope.launch { + saveLastMainBarPositionUseCase.invoke(x = x, y = y) + } } override fun onLanguageBlockClicked() { From 9ab6365455b52c64c3f1b5d20eea01fefde02418 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 6 Jan 2024 22:26:37 +0900 Subject: [PATCH 007/121] Open language selection panel --- .../floatings/compose/mainbar/MainBarFloatingView.kt | 6 ++++++ .../floatings/compose/mainbar/MainBarViewModel.kt | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index b6125948..60db5c24 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -34,6 +34,12 @@ class MainBarFloatingView @Inject constructor( when (state) { MainBarAction.RescheduleFadeOut -> rescheduleFadeOut() + + MainBarAction.OpenLanguageSelectionPanel -> { + rescheduleFadeOut() + // TODO wait to be refactored + TranslationSelectPanel(context).attachToScreen() + } } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 77f20db7..7cc08dba 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -48,6 +48,7 @@ data class MainBarState( sealed interface MainBarAction { data object RescheduleFadeOut : MainBarAction + data object OpenLanguageSelectionPanel : MainBarAction } @Suppress("LongParameterList", "TooManyFunctions") @@ -184,7 +185,7 @@ class MainBarViewModelImpl @Inject constructor( override fun onLanguageBlockClicked() { scope.launch { - action.emit(MainBarAction.RescheduleFadeOut) + action.emit(MainBarAction.OpenLanguageSelectionPanel) } } } From 7191b70fcb0cb8c20b3499fedb4b052187d6bc75 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 6 Jan 2024 23:27:17 +0900 Subject: [PATCH 008/121] Implement MainBarMenu --- .../floatings/compose/base/ComposeUtils.kt | 6 ++ .../compose/mainbar/MainBarContent.kt | 7 ++- .../compose/mainbar/MainBarFloatingView.kt | 47 ++++++++++++---- .../floatings/compose/mainbar/MainBarMenu.kt | 42 ++++++++++++++ .../compose/mainbar/MainBarViewModel.kt | 55 ++++++++++++++++++- .../floatings/manager/StateNavigator.kt | 20 +++++-- 6 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt index 75245c2f..087bbe14 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt @@ -6,6 +6,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first @Composable fun Flow.collectOnLifecycleResumed(state: (T) -> Unit) { @@ -16,3 +18,7 @@ fun Flow.collectOnLifecycleResumed(state: (T) -> Unit) { } } } + +suspend fun MutableSharedFlow.awaitForSubscriber() { + subscriptionCount.first { it > 0 } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 7ff33551..3704eb63 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -96,6 +96,10 @@ fun MainBarContent( onDragCancel = onDragCancel, onDrag = onDrag, ) + MainBarMenu( + expanded = state.displayMainBarMenu, + onMenuOptionSelected = viewModel::onMenuOptionSelected, + ) } } } @@ -218,8 +222,9 @@ private fun MainBarContentPreview( override fun onCloseClicked() = Unit override fun onMenuButtonClicked() = Unit override fun onAttachedToScreen() = Unit - override fun saveLastPosition(x: Int, y: Int) = Unit + override fun onDragEnd(x: Int, y: Int) = Unit override fun onLanguageBlockClicked() = Unit + override fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) = Unit } AppTheme { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index 60db5c24..1d69ba84 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -5,14 +5,19 @@ import android.graphics.Point import androidx.compose.runtime.Composable import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding +import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.floatings.compose.base.ComposeMovableFloatingView import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed +import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.floatings.menu.MenuView +import tw.firemaples.onscreenocr.floatings.readme.ReadmeView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel import tw.firemaples.onscreenocr.log.FirebaseEvent +import tw.firemaples.onscreenocr.pages.setting.SettingActivity import tw.firemaples.onscreenocr.pages.setting.SettingManager +import tw.firemaples.onscreenocr.utils.Utils import tw.firemaples.onscreenocr.utils.clickOnce import javax.inject.Inject @@ -30,8 +35,8 @@ class MainBarFloatingView @Inject constructor( @Composable override fun RootContent() { - viewModel.action.collectOnLifecycleResumed { state -> - when (state) { + viewModel.action.collectOnLifecycleResumed { action -> + when (action) { MainBarAction.RescheduleFadeOut -> rescheduleFadeOut() @@ -40,13 +45,40 @@ class MainBarFloatingView @Inject constructor( // TODO wait to be refactored TranslationSelectPanel(context).attachToScreen() } + + is MainBarAction.OpenBrowser -> + // TODO wait to be refactored + Utils.openBrowser(action.url) + + MainBarAction.OpenReadme -> + // TODO wait to be refactored + ReadmeView(context).attachToScreen() + + MainBarAction.OpenSettings -> + // TODO wait to be refactored + SettingActivity.start(context) + + MainBarAction.OpenVersionHistory -> + // TODO wait to be refactored + VersionHistoryView(context).attachToScreen() + + MainBarAction.HideMainBar -> + // TODO wait to be refactored + ViewHolderService.hideViews(context) + + MainBarAction.ExitApp -> + // TODO wait to be refactored + ViewHolderService.exit(context) } } MainBarContent( viewModel = viewModel, onDragStart = onDragStart, - onDragEnd = onDragEnd, + onDragEnd = { + onDragEnd.invoke() + viewModel.onDragEnd(params.x, params.y) + }, onDragCancel = onDragCancel, onDrag = onDrag, ) @@ -171,13 +203,8 @@ class MainBarFloatingView @Inject constructor( // } } - override fun onAttachedToScreen() { - super.onAttachedToScreen() + override fun attachToScreen() { + super.attachToScreen() viewModel.onAttachedToScreen() } - - override fun onDetachedFromScreen() { - super.onDetachedFromScreen() - viewModel.saveLastPosition(params.x, params.y) - } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt new file mode 100644 index 00000000..061654fd --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt @@ -0,0 +1,42 @@ +package tw.firemaples.onscreenocr.floatings.compose.mainbar + +import androidx.annotation.StringRes +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import tw.firemaples.onscreenocr.R + +@Composable +fun MainBarMenu( + expanded: Boolean, + onMenuOptionSelected: (MainBarMenuOption?) -> Unit, +) { + DropdownMenu( + expanded = expanded, + onDismissRequest = { onMenuOptionSelected.invoke(null) }, + ) { + MainBarMenuOption.entries.forEach { option -> + DropdownMenuItem( + text = { + Text(text = stringResource(id = option.text)) + }, + onClick = { onMenuOptionSelected.invoke(option) } + ) + } + } +} + +enum class MainBarMenuOption( + @StringRes + val text: Int, +) { + SETTING(R.string.menu_setting), + PRIVACY_POLICY(R.string.menu_privacy_policy), + ABOUT(R.string.menu_about), + VERSION_HISTORY(R.string.menu_version_history), + README(R.string.menu_readme), + HIDE(R.string.menu_hide), + EXIT(R.string.menu_exit), +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 7cc08dba..4998134f 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -18,8 +18,10 @@ import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslatorTypeUseCase import tw.firemaples.onscreenocr.data.usecase.GetMainBarInitialPositionUseCase import tw.firemaples.onscreenocr.data.usecase.SaveLastMainBarPositionUseCase import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject @@ -34,8 +36,9 @@ interface MainBarViewModel { fun onCloseClicked() fun onMenuButtonClicked() fun onAttachedToScreen() - fun saveLastPosition(x: Int, y: Int) + fun onDragEnd(x: Int, y: Int) fun onLanguageBlockClicked() + fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) } data class MainBarState( @@ -44,11 +47,18 @@ data class MainBarState( val displaySelectButton: Boolean = false, val displayTranslateButton: Boolean = false, val displayCloseButton: Boolean = false, + val displayMainBarMenu: Boolean = false, ) sealed interface MainBarAction { data object RescheduleFadeOut : MainBarAction data object OpenLanguageSelectionPanel : MainBarAction + data object OpenSettings : MainBarAction + data class OpenBrowser(val url: String) : MainBarAction + data object OpenVersionHistory : MainBarAction + data object OpenReadme : MainBarAction + data object HideMainBar : MainBarAction + data object ExitApp : MainBarAction } @Suppress("LongParameterList", "TooManyFunctions") @@ -168,16 +178,22 @@ class MainBarViewModelImpl @Inject constructor( override fun onMenuButtonClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) + state.update { + it.copy( + displayMainBarMenu = true, + ) + } } } override fun onAttachedToScreen() { scope.launch { + action.awaitForSubscriber() action.emit(MainBarAction.RescheduleFadeOut) } } - override fun saveLastPosition(x: Int, y: Int) { + override fun onDragEnd(x: Int, y: Int) { scope.launch { saveLastMainBarPositionUseCase.invoke(x = x, y = y) } @@ -188,4 +204,39 @@ class MainBarViewModelImpl @Inject constructor( action.emit(MainBarAction.OpenLanguageSelectionPanel) } } + + override fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) { + scope.launch { + state.update { + it.copy( + displayMainBarMenu = false, + ) + } + + when (mainBarMenuOption) { + MainBarMenuOption.SETTING -> + action.emit(MainBarAction.OpenSettings) + + MainBarMenuOption.PRIVACY_POLICY -> + action.emit(MainBarAction.OpenBrowser(RemoteConfigManager.privacyPolicyUrl)) + + MainBarMenuOption.ABOUT -> + action.emit(MainBarAction.OpenBrowser(RemoteConfigManager.aboutUrl)) + + MainBarMenuOption.VERSION_HISTORY -> + action.emit(MainBarAction.OpenVersionHistory) + + MainBarMenuOption.README -> + action.emit(MainBarAction.OpenReadme) + + MainBarMenuOption.HIDE -> + action.emit(MainBarAction.HideMainBar) + + MainBarMenuOption.EXIT -> + action.emit(MainBarAction.ExitApp) + + null -> {} + } + } + } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 9695fb49..1508be4b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first +import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject @@ -32,9 +32,15 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { override val currentNavState = MutableStateFlow(NavState.Idle) private val nextStates: Map, Set>> = mapOf( - NavState.Idle::class to setOf(NavState.ScreenCircling::class), - NavState.ScreenCircling::class to setOf(NavState.Idle::class, NavState.ScreenCircled::class), - NavState.ScreenCircled::class to setOf(NavState.Idle::class, NavState.ScreenCapturing::class), + NavState.Idle::class to setOf( + NavState.Idle::class, NavState.ScreenCircling::class, + ), + NavState.ScreenCircling::class to setOf( + NavState.Idle::class, NavState.ScreenCircled::class, + ), + NavState.ScreenCircled::class to setOf( + NavState.Idle::class, NavState.ScreenCapturing::class, + ), NavState.ScreenCapturing::class to setOf( NavState.Idle::class, NavState.TextRecognizing::class, NavState.ErrorDisplaying::class, ), @@ -44,13 +50,15 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { NavState.TextTranslating::class to setOf( NavState.ResultDisplaying::class, NavState.ErrorDisplaying::class, NavState.Idle::class, ), - NavState.ResultDisplaying::class to setOf(NavState.Idle::class, NavState.TextTranslating::class), + NavState.ResultDisplaying::class to setOf( + NavState.Idle::class, NavState.TextTranslating::class, + ), NavState.ErrorDisplaying::class to setOf(NavState.Idle::class), ) override suspend fun navigate(action: NavigationAction) { logger.debug("Receive NavigationAction: $action") - navigationAction.subscriptionCount.first { it > 0 } + navigationAction.awaitForSubscriber() navigationAction.emit(action) } From f1d2f675ee162aacbeb1ac3573f8f27b4724feb4 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 6 Jan 2024 23:38:21 +0900 Subject: [PATCH 009/121] Implement MainBar button actions --- .../floatings/compose/mainbar/MainBarViewModel.kt | 6 +++++- .../onscreenocr/floatings/main/MainBarViewModel.kt | 2 +- .../floatings/manager/FloatingViewCoordinator.kt | 6 +++--- .../onscreenocr/floatings/manager/StateNavigator.kt | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 4998134f..6f0091ca 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -20,6 +20,7 @@ import tw.firemaples.onscreenocr.data.usecase.SaveLastMainBarPositionUseCase import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.floatings.manager.NavState +import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.translator.TranslationProviderType @@ -65,7 +66,7 @@ sealed interface MainBarAction { class MainBarViewModelImpl @Inject constructor( @MainImmediateCoroutineScope private val scope: CoroutineScope, - stateNavigator: StateNavigator, + private val stateNavigator: StateNavigator, private val getCurrentOCRDisplayLangCodeUseCase: GetCurrentOCRDisplayLangCodeUseCase, private val getCurrentTranslatorTypeUseCase: GetCurrentTranslatorTypeUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, @@ -160,18 +161,21 @@ class MainBarViewModelImpl @Inject constructor( override fun onSelectClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) + stateNavigator.navigate(NavigationAction.NavigateToScreenCircling) } } override fun onTranslateClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) + stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing) } } override fun onCloseClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) + stateNavigator.navigate(NavigationAction.CancelScreenCircling) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt index 2e1a990b..d02cd5f4 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt @@ -249,7 +249,7 @@ class MainBarViewModel @Inject constructor( fun onTranslateClicked() { viewScope.launch { - stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing(selectedOCRLang)) + stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index bd0a5084..219fbeac 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -88,7 +88,7 @@ class FloatingViewCoordinator @Inject constructor( is NavigationAction.NavigateToScreenCapturing -> - startScreenCapturing(it.selectedOCRLang) + startScreenCapturing() is NavigationAction.NavigateToTextRecognition -> startRecognition( @@ -178,13 +178,13 @@ class FloatingViewCoordinator @Inject constructor( screenCirclingView.detachFromScreen() } - private fun startScreenCapturing(selectedOCRLang: String) = + private fun startScreenCapturing() = checkNextState(NavState.ScreenCapturing::class) { if (!Translator.getTranslator().checkResources(scope)) { return@checkNextState } - this@FloatingViewCoordinator.selectedOCRLang = selectedOCRLang + this@FloatingViewCoordinator.selectedOCRLang = AppPref.selectedOCRLang val parent = parentRect ?: return@checkNextState val selected = selectedRect ?: return@checkNextState logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 1508be4b..453cf304 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -89,9 +89,7 @@ sealed interface NavigationAction { data object CancelScreenCircling : NavigationAction - data class NavigateToScreenCapturing( - val selectedOCRLang: String, - ) : NavigationAction + data object NavigateToScreenCapturing : NavigationAction data class NavigateToTextRecognition( val croppedBitmap: Bitmap, From dcb67ddf0112ee06b9e3ef7d284bc1e75d8033a4 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 6 Jan 2024 23:54:07 +0900 Subject: [PATCH 010/121] Finish MainBar --- .../compose/mainbar/MainBarContent.kt | 3 + .../compose/mainbar/MainBarFloatingView.kt | 126 +----------------- .../compose/mainbar/MainBarViewModel.kt | 24 +++- 3 files changed, 31 insertions(+), 122 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 3704eb63..d6892ff4 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -216,6 +216,9 @@ private fun MainBarContentPreview( get() = MutableSharedFlow() override fun getInitialPosition(): Point = Point() + override fun getFadeOutAfterMoved(): Boolean = false + override fun getFadeOutDelay(): Long = 0L + override fun getFadeOutDestinationAlpha(): Float = 0f override fun onMenuItemClicked(key: String) = Unit override fun onSelectClicked() = Unit override fun onTranslateClicked() = Unit diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index 1d69ba84..70a267f1 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -4,32 +4,21 @@ import android.content.Context import android.graphics.Point import androidx.compose.runtime.Composable import dagger.hilt.android.qualifiers.ApplicationContext -import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.floatings.compose.base.ComposeMovableFloatingView import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView -import tw.firemaples.onscreenocr.floatings.manager.NavState -import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.floatings.menu.MenuView import tw.firemaples.onscreenocr.floatings.readme.ReadmeView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel -import tw.firemaples.onscreenocr.log.FirebaseEvent import tw.firemaples.onscreenocr.pages.setting.SettingActivity -import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.utils.Utils -import tw.firemaples.onscreenocr.utils.clickOnce import javax.inject.Inject class MainBarFloatingView @Inject constructor( @ApplicationContext context: Context, - private val stateNavigator: StateNavigator, private val viewModel: MainBarViewModel, ) : ComposeMovableFloatingView(context) { -// override val layoutId: Int -// get() = R.layout.floating_main_bar - override val initialPosition: Point get() = viewModel.getInitialPosition() @@ -40,6 +29,9 @@ class MainBarFloatingView @Inject constructor( MainBarAction.RescheduleFadeOut -> rescheduleFadeOut() + MainBarAction.MoveToEdgeIfEnabled -> + moveToEdgeIfEnabled() + MainBarAction.OpenLanguageSelectionPanel -> { rescheduleFadeOut() // TODO wait to be refactored @@ -91,117 +83,11 @@ class MainBarFloatingView @Inject constructor( get() = true override val fadeOutAfterMoved: Boolean - get() = !arrayOf(NavState.ScreenCircling, NavState.ScreenCircled) - .contains(stateNavigator.currentNavState.value) - && !menuView.attached - && SettingManager.enableFadingOutWhileIdle + get() = viewModel.getFadeOutAfterMoved() override val fadeOutDelay: Long - get() = SettingManager.timeoutToFadeOut + get() = viewModel.getFadeOutDelay() override val fadeOutDestinationAlpha: Float - get() = SettingManager.opaquePercentageToFadeOut - -// private val binding: FloatingMainBarBinding = FloatingMainBarBinding.bind(rootLayout) - - private val menuView: MenuView by lazy { - MenuView(context, false).apply { -// setAnchor(binding.btMenu) - - onAttached = { rescheduleFadeOut() } - onDetached = { rescheduleFadeOut() } - onItemSelected = { view, key -> - view.detachFromScreen() - viewModel.onMenuItemClicked(key) - rescheduleFadeOut() - } - } - } - - init { -// binding.setViews() -// setDragView(binding.btMenu) - } - - private fun FloatingMainBarBinding.setViews() { - btLangSelector.clickOnce { - rescheduleFadeOut() - TranslationSelectPanel(context).attachToScreen() - } - - btSelect.clickOnce { - viewModel.onSelectClicked() - } - - btTranslate.clickOnce { - FirebaseEvent.logClickTranslationStartButton() - viewModel.onTranslateClicked() - } - - btClose.clickOnce { - viewModel.onCloseClicked() - } - - btMenu.clickOnce { - viewModel.onMenuButtonClicked() - } - -// viewModel.languageText.observe(lifecycleOwner) { -// tvLang.text = it -// moveToEdgeIfEnabled() -// } -// -// viewModel.displayTranslatorIcon.observe(lifecycleOwner) { -// if (it == null) { -// ivGoogleTranslator.setImageDrawable(null) -// ivGoogleTranslator.hide() -// } else { -// ivGoogleTranslator.setImageResource(it) -// ivGoogleTranslator.show() -// } -// moveToEdgeIfEnabled() -// } -// -// viewModel.displaySelectButton.observe(lifecycleOwner) { -// btSelect.showOrHide(it) -// moveToEdgeIfEnabled() -// } -// -// viewModel.displayTranslateButton.observe(lifecycleOwner) { -// btTranslate.showOrHide(it) -// moveToEdgeIfEnabled() -// } -// -// viewModel.displayCloseButton.observe(lifecycleOwner) { -// btClose.showOrHide(it) -// moveToEdgeIfEnabled() -// } -// -// viewModel.displayMenuItems.observe(lifecycleOwner) { -// with(menuView) { -// updateData(it) -// attachToScreen() -// } -// } -// -// viewModel.rescheduleFadeOut.observe(lifecycleOwner) { -// rescheduleFadeOut() -// } -// -// viewModel.showSettingPage.observe(lifecycleOwner) { -// SettingActivity.start(context) -// } -// -// viewModel.openBrowser.observe(lifecycleOwner) { -// Utils.openBrowser(it) -// } -// -// viewModel.showVersionHistory.observe(lifecycleOwner) { -// VersionHistoryView(context).attachToScreen() -// } -// -// viewModel.showReadme.observe(lifecycleOwner) { -// ReadmeView(context).attachToScreen() -// } - } + get() = viewModel.getFadeOutDestinationAlpha() override fun attachToScreen() { super.attachToScreen() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 6f0091ca..aa1ccfcf 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -22,6 +22,7 @@ import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.utils.Logger @@ -31,6 +32,9 @@ interface MainBarViewModel { val state: StateFlow val action: SharedFlow fun getInitialPosition(): Point + fun getFadeOutAfterMoved(): Boolean + fun getFadeOutDelay(): Long + fun getFadeOutDestinationAlpha(): Float fun onMenuItemClicked(key: String) fun onSelectClicked() fun onTranslateClicked() @@ -53,6 +57,7 @@ data class MainBarState( sealed interface MainBarAction { data object RescheduleFadeOut : MainBarAction + data object MoveToEdgeIfEnabled : MainBarAction data object OpenLanguageSelectionPanel : MainBarAction data object OpenSettings : MainBarAction data class OpenBrowser(val url: String) : MainBarAction @@ -94,7 +99,7 @@ class MainBarViewModelImpl @Inject constructor( navState == NavState.ScreenCircling || navState == NavState.ScreenCircled, ) } - action.emit(MainBarAction.RescheduleFadeOut) + action.emit(MainBarAction.MoveToEdgeIfEnabled) } private fun subscribeLanguageStateChanges() { @@ -111,7 +116,7 @@ class MainBarViewModelImpl @Inject constructor( }.launchIn(scope) } - private fun updateLanguageStates( + private suspend fun updateLanguageStates( ocrLang: String, translationProviderType: TranslationProviderType, translationLang: String, @@ -147,11 +152,26 @@ class MainBarViewModelImpl @Inject constructor( translatorIcon = icon, ) } + action.emit(MainBarAction.MoveToEdgeIfEnabled) } override fun getInitialPosition(): Point = getMainBarInitialPositionUseCase.invoke() + override fun getFadeOutAfterMoved(): Boolean { + val navState = stateNavigator.currentNavState.value + + return navState != NavState.ScreenCircling && navState != NavState.ScreenCircled + && !state.value.displayMainBarMenu + && SettingManager.enableFadingOutWhileIdle //TODO move logic + } + + override fun getFadeOutDelay(): Long = + SettingManager.timeoutToFadeOut //TODO move logic + + override fun getFadeOutDestinationAlpha(): Float = + SettingManager.opaquePercentageToFadeOut //TODO move logic + override fun onMenuItemClicked(key: String) { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) From d93af1070442f60206770c9811435a9dc77f8537 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 7 Jan 2024 00:26:42 +0900 Subject: [PATCH 011/121] Fix last circled scope not saved --- .../onscreenocr/floatings/manager/FloatingViewCoordinator.kt | 1 - .../firemaples/onscreenocr/floatings/manager/StateNavigator.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index 219fbeac..1d1d0c3b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -86,7 +86,6 @@ class FloatingViewCoordinator @Inject constructor( selectedRect = it.selectedRect, ) - is NavigationAction.NavigateToScreenCapturing -> startScreenCapturing() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 453cf304..dcb28e51 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -39,7 +39,7 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { NavState.Idle::class, NavState.ScreenCircled::class, ), NavState.ScreenCircled::class to setOf( - NavState.Idle::class, NavState.ScreenCapturing::class, + NavState.Idle::class, NavState.ScreenCapturing::class, NavState.ScreenCircled::class, ), NavState.ScreenCapturing::class to setOf( NavState.Idle::class, NavState.TextRecognizing::class, NavState.ErrorDisplaying::class, From 03a07c1e64b6b36a635637ae8825bfae56e4fbb6 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 7 Jan 2024 04:40:26 +0900 Subject: [PATCH 012/121] Move state operations out from FloatingViewCoordinator --- .../data/usecase/GetCurrentOCRLangUseCase.kt | 15 + .../di/CoroutinesDispatchersModule.kt | 17 +- .../onscreenocr/di/CoroutinesQualifiers.kt | 6 +- .../onscreenocr/di/SingletonModule.kt | 5 + .../compose/mainbar/MainBarViewModel.kt | 17 +- .../onscreenocr/floatings/main/MainBar.kt | 336 +++++------ .../floatings/main/MainBarViewModel.kt | 522 +++++++++--------- .../manager/FloatingViewCoordinator.kt | 377 ++----------- .../floatings/manager/StateNavigator.kt | 40 +- .../floatings/manager/StateOperator.kt | 442 +++++++++++++++ 10 files changed, 999 insertions(+), 778 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRLangUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRLangUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRLangUseCase.kt new file mode 100644 index 00000000..a35723b8 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRLangUseCase.kt @@ -0,0 +1,15 @@ +package tw.firemaples.onscreenocr.data.usecase + +import kotlinx.coroutines.flow.combine +import tw.firemaples.onscreenocr.data.repo.RecognitionRepository +import javax.inject.Inject + +class GetCurrentOCRLangUseCase @Inject constructor( + private val recognitionRepository: RecognitionRepository, +) { + operator fun invoke() = + combine( + recognitionRepository.ocrProvider, + recognitionRepository.ocrLanguage, + ) { provider, lang -> provider to lang } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt index d672ac1d..46268bf5 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt @@ -32,12 +32,21 @@ object CoroutinesDispatchersModule { @Singleton @MainImmediateCoroutineScope @Provides - fun provideMainImmediateCoroutineScope(@MainImmediateDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope = - CoroutineScope(SupervisorJob() + mainDispatcher) + fun provideMainImmediateCoroutineScope( + @MainImmediateDispatcher dispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) @Singleton @MainCoroutineScope @Provides - fun provideMainCoroutineScope(@MainDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope = - CoroutineScope(SupervisorJob() + mainDispatcher) + fun provideMainCoroutineScope( + @MainDispatcher dispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + + @Singleton + @DefaultCoroutineScope + @Provides + fun provideDefaultCoroutineScope( + @DefaultDispatcher dispatcher: CoroutineDispatcher, + ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) } \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt index 40c7eb81..6fb4929b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesQualifiers.kt @@ -24,4 +24,8 @@ annotation class MainImmediateCoroutineScope @Retention(AnnotationRetention.RUNTIME) @Qualifier -annotation class MainCoroutineScope \ No newline at end of file +annotation class MainCoroutineScope + +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class DefaultCoroutineScope diff --git a/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt index f3859e4c..c23ed573 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/SingletonModule.kt @@ -6,6 +6,8 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.floatings.manager.StateNavigatorImpl +import tw.firemaples.onscreenocr.floatings.manager.StateOperator +import tw.firemaples.onscreenocr.floatings.manager.StateOperatorImpl @Module @InstallIn(SingletonComponent::class) @@ -13,4 +15,7 @@ interface SingletonModule { @Binds fun bindStateNavigator(stateNavigatorImpl: StateNavigatorImpl): StateNavigator + + @Binds + fun bindStateOperator(stateOperatorImpl: StateOperatorImpl): StateOperator } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index aa1ccfcf..b7bac95e 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -7,12 +7,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.data.usecase.GetCurrentOCRDisplayLangCodeUseCase +import tw.firemaples.onscreenocr.data.usecase.GetCurrentOCRLangUseCase import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslatorTypeUseCase import tw.firemaples.onscreenocr.data.usecase.GetMainBarInitialPositionUseCase @@ -72,6 +74,7 @@ class MainBarViewModelImpl @Inject constructor( @MainImmediateCoroutineScope private val scope: CoroutineScope, private val stateNavigator: StateNavigator, + private val getCurrentOCRLangUseCase: GetCurrentOCRLangUseCase, private val getCurrentOCRDisplayLangCodeUseCase: GetCurrentOCRDisplayLangCodeUseCase, private val getCurrentTranslatorTypeUseCase: GetCurrentTranslatorTypeUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, @@ -94,9 +97,9 @@ class MainBarViewModelImpl @Inject constructor( state.update { it.copy( displaySelectButton = navState == NavState.Idle, - displayTranslateButton = navState == NavState.ScreenCircled, + displayTranslateButton = navState is NavState.ScreenCircled, displayCloseButton = - navState == NavState.ScreenCircling || navState == NavState.ScreenCircled, + navState == NavState.ScreenCircling || navState is NavState.ScreenCircled, ) } action.emit(MainBarAction.MoveToEdgeIfEnabled) @@ -161,7 +164,7 @@ class MainBarViewModelImpl @Inject constructor( override fun getFadeOutAfterMoved(): Boolean { val navState = stateNavigator.currentNavState.value - return navState != NavState.ScreenCircling && navState != NavState.ScreenCircled + return navState != NavState.ScreenCircling && navState !is NavState.ScreenCircled && !state.value.displayMainBarMenu && SettingManager.enableFadingOutWhileIdle //TODO move logic } @@ -188,7 +191,13 @@ class MainBarViewModelImpl @Inject constructor( override fun onTranslateClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) - stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing) + val (ocrProvider, ocrLang) = getCurrentOCRLangUseCase.invoke().first() + stateNavigator.navigate( + NavigationAction.NavigateToScreenCapturing( + ocrLang = ocrLang, + ocrProvider = ocrProvider, + ) + ) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt index 0df1377f..d3b86c63 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBar.kt @@ -1,168 +1,168 @@ -package tw.firemaples.onscreenocr.floatings.main - -import android.content.Context -import android.graphics.Point -import dagger.hilt.android.qualifiers.ApplicationContext -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding -import tw.firemaples.onscreenocr.floatings.base.MovableFloatingView -import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView -import tw.firemaples.onscreenocr.floatings.manager.NavState -import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.floatings.menu.MenuView -import tw.firemaples.onscreenocr.floatings.readme.ReadmeView -import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel -import tw.firemaples.onscreenocr.log.FirebaseEvent -import tw.firemaples.onscreenocr.pages.setting.SettingActivity -import tw.firemaples.onscreenocr.pages.setting.SettingManager -import tw.firemaples.onscreenocr.pref.AppPref -import tw.firemaples.onscreenocr.utils.Utils -import tw.firemaples.onscreenocr.utils.clickOnce -import tw.firemaples.onscreenocr.utils.hide -import tw.firemaples.onscreenocr.utils.show -import tw.firemaples.onscreenocr.utils.showOrHide -import javax.inject.Inject - -class MainBar @Inject constructor( - @ApplicationContext context: Context, - private val stateNavigator: StateNavigator, - private val viewModel: MainBarViewModel, -) : MovableFloatingView(context) { - - override val layoutId: Int - get() = R.layout.floating_main_bar - - override val initialPosition: Point - get() = - if (SettingManager.restoreMainBarPosition) AppPref.lastMainBarPosition - else Point(0, 0) - - override val enableDeviceDirectionTracker: Boolean - get() = true - - override val moveToEdgeAfterMoved: Boolean - get() = true - - override val fadeOutAfterMoved: Boolean - get() = !arrayOf(NavState.ScreenCircling, NavState.ScreenCircled) - .contains(stateNavigator.currentNavState.value) - && !menuView.attached - && SettingManager.enableFadingOutWhileIdle - override val fadeOutDelay: Long - get() = SettingManager.timeoutToFadeOut - override val fadeOutDestinationAlpha: Float - get() = SettingManager.opaquePercentageToFadeOut - - private val binding: FloatingMainBarBinding = FloatingMainBarBinding.bind(rootLayout) - - private val menuView: MenuView by lazy { - MenuView(context, false).apply { - setAnchor(binding.btMenu) - - onAttached = { rescheduleFadeOut() } - onDetached = { rescheduleFadeOut() } - onItemSelected = { view, key -> - view.detachFromScreen() - viewModel.onMenuItemClicked(key) - rescheduleFadeOut() - } - } - } - - init { - binding.setViews() - setDragView(binding.btMenu) - } - - private fun FloatingMainBarBinding.setViews() { - btLangSelector.clickOnce { - rescheduleFadeOut() - TranslationSelectPanel(context).attachToScreen() - } - - btSelect.clickOnce { - viewModel.onSelectClicked() - } - - btTranslate.clickOnce { - FirebaseEvent.logClickTranslationStartButton() - viewModel.onTranslateClicked() - } - - btClose.clickOnce { - viewModel.onCloseClicked() - } - - btMenu.clickOnce { - viewModel.onMenuButtonClicked() - } - - viewModel.languageText.observe(lifecycleOwner) { - tvLang.text = it - moveToEdgeIfEnabled() - } - - viewModel.displayTranslatorIcon.observe(lifecycleOwner) { - if (it == null) { - ivGoogleTranslator.setImageDrawable(null) - ivGoogleTranslator.hide() - } else { - ivGoogleTranslator.setImageResource(it) - ivGoogleTranslator.show() - } - moveToEdgeIfEnabled() - } - - viewModel.displaySelectButton.observe(lifecycleOwner) { - btSelect.showOrHide(it) - moveToEdgeIfEnabled() - } - - viewModel.displayTranslateButton.observe(lifecycleOwner) { - btTranslate.showOrHide(it) - moveToEdgeIfEnabled() - } - - viewModel.displayCloseButton.observe(lifecycleOwner) { - btClose.showOrHide(it) - moveToEdgeIfEnabled() - } - - viewModel.displayMenuItems.observe(lifecycleOwner) { - with(menuView) { - updateData(it) - attachToScreen() - } - } - - viewModel.rescheduleFadeOut.observe(lifecycleOwner) { - rescheduleFadeOut() - } - - viewModel.showSettingPage.observe(lifecycleOwner) { - SettingActivity.start(context) - } - - viewModel.openBrowser.observe(lifecycleOwner) { - Utils.openBrowser(it) - } - - viewModel.showVersionHistory.observe(lifecycleOwner) { - VersionHistoryView(context).attachToScreen() - } - - viewModel.showReadme.observe(lifecycleOwner) { - ReadmeView(context).attachToScreen() - } - } - - override fun onAttachedToScreen() { - super.onAttachedToScreen() - viewModel.onAttachedToScreen() - } - - override fun onDetachedFromScreen() { - super.onDetachedFromScreen() - viewModel.saveLastPosition(params.x, params.y) - } -} +//package tw.firemaples.onscreenocr.floatings.main +// +//import android.content.Context +//import android.graphics.Point +//import dagger.hilt.android.qualifiers.ApplicationContext +//import tw.firemaples.onscreenocr.R +//import tw.firemaples.onscreenocr.databinding.FloatingMainBarBinding +//import tw.firemaples.onscreenocr.floatings.base.MovableFloatingView +//import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView +//import tw.firemaples.onscreenocr.floatings.manager.NavState +//import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +//import tw.firemaples.onscreenocr.floatings.menu.MenuView +//import tw.firemaples.onscreenocr.floatings.readme.ReadmeView +//import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel +//import tw.firemaples.onscreenocr.log.FirebaseEvent +//import tw.firemaples.onscreenocr.pages.setting.SettingActivity +//import tw.firemaples.onscreenocr.pages.setting.SettingManager +//import tw.firemaples.onscreenocr.pref.AppPref +//import tw.firemaples.onscreenocr.utils.Utils +//import tw.firemaples.onscreenocr.utils.clickOnce +//import tw.firemaples.onscreenocr.utils.hide +//import tw.firemaples.onscreenocr.utils.show +//import tw.firemaples.onscreenocr.utils.showOrHide +//import javax.inject.Inject +// +//class MainBar @Inject constructor( +// @ApplicationContext context: Context, +// private val stateNavigator: StateNavigator, +// private val viewModel: MainBarViewModel, +//) : MovableFloatingView(context) { +// +// override val layoutId: Int +// get() = R.layout.floating_main_bar +// +// override val initialPosition: Point +// get() = +// if (SettingManager.restoreMainBarPosition) AppPref.lastMainBarPosition +// else Point(0, 0) +// +// override val enableDeviceDirectionTracker: Boolean +// get() = true +// +// override val moveToEdgeAfterMoved: Boolean +// get() = true +// +// override val fadeOutAfterMoved: Boolean +// get() = !arrayOf(NavState.ScreenCircling, NavState.ScreenCircled) +// .contains(stateNavigator.currentNavState.value) +// && !menuView.attached +// && SettingManager.enableFadingOutWhileIdle +// override val fadeOutDelay: Long +// get() = SettingManager.timeoutToFadeOut +// override val fadeOutDestinationAlpha: Float +// get() = SettingManager.opaquePercentageToFadeOut +// +// private val binding: FloatingMainBarBinding = FloatingMainBarBinding.bind(rootLayout) +// +// private val menuView: MenuView by lazy { +// MenuView(context, false).apply { +// setAnchor(binding.btMenu) +// +// onAttached = { rescheduleFadeOut() } +// onDetached = { rescheduleFadeOut() } +// onItemSelected = { view, key -> +// view.detachFromScreen() +// viewModel.onMenuItemClicked(key) +// rescheduleFadeOut() +// } +// } +// } +// +// init { +// binding.setViews() +// setDragView(binding.btMenu) +// } +// +// private fun FloatingMainBarBinding.setViews() { +// btLangSelector.clickOnce { +// rescheduleFadeOut() +// TranslationSelectPanel(context).attachToScreen() +// } +// +// btSelect.clickOnce { +// viewModel.onSelectClicked() +// } +// +// btTranslate.clickOnce { +// FirebaseEvent.logClickTranslationStartButton() +// viewModel.onTranslateClicked() +// } +// +// btClose.clickOnce { +// viewModel.onCloseClicked() +// } +// +// btMenu.clickOnce { +// viewModel.onMenuButtonClicked() +// } +// +// viewModel.languageText.observe(lifecycleOwner) { +// tvLang.text = it +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayTranslatorIcon.observe(lifecycleOwner) { +// if (it == null) { +// ivGoogleTranslator.setImageDrawable(null) +// ivGoogleTranslator.hide() +// } else { +// ivGoogleTranslator.setImageResource(it) +// ivGoogleTranslator.show() +// } +// moveToEdgeIfEnabled() +// } +// +// viewModel.displaySelectButton.observe(lifecycleOwner) { +// btSelect.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayTranslateButton.observe(lifecycleOwner) { +// btTranslate.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayCloseButton.observe(lifecycleOwner) { +// btClose.showOrHide(it) +// moveToEdgeIfEnabled() +// } +// +// viewModel.displayMenuItems.observe(lifecycleOwner) { +// with(menuView) { +// updateData(it) +// attachToScreen() +// } +// } +// +// viewModel.rescheduleFadeOut.observe(lifecycleOwner) { +// rescheduleFadeOut() +// } +// +// viewModel.showSettingPage.observe(lifecycleOwner) { +// SettingActivity.start(context) +// } +// +// viewModel.openBrowser.observe(lifecycleOwner) { +// Utils.openBrowser(it) +// } +// +// viewModel.showVersionHistory.observe(lifecycleOwner) { +// VersionHistoryView(context).attachToScreen() +// } +// +// viewModel.showReadme.observe(lifecycleOwner) { +// ReadmeView(context).attachToScreen() +// } +// } +// +// override fun onAttachedToScreen() { +// super.onAttachedToScreen() +// viewModel.onAttachedToScreen() +// } +// +// override fun onDetachedFromScreen() { +// super.onDetachedFromScreen() +// viewModel.saveLastPosition(params.x, params.y) +// } +//} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt index d02cd5f4..52438713 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/main/MainBarViewModel.kt @@ -1,261 +1,261 @@ -package tw.firemaples.onscreenocr.floatings.main - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope -import tw.firemaples.onscreenocr.floatings.ViewHolderService -import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel -import tw.firemaples.onscreenocr.floatings.manager.NavState -import tw.firemaples.onscreenocr.floatings.manager.NavigationAction -import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.pref.AppPref -import tw.firemaples.onscreenocr.recognition.TextRecognizer -import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager -import tw.firemaples.onscreenocr.repo.GeneralRepository -import tw.firemaples.onscreenocr.repo.OCRRepository -import tw.firemaples.onscreenocr.repo.TranslationRepository -import tw.firemaples.onscreenocr.translator.TranslationProviderType -import tw.firemaples.onscreenocr.utils.Constants -import tw.firemaples.onscreenocr.utils.Logger -import tw.firemaples.onscreenocr.utils.SingleLiveEvent -import tw.firemaples.onscreenocr.utils.Utils -import javax.inject.Inject - -class MainBarViewModel @Inject constructor( - @MainImmediateCoroutineScope viewScope: CoroutineScope, - private val stateNavigator: StateNavigator, -) : FloatingViewModel(viewScope) { - - companion object { - private const val MENU_SETTING = "setting" - private const val MENU_PRIVACY_POLICY = "privacy_policy" - private const val MENU_ABOUT = "about" - private const val MENU_VERSION_HISTORY = "version_history" - private const val MENU_README = "readme" - private const val MENU_HIDE = "hide" - private const val MENU_EXIT = "exit" - } - - private val _languageText = MutableLiveData() - val languageText: LiveData = _languageText - - private val _displayTranslatorIcon = MutableLiveData() - val displayTranslatorIcon: LiveData = _displayTranslatorIcon - - private val _displaySelectButton = MutableLiveData() - val displaySelectButton: LiveData = _displaySelectButton - - private val _displayTranslateButton = MutableLiveData() - val displayTranslateButton: LiveData = _displayTranslateButton - - private val _displayCloseButton = MutableLiveData() - val displayCloseButton: LiveData = _displayCloseButton - - private val _displayMenuItems = MutableLiveData>() - val displayMenuItems: LiveData> = _displayMenuItems - - private val _rescheduleFadeOut = MutableLiveData() - val rescheduleFadeOut: LiveData = _rescheduleFadeOut - - private val _showSettingPage = SingleLiveEvent() - val showSettingPage: LiveData = _showSettingPage - - private val _openBrowser = SingleLiveEvent() - val openBrowser: LiveData = _openBrowser - - private val _showVersionHistory = SingleLiveEvent() - val showVersionHistory: LiveData = _showVersionHistory - - private val _showReadme = SingleLiveEvent() - val showReadme: LiveData = _showReadme - - private val logger: Logger by lazy { Logger(MainBarViewModel::class) } - private val context: Context by lazy { Utils.context } - - private val menuItems = mapOf( - MENU_SETTING to context.getString(R.string.menu_setting), - MENU_PRIVACY_POLICY to context.getString(R.string.menu_privacy_policy), - MENU_ABOUT to context.getString(R.string.menu_about), - MENU_VERSION_HISTORY to context.getString(R.string.menu_version_history), - MENU_README to context.getString(R.string.menu_readme), - MENU_HIDE to context.getString(R.string.menu_hide), - MENU_EXIT to context.getString(R.string.menu_exit), - ) - - private val repo by lazy { GeneralRepository() } - private val ocrRepo by lazy { OCRRepository() } - private val translateRepo by lazy { TranslationRepository() } - - private var _selectedOCRLang: String = Constants.DEFAULT_OCR_LANG - val selectedOCRLang: String get() = _selectedOCRLang - - // private var selectedTranslationProvider: TranslationProvider = -// TranslationProvider.fromType(context, Constraints.DEFAULT_TRANSLATION_PROVIDER) - private var selectedTranslationProviderType: TranslationProviderType = - Constants.DEFAULT_TRANSLATION_PROVIDER - private var selectedTranslationLang: String = Constants.DEFAULT_TRANSLATION_LANG - - init { - logger.debug("register FloatingStateManager.onStateChanged") - stateNavigator.currentNavState - .onEach { onStateChanged(it) } - .launchIn(viewScope) - } - - fun onAttachedToScreen() { - logger.debug("onAttachedToScreen()") - viewScope.launch { - ocrRepo.selectedOCRLangFlow.collect { onSelectedLangChanged(_ocrLang = it) } - } - viewScope.launch { - translateRepo.selectedProviderTypeFlow.collect { - onSelectedLangChanged(translationProviderType = it) - } - } - viewScope.launch { - translateRepo.selectedTranslationLangFlow.collect { - onSelectedLangChanged(translationLang = it) - } - } - viewScope.launch { -// setupButtons(floatingStateManager.currentState) - - if (!repo.isReadmeAlreadyShown().first()) { - _showReadme.value = true - } - - if (repo.showVersionHistory().first()) { - _showVersionHistory.value = true - } - } - } - - private suspend fun onStateChanged(state: NavState) { - logger.debug("onStateChanged(): $state") - setupButtons(state) - _rescheduleFadeOut.value = true - } - - @Suppress("RedundantSuspendModifier") - private suspend fun setupButtons(state: NavState) { - logger.debug("setupButtons(): $state") - _displaySelectButton.value = state == NavState.Idle - _displayTranslateButton.value = state == NavState.ScreenCircled - _displayCloseButton.value = - state == NavState.ScreenCircling || state == NavState.ScreenCircled - } - - @Suppress("RedundantSuspendModifier") - private suspend fun onSelectedLangChanged( - _ocrLang: String = _selectedOCRLang, - translationProviderType: TranslationProviderType = selectedTranslationProviderType, - translationLang: String = selectedTranslationLang, - ) { - this._selectedOCRLang = _ocrLang - this.selectedTranslationProviderType = translationProviderType - this.selectedTranslationLang = translationLang - - logger.debug("onSelectedLangChanged(), ocrLang: $_ocrLang, provider: $translationProviderType, translationLang: $translationLang") - - val ocrLang = TextRecognizer - .getRecognizer(AppPref.selectedOCRProvider) - .parseToDisplayLangCode(_ocrLang) - - _displayTranslatorIcon.value = when (translationProviderType) { - TranslationProviderType.GoogleTranslateApp -> R.drawable.ic_google_translate_dark_grey - TranslationProviderType.BingTranslateApp -> R.drawable.ic_microsoft_bing - TranslationProviderType.OtherTranslateApp -> R.drawable.ic_open_in_app - TranslationProviderType.MicrosoftAzure, - TranslationProviderType.GoogleMLKit, - TranslationProviderType.MyMemory, - TranslationProviderType.PapagoTranslateApp, - TranslationProviderType.YandexTranslateApp, - TranslationProviderType.OCROnly -> null - } - - _languageText.value = when (translationProviderType) { - TranslationProviderType.GoogleTranslateApp, - TranslationProviderType.BingTranslateApp, - TranslationProviderType.OtherTranslateApp -> "$ocrLang>" - - TranslationProviderType.YandexTranslateApp -> "$ocrLang > Y" - TranslationProviderType.PapagoTranslateApp -> "$ocrLang > P" - TranslationProviderType.OCROnly -> " $ocrLang " - TranslationProviderType.MicrosoftAzure, - TranslationProviderType.GoogleMLKit, - TranslationProviderType.MyMemory -> "$ocrLang>$translationLang" - } - } - - fun onMenuButtonClicked() { - viewScope.launch { - _rescheduleFadeOut.value = true - _displayMenuItems.value = menuItems - } - } - - fun onMenuItemClicked(action: String) { - logger.debug("onMenuItemClicked(), action: $action") - - when (action) { - MENU_SETTING -> { - _showSettingPage.value = true - } - - MENU_PRIVACY_POLICY -> { - _openBrowser.value = RemoteConfigManager.privacyPolicyUrl - } - - MENU_ABOUT -> { - _openBrowser.value = RemoteConfigManager.aboutUrl - } - - MENU_VERSION_HISTORY -> { - _showVersionHistory.value = true - } - - MENU_README -> { - _showReadme.value = true - } - - MENU_HIDE -> { - ViewHolderService.hideViews(context) - } - - MENU_EXIT -> { - ViewHolderService.exit(context) - } - } - } - - fun saveLastPosition(x: Int, y: Int) { - viewScope.launch { - repo.saveLastMainBarPosition(x, y) - } - } - - fun onSelectClicked() { - viewScope.launch { - stateNavigator.navigate(NavigationAction.NavigateToScreenCircling) - } - } - - fun onTranslateClicked() { - viewScope.launch { - stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing) - } - } - - fun onCloseClicked() { - viewScope.launch { - stateNavigator.navigate(NavigationAction.CancelScreenCircling) - } - } -} \ No newline at end of file +//package tw.firemaples.onscreenocr.floatings.main +// +//import android.content.Context +//import androidx.lifecycle.LiveData +//import androidx.lifecycle.MutableLiveData +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.flow.first +//import kotlinx.coroutines.flow.launchIn +//import kotlinx.coroutines.flow.onEach +//import kotlinx.coroutines.launch +//import tw.firemaples.onscreenocr.R +//import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +//import tw.firemaples.onscreenocr.floatings.ViewHolderService +//import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel +//import tw.firemaples.onscreenocr.floatings.manager.NavState +//import tw.firemaples.onscreenocr.floatings.manager.NavigationAction +//import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +//import tw.firemaples.onscreenocr.pref.AppPref +//import tw.firemaples.onscreenocr.recognition.TextRecognizer +//import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager +//import tw.firemaples.onscreenocr.repo.GeneralRepository +//import tw.firemaples.onscreenocr.repo.OCRRepository +//import tw.firemaples.onscreenocr.repo.TranslationRepository +//import tw.firemaples.onscreenocr.translator.TranslationProviderType +//import tw.firemaples.onscreenocr.utils.Constants +//import tw.firemaples.onscreenocr.utils.Logger +//import tw.firemaples.onscreenocr.utils.SingleLiveEvent +//import tw.firemaples.onscreenocr.utils.Utils +//import javax.inject.Inject +// +//class MainBarViewModel @Inject constructor( +// @MainImmediateCoroutineScope viewScope: CoroutineScope, +// private val stateNavigator: StateNavigator, +//) : FloatingViewModel(viewScope) { +// +// companion object { +// private const val MENU_SETTING = "setting" +// private const val MENU_PRIVACY_POLICY = "privacy_policy" +// private const val MENU_ABOUT = "about" +// private const val MENU_VERSION_HISTORY = "version_history" +// private const val MENU_README = "readme" +// private const val MENU_HIDE = "hide" +// private const val MENU_EXIT = "exit" +// } +// +// private val _languageText = MutableLiveData() +// val languageText: LiveData = _languageText +// +// private val _displayTranslatorIcon = MutableLiveData() +// val displayTranslatorIcon: LiveData = _displayTranslatorIcon +// +// private val _displaySelectButton = MutableLiveData() +// val displaySelectButton: LiveData = _displaySelectButton +// +// private val _displayTranslateButton = MutableLiveData() +// val displayTranslateButton: LiveData = _displayTranslateButton +// +// private val _displayCloseButton = MutableLiveData() +// val displayCloseButton: LiveData = _displayCloseButton +// +// private val _displayMenuItems = MutableLiveData>() +// val displayMenuItems: LiveData> = _displayMenuItems +// +// private val _rescheduleFadeOut = MutableLiveData() +// val rescheduleFadeOut: LiveData = _rescheduleFadeOut +// +// private val _showSettingPage = SingleLiveEvent() +// val showSettingPage: LiveData = _showSettingPage +// +// private val _openBrowser = SingleLiveEvent() +// val openBrowser: LiveData = _openBrowser +// +// private val _showVersionHistory = SingleLiveEvent() +// val showVersionHistory: LiveData = _showVersionHistory +// +// private val _showReadme = SingleLiveEvent() +// val showReadme: LiveData = _showReadme +// +// private val logger: Logger by lazy { Logger(MainBarViewModel::class) } +// private val context: Context by lazy { Utils.context } +// +// private val menuItems = mapOf( +// MENU_SETTING to context.getString(R.string.menu_setting), +// MENU_PRIVACY_POLICY to context.getString(R.string.menu_privacy_policy), +// MENU_ABOUT to context.getString(R.string.menu_about), +// MENU_VERSION_HISTORY to context.getString(R.string.menu_version_history), +// MENU_README to context.getString(R.string.menu_readme), +// MENU_HIDE to context.getString(R.string.menu_hide), +// MENU_EXIT to context.getString(R.string.menu_exit), +// ) +// +// private val repo by lazy { GeneralRepository() } +// private val ocrRepo by lazy { OCRRepository() } +// private val translateRepo by lazy { TranslationRepository() } +// +// private var _selectedOCRLang: String = Constants.DEFAULT_OCR_LANG +// val selectedOCRLang: String get() = _selectedOCRLang +// +// // private var selectedTranslationProvider: TranslationProvider = +//// TranslationProvider.fromType(context, Constraints.DEFAULT_TRANSLATION_PROVIDER) +// private var selectedTranslationProviderType: TranslationProviderType = +// Constants.DEFAULT_TRANSLATION_PROVIDER +// private var selectedTranslationLang: String = Constants.DEFAULT_TRANSLATION_LANG +// +// init { +// logger.debug("register FloatingStateManager.onStateChanged") +// stateNavigator.currentNavState +// .onEach { onStateChanged(it) } +// .launchIn(viewScope) +// } +// +// fun onAttachedToScreen() { +// logger.debug("onAttachedToScreen()") +// viewScope.launch { +// ocrRepo.selectedOCRLangFlow.collect { onSelectedLangChanged(_ocrLang = it) } +// } +// viewScope.launch { +// translateRepo.selectedProviderTypeFlow.collect { +// onSelectedLangChanged(translationProviderType = it) +// } +// } +// viewScope.launch { +// translateRepo.selectedTranslationLangFlow.collect { +// onSelectedLangChanged(translationLang = it) +// } +// } +// viewScope.launch { +//// setupButtons(floatingStateManager.currentState) +// +// if (!repo.isReadmeAlreadyShown().first()) { +// _showReadme.value = true +// } +// +// if (repo.showVersionHistory().first()) { +// _showVersionHistory.value = true +// } +// } +// } +// +// private suspend fun onStateChanged(state: NavState) { +// logger.debug("onStateChanged(): $state") +// setupButtons(state) +// _rescheduleFadeOut.value = true +// } +// +// @Suppress("RedundantSuspendModifier") +// private suspend fun setupButtons(state: NavState) { +// logger.debug("setupButtons(): $state") +// _displaySelectButton.value = state == NavState.Idle +// _displayTranslateButton.value = state == NavState.ScreenCircled +// _displayCloseButton.value = +// state == NavState.ScreenCircling || state == NavState.ScreenCircled +// } +// +// @Suppress("RedundantSuspendModifier") +// private suspend fun onSelectedLangChanged( +// _ocrLang: String = _selectedOCRLang, +// translationProviderType: TranslationProviderType = selectedTranslationProviderType, +// translationLang: String = selectedTranslationLang, +// ) { +// this._selectedOCRLang = _ocrLang +// this.selectedTranslationProviderType = translationProviderType +// this.selectedTranslationLang = translationLang +// +// logger.debug("onSelectedLangChanged(), ocrLang: $_ocrLang, provider: $translationProviderType, translationLang: $translationLang") +// +// val ocrLang = TextRecognizer +// .getRecognizer(AppPref.selectedOCRProvider) +// .parseToDisplayLangCode(_ocrLang) +// +// _displayTranslatorIcon.value = when (translationProviderType) { +// TranslationProviderType.GoogleTranslateApp -> R.drawable.ic_google_translate_dark_grey +// TranslationProviderType.BingTranslateApp -> R.drawable.ic_microsoft_bing +// TranslationProviderType.OtherTranslateApp -> R.drawable.ic_open_in_app +// TranslationProviderType.MicrosoftAzure, +// TranslationProviderType.GoogleMLKit, +// TranslationProviderType.MyMemory, +// TranslationProviderType.PapagoTranslateApp, +// TranslationProviderType.YandexTranslateApp, +// TranslationProviderType.OCROnly -> null +// } +// +// _languageText.value = when (translationProviderType) { +// TranslationProviderType.GoogleTranslateApp, +// TranslationProviderType.BingTranslateApp, +// TranslationProviderType.OtherTranslateApp -> "$ocrLang>" +// +// TranslationProviderType.YandexTranslateApp -> "$ocrLang > Y" +// TranslationProviderType.PapagoTranslateApp -> "$ocrLang > P" +// TranslationProviderType.OCROnly -> " $ocrLang " +// TranslationProviderType.MicrosoftAzure, +// TranslationProviderType.GoogleMLKit, +// TranslationProviderType.MyMemory -> "$ocrLang>$translationLang" +// } +// } +// +// fun onMenuButtonClicked() { +// viewScope.launch { +// _rescheduleFadeOut.value = true +// _displayMenuItems.value = menuItems +// } +// } +// +// fun onMenuItemClicked(action: String) { +// logger.debug("onMenuItemClicked(), action: $action") +// +// when (action) { +// MENU_SETTING -> { +// _showSettingPage.value = true +// } +// +// MENU_PRIVACY_POLICY -> { +// _openBrowser.value = RemoteConfigManager.privacyPolicyUrl +// } +// +// MENU_ABOUT -> { +// _openBrowser.value = RemoteConfigManager.aboutUrl +// } +// +// MENU_VERSION_HISTORY -> { +// _showVersionHistory.value = true +// } +// +// MENU_README -> { +// _showReadme.value = true +// } +// +// MENU_HIDE -> { +// ViewHolderService.hideViews(context) +// } +// +// MENU_EXIT -> { +// ViewHolderService.exit(context) +// } +// } +// } +// +// fun saveLastPosition(x: Int, y: Int) { +// viewScope.launch { +// repo.saveLastMainBarPosition(x, y) +// } +// } +// +// fun onSelectClicked() { +// viewScope.launch { +// stateNavigator.navigate(NavigationAction.NavigateToScreenCircling) +// } +// } +// +// fun onTranslateClicked() { +// viewScope.launch { +// stateNavigator.navigate(NavigationAction.NavigateToScreenCapturing) +// } +// } +// +// fun onCloseClicked() { +// viewScope.launch { +// stateNavigator.navigate(NavigationAction.CancelScreenCircling) +// } +// } +//} \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index 1d1d0c3b..68950084 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -1,61 +1,44 @@ package tw.firemaples.onscreenocr.floatings.manager import android.content.Context -import android.graphics.Bitmap -import android.graphics.Rect import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.di.MainCoroutineScope +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.base.FloatingView import tw.firemaples.onscreenocr.floatings.compose.mainbar.MainBarFloatingView import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog import tw.firemaples.onscreenocr.floatings.result.ResultView import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView -import tw.firemaples.onscreenocr.log.FirebaseEvent -import tw.firemaples.onscreenocr.pages.setting.SettingManager -import tw.firemaples.onscreenocr.pref.AppPref -import tw.firemaples.onscreenocr.recognition.RecognitionResult -import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType -import tw.firemaples.onscreenocr.recognition.TextRecognizer -import tw.firemaples.onscreenocr.screenshot.ScreenExtractor -import tw.firemaples.onscreenocr.translator.TranslationProviderType -import tw.firemaples.onscreenocr.translator.TranslationResult -import tw.firemaples.onscreenocr.translator.Translator -import tw.firemaples.onscreenocr.translator.azure.MicrosoftAzureTranslator -import tw.firemaples.onscreenocr.utils.Constants import tw.firemaples.onscreenocr.utils.Logger -import tw.firemaples.onscreenocr.utils.setReusable -import java.io.IOException import javax.inject.Inject import javax.inject.Singleton -import kotlin.reflect.KClass @Singleton class FloatingViewCoordinator @Inject constructor( @ApplicationContext private val context: Context, + @MainImmediateCoroutineScope private val scope: CoroutineScope, private val stateNavigator: StateNavigator, - @MainCoroutineScope private val scope: CoroutineScope, + stateOperator: StateOperator, private val mainBar: MainBarFloatingView, private val resultView: ResultView, ) { private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } - private val currentNavState: NavState - get() = stateNavigator.currentNavState.value - private val screenCirclingView: ScreenCirclingView by lazy { ScreenCirclingView(context).apply { onAreaSelected = { parent, selected -> - this@FloatingViewCoordinator.onAreaSelected(parent, selected) + scope.launch { + stateNavigator.navigate( + NavigationAction.NavigateToScreenCircled( + parentRect = parent, + selectedRect = selected, + ) + ) + } } } } @@ -64,55 +47,51 @@ class FloatingViewCoordinator @Inject constructor( val isMainBarAttached: Boolean get() = mainBar.attached - private var selectedOCRLang: String = Constants.DEFAULT_OCR_LANG - private val selectedOCRProvider: TextRecognitionProviderType get() = AppPref.selectedOCRProvider - private var parentRect: Rect? = null - private var selectedRect: Rect? = null - private var croppedBitmap: Bitmap? = null - init { - stateNavigator.navigationAction - .onEach { - when (it) { - NavigationAction.NavigateToIdle -> - backToIdle() - - NavigationAction.NavigateToScreenCircling -> - startScreenCircling() - - is NavigationAction.NavigateToScreenCircled -> - onAreaSelected( - parentRect = it.parentRect, - selectedRect = it.selectedRect, - ) - - is NavigationAction.NavigateToScreenCapturing -> - startScreenCapturing() - - is NavigationAction.NavigateToTextRecognition -> - startRecognition( - croppedBitmap = it.croppedBitmap, - parent = it.parent, - selected = it.selected, + stateOperator.action + .onEach { action -> + when (action) { + StateOperatorAction.TopMainBar -> arrangeMainBarToTop() + StateOperatorAction.HideMainBar -> hideMainBar() + StateOperatorAction.ShowMainBar -> showMainBar() + + StateOperatorAction.ShowScreenCirclingView -> + screenCirclingView.attachToScreen() + + StateOperatorAction.HideScreenCirclingView -> + screenCirclingView.detachFromScreen() + + StateOperatorAction.ResultViewStartRecognition -> + resultView.startRecognition() + + is StateOperatorAction.ResultViewSetRecognized -> + resultView.textRecognized( + result = action.result, + parent = action.parentRect, + selected = action.selectedRect, + croppedBitmap = action.croppedBitmap, ) - is NavigationAction.NavigateToStartTranslation -> - startTranslation(it.recognitionResult) + is StateOperatorAction.ResultViewStartTranslation -> + resultView.startTranslation(action.translationProviderType) - is NavigationAction.NavigateToTranslated -> - showResult(it.result) + is StateOperatorAction.ResultViewTextTranslated -> + resultView.textTranslated(action.result) - is NavigationAction.ShowError -> - showError(it.error) + StateOperatorAction.ResultViewBackToIdle -> + resultView.backToIdle() - NavigationAction.CancelScreenCircling -> - cancelScreenCircling() + is StateOperatorAction.ShowErrorDialog -> + context.showErrorDialog(action.error) } } .launchIn(scope) resultView.onUserDismiss = { - this@FloatingViewCoordinator.backToIdle() + scope.launch { + resultView.backToIdle() + stateNavigator.navigate(NavigationAction.NavigateToIdle) + } } } @@ -138,272 +117,12 @@ class FloatingViewCoordinator @Inject constructor( } fun detachAllViews() { - backToIdle() + scope.launch { + stateNavigator.navigate(NavigationAction.NavigateToIdle) + } scope.launch { hideMainBar() FloatingView.detachAllFloatingViews() } } - - private fun startScreenCircling() = checkNextState(NavState.ScreenCircling::class) { - if (!Translator.getTranslator().checkResources(scope)) { - return@checkNextState - } - - logger.debug("startScreenCircling()") - stateNavigator.updateState(NavState.ScreenCircling) - FirebaseEvent.logStartAreaSelection() - screenCirclingView.attachToScreen() - arrangeMainBarToTop() - } - - private fun onAreaSelected(parentRect: Rect, selectedRect: Rect) = - checkNextState(NavState.ScreenCircled::class) { - logger.debug( - "onAreaSelected(), parentRect: $parentRect, " + - "selectedRect: $selectedRect, " + - "size: ${selectedRect.width()}x${selectedRect.height()}" - ) - if (currentNavState != NavState.ScreenCircled) { - stateNavigator.updateState(NavState.ScreenCircled) - } - this@FloatingViewCoordinator.selectedRect = selectedRect - this@FloatingViewCoordinator.parentRect = parentRect - } - - private fun cancelScreenCircling() = checkNextState(NavState.Idle::class) { - logger.debug("cancelScreenCircling()") - stateNavigator.updateState(NavState.Idle) - screenCirclingView.detachFromScreen() - } - - private fun startScreenCapturing() = - checkNextState(NavState.ScreenCapturing::class) { - if (!Translator.getTranslator().checkResources(scope)) { - return@checkNextState - } - - this@FloatingViewCoordinator.selectedOCRLang = AppPref.selectedOCRLang - val parent = parentRect ?: return@checkNextState - val selected = selectedRect ?: return@checkNextState - logger.debug("startScreenCapturing(), parentRect: $parent, selectedRect: $selected") - stateNavigator.updateState(NavState.ScreenCapturing) - mainBar.detachFromScreen() - screenCirclingView.detachFromScreen() - - delay(100L) - - try { - FirebaseEvent.logStartCaptureScreen() - val croppedBitmap = - ScreenExtractor.extractBitmapFromScreen( - parentRect = parent, - cropRect = selected - ) - this@FloatingViewCoordinator.croppedBitmap = croppedBitmap - FirebaseEvent.logCaptureScreenFinished() - - mainBar.attachToScreen() - - startRecognition(croppedBitmap, parent, selected) - } catch (t: TimeoutCancellationException) { - logger.debug(t = t) - showError(context.getString(R.string.error_capture_screen_timeout)) - FirebaseEvent.logCaptureScreenFailed(t) - } catch (t: Throwable) { - logger.debug(t = t) - showError( - t.message ?: context.getString(R.string.error_unknown_error_capturing_screen) - ) - FirebaseEvent.logCaptureScreenFailed(t) - } -// screenCirclingView.detachFromScreen() // To test circled area - } - - private fun startRecognition(croppedBitmap: Bitmap, parent: Rect, selected: Rect) = - checkNextState(NavState.TextRecognizing::class) { - stateNavigator.updateState(NavState.TextRecognizing) - try { - resultView.startRecognition() - val recognizer = TextRecognizer.getRecognizer(selectedOCRProvider) - FirebaseEvent.logStartOCR(recognizer.name) - var result = withContext(Dispatchers.Default) { - recognizer.recognize( - TextRecognizer.getLanguage(selectedOCRLang, selectedOCRProvider)!!, - croppedBitmap - ) - } - logger.debug("On text recognized: $result") -// croppedBitmap.recycle() // to be used in the text editor view - if (SettingManager.removeSpacesInCJK) { - val cjkLang = arrayOf("zh", "ja", "ko") - if (cjkLang.contains(selectedOCRLang.split("-").getOrNull(0))) { - result = result.copy( - result = result.result.replace(" ", "") - ) - } - logger.debug("Remove CJK spaces: $result") - } - FirebaseEvent.logOCRFinished(recognizer.name) - resultView.textRecognized(result, parent, selected, croppedBitmap) - startTranslation(result) - } catch (e: Exception) { - val error = - if (e.message?.contains(Constants.errorInputImageIsTooSmall) == true) { - context.getString(R.string.error_selected_area_too_small) - } else - e.message - ?: context.getString(R.string.error_an_unknown_error_found_while_recognition_text) - - logger.warn(t = e) - showError(error) - FirebaseEvent.logOCRFailed( - TextRecognizer.getRecognizer(selectedOCRProvider).name, e - ) - } - } - - private fun startTranslation(recognitionResult: RecognitionResult) = - checkNextState(NavState.TextTranslating::class) { - try { - stateNavigator.updateState(NavState.TextTranslating) - - val translator = Translator.getTranslator() - - resultView.startTranslation(translator.type) - - FirebaseEvent.logStartTranslationText( - recognitionResult.result, - recognitionResult.langCode, - translator - ) - - val translationResult = translator - .translate(recognitionResult.result, recognitionResult.langCode) - - when (translationResult) { - TranslationResult.OuterTranslatorLaunched -> { - FirebaseEvent.logTranslationTextFinished(translator) - backToIdle() - } - - is TranslationResult.SourceLangNotSupport -> { - FirebaseEvent.logTranslationSourceLangNotSupport( - translator, recognitionResult.langCode, - ) - showResult( - Result.SourceLangNotSupport( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - providerType = translationResult.type, - ) - ) - } - - TranslationResult.OCROnlyResult -> { - FirebaseEvent.logTranslationTextFinished(translator) - showResult( - Result.OCROnly( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - ) - ) - } - - is TranslationResult.TranslatedResult -> { - FirebaseEvent.logTranslationTextFinished(translator) - showResult( - Result.Translated( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - translatedText = translationResult.result, - providerType = translationResult.type, - ) - ) - } - - is TranslationResult.TranslationFailed -> { - FirebaseEvent.logTranslationTextFailed(translator) - val error = translationResult.error - - if (error is MicrosoftAzureTranslator.Error) { - FirebaseEvent.logMicrosoftTranslationError(error) - } - - if (error is IOException) { - showError(context.getString(R.string.error_can_not_connect_to_translation_server)) - } else { - FirebaseEvent.logException(error) - showError( - error.localizedMessage - ?: context.getString(R.string.error_unknown) - ) - } - } - } - } catch (e: Exception) { - logger.warn(t = e) - FirebaseEvent.logException(e) - showError(e.message ?: "Unknown error found while translating") - } - } - - private fun showResult(result: Result) = - checkNextState(NavState.ResultDisplaying::class) { - logger.debug("showResult(), $result") - stateNavigator.updateState(NavState.ResultDisplaying) - - resultView.textTranslated(result) - } - - private fun showError(error: String) = checkNextState(NavState.ErrorDisplaying::class) { - stateNavigator.updateState(NavState.ErrorDisplaying(error)) - logger.error(error) - context.showErrorDialog(error) - backToIdle() - } - - private fun backToIdle() = checkNextState(NavState.Idle::class) { - if (currentNavState != NavState.Idle) stateNavigator.updateState(NavState.Idle) - croppedBitmap?.setReusable() - resultView.backToIdle() - showMainBar() - } - - private fun checkNextState( - vararg nextStates: KClass, - block: suspend CoroutineScope.() -> Unit, - ) { - val notAllowed = nextStates.filterNot { stateNavigator.allowedNextState(it) } - - if (notAllowed.isEmpty()) { - scope.launch { block.invoke(this) } - } else { - val error = "Transit from $notAllowed to $currentNavState is not allowed" - logger.error(t = IllegalStateException(error)) - } - } -} - -sealed class Result( - open val ocrText: String, - open val boundingBoxes: List, -) { - data class Translated( - override val ocrText: String, - override val boundingBoxes: List, - val translatedText: String, - val providerType: TranslationProviderType, - ) : Result(ocrText, boundingBoxes) - - data class SourceLangNotSupport( - override val ocrText: String, - override val boundingBoxes: List, - val providerType: TranslationProviderType, - ) : Result(ocrText, boundingBoxes) - - data class OCROnly( - override val ocrText: String, - override val boundingBoxes: List, - ) : Result(ocrText, boundingBoxes) } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index dcb28e51..9a920689 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.recognition.RecognitionResult +import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -42,18 +43,17 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { NavState.Idle::class, NavState.ScreenCapturing::class, NavState.ScreenCircled::class, ), NavState.ScreenCapturing::class to setOf( - NavState.Idle::class, NavState.TextRecognizing::class, NavState.ErrorDisplaying::class, + NavState.Idle::class, NavState.TextRecognizing::class, ), NavState.TextRecognizing::class to setOf( - NavState.Idle::class, NavState.TextTranslating::class, NavState.ErrorDisplaying::class, + NavState.Idle::class, NavState.TextTranslating::class, ), NavState.TextTranslating::class to setOf( - NavState.ResultDisplaying::class, NavState.ErrorDisplaying::class, NavState.Idle::class, + NavState.TextTranslated::class, NavState.Idle::class, ), - NavState.ResultDisplaying::class to setOf( + NavState.TextTranslated::class to setOf( NavState.Idle::class, NavState.TextTranslating::class, ), - NavState.ErrorDisplaying::class to setOf(NavState.Idle::class), ) override suspend fun navigate(action: NavigationAction) { @@ -89,7 +89,10 @@ sealed interface NavigationAction { data object CancelScreenCircling : NavigationAction - data object NavigateToScreenCapturing : NavigationAction + data class NavigateToScreenCapturing( + val ocrLang: String, + val ocrProvider: TextRecognitionProviderType, + ) : NavigationAction data class NavigateToTextRecognition( val croppedBitmap: Bitmap, @@ -117,10 +120,25 @@ sealed class NavState { object Idle : NavState() object ScreenCircling : NavState() - object ScreenCircled : NavState() + data class ScreenCircled(val parentRect: Rect, val selectedRect: Rect) : NavState() object ScreenCapturing : NavState() - object TextRecognizing : NavState() - object TextTranslating : NavState() - object ResultDisplaying : NavState() - data class ErrorDisplaying(val error: String) : NavState() + data class TextRecognizing(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + override val bitmap: Bitmap + get() = croppedBitmap + } + + data class TextTranslating(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + override val bitmap: Bitmap + get() = croppedBitmap + } + + data class TextTranslated(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + override val bitmap: Bitmap + get() = croppedBitmap + } +// data class ErrorDisplaying(val error: String) : NavState() +} + +interface BitmapIncluded { + val bitmap: Bitmap } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt new file mode 100644 index 00000000..ccd0de98 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -0,0 +1,442 @@ +package tw.firemaples.onscreenocr.floatings.manager + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.di.MainCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.StateOperator.Companion.SCREENSHOT_DELAY +import tw.firemaples.onscreenocr.log.FirebaseEvent +import tw.firemaples.onscreenocr.pages.setting.SettingManager +import tw.firemaples.onscreenocr.recognition.RecognitionResult +import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType +import tw.firemaples.onscreenocr.recognition.TextRecognizer +import tw.firemaples.onscreenocr.screenshot.ScreenExtractor +import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.translator.TranslationResult +import tw.firemaples.onscreenocr.translator.Translator +import tw.firemaples.onscreenocr.translator.azure.MicrosoftAzureTranslator +import tw.firemaples.onscreenocr.utils.Constants +import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.setReusable +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +interface StateOperator { + val action: SharedFlow + + companion object { + const val SCREENSHOT_DELAY = 100L + } +} + +@Singleton +class StateOperatorImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val stateNavigator: StateNavigator, + @MainCoroutineScope + private val scope: CoroutineScope, +) : StateOperator { + private val logger: Logger by lazy { Logger(this::class) } + + override val action = MutableSharedFlow() + + private val currentNavState: NavState + get() = stateNavigator.currentNavState.value + + init { + stateNavigator.navigationAction + .onEach { action -> + logger.debug("Receive navigationAction: $action") + when (action) { + NavigationAction.NavigateToScreenCircling -> + startScreenCircling() + + is NavigationAction.NavigateToScreenCircled -> + onAreaSelected( + parentRect = action.parentRect, + selectedRect = action.selectedRect, + ) + + NavigationAction.CancelScreenCircling -> + cancelScreenCircling() + + is NavigationAction.NavigateToScreenCapturing -> + startScreenCapturing( + ocrLang = action.ocrLang, + ocrProvider = action.ocrProvider, + ) + + is NavigationAction.NavigateToStartTranslation -> { + val croppedBitmap = currentNavState.getBitmap() + ?: throw IllegalStateException("Navigate to StartTranslation failed, bitmap is null: $currentNavState") + startTranslation( + croppedBitmap = croppedBitmap, + recognitionResult = action.recognitionResult, + ) + } + + NavigationAction.NavigateToIdle -> backToIdle() + + is NavigationAction.NavigateToTextRecognition -> TODO() + is NavigationAction.NavigateToTranslated -> TODO() + is NavigationAction.ShowError -> TODO() + } + }.launchIn(scope) + } + + private fun startScreenCircling() = scope.launch { + if (!Translator.getTranslator().checkResources(scope)) { + return@launch + } + + logger.debug("startScreenCircling()") + stateNavigator.updateState(NavState.ScreenCircling) + FirebaseEvent.logStartAreaSelection() + + action.emit(StateOperatorAction.ShowScreenCirclingView) + action.emit(StateOperatorAction.TopMainBar) + } + + private fun onAreaSelected(parentRect: Rect, selectedRect: Rect) = scope.launch { + logger.debug( + "onAreaSelected(), parentRect: $parentRect, " + + "selectedRect: $selectedRect," + + "selectedSize: ${selectedRect.width()}x${selectedRect.height()}" + ) + + stateNavigator.updateState( + NavState.ScreenCircled( + parentRect = parentRect, selectedRect = selectedRect, + ) + ) + } + + private fun cancelScreenCircling() = scope.launch { + logger.debug("cancelScreenCircling()") + stateNavigator.updateState(NavState.Idle) + action.emit(StateOperatorAction.HideScreenCirclingView) + } + + private fun startScreenCapturing( + ocrLang: String, + ocrProvider: TextRecognitionProviderType, + ) = scope.launch { + if (!Translator.getTranslator().checkResources(scope)) { + return@launch + } + + val state = currentNavState + if (state !is NavState.ScreenCircled) { + val error = "State should be ScreenCircled but $state" + logger.error(t = IllegalStateException(error)) + showError(error) + return@launch + } + val parentRect = state.parentRect + val selectedRect = state.selectedRect + logger.debug( + "startScreenCapturing(), " + + "parentRect: $parentRect, selectedRect: $selectedRect" + ) + + stateNavigator.updateState(NavState.ScreenCapturing) + + action.emit(StateOperatorAction.HideScreenCirclingView) + action.emit(StateOperatorAction.HideMainBar) + + delay(SCREENSHOT_DELAY) + + var bitmap: Bitmap? = null + try { + FirebaseEvent.logStartCaptureScreen() + val croppedBitmap = ScreenExtractor.extractBitmapFromScreen( + parentRect = parentRect, + cropRect = selectedRect, + ).also { + bitmap = it + } + FirebaseEvent.logCaptureScreenFinished() + + action.emit(StateOperatorAction.ShowMainBar) + + startRecognition( + ocrLang = ocrLang, + ocrProvider = ocrProvider, + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + ) + } catch (t: TimeoutCancellationException) { + logger.debug(t = t) + showError(context.getString(R.string.error_capture_screen_timeout)) + FirebaseEvent.logCaptureScreenFailed(t) + bitmap?.setReusable() + } catch (t: Throwable) { + logger.debug(t = t) + val errorMsg = + t.message ?: context.getString(R.string.error_unknown_error_capturing_screen) + showError(errorMsg) + FirebaseEvent.logCaptureScreenFailed(t) + bitmap?.setReusable() + } + } + + private fun startRecognition( + ocrLang: String, + ocrProvider: TextRecognitionProviderType, + croppedBitmap: Bitmap, + parentRect: Rect, + selectedRect: Rect, + ) = scope.launch { + stateNavigator.updateState(NavState.TextRecognizing(croppedBitmap)) + + try { + action.emit(StateOperatorAction.ResultViewStartRecognition) + val recognizer = TextRecognizer.getRecognizer(ocrProvider) + val language = TextRecognizer.getLanguage(ocrLang, ocrProvider)!! + + FirebaseEvent.logStartOCR(recognizer.name) + var result = withContext(Dispatchers.Default) { + recognizer.recognize( + lang = language, + bitmap = croppedBitmap, + ) + } + logger.debug("On text recognized: $result") +// croppedBitmap.recycle() // to be used in the text editor view + + // TODO move logic + if (SettingManager.removeSpacesInCJK) { + val cjkLang = arrayOf("zh", "ja", "ko") + if (cjkLang.contains(ocrLang.split("-").getOrNull(0))) { + result = result.copy( + result = result.result.replace(" ", "") + ) + } + logger.debug("Remove CJK spaces: $result") + } + + FirebaseEvent.logOCRFinished(recognizer.name) + + action.emit( + StateOperatorAction.ResultViewSetRecognized( + result = result, + parentRect = parentRect, + selectedRect = selectedRect, + croppedBitmap = croppedBitmap, + ) + ) + startTranslation( + croppedBitmap = croppedBitmap, + recognitionResult = result, + ) + } catch (e: Exception) { + val error = + if (e.message?.contains(Constants.errorInputImageIsTooSmall) == true) { + context.getString(R.string.error_selected_area_too_small) + } else + e.message + ?: context.getString(R.string.error_an_unknown_error_found_while_recognition_text) + + logger.warn(t = e) + showError(error) + FirebaseEvent.logOCRFailed( + TextRecognizer.getRecognizer(ocrProvider).name, e + ) + } + } + + private fun startTranslation( + croppedBitmap: Bitmap, + recognitionResult: RecognitionResult + ) = scope.launch { + try { + stateNavigator.updateState(NavState.TextTranslating(croppedBitmap)) + + val translator = Translator.getTranslator() + + action.emit( + StateOperatorAction.ResultViewStartTranslation( + translationProviderType = translator.type, + ) + ) + + FirebaseEvent.logStartTranslationText( + text = recognitionResult.result, + fromLang = recognitionResult.langCode, + translator = translator, + ) + + val translationResult = translator.translate( + text = recognitionResult.result, + sourceLangCode = recognitionResult.langCode, + ) + + onTranslated( + translator = translator, + translationResult = translationResult, + recognitionResult = recognitionResult, + ) + } catch (e: Exception) { + logger.warn(t = e) + FirebaseEvent.logException(e) + action.emit(StateOperatorAction.ResultViewBackToIdle) + showError(e.message ?: "Unknown error found while translating") + } + } + + private suspend fun onTranslated( + translator: Translator, + translationResult: TranslationResult, + recognitionResult: RecognitionResult, + ) { + when (translationResult) { + TranslationResult.OuterTranslatorLaunched -> { + FirebaseEvent.logTranslationTextFinished(translator) + action.emit(StateOperatorAction.ResultViewBackToIdle) + backToIdle() + } + + is TranslationResult.SourceLangNotSupport -> { + FirebaseEvent.logTranslationSourceLangNotSupport( + translator, recognitionResult.langCode, + ) + action.emit( + StateOperatorAction.ResultViewTextTranslated( + Result.SourceLangNotSupport( + ocrText = recognitionResult.result, + boundingBoxes = recognitionResult.boundingBoxes, + providerType = translationResult.type, + ) + ) + ) + } + + TranslationResult.OCROnlyResult -> { + FirebaseEvent.logTranslationTextFinished(translator) + action.emit( + StateOperatorAction.ResultViewTextTranslated( + Result.OCROnly( + ocrText = recognitionResult.result, + boundingBoxes = recognitionResult.boundingBoxes, + ) + ) + ) + } + + is TranslationResult.TranslatedResult -> { + FirebaseEvent.logTranslationTextFinished(translator) + action.emit( + StateOperatorAction.ResultViewTextTranslated( + Result.Translated( + ocrText = recognitionResult.result, + boundingBoxes = recognitionResult.boundingBoxes, + translatedText = translationResult.result, + providerType = translationResult.type, + ) + ) + ) + } + + is TranslationResult.TranslationFailed -> { + FirebaseEvent.logTranslationTextFailed(translator) + val error = translationResult.error + + if (error is MicrosoftAzureTranslator.Error) { + FirebaseEvent.logMicrosoftTranslationError(error) + } + + action.emit(StateOperatorAction.ResultViewBackToIdle) + + if (error is IOException) { + showError(context.getString(R.string.error_can_not_connect_to_translation_server)) + } else { + FirebaseEvent.logException(error) + showError( + error.localizedMessage + ?: context.getString(R.string.error_unknown) + ) + } + } + } + } + + private fun showError(error: String) = scope.launch { + logger.error("showError(): $error") + backToIdle() + action.emit(StateOperatorAction.ShowErrorDialog(error)) + } + + private fun backToIdle() = scope.launch { + if (currentNavState != NavState.Idle) + stateNavigator.updateState(NavState.Idle) + action.emit(StateOperatorAction.ShowMainBar) + + currentNavState.getBitmap()?.setReusable() + } + + private fun NavState.getBitmap(): Bitmap? = + (this as? BitmapIncluded)?.bitmap +} + +sealed interface StateOperatorAction { + data object TopMainBar : StateOperatorAction + data object HideMainBar : StateOperatorAction + data object ShowMainBar : StateOperatorAction + data object ShowScreenCirclingView : StateOperatorAction + data object HideScreenCirclingView : StateOperatorAction + data object ResultViewStartRecognition : StateOperatorAction //TODO subscribe state in view + data class ResultViewSetRecognized( + val result: RecognitionResult, + val parentRect: Rect, + val selectedRect: Rect, + val croppedBitmap: Bitmap, + ) : StateOperatorAction //TODO subscribe state in view + + data class ResultViewStartTranslation( + val translationProviderType: TranslationProviderType, + ) : StateOperatorAction //TODO subscribe state in view + + data class ResultViewTextTranslated(val result: Result) : + StateOperatorAction //TODO subscribe state in view + + data object ResultViewBackToIdle : StateOperatorAction //TODO subscribe state in view + + data class ShowErrorDialog(val error: String) : StateOperatorAction +} + +sealed class Result( + open val ocrText: String, + open val boundingBoxes: List, +) { + data class Translated( + override val ocrText: String, + override val boundingBoxes: List, + val translatedText: String, + val providerType: TranslationProviderType, + ) : Result(ocrText, boundingBoxes) + + data class SourceLangNotSupport( + override val ocrText: String, + override val boundingBoxes: List, + val providerType: TranslationProviderType, + ) : Result(ocrText, boundingBoxes) + + data class OCROnly( + override val ocrText: String, + override val boundingBoxes: List, + ) : Result(ocrText, boundingBoxes) +} From 2c2cc06fe6d246f3f5284bd12da111965f306ffd Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 7 Jan 2024 05:51:22 +0900 Subject: [PATCH 013/121] Fix hide action --- .../compose/base/ComposeFloatingView.kt | 47 ++++++++++++++++--- .../compose/mainbar/MainBarFloatingView.kt | 4 +- .../manager/FloatingViewCoordinator.kt | 6 +-- .../floatings/manager/StateNavigator.kt | 2 +- .../floatings/manager/StateOperator.kt | 9 ++-- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 63a48f7b..14f97702 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -11,8 +11,13 @@ import android.view.OrientationEventListener import android.view.WindowManager import androidx.annotation.CallSuper import androidx.annotation.MainThread -import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleRegistry @@ -85,8 +90,14 @@ abstract class ComposeFloatingView(protected val context: Context) { private val homeButtonWatcher: HomeButtonWatcher by lazy { HomeButtonWatcher( context = context, - onHomeButtonPressed = { onHomeButtonPressed() }, - onHomeButtonLongPressed = { onHomeButtonLongPressed() }, + onHomeButtonPressed = { + logger.debug("onHomeButtonPressed()") + onHomeButtonPressed() + }, + onHomeButtonLongPressed = { + logger.debug("onHomeButtonLongPressed()") + onHomeButtonLongPressed() + }, ) } @@ -130,11 +141,35 @@ abstract class ComposeFloatingView(protected val context: Context) { // } protected val rootView by lazy { ComposeView(context).apply { - + setOnKeyListener { v, keyCode, event -> //TODO check or remove + logger.debug("setOnKeyListener, keyCode: $keyCode, event: $event") + false + } setContent { AppTheme { - logger.debug("is dark theme: ${isSystemInDarkTheme()}") - RootContent() + Box( + modifier = Modifier + .onKeyEvent { event -> //TODO check or remove + logger.debug("onKeyEvent: $event") + false + } + .onPreviewKeyEvent { event -> //TODO check or remove + logger.debug("onPreviewKeyEvent: $event") + false + } + ) { + LaunchedEffect(Unit) { + onAttachedToScreen() + } + + DisposableEffect(Unit) { + onDispose { + onDetachedFromScreen() + } + } + + RootContent() + } } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index 70a267f1..f58c0e6d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -89,8 +89,8 @@ class MainBarFloatingView @Inject constructor( override val fadeOutDestinationAlpha: Float get() = viewModel.getFadeOutDestinationAlpha() - override fun attachToScreen() { - super.attachToScreen() + override fun onAttachedToScreen() { + super.onAttachedToScreen() viewModel.onAttachedToScreen() } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index 68950084..e884dd5d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -90,7 +90,7 @@ class FloatingViewCoordinator @Inject constructor( resultView.onUserDismiss = { scope.launch { resultView.backToIdle() - stateNavigator.navigate(NavigationAction.NavigateToIdle) + stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = true)) } } } @@ -118,9 +118,7 @@ class FloatingViewCoordinator @Inject constructor( fun detachAllViews() { scope.launch { - stateNavigator.navigate(NavigationAction.NavigateToIdle) - } - scope.launch { + stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = false)) hideMainBar() FloatingView.detachAllFloatingViews() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 9a920689..ed307d6e 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -78,7 +78,7 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { } sealed interface NavigationAction { - data object NavigateToIdle : NavigationAction + data class NavigateToIdle(val showMainBar: Boolean) : NavigationAction data object NavigateToScreenCircling : NavigationAction diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index ccd0de98..06b00198 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -88,7 +88,8 @@ class StateOperatorImpl @Inject constructor( ) } - NavigationAction.NavigateToIdle -> backToIdle() + is NavigationAction.NavigateToIdle -> + backToIdle(showMainBar = action.showMainBar) is NavigationAction.NavigateToTextRecognition -> TODO() is NavigationAction.NavigateToTranslated -> TODO() @@ -380,10 +381,12 @@ class StateOperatorImpl @Inject constructor( action.emit(StateOperatorAction.ShowErrorDialog(error)) } - private fun backToIdle() = scope.launch { + private fun backToIdle(showMainBar: Boolean = true) = scope.launch { if (currentNavState != NavState.Idle) stateNavigator.updateState(NavState.Idle) - action.emit(StateOperatorAction.ShowMainBar) + + if (showMainBar) + action.emit(StateOperatorAction.ShowMainBar) currentNavState.getBitmap()?.setReusable() } From 324d3bce2c72274bb5fba04ad8dba162ee777262 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 10 Jan 2024 22:06:45 +0900 Subject: [PATCH 014/121] Setup detekt for Compose --- build.gradle | 4 +++ detekt.yml | 85 +++++++++++++++++++++++++++++++++++++++++++++++ main/build.gradle | 3 ++ 3 files changed, 92 insertions(+) create mode 100644 detekt.yml diff --git a/build.gradle b/build.gradle index 14e98d20..74841978 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ buildscript { ext.nav_version = "2.6.0" // require SDK 34 from 2.7.0 repositories { google() + gradlePluginPortal() mavenCentral() maven { url "https://jitpack.io" @@ -24,6 +25,9 @@ buildscript { // Refactor classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + + // Detekt + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.3" } } diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..c7f9f196 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,85 @@ +# From https://mrmans0n.github.io/compose-rules/detekt/ +Compose: + ComposableAnnotationNaming: + active: true + ComposableNaming: + active: true + # -- You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters) + # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter + ComposableParamOrder: + active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType + CompositionLocalAllowlist: + active: true + # -- You can optionally define a list of CompositionLocals that are allowed here + # allowedCompositionLocals: LocalSomething,LocalSomethingElse + CompositionLocalNaming: + active: true + ContentEmitterReturningValues: + active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + DefaultsVisibility: + active: true + ModifierClickableOrder: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierComposable: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierMissing: + active: true + # -- You can optionally control the visibility of which composables to check for here + # -- Possible values are: `only_public`, `public_and_internal` and `all` (default is `only_public`) + # checkModifiersForVisibility: only_public + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierNaming: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierNotUsedAtRoot: + active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierReused: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierWithoutDefault: + active: true + MultipleEmitters: + active: true + # -- You can optionally add your own composables here that will count as content emitters + # contentEmitters: MyComposable,MyOtherComposable + # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals) + # contentEmittersDenylist: MyNonEmitterComposable + MutableParams: + active: true + MutableStateParam: + active: true + PreviewAnnotationNaming: + active: true + PreviewPublic: + active: true + RememberMissing: + active: true + RememberContentMissing: + active: true + UnstableCollections: + active: true + ViewModelForwarding: + active: true + # -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here: + # allowedStateHolderNames: .*ViewModel,.*Presenter + # -- You can optionally add an allowlist for Composable names that won't be affected by this rule + # allowedForwarding: .*Content,.*FancyStuff + ViewModelInjection: + active: true + # -- You can optionally add your own ViewModel factories here + # viewModelFactories: hiltViewModel,potatoViewModel \ No newline at end of file diff --git a/main/build.gradle b/main/build.gradle index 909ca7e4..38369b70 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -7,6 +7,7 @@ plugins { id 'com.google.firebase.crashlytics' id 'com.google.firebase.firebase-perf' id 'com.google.dagger.hilt.android' + id "io.gitlab.arturbosch.detekt" } def buildParams = getGradle().getStartParameter().toString().toLowerCase() @@ -110,6 +111,8 @@ kapt { } dependencies { + detektPlugins "io.nlopez.compose.rules:detekt:0.3.9" + implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation 'androidx.core:core-ktx:1.10.1' From b394ae6e95e868a9e8e76bd2f6af663100adc783 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 10 Jan 2024 23:25:21 +0900 Subject: [PATCH 015/121] Draw result view UI & state --- .../floatings/compose/base/AppTheme.kt | 5 + .../compose/base/BackButtonTrackerView.kt | 44 +++ .../floatings/compose/base/ComposeUtils.kt | 8 + .../compose/resultview/ResultViewContent.kt | 301 ++++++++++++++++++ .../resultview/ResultViewFloatingView.kt | 263 +++++++++++++++ .../compose/resultview/ResultViewModel.kt | 139 ++++++++ .../compose/resultview/ResultViewModule.kt | 13 + .../manager/FloatingViewCoordinator.kt | 4 +- .../floatings/manager/StateNavigator.kt | 30 +- .../floatings/manager/StateOperator.kt | 81 ++++- .../wigets/BackButtonTrackerView.kt | 4 + 11 files changed, 874 insertions(+), 18 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/BackButtonTrackerView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModule.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt index 6b009433..f61442f1 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.sp lateinit var AppColorScheme: ColorScheme @@ -20,4 +21,8 @@ fun AppTheme( colorScheme = AppColorScheme, content = content, ) +} + +object FontSize { + val Small = 14.sp } \ No newline at end of file diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/BackButtonTrackerView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/BackButtonTrackerView.kt new file mode 100644 index 00000000..32a62315 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/BackButtonTrackerView.kt @@ -0,0 +1,44 @@ +package tw.firemaples.onscreenocr.floatings.compose.base + +import android.content.Context +import android.util.AttributeSet +import android.view.KeyEvent +import android.widget.FrameLayout + +open class BackButtonTrackerView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, + var onAttachedToWindow: (() -> Unit)? = null, + var onDetachedFromWindow: (() -> Unit)? = null, + var onBackButtonPressed: (() -> Boolean)? = null, +) : FrameLayout(context, attrs, defStyleAttr) { + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + onAttachedToWindow?.invoke() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + onDetachedFromWindow?.invoke() + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.keyCode == KeyEvent.KEYCODE_BACK && keyDispatcherState != null) { + if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { + keyDispatcherState.startTracking(event, this) + + return true + } else if (event.action == KeyEvent.ACTION_UP) { + keyDispatcherState.handleUpEvent(event) + + if (event.isTracking && !event.isCanceled) { + if (onBackButtonPressed?.invoke() == true) { + return true + } + } + } + } + + return super.dispatchKeyEvent(event) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt index 087bbe14..47fbd088 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt @@ -2,7 +2,9 @@ package tw.firemaples.onscreenocr.floatings.compose.base import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.Dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow @@ -22,3 +24,9 @@ fun Flow.collectOnLifecycleResumed(state: (T) -> Unit) { suspend fun MutableSharedFlow.awaitForSubscriber() { subscriptionCount.first { it > 0 } } + +@Composable +fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() } + +@Composable +fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt new file mode 100644 index 00000000..65b5509c --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -0,0 +1,301 @@ +package tw.firemaples.onscreenocr.floatings.compose.resultview + +import android.graphics.Rect +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme +import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme +import tw.firemaples.onscreenocr.floatings.compose.base.FontSize +import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp + +@Composable +fun ResultViewContent( + resultViewModel: ResultViewModel, +) { + val state by resultViewModel.state.collectAsState() + + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .background(colorResource(id = R.color.dialogOutside)) + .clickable(onClick = resultViewModel::onDialogOutsideClicked), + ) { + state.highlightArea?.let { + TextHighlightBox( + highlightArea = it, + ) + } + ResultPanel( + ocrState = state.ocrState, + translationState = state.translationState, + ) + } +} + +@Composable +private fun TextHighlightBox(highlightArea: Rect) { + Box( + modifier = Modifier + .absoluteOffset( + x = highlightArea.left.pxToDp(), + y = highlightArea.top.pxToDp(), + ) + .size( + width = highlightArea + .width() + .pxToDp(), + height = highlightArea + .height() + .pxToDp(), + ) + .background(colorResource(id = R.color.resultView_recognizedBoundingBoxes)) + ) +} + +@Composable +private fun ResultPanel( + ocrState: OCRState, + translationState: TranslationState, +) { + Column( + modifier = Modifier + .widthIn(max = 300.dp) + .background(AppColorScheme.background) + .padding(horizontal = 6.dp, vertical = 4.dp), + ) { + OCRToolBar( + textSearchEnabled = ocrState.textSearchEnabled, + ) + OCRTextArea( + showProcessing = ocrState.showProcessing, + ocrText = ocrState.ocrText, + ) + + if (translationState.showTranslationArea) { + TranslationToolBar() + TranslationTextArea( + showProcessing = translationState.showProcessing, + translatedText = translationState.translatedText, + ) + TranslationProviderBar( + translationProviderText = translationState.translationProviderText, + translationProviderIcon = translationState.translationProviderIcon, + ) + } + } +} + +@Composable +private fun OCRToolBar(textSearchEnabled: Boolean) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.text_ocr_text), + fontSize = FontSize.Small, + fontWeight = FontWeight.Bold, + ) + +// Image(painter = painterResource(id = R.drawable.ic_play), contentDescription = "") + + Spacer(modifier = Modifier.size(4.dp)) + + val textSearchTintColor = if (textSearchEnabled) + colorResource(id = R.color.md_blue_800) + else AppColorScheme.onBackground + Image( + painter = painterResource(id = R.drawable.ic_text_search), + contentDescription = "", + colorFilter = ColorFilter.tint(textSearchTintColor), + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_square_edit_outline), + contentDescription = "", + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_copy), + contentDescription = "", + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_font_size), + contentDescription = "", + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_google_translate), + contentDescription = "", + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_export), + contentDescription = "", + ) + } +} + +@Composable +private fun OCRTextArea( + showProcessing: Boolean, + ocrText: String?, +) { + if (showProcessing) { + CircularProgressIndicator() + } + + if (ocrText != null) { + //TODO implement text search selector + //TODO implement text overflow + Text(text = ocrText) + } +} + +@Composable +private fun TranslationToolBar() { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.text_translated_text), + fontSize = FontSize.Small, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_copy), + contentDescription = "", + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_google_translate), + contentDescription = "", + ) + } +} + +@Composable +private fun TranslationTextArea( + showProcessing: Boolean, + translatedText: String?, +) { + + if (showProcessing) { + CircularProgressIndicator() + } + + if (translatedText != null) { + //TODO implement text overflow + Text(text = translatedText) + } +} + +@Composable +private fun ColumnScope.TranslationProviderBar( + translationProviderText: String?, + translationProviderIcon: Int? +) { + if (translationProviderText != null || translationProviderIcon != null) { + Spacer(modifier = Modifier.size(2.dp)) + } + + if (translationProviderText != null) { + Text( + modifier = Modifier.align(Alignment.End), + text = translationProviderText, + color = AppColorScheme.secondary, + fontSize = 12.sp, + maxLines = 1, + ) + } + + if (translationProviderIcon != null) { + Image( + modifier = Modifier.align(Alignment.End), + painter = painterResource(id = translationProviderIcon), + contentDescription = "", + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ResultViewContentPreview() { + val state = ResultViewState( + highlightArea = Rect(10, 20, 80, 90), + ocrState = OCRState( + showProcessing = true, + ocrText = "Test OCR text", + textSearchEnabled = true, + ), + translationState = TranslationState( + showTranslationArea = true, + showProcessing = true, + translatedText = "Test result text", + translationProviderText = "Test Translation Provider", + translationProviderIcon = R.drawable.img_translated_by_google, + ), + ) + + val viewModel = object : ResultViewModel { + override val state: StateFlow + get() = MutableStateFlow(state) + override val action: SharedFlow + get() = MutableSharedFlow() + + override fun onDialogOutsideClicked() = Unit + } + + AppTheme { + ResultViewContent( + resultViewModel = viewModel, + ) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt new file mode 100644 index 00000000..8521bf13 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt @@ -0,0 +1,263 @@ +package tw.firemaples.onscreenocr.floatings.compose.resultview + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.WindowManager +import androidx.compose.runtime.Composable +import dagger.hilt.android.qualifiers.ApplicationContext +import tw.firemaples.onscreenocr.floatings.compose.base.ComposeFloatingView +import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed +import tw.firemaples.onscreenocr.floatings.manager.Result +import tw.firemaples.onscreenocr.recognition.RecognitionResult +import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.setReusable +import javax.inject.Inject + +class ResultViewFloatingView @Inject constructor( + @ApplicationContext context: Context, + private val viewModel: ResultViewModel, +) : ComposeFloatingView(context) { + companion object { + private const val LABEL_RECOGNIZED_TEXT = "Recognized text" + private const val LABEL_TRANSLATED_TEXT = "Translated text" + } + + private val logger: Logger by lazy { Logger(ResultViewFloatingView::class) } + +// override val layoutId: Int +// get() = R.layout.floating_result_view + + override val layoutWidth: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + override val layoutHeight: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + override val enableHomeButtonWatcher: Boolean + get() = true + +// private val binding: FloatingResultViewBinding = FloatingResultViewBinding.bind(rootLayout) + +// private val viewRoot: RelativeLayout = binding.viewRoot + + var onUserDismiss: (() -> Unit)? = null + +// private val viewResultWindow: View = binding.viewResultWindow + + private var unionRect: Rect = Rect() + + private var croppedBitmap: Bitmap? = null + + @Composable + override fun RootContent() { + viewModel.action.collectOnLifecycleResumed { action -> + when (action) { + ResultViewAction.Close ->{ + detachFromScreen() + } + + } + } + + ResultViewContent(viewModel) + } + + init { +// binding.resultPanel.setViews() + } + +// private fun ViewResultPanelBinding.setViews() { +// viewModel.displayOCROperationProgress.observe(lifecycleOwner) { +// pbOcrOperating.showOrHide(it) +// } +// viewModel.displayTranslationProgress.observe(lifecycleOwner) { +// pbTranslationOperating.showOrHide(it) +// } +// viewModel.displaySelectableText.observe(lifecycleOwner) { +// textSelectable.isChecked = it +// tvOcrText.showOrHide(!it) +// tvWordBreakOcrText.showOrHide(it) +// } +// viewModel.ocrText.observe(lifecycleOwner) { +// tvWordBreakOcrText.setContent(it?.text(), it?.locale() ?: Locale.getDefault()) +// tvOcrText.text = it?.text() +// } +// viewModel.translatedText.observe(lifecycleOwner) { +// if (it == null) { +// tvTranslatedText.text = null +// } else { +// val (text, color) = it +// tvTranslatedText.text = text +// tvTranslatedText.setTextColor(ContextCompat.getColor(context, color)) +// } +// +// reposition() +// } +// +// viewModel.displayRecognitionBlock.observe(lifecycleOwner) { +// groupRecognitionViews.showOrHide(it) +// } +// viewModel.displayTranslatedBlock.observe(lifecycleOwner) { +// groupTranslationViews.showOrHide(it) +// } +// +// viewModel.translationProviderText.observe(lifecycleOwner) { +// tvTranslationProvider.setTextOrGone(it) +// } +// viewModel.displayTranslatedByGoogle.observe(lifecycleOwner) { +// ivTranslatedByGoogle.showOrHide(it) +// } +// +// viewModel.displayRecognizedTextAreas.observe(lifecycleOwner) { +// val (boundingBoxes, unionRect) = it +// binding.viewTextBoundingBoxView.boundingBoxes = boundingBoxes +// updateSelectedAreas(unionRect) +// } +// +// viewModel.copyRecognizedText.observe(lifecycleOwner) { +// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, it) +// } +// +// viewModel.fontSize.observe(lifecycleOwner) { +// tvOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// tvWordBreakOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// tvTranslatedText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// } +// +// viewModel.displayTextInfoSearchView.observe(lifecycleOwner) { +// TextInfoSearchView(context, it.text, it.sourceLang, it.targetLang) +// .attachToScreen() +// } +// +// textSelectable.setOnCheckedChangeListener { _, checked -> +// viewModel.onTextSelectableChecked(checked) +// } +// tvWordBreakOcrText.onWordClicked = { word -> +// if (word != null) { +// viewModel.onWordSelected(word) +// tvWordBreakOcrText.clearSelection() +// } +// } +// tvOcrText.movementMethod = ScrollingMovementMethod() +// tvTranslatedText.movementMethod = ScrollingMovementMethod() +// viewRoot.clickOnce { onUserDismiss?.invoke() } +// btEditOCRText.clickOnce { +// showRecognizedTextEditor(viewModel.ocrText.value?.text() ?: "") +// } +// btCopyOCRText.clickOnce { +// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, viewModel.ocrText.value?.text() ?: "") +// } +// btCopyTranslatedText.clickOnce { +// Utils.copyToClipboard(LABEL_TRANSLATED_TEXT, tvTranslatedText.text.toString()) +// } +// btTranslateOCRTextWithGoogleTranslate.clickOnce { +// GoogleTranslateUtils.launchTranslator(viewModel.ocrText.value?.text() ?: "") +// onUserDismiss?.invoke() +// } +// btTranslateTranslatedTextWithGoogleTranslate.clickOnce { +// GoogleTranslateUtils.launchTranslator(tvTranslatedText.text.toString()) +// onUserDismiss?.invoke() +// } +// btShareOCRText.clickOnce { +// val ocrText = viewModel.ocrText.value?.text() ?: return@clickOnce +// Utils.shareText(ocrText) +// onUserDismiss?.invoke() +// } +// btAdjustFontSize.clickOnce { +// FontSizeAdjuster(context).attachToScreen() +// } +// } + +// private fun showRecognizedTextEditor(recognizedText: String) { +// RecognizedTextEditor( +// context = context, +// review = croppedBitmap, +// text = recognizedText, +// onSubmit = { +// if (it.isNotBlank() && it.trim() != recognizedText) { +// viewModel.onOCRTextEdited(it.trim()) +// } +// }, +// ).attachToScreen() +// } + + override fun onAttachedToScreen() { + super.onAttachedToScreen() +// viewResultWindow.visibility = View.INVISIBLE + } + + override fun onDetachedFromScreen() { + super.onDetachedFromScreen() + this.croppedBitmap?.setReusable() + this.croppedBitmap = null + } + + override fun onHomeButtonPressed() { + super.onHomeButtonPressed() + onUserDismiss?.invoke() + } + + fun startRecognition() { + attachToScreen() +// viewModel.startRecognition() + } + + fun textRecognized( + result: RecognitionResult, + parent: Rect, + selected: Rect, + croppedBitmap: Bitmap + ) { + this.croppedBitmap = croppedBitmap +// viewModel.textRecognized(result, parent, selected, rootView.getViewRect()) + } + + fun startTranslation(translationProviderType: TranslationProviderType) { +// viewModel.startTranslation(translationProviderType) + } + + fun textTranslated(result: Result) { +// viewModel.textTranslated(result) + } + + fun backToIdle() { + detachFromScreen() + } + + private fun updateSelectedAreas(unionRect: Rect) { + this.unionRect = unionRect + reposition() + } + + private fun reposition() { +// rootView.post { +// val parentRect = viewRoot.getViewRect() +// val anchorRect = Rect(unionRect).apply { +// top += parentRect.top +// left += parentRect.left +// bottom += parentRect.top +// right += parentRect.left +// } +// val windowRect = viewResultWindow.getViewRect() +// +// val (leftMargin, topMargin) = UIUtils.countViewPosition( +// anchorRect, parentRect, +// windowRect.width(), windowRect.height(), 2.dpToPx(), +// ) +// +// val layoutParams = +// (viewResultWindow.layoutParams as RelativeLayout.LayoutParams).apply { +// this.leftMargin = leftMargin +// this.topMargin = topMargin +// } +// +// viewRoot.updateViewLayout(viewResultWindow, layoutParams) +// +// viewRoot.post { +// viewResultWindow.visibility = View.VISIBLE +// } +// } + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt new file mode 100644 index 00000000..a2c9a13e --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -0,0 +1,139 @@ +package tw.firemaples.onscreenocr.floatings.compose.resultview + +import android.graphics.Rect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.NavigationAction +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import javax.inject.Inject + +interface ResultViewModel { + val state: StateFlow + val action: SharedFlow + + fun onDialogOutsideClicked() +} + +data class ResultViewState( + val highlightArea: Rect? = null, + val ocrState: OCRState = OCRState(), + val translationState: TranslationState = TranslationState(), +) + +data class OCRState( + val showProcessing: Boolean = false, + val ocrText: String? = null, + val textSearchEnabled: Boolean = false, +) + +data class TranslationState( + val showTranslationArea: Boolean = false, + val showProcessing: Boolean = false, + val translatedText: String? = null, + val translationProviderText: String? = null, + val translationProviderIcon: Int? = null, +) + +//sealed interface ResultViewState { +// data object Default : ResultViewState +// data object TextRecognizing : ResultViewState +// interface TextRecognized : ResultViewState { +// val parentRect: Rect +// val selectedRect: Rect +// val ocrText: String +// } +// +// data class TextTranslating( +// override val parentRect: Rect, +// override val selectedRect: Rect, +// override val ocrText: String, +// ) : TextRecognized +// +// data class OCROnlyResult( +// override val parentRect: Rect, +// override val selectedRect: Rect, +// override val ocrText: String, +// ) : TextRecognized +// +// data class TranslationResult( +// override val parentRect: Rect, +// override val selectedRect: Rect, +// override val ocrText: String, +// val translatedText: String, +// ) : TextRecognized +//} + +sealed interface ResultViewAction { + data object Close : ResultViewAction +} + +class ResultViewModelImpl @Inject constructor( + @MainImmediateCoroutineScope + private val scope: CoroutineScope, + private val stateNavigator: StateNavigator, +) : ResultViewModel { + override val state = MutableStateFlow(ResultViewState()) + override val action = MutableSharedFlow() + +// init { +// stateNavigator.currentNavState +// .onEach { navState -> +// updateViewStateWithNavState(navState) +// } +// .launchIn(scope) +// } + +// private fun updateViewStateWithNavState(navState: NavState) = scope.launch { +// when (navState) { +// is NavState.TextRecognizing -> +// state.value = ResultViewState.TextRecognizing +// +// is NavState.TextTranslating -> +// state.value = ResultViewState.TextTranslating( +// parentRect = navState.parentRect, +// selectedRect = navState.selectedRect, +// ocrText = navState.recognitionResult.result, +// ) +// +// is NavState.TextTranslated -> +// state.value = when (navState.resultInfo) { +// ResultInfo.OCROnly -> ResultViewState.OCROnlyResult( +// parentRect = navState.parentRect, +// selectedRect = navState.selectedRect, +// ocrText = navState.recognitionResult.result, +// ) +// +// is ResultInfo.Translated -> ResultViewState.TranslationResult( +// parentRect = navState.parentRect, +// selectedRect = navState.selectedRect, +// ocrText = navState.recognitionResult.result, +// translatedText = navState.resultInfo.translatedText, +// ) +// is ResultInfo.Error -> +// ResultViewState.Default //TODO +// } +// +// else -> { +// setToDefault() +// } +// } +// } +// +// private fun setToDefault() { +// scope.launch { +// state.value = ResultViewState.Default +// } +// } + + override fun onDialogOutsideClicked() { + scope.launch { + action.emit(ResultViewAction.Close) + stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = true)) + } + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModule.kt new file mode 100644 index 00000000..9080a236 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModule.kt @@ -0,0 +1,13 @@ +package tw.firemaples.onscreenocr.floatings.compose.resultview + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface ResultViewModule { + @Binds + fun bindResultViewModel(resultViewModelImpl: ResultViewModelImpl): ResultViewModel +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index e884dd5d..f12aab20 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.base.FloatingView import tw.firemaples.onscreenocr.floatings.compose.mainbar.MainBarFloatingView +import tw.firemaples.onscreenocr.floatings.compose.resultview.ResultViewFloatingView import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog -import tw.firemaples.onscreenocr.floatings.result.ResultView import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject @@ -24,7 +24,7 @@ class FloatingViewCoordinator @Inject constructor( private val stateNavigator: StateNavigator, stateOperator: StateOperator, private val mainBar: MainBarFloatingView, - private val resultView: ResultView, + private val resultView: ResultViewFloatingView, ) { private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index ed307d6e..5b713841 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -100,10 +100,19 @@ sealed interface NavigationAction { val selected: Rect, ) : NavigationAction + //TODO remove + @Deprecated("Replaced by ReStartTranslation") data class NavigateToStartTranslation( val recognitionResult: RecognitionResult, ) : NavigationAction + data class ReStartTranslation( + val croppedBitmap: Bitmap, + val parent: Rect, + val selected: Rect, + val recognitionResult: RecognitionResult, + ) : NavigationAction + data class NavigateToTranslated( val result: Result, ) : NavigationAction @@ -122,17 +131,32 @@ sealed class NavState { object ScreenCircling : NavState() data class ScreenCircled(val parentRect: Rect, val selectedRect: Rect) : NavState() object ScreenCapturing : NavState() - data class TextRecognizing(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + data class TextRecognizing( + val parentRect: Rect, + val selectedRect: Rect, + val croppedBitmap: Bitmap, + ) : NavState(), BitmapIncluded { override val bitmap: Bitmap get() = croppedBitmap } - data class TextTranslating(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + data class TextTranslating( + val parentRect: Rect, + val selectedRect: Rect, + val croppedBitmap: Bitmap, + val recognitionResult: RecognitionResult, + ) : NavState(), BitmapIncluded { override val bitmap: Bitmap get() = croppedBitmap } - data class TextTranslated(val croppedBitmap: Bitmap) : NavState(), BitmapIncluded { + data class TextTranslated( + val parentRect: Rect, + val selectedRect: Rect, + val croppedBitmap: Bitmap, + val recognitionResult: RecognitionResult, + val resultInfo: ResultInfo, + ) : NavState(), BitmapIncluded { override val bitmap: Bitmap get() = croppedBitmap } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index 06b00198..3707f826 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -80,10 +80,20 @@ class StateOperatorImpl @Inject constructor( ) is NavigationAction.NavigateToStartTranslation -> { - val croppedBitmap = currentNavState.getBitmap() - ?: throw IllegalStateException("Navigate to StartTranslation failed, bitmap is null: $currentNavState") + //TODO remove +// val croppedBitmap = currentNavState.getBitmap() +// ?: throw IllegalStateException("Navigate to StartTranslation failed, bitmap is null: $currentNavState") +// startTranslation( +// croppedBitmap = croppedBitmap, +// recognitionResult = action.recognitionResult, +// ) + } + + is NavigationAction.ReStartTranslation -> { startTranslation( - croppedBitmap = croppedBitmap, + croppedBitmap = action.croppedBitmap, + parentRect = action.parent, + selectedRect = action.selected, recognitionResult = action.recognitionResult, ) } @@ -202,7 +212,13 @@ class StateOperatorImpl @Inject constructor( parentRect: Rect, selectedRect: Rect, ) = scope.launch { - stateNavigator.updateState(NavState.TextRecognizing(croppedBitmap)) + stateNavigator.updateState( + NavState.TextRecognizing( + parentRect = parentRect, + selectedRect = selectedRect, + croppedBitmap = croppedBitmap, + ) + ) try { action.emit(StateOperatorAction.ResultViewStartRecognition) @@ -242,6 +258,8 @@ class StateOperatorImpl @Inject constructor( ) startTranslation( croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, recognitionResult = result, ) } catch (e: Exception) { @@ -262,10 +280,19 @@ class StateOperatorImpl @Inject constructor( private fun startTranslation( croppedBitmap: Bitmap, - recognitionResult: RecognitionResult + parentRect: Rect, + selectedRect: Rect, + recognitionResult: RecognitionResult, ) = scope.launch { try { - stateNavigator.updateState(NavState.TextTranslating(croppedBitmap)) + stateNavigator.updateState( + NavState.TextTranslating( + parentRect = parentRect, + selectedRect = selectedRect, + croppedBitmap = croppedBitmap, + recognitionResult = recognitionResult, + ) + ) val translator = Translator.getTranslator() @@ -315,14 +342,19 @@ class StateOperatorImpl @Inject constructor( FirebaseEvent.logTranslationSourceLangNotSupport( translator, recognitionResult.langCode, ) + val result = Result.SourceLangNotSupport( + ocrText = recognitionResult.result, + boundingBoxes = recognitionResult.boundingBoxes, + providerType = translationResult.type, + ) + //TODO +// stateNavigator.updateState( +// NavState.TextTranslated( +// +// ) +// ) action.emit( - StateOperatorAction.ResultViewTextTranslated( - Result.SourceLangNotSupport( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - providerType = translationResult.type, - ) - ) + StateOperatorAction.ResultViewTextTranslated(result) ) } @@ -402,6 +434,8 @@ sealed interface StateOperatorAction { data object ShowScreenCirclingView : StateOperatorAction data object HideScreenCirclingView : StateOperatorAction data object ResultViewStartRecognition : StateOperatorAction //TODO subscribe state in view + + @Deprecated("subscribe state in view") data class ResultViewSetRecognized( val result: RecognitionResult, val parentRect: Rect, @@ -409,13 +443,16 @@ sealed interface StateOperatorAction { val croppedBitmap: Bitmap, ) : StateOperatorAction //TODO subscribe state in view + @Deprecated("subscribe state in view") data class ResultViewStartTranslation( val translationProviderType: TranslationProviderType, ) : StateOperatorAction //TODO subscribe state in view + @Deprecated("subscribe state in view") data class ResultViewTextTranslated(val result: Result) : StateOperatorAction //TODO subscribe state in view + @Deprecated("subscribe state in view") data object ResultViewBackToIdle : StateOperatorAction //TODO subscribe state in view data class ShowErrorDialog(val error: String) : StateOperatorAction @@ -443,3 +480,21 @@ sealed class Result( override val boundingBoxes: List, ) : Result(ocrText, boundingBoxes) } + +sealed interface ResultInfo { + data class Translated( + val translatedText: String, + val providerType: TranslationProviderType, + ) : ResultInfo + + data class Error( + val providerType: TranslationProviderType, + val resultError: ResultError, + ) : ResultInfo + + data object OCROnly : ResultInfo +} + +enum class ResultError { + SourceLangNotSupport, +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/wigets/BackButtonTrackerView.kt b/main/src/main/java/tw/firemaples/onscreenocr/wigets/BackButtonTrackerView.kt index 6939eb9d..0d1b4d16 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/wigets/BackButtonTrackerView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/wigets/BackButtonTrackerView.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.AttributeSet import android.view.KeyEvent import android.widget.FrameLayout +import tw.firemaples.onscreenocr.utils.Logger open class BackButtonTrackerView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -12,6 +13,8 @@ open class BackButtonTrackerView @JvmOverloads constructor( var onBackButtonPressed: (() -> Boolean)? = null, ) : FrameLayout(context, attrs) { + private val logger by lazy { Logger(this::class) } + override fun onAttachedToWindow() { super.onAttachedToWindow() onAttachedToWindow?.invoke() @@ -23,6 +26,7 @@ open class BackButtonTrackerView @JvmOverloads constructor( } override fun dispatchKeyEvent(event: KeyEvent): Boolean { + logger.debug("dispatchKeyEvent(), $event") if (event.keyCode == KeyEvent.KEYCODE_BACK && keyDispatcherState != null) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { keyDispatcherState.startTracking(event, this) From 02a51182f9b8a1304935d67a737bd7e36d0ab6d4 Mon Sep 17 00:00:00 2001 From: firemaples Date: Fri, 12 Jan 2024 23:24:32 +0900 Subject: [PATCH 016/121] Display result view data --- .../compose/resultview/ResultViewContent.kt | 120 ++++++++-- .../resultview/ResultViewFloatingView.kt | 8 +- .../compose/resultview/ResultViewModel.kt | 224 +++++++++++------- .../floatings/manager/StateNavigator.kt | 2 + .../floatings/manager/StateOperator.kt | 43 +++- 5 files changed, 286 insertions(+), 111 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 65b5509c..9b5fb75d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -1,5 +1,6 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview +import android.content.res.Configuration import android.graphics.Rect import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -12,17 +13,25 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -39,31 +48,87 @@ import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme import tw.firemaples.onscreenocr.floatings.compose.base.FontSize import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp +import tw.firemaples.onscreenocr.utils.UIUtils +import tw.firemaples.onscreenocr.utils.dpToPx @Composable fun ResultViewContent( - resultViewModel: ResultViewModel, + viewModel: ResultViewModel, + requestRootLocationOnScreen: () -> Rect, ) { - val state by resultViewModel.state.collectAsState() + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + val rootLocation = requestRootLocationOnScreen.invoke() + viewModel.onRootViewPositioned( + xOffset = rootLocation.left, + yOffset = rootLocation.top, + ) + } BoxWithConstraints( modifier = Modifier .fillMaxSize() .background(colorResource(id = R.color.dialogOutside)) - .clickable(onClick = resultViewModel::onDialogOutsideClicked), + .clickable(onClick = viewModel::onDialogOutsideClicked), ) { - state.highlightArea?.let { + state.highlightArea.forEach { TextHighlightBox( highlightArea = it, ) } + + val xOffset = remember { mutableStateOf(0) } + val yOffset = remember { mutableStateOf(0) } + ResultPanel( + modifier = Modifier + .calculateOffset( + requestRootLocationOnScreen = requestRootLocationOnScreen, + highlightUnion = state.highlightUnion, + xOffset = xOffset, + yOffset = yOffset, + ) + .offset( + xOffset.value.pxToDp(), + yOffset.value.pxToDp(), + ), ocrState = state.ocrState, translationState = state.translationState, ) } } +private fun Modifier.calculateOffset( + requestRootLocationOnScreen: () -> Rect, + highlightUnion: Rect, + xOffset: MutableState, + yOffset: MutableState, +): Modifier = onGloballyPositioned { coordinates -> + val parentRect = requestRootLocationOnScreen.invoke() + val anchorRect = Rect(highlightUnion).apply { + top += parentRect.top + left += parentRect.left + bottom += parentRect.top + right += parentRect.left + } + + val bounds = coordinates.boundsInRoot() + val left = parentRect.left + bounds.left + val top = parentRect.top + bounds.top + val right = parentRect.left + bounds.right + val bottom = parentRect.top + bounds.bottom + val windowRect = Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) + + val (leftMargin, topMargin) = UIUtils.countViewPosition( + anchorRect, parentRect, + windowRect.width(), windowRect.height(), 2.dpToPx(), + ) + + xOffset.value = leftMargin + yOffset.value = topMargin +} + @Composable private fun TextHighlightBox(highlightArea: Rect) { Box( @@ -86,13 +151,18 @@ private fun TextHighlightBox(highlightArea: Rect) { @Composable private fun ResultPanel( + modifier: Modifier, ocrState: OCRState, translationState: TranslationState, ) { Column( - modifier = Modifier + modifier = modifier .widthIn(max = 300.dp) - .background(AppColorScheme.background) + .background( + color = AppColorScheme.background, + shape = RoundedCornerShape(8.dp), + ) + .clickable { } .padding(horizontal = 6.dp, vertical = 4.dp), ) { OCRToolBar( @@ -110,8 +180,8 @@ private fun ResultPanel( translatedText = translationState.translatedText, ) TranslationProviderBar( - translationProviderText = translationState.translationProviderText, - translationProviderIcon = translationState.translationProviderIcon, + translationProviderText = translationState.providerText, + translationProviderIcon = translationState.providerIcon, ) } } @@ -126,6 +196,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { text = stringResource(id = R.string.text_ocr_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, + color = AppColorScheme.onBackground, ) // Image(painter = painterResource(id = R.drawable.ic_play), contentDescription = "") @@ -184,13 +255,16 @@ private fun OCRTextArea( ocrText: String?, ) { if (showProcessing) { - CircularProgressIndicator() + ProgressIndicator() } if (ocrText != null) { //TODO implement text search selector //TODO implement text overflow - Text(text = ocrText) + Text( + text = ocrText, + color = AppColorScheme.onBackground, + ) } } @@ -203,6 +277,7 @@ private fun TranslationToolBar() { text = stringResource(id = R.string.text_translated_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, + color = AppColorScheme.onBackground, ) Spacer(modifier = Modifier.size(4.dp)) @@ -228,12 +303,15 @@ private fun TranslationTextArea( ) { if (showProcessing) { - CircularProgressIndicator() + ProgressIndicator() } if (translatedText != null) { //TODO implement text overflow - Text(text = translatedText) + Text( + text = translatedText, + color = AppColorScheme.onBackground, + ) } } @@ -265,11 +343,19 @@ private fun ColumnScope.TranslationProviderBar( } } +@Composable +private fun ProgressIndicator() { + CircularProgressIndicator( + modifier = Modifier.size(30.dp), + ) +} + @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ResultViewContentPreview() { val state = ResultViewState( - highlightArea = Rect(10, 20, 80, 90), + highlightArea = listOf(Rect(10, 20, 80, 90)), ocrState = OCRState( showProcessing = true, ocrText = "Test OCR text", @@ -279,8 +365,8 @@ private fun ResultViewContentPreview() { showTranslationArea = true, showProcessing = true, translatedText = "Test result text", - translationProviderText = "Test Translation Provider", - translationProviderIcon = R.drawable.img_translated_by_google, + providerText = "Test Translation Provider", + providerIcon = R.drawable.img_translated_by_google, ), ) @@ -290,12 +376,14 @@ private fun ResultViewContentPreview() { override val action: SharedFlow get() = MutableSharedFlow() + override fun onRootViewPositioned(xOffset: Int, yOffset: Int) = Unit override fun onDialogOutsideClicked() = Unit } AppTheme { ResultViewContent( - resultViewModel = viewModel, + viewModel = viewModel, + requestRootLocationOnScreen = { Rect() } ) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt index 8521bf13..434bfffd 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt @@ -12,6 +12,7 @@ import tw.firemaples.onscreenocr.floatings.manager.Result import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.getViewRect import tw.firemaples.onscreenocr.utils.setReusable import javax.inject.Inject @@ -54,14 +55,17 @@ class ResultViewFloatingView @Inject constructor( override fun RootContent() { viewModel.action.collectOnLifecycleResumed { action -> when (action) { - ResultViewAction.Close ->{ + ResultViewAction.Close -> { detachFromScreen() } } } - ResultViewContent(viewModel) + ResultViewContent( + viewModel = viewModel, + requestRootLocationOnScreen = rootView::getViewRect, + ) } init { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index a2c9a13e..d0feca6d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -1,26 +1,37 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview +import android.content.Context import android.graphics.Rect +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.NavigationAction +import tw.firemaples.onscreenocr.floatings.manager.ResultInfo import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.recognition.RecognitionResult +import tw.firemaples.onscreenocr.translator.TranslationProviderType import javax.inject.Inject interface ResultViewModel { val state: StateFlow val action: SharedFlow - + fun onRootViewPositioned(xOffset: Int, yOffset: Int) fun onDialogOutsideClicked() } data class ResultViewState( - val highlightArea: Rect? = null, + val highlightArea: List = listOf(), + val highlightUnion: Rect = Rect(), val ocrState: OCRState = OCRState(), val translationState: TranslationState = TranslationState(), ) @@ -35,100 +46,147 @@ data class TranslationState( val showTranslationArea: Boolean = false, val showProcessing: Boolean = false, val translatedText: String? = null, - val translationProviderText: String? = null, - val translationProviderIcon: Int? = null, + val providerText: String? = null, + val providerIcon: Int? = null, ) -//sealed interface ResultViewState { -// data object Default : ResultViewState -// data object TextRecognizing : ResultViewState -// interface TextRecognized : ResultViewState { -// val parentRect: Rect -// val selectedRect: Rect -// val ocrText: String -// } -// -// data class TextTranslating( -// override val parentRect: Rect, -// override val selectedRect: Rect, -// override val ocrText: String, -// ) : TextRecognized -// -// data class OCROnlyResult( -// override val parentRect: Rect, -// override val selectedRect: Rect, -// override val ocrText: String, -// ) : TextRecognized -// -// data class TranslationResult( -// override val parentRect: Rect, -// override val selectedRect: Rect, -// override val ocrText: String, -// val translatedText: String, -// ) : TextRecognized -//} - sealed interface ResultViewAction { data object Close : ResultViewAction } class ResultViewModelImpl @Inject constructor( + @ApplicationContext + private val context: Context, @MainImmediateCoroutineScope private val scope: CoroutineScope, private val stateNavigator: StateNavigator, ) : ResultViewModel { - override val state = MutableStateFlow(ResultViewState()) + override val state = MutableStateFlow(ResultViewState()) override val action = MutableSharedFlow() -// init { -// stateNavigator.currentNavState -// .onEach { navState -> -// updateViewStateWithNavState(navState) -// } -// .launchIn(scope) -// } - -// private fun updateViewStateWithNavState(navState: NavState) = scope.launch { -// when (navState) { -// is NavState.TextRecognizing -> -// state.value = ResultViewState.TextRecognizing -// -// is NavState.TextTranslating -> -// state.value = ResultViewState.TextTranslating( -// parentRect = navState.parentRect, -// selectedRect = navState.selectedRect, -// ocrText = navState.recognitionResult.result, -// ) -// -// is NavState.TextTranslated -> -// state.value = when (navState.resultInfo) { -// ResultInfo.OCROnly -> ResultViewState.OCROnlyResult( -// parentRect = navState.parentRect, -// selectedRect = navState.selectedRect, -// ocrText = navState.recognitionResult.result, -// ) -// -// is ResultInfo.Translated -> ResultViewState.TranslationResult( -// parentRect = navState.parentRect, -// selectedRect = navState.selectedRect, -// ocrText = navState.recognitionResult.result, -// translatedText = navState.resultInfo.translatedText, -// ) -// is ResultInfo.Error -> -// ResultViewState.Default //TODO -// } -// -// else -> { -// setToDefault() -// } -// } -// } -// -// private fun setToDefault() { -// scope.launch { -// state.value = ResultViewState.Default -// } -// } + private var rootViewXOffset: Int = 0 + private var rootViewYOffset: Int = 0 + + init { + stateNavigator.currentNavState + .onEach { navState -> + updateViewStateWithNavState(navState) + } + .launchIn(scope) + } + + private fun updateViewStateWithNavState(navState: NavState) = scope.launch { + when (navState) { + is NavState.TextRecognizing -> + state.update { + it.copy( + highlightArea = listOf(navState.selectedRect), + ocrState = it.ocrState.copy( + showProcessing = true, + ) + ) + } + + is NavState.TextTranslating -> + state.update { + val needTranslate = !navState.translationProviderType.nonTranslation + val (textAreas, unionArea) = calculateTextAreas( + navState.recognitionResult, + navState.parentRect, + navState.selectedRect, + ) + + it.copy( + highlightArea = textAreas, + highlightUnion = unionArea, + ocrState = it.ocrState.copy( + showProcessing = false, + ocrText = navState.recognitionResult.result, + ), + translationState = it.translationState.copy( + showTranslationArea = needTranslate, + showProcessing = needTranslate, + ) + ) + } + + is NavState.TextTranslated -> { + when (val resultInfo = navState.resultInfo) { + is ResultInfo.Error -> + setToDefault() + + ResultInfo.OCROnly -> + state.update { + it.copy( + translationState = it.translationState.copy( + showTranslationArea = false, + ) + ) + } + + is ResultInfo.Translated -> { + val providerType = resultInfo.providerType + val needTranslate = !providerType.nonTranslation + val providerIcon = + if (providerType == TranslationProviderType.GoogleMLKit) + R.drawable.img_translated_by_google + else null + + val providerLabel = if (providerIcon == null) { + val providerName = context.getString(providerType.nameRes) + "${context.getString(R.string.text_translated_by)} $providerName" + } else null + + state.update { + it.copy( + translationState = it.translationState.copy( + showTranslationArea = needTranslate, + showProcessing = false, + translatedText = resultInfo.translatedText, + providerText = providerLabel, + providerIcon = providerIcon, + ) + ) + } + } + } + } + + else -> { + setToDefault() + } + } + } + + private fun setToDefault() { + state.value = ResultViewState() + } + + private fun calculateTextAreas( + result: RecognitionResult, + parent: Rect, + selected: Rect, + ): Pair, Rect> { + val topOffset = parent.top + selected.top - rootViewYOffset + val leftOffset = parent.left + selected.left - rootViewXOffset + val textAreas = result.boundingBoxes.map { + Rect( + it.left + leftOffset, + it.top + topOffset, + it.right + leftOffset, + it.bottom + topOffset + ) + } + val unionRect = Rect() + textAreas.forEach { unionRect.union(it) } + + return textAreas to unionRect + } + + override fun onRootViewPositioned(xOffset: Int, yOffset: Int) { + rootViewXOffset = xOffset + rootViewYOffset = yOffset + } override fun onDialogOutsideClicked() { scope.launch { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 5b713841..abc5393c 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType +import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -145,6 +146,7 @@ sealed class NavState { val selectedRect: Rect, val croppedBitmap: Bitmap, val recognitionResult: RecognitionResult, + val translationProviderType: TranslationProviderType, ) : NavState(), BitmapIncluded { override val bitmap: Bitmap get() = croppedBitmap diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index 3707f826..b465d98c 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -285,17 +285,18 @@ class StateOperatorImpl @Inject constructor( recognitionResult: RecognitionResult, ) = scope.launch { try { + val translator = Translator.getTranslator() + stateNavigator.updateState( NavState.TextTranslating( parentRect = parentRect, selectedRect = selectedRect, croppedBitmap = croppedBitmap, recognitionResult = recognitionResult, + translationProviderType = translator.type, ) ) - val translator = Translator.getTranslator() - action.emit( StateOperatorAction.ResultViewStartTranslation( translationProviderType = translator.type, @@ -314,9 +315,12 @@ class StateOperatorImpl @Inject constructor( ) onTranslated( + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + recognitionResult = recognitionResult, translator = translator, translationResult = translationResult, - recognitionResult = recognitionResult, ) } catch (e: Exception) { logger.warn(t = e) @@ -327,9 +331,12 @@ class StateOperatorImpl @Inject constructor( } private suspend fun onTranslated( + croppedBitmap: Bitmap, + parentRect: Rect, + selectedRect: Rect, + recognitionResult: RecognitionResult, translator: Translator, translationResult: TranslationResult, - recognitionResult: RecognitionResult, ) { when (translationResult) { TranslationResult.OuterTranslatorLaunched -> { @@ -347,15 +354,10 @@ class StateOperatorImpl @Inject constructor( boundingBoxes = recognitionResult.boundingBoxes, providerType = translationResult.type, ) - //TODO -// stateNavigator.updateState( -// NavState.TextTranslated( -// -// ) -// ) action.emit( StateOperatorAction.ResultViewTextTranslated(result) ) + showError(context.getString(R.string.msg_translator_provider_does_not_support_the_ocr_lang)) } TranslationResult.OCROnlyResult -> { @@ -368,6 +370,15 @@ class StateOperatorImpl @Inject constructor( ) ) ) + stateNavigator.updateState( + NavState.TextTranslated( + parentRect = parentRect, + selectedRect = selectedRect, + croppedBitmap = croppedBitmap, + recognitionResult = recognitionResult, + resultInfo = ResultInfo.OCROnly, + ) + ) } is TranslationResult.TranslatedResult -> { @@ -382,6 +393,18 @@ class StateOperatorImpl @Inject constructor( ) ) ) + stateNavigator.updateState( + NavState.TextTranslated( + parentRect = parentRect, + selectedRect = selectedRect, + croppedBitmap = croppedBitmap, + recognitionResult = recognitionResult, + resultInfo = ResultInfo.Translated( + translatedText = translationResult.result, + providerType = translationResult.type, + ), + ) + ) } is TranslationResult.TranslationFailed -> { From b61a54d03bc5b9098041574da715fc109f00fde2 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 13 Jan 2024 00:33:28 +0900 Subject: [PATCH 017/121] Implement basic result view buttons --- .../compose/resultview/ResultViewContent.kt | 67 +++++++-- .../resultview/ResultViewFloatingView.kt | 51 +++++-- .../compose/resultview/ResultViewModel.kt | 132 +++++++++++++++++- .../floatings/manager/StateNavigator.kt | 20 +-- .../floatings/manager/StateOperator.kt | 4 +- 5 files changed, 231 insertions(+), 43 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 9b5fb75d..a65eac13 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -93,6 +93,8 @@ fun ResultViewContent( xOffset.value.pxToDp(), yOffset.value.pxToDp(), ), + viewModel = viewModel, + textSearchEnabled = state.textSearchEnabled, ocrState = state.ocrState, translationState = state.translationState, ) @@ -152,6 +154,8 @@ private fun TextHighlightBox(highlightArea: Rect) { @Composable private fun ResultPanel( modifier: Modifier, + viewModel: ResultViewModel, + textSearchEnabled: Boolean, ocrState: OCRState, translationState: TranslationState, ) { @@ -166,7 +170,13 @@ private fun ResultPanel( .padding(horizontal = 6.dp, vertical = 4.dp), ) { OCRToolBar( - textSearchEnabled = ocrState.textSearchEnabled, + textSearchEnabled = textSearchEnabled, + onSearchClicked = viewModel::onTextSearchClicked, + onEditClicked = viewModel::onOCRTextEditClicked, + onCopyClicked = { viewModel.onCopyClicked(TextType.OCRText) }, + onFontSizeClicked = viewModel::onAdjustFontSizeClicked, + onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.OCRText) }, + onExportClicked = viewModel::onShareOCRTextClicked, ) OCRTextArea( showProcessing = ocrState.showProcessing, @@ -174,7 +184,10 @@ private fun ResultPanel( ) if (translationState.showTranslationArea) { - TranslationToolBar() + TranslationToolBar( + onCopyClicked = { viewModel.onCopyClicked(TextType.TranslationResult) }, + onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.TranslationResult) } + ) TranslationTextArea( showProcessing = translationState.showProcessing, translatedText = translationState.translatedText, @@ -188,7 +201,15 @@ private fun ResultPanel( } @Composable -private fun OCRToolBar(textSearchEnabled: Boolean) { +private fun OCRToolBar( + textSearchEnabled: Boolean, + onSearchClicked: () -> Unit, + onEditClicked: () -> Unit, + onCopyClicked: () -> Unit, + onFontSizeClicked: () -> Unit, + onGoogleTranslateClicked: () -> Unit, + onExportClicked: () -> Unit, +) { Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -207,6 +228,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { colorResource(id = R.color.md_blue_800) else AppColorScheme.onBackground Image( + modifier = Modifier.clickable(onClick = onSearchClicked), painter = painterResource(id = R.drawable.ic_text_search), contentDescription = "", colorFilter = ColorFilter.tint(textSearchTintColor), @@ -215,6 +237,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onEditClicked), painter = painterResource(id = R.drawable.ic_square_edit_outline), contentDescription = "", ) @@ -222,6 +245,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onCopyClicked), painter = painterResource(id = R.drawable.ic_copy), contentDescription = "", ) @@ -229,6 +253,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onFontSizeClicked), painter = painterResource(id = R.drawable.ic_font_size), contentDescription = "", ) @@ -236,6 +261,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onGoogleTranslateClicked), painter = painterResource(id = R.drawable.ic_google_translate), contentDescription = "", ) @@ -243,6 +269,7 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onExportClicked), painter = painterResource(id = R.drawable.ic_export), contentDescription = "", ) @@ -252,13 +279,11 @@ private fun OCRToolBar(textSearchEnabled: Boolean) { @Composable private fun OCRTextArea( showProcessing: Boolean, - ocrText: String?, + ocrText: String, ) { if (showProcessing) { ProgressIndicator() - } - - if (ocrText != null) { + } else { //TODO implement text search selector //TODO implement text overflow Text( @@ -266,10 +291,14 @@ private fun OCRTextArea( color = AppColorScheme.onBackground, ) } + } @Composable -private fun TranslationToolBar() { +private fun TranslationToolBar( + onCopyClicked: () -> Unit, + onGoogleTranslateClicked: () -> Unit, +) { Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -283,6 +312,7 @@ private fun TranslationToolBar() { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onCopyClicked), painter = painterResource(id = R.drawable.ic_copy), contentDescription = "", ) @@ -290,6 +320,7 @@ private fun TranslationToolBar() { Spacer(modifier = Modifier.size(4.dp)) Image( + modifier = Modifier.clickable(onClick = onGoogleTranslateClicked), painter = painterResource(id = R.drawable.ic_google_translate), contentDescription = "", ) @@ -299,20 +330,19 @@ private fun TranslationToolBar() { @Composable private fun TranslationTextArea( showProcessing: Boolean, - translatedText: String?, + translatedText: String, ) { if (showProcessing) { ProgressIndicator() - } - - if (translatedText != null) { + } else { //TODO implement text overflow Text( text = translatedText, color = AppColorScheme.onBackground, ) } + } @Composable @@ -354,12 +384,14 @@ private fun ProgressIndicator() { @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ResultViewContentPreview() { + val areaRect = Rect(10, 20, 80, 90) val state = ResultViewState( - highlightArea = listOf(Rect(10, 20, 80, 90)), + highlightArea = listOf(areaRect), + highlightUnion = areaRect, + textSearchEnabled = true, ocrState = OCRState( showProcessing = true, ocrText = "Test OCR text", - textSearchEnabled = true, ), translationState = TranslationState( showTranslationArea = true, @@ -378,6 +410,13 @@ private fun ResultViewContentPreview() { override fun onRootViewPositioned(xOffset: Int, yOffset: Int) = Unit override fun onDialogOutsideClicked() = Unit + override fun onTextSearchClicked() = Unit + override fun onOCRTextEditClicked() = Unit + override fun onOCRTextEdited(text: String) = Unit + override fun onCopyClicked(textType: TextType) = Unit + override fun onAdjustFontSizeClicked() = Unit + override fun onGoogleTranslateClicked(textType: TextType) = Unit + override fun onShareOCRTextClicked() = Unit } AppTheme { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt index 434bfffd..f03c2592 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt @@ -9,9 +9,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.floatings.compose.base.ComposeFloatingView import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed import tw.firemaples.onscreenocr.floatings.manager.Result +import tw.firemaples.onscreenocr.floatings.recognizedTextEditor.RecognizedTextEditor +import tw.firemaples.onscreenocr.floatings.result.FontSizeAdjuster import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.translator.utils.GoogleTranslateUtils import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.Utils import tw.firemaples.onscreenocr.utils.getViewRect import tw.firemaples.onscreenocr.utils.setReusable import javax.inject.Inject @@ -55,10 +59,27 @@ class ResultViewFloatingView @Inject constructor( override fun RootContent() { viewModel.action.collectOnLifecycleResumed { action -> when (action) { + is ResultViewAction.LaunchGoogleTranslator -> { + GoogleTranslateUtils.launchTranslator(action.text) + } + + is ResultViewAction.ShareText -> { + Utils.shareText(action.text) + } + + ResultViewAction.ShowFontSizeAdjuster -> + FontSizeAdjuster(context).attachToScreen() + + is ResultViewAction.ShowOCRTextEditor -> + showRecognizedTextEditor( + text = action.text, + croppedBitmap = action.croppedBitmap, + onTextEdited = viewModel::onOCRTextEdited, + ) + ResultViewAction.Close -> { detachFromScreen() } - } } @@ -174,18 +195,22 @@ class ResultViewFloatingView @Inject constructor( // } // } -// private fun showRecognizedTextEditor(recognizedText: String) { -// RecognizedTextEditor( -// context = context, -// review = croppedBitmap, -// text = recognizedText, -// onSubmit = { -// if (it.isNotBlank() && it.trim() != recognizedText) { -// viewModel.onOCRTextEdited(it.trim()) -// } -// }, -// ).attachToScreen() -// } + private fun showRecognizedTextEditor( + text: String, + croppedBitmap: Bitmap, + onTextEdited: (String) -> Unit, + ) { + RecognizedTextEditor( + context = context, + review = croppedBitmap, + text = text, + onSubmit = { + if (it.isNotBlank() && it.trim() != text) { + onTextEdited.invoke(it.trim()) + } + }, + ).attachToScreen() + } override fun onAttachedToScreen() { super.onAttachedToScreen() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index d0feca6d..5af4ab04 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -1,6 +1,7 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.Context +import android.graphics.Bitmap import android.graphics.Rect import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope @@ -14,12 +15,15 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.BitmapIncluded import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.ResultInfo import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.utils.Logger +import tw.firemaples.onscreenocr.utils.Utils import javax.inject.Inject interface ResultViewModel { @@ -27,33 +31,48 @@ interface ResultViewModel { val action: SharedFlow fun onRootViewPositioned(xOffset: Int, yOffset: Int) fun onDialogOutsideClicked() + fun onTextSearchClicked() + fun onOCRTextEditClicked() + fun onOCRTextEdited(text: String) + fun onCopyClicked(textType: TextType) + fun onAdjustFontSizeClicked() + fun onGoogleTranslateClicked(textType: TextType) + fun onShareOCRTextClicked() } data class ResultViewState( val highlightArea: List = listOf(), val highlightUnion: Rect = Rect(), + val textSearchEnabled: Boolean = false, val ocrState: OCRState = OCRState(), val translationState: TranslationState = TranslationState(), ) data class OCRState( val showProcessing: Boolean = false, - val ocrText: String? = null, - val textSearchEnabled: Boolean = false, + val ocrText: String = "", ) data class TranslationState( val showTranslationArea: Boolean = false, val showProcessing: Boolean = false, - val translatedText: String? = null, + val translatedText: String = "", val providerText: String? = null, val providerIcon: Int? = null, ) sealed interface ResultViewAction { + data class ShowOCRTextEditor(val text: String, val croppedBitmap: Bitmap) : ResultViewAction + data object ShowFontSizeAdjuster : ResultViewAction + data class LaunchGoogleTranslator(val text: String) : ResultViewAction + data class ShareText(val text: String) : ResultViewAction data object Close : ResultViewAction } +enum class TextType { + OCRText, TranslationResult +} + class ResultViewModelImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -61,11 +80,17 @@ class ResultViewModelImpl @Inject constructor( private val scope: CoroutineScope, private val stateNavigator: StateNavigator, ) : ResultViewModel { + private val logger by lazy { Logger(this::class) } + override val state = MutableStateFlow(ResultViewState()) override val action = MutableSharedFlow() private var rootViewXOffset: Int = 0 private var rootViewYOffset: Int = 0 + private var parentRect: Rect? = null + private var selectedRect: Rect? = null + private var croppedBitmap: Bitmap? = null + private var lastRecognitionResult: RecognitionResult? = null init { stateNavigator.currentNavState @@ -76,6 +101,12 @@ class ResultViewModelImpl @Inject constructor( } private fun updateViewStateWithNavState(navState: NavState) = scope.launch { + if (navState is BitmapIncluded) { + this@ResultViewModelImpl.parentRect = navState.parentRect + this@ResultViewModelImpl.selectedRect = navState.selectedRect + this@ResultViewModelImpl.croppedBitmap = navState.bitmap + } + when (navState) { is NavState.TextRecognizing -> state.update { @@ -89,6 +120,8 @@ class ResultViewModelImpl @Inject constructor( is NavState.TextTranslating -> state.update { + this@ResultViewModelImpl.lastRecognitionResult = navState.recognitionResult + val needTranslate = !navState.translationProviderType.nonTranslation val (textAreas, unionArea) = calculateTextAreas( navState.recognitionResult, @@ -152,6 +185,11 @@ class ResultViewModelImpl @Inject constructor( } } + NavState.Idle -> { + setToDefault() + action.emit(ResultViewAction.Close) + } + else -> { setToDefault() } @@ -190,8 +228,92 @@ class ResultViewModelImpl @Inject constructor( override fun onDialogOutsideClicked() { scope.launch { - action.emit(ResultViewAction.Close) - stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = true)) + stateNavigator.navigate(NavigationAction.NavigateToIdle()) } } + + override fun onTextSearchClicked() { + scope.launch { + state.update { + it.copy( + textSearchEnabled = it.textSearchEnabled.not(), + ) + } + } + } + + override fun onOCRTextEditClicked() { + scope.launch { + val croppedBitmap = croppedBitmap ?: return@launch + val text = state.value.ocrState.ocrText + action.emit( + ResultViewAction.ShowOCRTextEditor( + text = text, + croppedBitmap = croppedBitmap, + ) + ) + } + } + + override fun onOCRTextEdited(text: String) { + scope.launch { + val parentRect = parentRect ?: return@launch + val selectedRect = selectedRect ?: return@launch + val croppedBitmap = croppedBitmap ?: return@launch + val recognitionResult = lastRecognitionResult ?: return@launch + stateNavigator.navigate( + NavigationAction.ReStartTranslation( + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + recognitionResult = recognitionResult.copy(result = text), + ) + ) + } + } + + override fun onCopyClicked(textType: TextType) { + scope.launch { + val label = when (textType) { + TextType.OCRText -> LABEL_RECOGNIZED_TEXT + TextType.TranslationResult -> LABEL_TRANSLATED_TEXT + } + + Utils.copyToClipboard( + label = label, + text = textType.getTargetText() + ) + } + } + + override fun onAdjustFontSizeClicked() { + scope.launch { + //TODO subscribe to the font size changes + action.emit(ResultViewAction.ShowFontSizeAdjuster) + } + } + + override fun onGoogleTranslateClicked(textType: TextType) { + scope.launch { + action.emit(ResultViewAction.LaunchGoogleTranslator(textType.getTargetText())) + stateNavigator.navigate(NavigationAction.NavigateToIdle()) + } + } + + override fun onShareOCRTextClicked() { + scope.launch { + action.emit(ResultViewAction.ShareText(state.value.ocrState.ocrText)) + stateNavigator.navigate(NavigationAction.NavigateToIdle()) + } + } + + private fun TextType.getTargetText(): String = when (this) { + TextType.OCRText -> state.value.ocrState.ocrText + TextType.TranslationResult -> state.value.translationState.translatedText + } + + companion object { + private const val LABEL_RECOGNIZED_TEXT = "Recognized text" + private const val LABEL_TRANSLATED_TEXT = "Translated text" + } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index abc5393c..d0a2763f 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -79,7 +79,7 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { } sealed interface NavigationAction { - data class NavigateToIdle(val showMainBar: Boolean) : NavigationAction + data class NavigateToIdle(val showMainBar: Boolean = true) : NavigationAction data object NavigateToScreenCircling : NavigationAction @@ -109,8 +109,8 @@ sealed interface NavigationAction { data class ReStartTranslation( val croppedBitmap: Bitmap, - val parent: Rect, - val selected: Rect, + val parentRect: Rect, + val selectedRect: Rect, val recognitionResult: RecognitionResult, ) : NavigationAction @@ -133,8 +133,8 @@ sealed class NavState { data class ScreenCircled(val parentRect: Rect, val selectedRect: Rect) : NavState() object ScreenCapturing : NavState() data class TextRecognizing( - val parentRect: Rect, - val selectedRect: Rect, + override val parentRect: Rect, + override val selectedRect: Rect, val croppedBitmap: Bitmap, ) : NavState(), BitmapIncluded { override val bitmap: Bitmap @@ -142,8 +142,8 @@ sealed class NavState { } data class TextTranslating( - val parentRect: Rect, - val selectedRect: Rect, + override val parentRect: Rect, + override val selectedRect: Rect, val croppedBitmap: Bitmap, val recognitionResult: RecognitionResult, val translationProviderType: TranslationProviderType, @@ -153,8 +153,8 @@ sealed class NavState { } data class TextTranslated( - val parentRect: Rect, - val selectedRect: Rect, + override val parentRect: Rect, + override val selectedRect: Rect, val croppedBitmap: Bitmap, val recognitionResult: RecognitionResult, val resultInfo: ResultInfo, @@ -166,5 +166,7 @@ sealed class NavState { } interface BitmapIncluded { + val parentRect: Rect + val selectedRect: Rect val bitmap: Bitmap } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index b465d98c..4a544c12 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -92,8 +92,8 @@ class StateOperatorImpl @Inject constructor( is NavigationAction.ReStartTranslation -> { startTranslation( croppedBitmap = action.croppedBitmap, - parentRect = action.parent, - selectedRect = action.selected, + parentRect = action.parentRect, + selectedRect = action.selectedRect, recognitionResult = action.recognitionResult, ) } From c86c28f29fd5ba695c6094e34d75d0e876239d9e Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 14 Jan 2024 16:13:29 +0900 Subject: [PATCH 018/121] Update the states and implement the font size subscribing on ResultView --- .../data/repo/PreferenceRepository.kt | 16 ++++++ .../usecase/GetResultViewFontSizeUseCase.kt | 10 ++++ .../GetShowTextSelectorOnResultViewUseCase.kt | 10 ++++ .../SetShowTextSelectorOnResultViewUseCase.kt | 12 ++++ .../compose/resultview/ResultViewContent.kt | 19 ++++++- .../compose/resultview/ResultViewModel.kt | 55 ++++++++++++++----- 6 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetResultViewFontSizeUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetShowTextSelectorOnResultViewUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetShowTextSelectorOnResultViewUseCase.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt index 3aa6f8b8..b397f3b0 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt @@ -1,6 +1,8 @@ package tw.firemaples.onscreenocr.data.repo import android.graphics.Point +import androidx.lifecycle.asFlow +import com.chibatching.kotpref.livedata.asLiveData import tw.firemaples.onscreenocr.pref.AppPref import javax.inject.Inject @@ -11,4 +13,18 @@ class PreferenceRepository @Inject constructor() { fun getLastMainBarPosition(): Point = AppPref.lastMainBarPosition + + fun getShowTextSelectionOnResultView() = + AppPref.asLiveData(AppPref::displaySelectedTextOnResultWindow).asFlow() + + fun setShowTextSelectionOnResultView(show: Boolean) { + AppPref.displaySelectedTextOnResultWindow = show + } + + fun getResultViewFontSize() = + AppPref.asLiveData(AppPref::resultWindowFontSize).asFlow() + + fun setResultViewFontSize(fontSize: Float) { + AppPref.resultWindowFontSize = fontSize + } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetResultViewFontSizeUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetResultViewFontSizeUseCase.kt new file mode 100644 index 00000000..783635d6 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetResultViewFontSizeUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class GetResultViewFontSizeUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke() = preferenceRepository.getResultViewFontSize() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetShowTextSelectorOnResultViewUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetShowTextSelectorOnResultViewUseCase.kt new file mode 100644 index 00000000..db215d9d --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetShowTextSelectorOnResultViewUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class GetShowTextSelectorOnResultViewUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke() = preferenceRepository.getShowTextSelectionOnResultView() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetShowTextSelectorOnResultViewUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetShowTextSelectorOnResultViewUseCase.kt new file mode 100644 index 00000000..fb6c2bb1 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetShowTextSelectorOnResultViewUseCase.kt @@ -0,0 +1,12 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class SetShowTextSelectorOnResultViewUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke(show: Boolean) { + preferenceRepository.setShowTextSelectionOnResultView(show = show) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index a65eac13..b7c0af65 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -16,8 +16,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -95,6 +98,7 @@ fun ResultViewContent( ), viewModel = viewModel, textSearchEnabled = state.textSearchEnabled, + fontSize = state.fontSize, ocrState = state.ocrState, translationState = state.translationState, ) @@ -156,6 +160,7 @@ private fun ResultPanel( modifier: Modifier, viewModel: ResultViewModel, textSearchEnabled: Boolean, + fontSize: Float, ocrState: OCRState, translationState: TranslationState, ) { @@ -179,6 +184,7 @@ private fun ResultPanel( onExportClicked = viewModel::onShareOCRTextClicked, ) OCRTextArea( + fontSize = fontSize, showProcessing = ocrState.showProcessing, ocrText = ocrState.ocrText, ) @@ -189,6 +195,7 @@ private fun ResultPanel( onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.TranslationResult) } ) TranslationTextArea( + fontSize = fontSize, showProcessing = translationState.showProcessing, translatedText = translationState.translatedText, ) @@ -280,15 +287,19 @@ private fun OCRToolBar( private fun OCRTextArea( showProcessing: Boolean, ocrText: String, + fontSize: Float, ) { if (showProcessing) { ProgressIndicator() } else { //TODO implement text search selector - //TODO implement text overflow Text( + modifier = Modifier + .sizeIn(maxHeight = 150.dp) + .verticalScroll(rememberScrollState()), text = ocrText, color = AppColorScheme.onBackground, + fontSize = fontSize.sp, ) } @@ -331,15 +342,19 @@ private fun TranslationToolBar( private fun TranslationTextArea( showProcessing: Boolean, translatedText: String, + fontSize: Float, ) { if (showProcessing) { ProgressIndicator() } else { - //TODO implement text overflow Text( + modifier = Modifier + .sizeIn(maxHeight = 150.dp) + .verticalScroll(rememberScrollState()), text = translatedText, color = AppColorScheme.onBackground, + fontSize = fontSize.sp, ) } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 5af4ab04..c94042ff 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -14,6 +14,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.data.usecase.GetResultViewFontSizeUseCase +import tw.firemaples.onscreenocr.data.usecase.GetShowTextSelectorOnResultViewUseCase +import tw.firemaples.onscreenocr.data.usecase.SetShowTextSelectorOnResultViewUseCase import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.manager.BitmapIncluded import tw.firemaples.onscreenocr.floatings.manager.NavState @@ -22,6 +25,7 @@ import tw.firemaples.onscreenocr.floatings.manager.ResultInfo import tw.firemaples.onscreenocr.floatings.manager.StateNavigator import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.utils.Constants import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.Utils import javax.inject.Inject @@ -41,9 +45,10 @@ interface ResultViewModel { } data class ResultViewState( + val textSearchEnabled: Boolean = false, + val fontSize: Float = Constants.DEFAULT_RESULT_WINDOW_FONT_SIZE, val highlightArea: List = listOf(), val highlightUnion: Rect = Rect(), - val textSearchEnabled: Boolean = false, val ocrState: OCRState = OCRState(), val translationState: TranslationState = TranslationState(), ) @@ -79,6 +84,9 @@ class ResultViewModelImpl @Inject constructor( @MainImmediateCoroutineScope private val scope: CoroutineScope, private val stateNavigator: StateNavigator, + getShowTextSelectorOnResultViewUseCase: GetShowTextSelectorOnResultViewUseCase, + private val setShowTextSelectorOnResultViewUseCase: SetShowTextSelectorOnResultViewUseCase, + getResultViewFontSizeUseCase: GetResultViewFontSizeUseCase, ) : ResultViewModel { private val logger by lazy { Logger(this::class) } @@ -96,8 +104,25 @@ class ResultViewModelImpl @Inject constructor( stateNavigator.currentNavState .onEach { navState -> updateViewStateWithNavState(navState) - } - .launchIn(scope) + }.launchIn(scope) + + getShowTextSelectorOnResultViewUseCase.invoke() + .onEach { show -> + state.update { + it.copy( + textSearchEnabled = show, + ) + } + }.launchIn(scope) + + getResultViewFontSizeUseCase.invoke() + .onEach { fontSize -> + state.update { + it.copy( + fontSize = fontSize, + ) + } + }.launchIn(scope) } private fun updateViewStateWithNavState(navState: NavState) = scope.launch { @@ -146,7 +171,7 @@ class ResultViewModelImpl @Inject constructor( is NavState.TextTranslated -> { when (val resultInfo = navState.resultInfo) { is ResultInfo.Error -> - setToDefault() + clearData() ResultInfo.OCROnly -> state.update { @@ -186,18 +211,25 @@ class ResultViewModelImpl @Inject constructor( } NavState.Idle -> { - setToDefault() + clearData() action.emit(ResultViewAction.Close) } else -> { - setToDefault() + clearData() } } } - private fun setToDefault() { - state.value = ResultViewState() + private fun clearData() { + state.update { + it.copy( + highlightArea = listOf(), + highlightUnion = Rect(), + ocrState = OCRState(), + translationState = TranslationState(), + ) + } } private fun calculateTextAreas( @@ -234,11 +266,8 @@ class ResultViewModelImpl @Inject constructor( override fun onTextSearchClicked() { scope.launch { - state.update { - it.copy( - textSearchEnabled = it.textSearchEnabled.not(), - ) - } + val show = state.value.textSearchEnabled.not() + setShowTextSelectorOnResultViewUseCase.invoke(show) } } From 0ae34a4e766253ac700c49518d418857072b20a7 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 14 Jan 2024 18:27:40 +0900 Subject: [PATCH 019/121] Implement WordSelectionText --- .../compose/resultview/ResultViewContent.kt | 45 ++++++-- .../resultview/ResultViewFloatingView.kt | 10 ++ .../compose/resultview/ResultViewModel.kt | 25 +++- .../compose/wigets/WordSelectionText.kt | 108 ++++++++++++++++++ .../textInfoSearch/TextInfoSearchView.kt | 2 +- .../textInfoSearch/TextInfoSearchViewModel.kt | 17 +-- 6 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index b7c0af65..b4dad290 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -38,6 +38,8 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -51,8 +53,10 @@ import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme import tw.firemaples.onscreenocr.floatings.compose.base.FontSize import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp +import tw.firemaples.onscreenocr.floatings.compose.wigets.WordSelectionText import tw.firemaples.onscreenocr.utils.UIUtils import tw.firemaples.onscreenocr.utils.dpToPx +import java.util.Locale @Composable fun ResultViewContent( @@ -187,6 +191,8 @@ private fun ResultPanel( fontSize = fontSize, showProcessing = ocrState.showProcessing, ocrText = ocrState.ocrText, + textSearchEnabled = textSearchEnabled, + onTextSelected = viewModel::onTextSearchWordSelected, ) if (translationState.showTranslationArea) { @@ -288,19 +294,39 @@ private fun OCRTextArea( showProcessing: Boolean, ocrText: String, fontSize: Float, + textSearchEnabled: Boolean, + onTextSelected: (String) -> Unit, ) { if (showProcessing) { ProgressIndicator() } else { - //TODO implement text search selector - Text( - modifier = Modifier - .sizeIn(maxHeight = 150.dp) - .verticalScroll(rememberScrollState()), - text = ocrText, - color = AppColorScheme.onBackground, - fontSize = fontSize.sp, - ) + if (textSearchEnabled) { + WordSelectionText( + modifier = Modifier + .sizeIn(maxHeight = 150.dp) + .verticalScroll(rememberScrollState()), + text = ocrText, + locale = Locale.US, + textStyle = TextStyle( + color = AppColorScheme.onBackground, + fontSize = fontSize.sp, + ), + selectedSpanStyle = SpanStyle( + color = AppColorScheme.onSecondary, + background = AppColorScheme.secondary, + ), + onTextSelected = onTextSelected, + ) + } else { + Text( + modifier = Modifier + .sizeIn(maxHeight = 150.dp) + .verticalScroll(rememberScrollState()), + text = ocrText, + color = AppColorScheme.onBackground, + fontSize = fontSize.sp, + ) + } } } @@ -426,6 +452,7 @@ private fun ResultViewContentPreview() { override fun onRootViewPositioned(xOffset: Int, yOffset: Int) = Unit override fun onDialogOutsideClicked() = Unit override fun onTextSearchClicked() = Unit + override fun onTextSearchWordSelected(word: String) = Unit override fun onOCRTextEditClicked() = Unit override fun onOCRTextEdited(text: String) = Unit override fun onCopyClicked(textType: TextType) = Unit diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt index f03c2592..bac735ca 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt @@ -11,6 +11,7 @@ import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResume import tw.firemaples.onscreenocr.floatings.manager.Result import tw.firemaples.onscreenocr.floatings.recognizedTextEditor.RecognizedTextEditor import tw.firemaples.onscreenocr.floatings.result.FontSizeAdjuster +import tw.firemaples.onscreenocr.floatings.textInfoSearch.TextInfoSearchView import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.translator.utils.GoogleTranslateUtils @@ -77,6 +78,15 @@ class ResultViewFloatingView @Inject constructor( onTextEdited = viewModel::onOCRTextEdited, ) + is ResultViewAction.ShowTextInfoSearchView -> { + TextInfoSearchView( + context = context, + text = action.text, + sourceLang = action.sourceLang, + targetLang = action.targetLang, + ).attachToScreen() + } + ResultViewAction.Close -> { detachFromScreen() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index c94042ff..30bd027c 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -9,11 +9,13 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase import tw.firemaples.onscreenocr.data.usecase.GetResultViewFontSizeUseCase import tw.firemaples.onscreenocr.data.usecase.GetShowTextSelectorOnResultViewUseCase import tw.firemaples.onscreenocr.data.usecase.SetShowTextSelectorOnResultViewUseCase @@ -36,6 +38,7 @@ interface ResultViewModel { fun onRootViewPositioned(xOffset: Int, yOffset: Int) fun onDialogOutsideClicked() fun onTextSearchClicked() + fun onTextSearchWordSelected(word: String) fun onOCRTextEditClicked() fun onOCRTextEdited(text: String) fun onCopyClicked(textType: TextType) @@ -71,6 +74,12 @@ sealed interface ResultViewAction { data object ShowFontSizeAdjuster : ResultViewAction data class LaunchGoogleTranslator(val text: String) : ResultViewAction data class ShareText(val text: String) : ResultViewAction + data class ShowTextInfoSearchView( + val text: String, + val sourceLang: String, + val targetLang: String, + ) : ResultViewAction + data object Close : ResultViewAction } @@ -87,6 +96,7 @@ class ResultViewModelImpl @Inject constructor( getShowTextSelectorOnResultViewUseCase: GetShowTextSelectorOnResultViewUseCase, private val setShowTextSelectorOnResultViewUseCase: SetShowTextSelectorOnResultViewUseCase, getResultViewFontSizeUseCase: GetResultViewFontSizeUseCase, + private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, ) : ResultViewModel { private val logger by lazy { Logger(this::class) } @@ -271,6 +281,20 @@ class ResultViewModelImpl @Inject constructor( } } + override fun onTextSearchWordSelected(word: String) { + scope.launch { + val sourceLang = lastRecognitionResult?.langCode ?: return@launch + val targetLang = getCurrentTranslationLangUseCase.invoke().first() + action.emit( + ResultViewAction.ShowTextInfoSearchView( + text = word, + sourceLang = sourceLang, + targetLang = targetLang, + ) + ) + } + } + override fun onOCRTextEditClicked() { scope.launch { val croppedBitmap = croppedBitmap ?: return@launch @@ -317,7 +341,6 @@ class ResultViewModelImpl @Inject constructor( override fun onAdjustFontSizeClicked() { scope.launch { - //TODO subscribe to the font size changes action.emit(ResultViewAction.ShowFontSizeAdjuster) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt new file mode 100644 index 00000000..3a69a4c8 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt @@ -0,0 +1,108 @@ +package tw.firemaples.onscreenocr.floatings.compose.wigets + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import tw.firemaples.onscreenocr.utils.WordBoundary +import java.util.Locale + +@Composable +fun WordSelectionText( + modifier: Modifier = Modifier, + text: String, + locale: Locale, + textStyle: TextStyle = TextStyle.Default, + selectedSpanStyle: SpanStyle = SpanStyle(), + onTextSelected: (String) -> Unit +) { + var selectedStart by remember { mutableStateOf(-1) } + val annotatedString = buildText( + text = text, + locale = locale, + selectedStart = selectedStart, + selectedSpanStyle = selectedSpanStyle, + ) + ClickableText( + modifier = modifier, + style = textStyle, + text = annotatedString, + onClick = { offset -> + val clicked = annotatedString.getStringAnnotations(offset, offset) + .firstOrNull() + if (clicked != null) { + if (selectedStart == clicked.start) { + selectedStart = -1 + } else { + selectedStart = clicked.start + onTextSelected.invoke(clicked.item) + } + } + }, + ) +} + +private fun buildText( + text: String, + locale: Locale, + selectedStart: Int, + selectedSpanStyle: SpanStyle +) = buildAnnotatedString { + if (text.isEmpty()) return@buildAnnotatedString + + val boundaries = WordBoundary.breakWords(text = text, locale = locale) + if (boundaries.isEmpty()) { + append(text) + return@buildAnnotatedString + } + + val unselectedStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + ) + val selectedStyle = selectedSpanStyle.copy( + textDecoration = TextDecoration.Underline + ) + + var textStart = 0 + var index = 0 + while (textStart < text.length || index < boundaries.size) { + val nextBoundary = boundaries.getOrNull(index) + if (nextBoundary == null) { + append(text.substring(textStart until text.length)) + textStart = text.length + } else if (textStart < nextBoundary.start) { + append(text.substring(textStart until nextBoundary.start)) + textStart = nextBoundary.start + } else if (textStart == nextBoundary.start) { + val style = if (textStart == selectedStart) selectedStyle else unselectedStyle + val word = text.substring(nextBoundary.start until nextBoundary.end) + withStyle(style = style) { + pushStringAnnotation(tag = word, annotation = word) + append(word) + } + textStart = nextBoundary.end + index++ + } + } +} + +@Preview +@Composable +private fun WordBreakTextPreview() { + val text = " Hello world test! word-breaker ! " + val locale = Locale.US + WordSelectionText( + text = text, + locale = locale, + onTextSelected = {}, + ) +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchView.kt index 3de2f94b..ce0a2484 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchView.kt @@ -38,7 +38,7 @@ class TextInfoSearchView( get() = true private val viewModel: TextInfoSearchViewModel by lazy { - TextInfoSearchViewModel(viewScope, text, sourceLang) + TextInfoSearchViewModel(viewScope, text, sourceLang, targetLang) } private val binding: FloatingTextInfoSearchBinding = diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt index 4f66094d..83058606 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt @@ -12,13 +12,14 @@ import java.net.URLEncoder class TextInfoSearchViewModel( viewScope: CoroutineScope, private val text: String, - private val sourceLang: String + private val sourceLang: String, + private val targetLang: String, ) : FloatingViewModel(viewScope) { private val _loadUrl = MutableLiveData() val loadUrl: LiveData = _loadUrl private var lastPageType: PageType - get() = PageType.values().firstOrNull { it.id == AppPref.lastTextInfoSearchPage } + get() = PageType.entries.firstOrNull { it.id == AppPref.lastTextInfoSearchPage } ?: Constants.DEFAULT_TEXT_INFO_SEARCH_PAGE set(value) { AppPref.lastTextInfoSearchPage = value.id @@ -27,7 +28,7 @@ class TextInfoSearchViewModel( fun onLoad() { viewScope.launch { val page: Page = when (lastPageType) { - PageType.GoogleTranslate -> Page.GoogleTranslate(text, sourceLang) + PageType.GoogleTranslate -> Page.GoogleTranslate(text, sourceLang, targetLang) PageType.GoogleSearch -> Page.GoogleSearch(text, sourceLang) PageType.GoogleDefinition -> Page.GoogleDefinition(text, sourceLang) PageType.GoogleImageSearch -> Page.GoogleImageSearch(text, sourceLang) @@ -45,7 +46,7 @@ class TextInfoSearchViewModel( fun onGoogleTranslateClicked() { viewScope.launch { - loadPage(Page.GoogleTranslate(text, sourceLang)) + loadPage(Page.GoogleTranslate(text, sourceLang, targetLang)) } } @@ -88,18 +89,18 @@ class TextInfoSearchViewModel( sealed class Page(val text: String, val sourceLang: String, val pageType: PageType) { companion object { - fun default(text: String, sourceLang: String): Page { - return GoogleTranslate(text, sourceLang) + fun default(text: String, sourceLang: String, targetLang: String): Page { + return GoogleTranslate(text, sourceLang, targetLang) } } abstract val url: String val encodedText: String get() = URLEncoder.encode(text, "utf-8") - class GoogleTranslate(text: String, sourceLang: String) : + class GoogleTranslate(text: String, sourceLang: String, val targetLang: String) : Page(text, sourceLang, PageType.GoogleTranslate) { override val url: String - get() = "https://translate.google.com/?sl=$sourceLang&text=$encodedText&op=translate" + get() = "https://translate.google.com/?sl=$sourceLang&tl=$targetLang&text=$encodedText&op=translate" } class Wikipedia(text: String, sourceLang: String) : From efd53b165f38a74b2f9ae344921dfddf15491268 Mon Sep 17 00:00:00 2001 From: firemaples Date: Mon, 15 Jan 2024 00:30:38 +0900 Subject: [PATCH 020/121] Implement hiding OCR area after translated --- .../data/repo/SettingRepository.kt | 3 ++ .../GetHidingOCRAreaAfterTranslated.kt | 10 ++++++ .../compose/resultview/ResultViewContent.kt | 34 ++++++++++--------- .../compose/resultview/ResultViewModel.kt | 8 +++++ 4 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt index 2b7d0f21..48e3e093 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt @@ -6,4 +6,7 @@ import javax.inject.Inject class SettingRepository @Inject constructor() { fun shouldRestoreMainBarPosition(): Boolean = SettingManager.restoreMainBarPosition + + fun hideOCRAreaAfterTranslated(): Boolean = + SettingManager.hideRecognizedResultAfterTranslated } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt new file mode 100644 index 00000000..450b6ad1 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.SettingRepository +import javax.inject.Inject + +class GetHidingOCRAreaAfterTranslated @Inject constructor( + private val settingRepository: SettingRepository, +) { + operator fun invoke() = settingRepository.hideOCRAreaAfterTranslated() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index b4dad290..a16ba65d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -178,22 +178,24 @@ private fun ResultPanel( .clickable { } .padding(horizontal = 6.dp, vertical = 4.dp), ) { - OCRToolBar( - textSearchEnabled = textSearchEnabled, - onSearchClicked = viewModel::onTextSearchClicked, - onEditClicked = viewModel::onOCRTextEditClicked, - onCopyClicked = { viewModel.onCopyClicked(TextType.OCRText) }, - onFontSizeClicked = viewModel::onAdjustFontSizeClicked, - onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.OCRText) }, - onExportClicked = viewModel::onShareOCRTextClicked, - ) - OCRTextArea( - fontSize = fontSize, - showProcessing = ocrState.showProcessing, - ocrText = ocrState.ocrText, - textSearchEnabled = textSearchEnabled, - onTextSelected = viewModel::onTextSearchWordSelected, - ) + if (ocrState.showRecognitionArea) { + OCRToolBar( + textSearchEnabled = textSearchEnabled, + onSearchClicked = viewModel::onTextSearchClicked, + onEditClicked = viewModel::onOCRTextEditClicked, + onCopyClicked = { viewModel.onCopyClicked(TextType.OCRText) }, + onFontSizeClicked = viewModel::onAdjustFontSizeClicked, + onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.OCRText) }, + onExportClicked = viewModel::onShareOCRTextClicked, + ) + OCRTextArea( + fontSize = fontSize, + showProcessing = ocrState.showProcessing, + ocrText = ocrState.ocrText, + textSearchEnabled = textSearchEnabled, + onTextSelected = viewModel::onTextSearchWordSelected, + ) + } if (translationState.showTranslationArea) { TranslationToolBar( diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 30bd027c..89ba1f6a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase +import tw.firemaples.onscreenocr.data.usecase.GetHidingOCRAreaAfterTranslated import tw.firemaples.onscreenocr.data.usecase.GetResultViewFontSizeUseCase import tw.firemaples.onscreenocr.data.usecase.GetShowTextSelectorOnResultViewUseCase import tw.firemaples.onscreenocr.data.usecase.SetShowTextSelectorOnResultViewUseCase @@ -57,6 +58,7 @@ data class ResultViewState( ) data class OCRState( + val showRecognitionArea: Boolean = true, val showProcessing: Boolean = false, val ocrText: String = "", ) @@ -97,6 +99,7 @@ class ResultViewModelImpl @Inject constructor( private val setShowTextSelectorOnResultViewUseCase: SetShowTextSelectorOnResultViewUseCase, getResultViewFontSizeUseCase: GetResultViewFontSizeUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, + private val getHidingOCRAreaAfterTranslated: GetHidingOCRAreaAfterTranslated, ) : ResultViewModel { private val logger by lazy { Logger(this::class) } @@ -168,6 +171,7 @@ class ResultViewModelImpl @Inject constructor( highlightArea = textAreas, highlightUnion = unionArea, ocrState = it.ocrState.copy( + showRecognitionArea = true, showProcessing = false, ocrText = navState.recognitionResult.result, ), @@ -204,9 +208,13 @@ class ResultViewModelImpl @Inject constructor( val providerName = context.getString(providerType.nameRes) "${context.getString(R.string.text_translated_by)} $providerName" } else null + val showRecognitionArea = getHidingOCRAreaAfterTranslated.invoke().not() state.update { it.copy( + ocrState = it.ocrState.copy( + showRecognitionArea = showRecognitionArea, + ), translationState = it.translationState.copy( showTranslationArea = needTranslate, showProcessing = false, From 7904cbe68a89f2254b81e29f6cdb44cc83900c64 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 14:19:07 +0900 Subject: [PATCH 021/121] Update state transition log --- .../onscreenocr/floatings/manager/StateNavigator.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index d0a2763f..28a43c27 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -69,11 +69,15 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { override fun updateState(newNavState: NavState) { val allowedNextStates = nextStates[currentNavState.value::class] + val transitionName = + "${currentNavState.value::class.simpleName} > ${newNavState::class.simpleName}" + val transitionInfo = "${currentNavState.value} > ${newNavState::class}" + if (allowedNextStates?.contains(newNavState::class) == true) { - logger.debug("Change state ${currentNavState.value} > $newNavState") + logger.debug("Change state $transitionName, info: $transitionInfo") currentNavState.value = newNavState } else { - logger.error("Change state from ${currentNavState.value} to $newNavState is not allowed") + logger.error("Change state from $transitionName is not allowed, info: $transitionInfo") } } } From a2634ae15327a3145988ee248ad863d51207e696 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 14:50:05 +0900 Subject: [PATCH 022/121] Clean up unused code on result view --- .../compose/resultview/ResultViewContent.kt | 1 + .../resultview/ResultViewFloatingView.kt | 209 +----------------- .../compose/resultview/ResultViewModel.kt | 10 +- .../manager/FloatingViewCoordinator.kt | 29 +-- .../floatings/manager/StateOperator.kt | 76 +------ 5 files changed, 22 insertions(+), 303 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index a16ba65d..81dcee89 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -453,6 +453,7 @@ private fun ResultViewContentPreview() { override fun onRootViewPositioned(xOffset: Int, yOffset: Int) = Unit override fun onDialogOutsideClicked() = Unit + override fun onHomeButtonPressed() = Unit override fun onTextSearchClicked() = Unit override fun onTextSearchWordSelected(word: String) = Unit override fun onOCRTextEditClicked() = Unit diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt index bac735ca..58ccda8e 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewFloatingView.kt @@ -2,39 +2,27 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.Context import android.graphics.Bitmap -import android.graphics.Rect import android.view.WindowManager import androidx.compose.runtime.Composable import dagger.hilt.android.qualifiers.ApplicationContext import tw.firemaples.onscreenocr.floatings.compose.base.ComposeFloatingView import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed -import tw.firemaples.onscreenocr.floatings.manager.Result import tw.firemaples.onscreenocr.floatings.recognizedTextEditor.RecognizedTextEditor import tw.firemaples.onscreenocr.floatings.result.FontSizeAdjuster import tw.firemaples.onscreenocr.floatings.textInfoSearch.TextInfoSearchView -import tw.firemaples.onscreenocr.recognition.RecognitionResult -import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.translator.utils.GoogleTranslateUtils import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.Utils import tw.firemaples.onscreenocr.utils.getViewRect -import tw.firemaples.onscreenocr.utils.setReusable import javax.inject.Inject class ResultViewFloatingView @Inject constructor( @ApplicationContext context: Context, private val viewModel: ResultViewModel, ) : ComposeFloatingView(context) { - companion object { - private const val LABEL_RECOGNIZED_TEXT = "Recognized text" - private const val LABEL_TRANSLATED_TEXT = "Translated text" - } private val logger: Logger by lazy { Logger(ResultViewFloatingView::class) } -// override val layoutId: Int -// get() = R.layout.floating_result_view - override val layoutWidth: Int get() = WindowManager.LayoutParams.MATCH_PARENT @@ -44,18 +32,6 @@ class ResultViewFloatingView @Inject constructor( override val enableHomeButtonWatcher: Boolean get() = true -// private val binding: FloatingResultViewBinding = FloatingResultViewBinding.bind(rootLayout) - -// private val viewRoot: RelativeLayout = binding.viewRoot - - var onUserDismiss: (() -> Unit)? = null - -// private val viewResultWindow: View = binding.viewResultWindow - - private var unionRect: Rect = Rect() - - private var croppedBitmap: Bitmap? = null - @Composable override fun RootContent() { viewModel.action.collectOnLifecycleResumed { action -> @@ -86,10 +62,6 @@ class ResultViewFloatingView @Inject constructor( targetLang = action.targetLang, ).attachToScreen() } - - ResultViewAction.Close -> { - detachFromScreen() - } } } @@ -99,112 +71,6 @@ class ResultViewFloatingView @Inject constructor( ) } - init { -// binding.resultPanel.setViews() - } - -// private fun ViewResultPanelBinding.setViews() { -// viewModel.displayOCROperationProgress.observe(lifecycleOwner) { -// pbOcrOperating.showOrHide(it) -// } -// viewModel.displayTranslationProgress.observe(lifecycleOwner) { -// pbTranslationOperating.showOrHide(it) -// } -// viewModel.displaySelectableText.observe(lifecycleOwner) { -// textSelectable.isChecked = it -// tvOcrText.showOrHide(!it) -// tvWordBreakOcrText.showOrHide(it) -// } -// viewModel.ocrText.observe(lifecycleOwner) { -// tvWordBreakOcrText.setContent(it?.text(), it?.locale() ?: Locale.getDefault()) -// tvOcrText.text = it?.text() -// } -// viewModel.translatedText.observe(lifecycleOwner) { -// if (it == null) { -// tvTranslatedText.text = null -// } else { -// val (text, color) = it -// tvTranslatedText.text = text -// tvTranslatedText.setTextColor(ContextCompat.getColor(context, color)) -// } -// -// reposition() -// } -// -// viewModel.displayRecognitionBlock.observe(lifecycleOwner) { -// groupRecognitionViews.showOrHide(it) -// } -// viewModel.displayTranslatedBlock.observe(lifecycleOwner) { -// groupTranslationViews.showOrHide(it) -// } -// -// viewModel.translationProviderText.observe(lifecycleOwner) { -// tvTranslationProvider.setTextOrGone(it) -// } -// viewModel.displayTranslatedByGoogle.observe(lifecycleOwner) { -// ivTranslatedByGoogle.showOrHide(it) -// } -// -// viewModel.displayRecognizedTextAreas.observe(lifecycleOwner) { -// val (boundingBoxes, unionRect) = it -// binding.viewTextBoundingBoxView.boundingBoxes = boundingBoxes -// updateSelectedAreas(unionRect) -// } -// -// viewModel.copyRecognizedText.observe(lifecycleOwner) { -// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, it) -// } -// -// viewModel.fontSize.observe(lifecycleOwner) { -// tvOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) -// tvWordBreakOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) -// tvTranslatedText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) -// } -// -// viewModel.displayTextInfoSearchView.observe(lifecycleOwner) { -// TextInfoSearchView(context, it.text, it.sourceLang, it.targetLang) -// .attachToScreen() -// } -// -// textSelectable.setOnCheckedChangeListener { _, checked -> -// viewModel.onTextSelectableChecked(checked) -// } -// tvWordBreakOcrText.onWordClicked = { word -> -// if (word != null) { -// viewModel.onWordSelected(word) -// tvWordBreakOcrText.clearSelection() -// } -// } -// tvOcrText.movementMethod = ScrollingMovementMethod() -// tvTranslatedText.movementMethod = ScrollingMovementMethod() -// viewRoot.clickOnce { onUserDismiss?.invoke() } -// btEditOCRText.clickOnce { -// showRecognizedTextEditor(viewModel.ocrText.value?.text() ?: "") -// } -// btCopyOCRText.clickOnce { -// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, viewModel.ocrText.value?.text() ?: "") -// } -// btCopyTranslatedText.clickOnce { -// Utils.copyToClipboard(LABEL_TRANSLATED_TEXT, tvTranslatedText.text.toString()) -// } -// btTranslateOCRTextWithGoogleTranslate.clickOnce { -// GoogleTranslateUtils.launchTranslator(viewModel.ocrText.value?.text() ?: "") -// onUserDismiss?.invoke() -// } -// btTranslateTranslatedTextWithGoogleTranslate.clickOnce { -// GoogleTranslateUtils.launchTranslator(tvTranslatedText.text.toString()) -// onUserDismiss?.invoke() -// } -// btShareOCRText.clickOnce { -// val ocrText = viewModel.ocrText.value?.text() ?: return@clickOnce -// Utils.shareText(ocrText) -// onUserDismiss?.invoke() -// } -// btAdjustFontSize.clickOnce { -// FontSizeAdjuster(context).attachToScreen() -// } -// } - private fun showRecognizedTextEditor( text: String, croppedBitmap: Bitmap, @@ -222,81 +88,8 @@ class ResultViewFloatingView @Inject constructor( ).attachToScreen() } - override fun onAttachedToScreen() { - super.onAttachedToScreen() -// viewResultWindow.visibility = View.INVISIBLE - } - - override fun onDetachedFromScreen() { - super.onDetachedFromScreen() - this.croppedBitmap?.setReusable() - this.croppedBitmap = null - } - override fun onHomeButtonPressed() { super.onHomeButtonPressed() - onUserDismiss?.invoke() - } - - fun startRecognition() { - attachToScreen() -// viewModel.startRecognition() - } - - fun textRecognized( - result: RecognitionResult, - parent: Rect, - selected: Rect, - croppedBitmap: Bitmap - ) { - this.croppedBitmap = croppedBitmap -// viewModel.textRecognized(result, parent, selected, rootView.getViewRect()) - } - - fun startTranslation(translationProviderType: TranslationProviderType) { -// viewModel.startTranslation(translationProviderType) - } - - fun textTranslated(result: Result) { -// viewModel.textTranslated(result) - } - - fun backToIdle() { - detachFromScreen() - } - - private fun updateSelectedAreas(unionRect: Rect) { - this.unionRect = unionRect - reposition() - } - - private fun reposition() { -// rootView.post { -// val parentRect = viewRoot.getViewRect() -// val anchorRect = Rect(unionRect).apply { -// top += parentRect.top -// left += parentRect.left -// bottom += parentRect.top -// right += parentRect.left -// } -// val windowRect = viewResultWindow.getViewRect() -// -// val (leftMargin, topMargin) = UIUtils.countViewPosition( -// anchorRect, parentRect, -// windowRect.width(), windowRect.height(), 2.dpToPx(), -// ) -// -// val layoutParams = -// (viewResultWindow.layoutParams as RelativeLayout.LayoutParams).apply { -// this.leftMargin = leftMargin -// this.topMargin = topMargin -// } -// -// viewRoot.updateViewLayout(viewResultWindow, layoutParams) -// -// viewRoot.post { -// viewResultWindow.visibility = View.VISIBLE -// } -// } + viewModel.onHomeButtonPressed() } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 89ba1f6a..9a397a82 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -38,6 +38,7 @@ interface ResultViewModel { val action: SharedFlow fun onRootViewPositioned(xOffset: Int, yOffset: Int) fun onDialogOutsideClicked() + fun onHomeButtonPressed() fun onTextSearchClicked() fun onTextSearchWordSelected(word: String) fun onOCRTextEditClicked() @@ -81,8 +82,6 @@ sealed interface ResultViewAction { val sourceLang: String, val targetLang: String, ) : ResultViewAction - - data object Close : ResultViewAction } enum class TextType { @@ -230,7 +229,6 @@ class ResultViewModelImpl @Inject constructor( NavState.Idle -> { clearData() - action.emit(ResultViewAction.Close) } else -> { @@ -282,6 +280,12 @@ class ResultViewModelImpl @Inject constructor( } } + override fun onHomeButtonPressed() { + scope.launch { + stateNavigator.navigate(NavigationAction.NavigateToIdle()) + } + } + override fun onTextSearchClicked() { scope.launch { val show = state.value.textSearchEnabled.not() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index f12aab20..e863adda 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -61,38 +61,17 @@ class FloatingViewCoordinator @Inject constructor( StateOperatorAction.HideScreenCirclingView -> screenCirclingView.detachFromScreen() - StateOperatorAction.ResultViewStartRecognition -> - resultView.startRecognition() + StateOperatorAction.ShowResultView -> + resultView.attachToScreen() - is StateOperatorAction.ResultViewSetRecognized -> - resultView.textRecognized( - result = action.result, - parent = action.parentRect, - selected = action.selectedRect, - croppedBitmap = action.croppedBitmap, - ) - - is StateOperatorAction.ResultViewStartTranslation -> - resultView.startTranslation(action.translationProviderType) - - is StateOperatorAction.ResultViewTextTranslated -> - resultView.textTranslated(action.result) - - StateOperatorAction.ResultViewBackToIdle -> - resultView.backToIdle() + StateOperatorAction.HideResultView -> + resultView.detachFromScreen() is StateOperatorAction.ShowErrorDialog -> context.showErrorDialog(action.error) } } .launchIn(scope) - - resultView.onUserDismiss = { - scope.launch { - resultView.backToIdle() - stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = true)) - } - } } fun showMainBar() { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index 4a544c12..ec109fdc 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -221,7 +221,8 @@ class StateOperatorImpl @Inject constructor( ) try { - action.emit(StateOperatorAction.ResultViewStartRecognition) + action.emit(StateOperatorAction.ShowResultView) + val recognizer = TextRecognizer.getRecognizer(ocrProvider) val language = TextRecognizer.getLanguage(ocrLang, ocrProvider)!! @@ -248,14 +249,6 @@ class StateOperatorImpl @Inject constructor( FirebaseEvent.logOCRFinished(recognizer.name) - action.emit( - StateOperatorAction.ResultViewSetRecognized( - result = result, - parentRect = parentRect, - selectedRect = selectedRect, - croppedBitmap = croppedBitmap, - ) - ) startTranslation( croppedBitmap = croppedBitmap, parentRect = parentRect, @@ -297,12 +290,6 @@ class StateOperatorImpl @Inject constructor( ) ) - action.emit( - StateOperatorAction.ResultViewStartTranslation( - translationProviderType = translator.type, - ) - ) - FirebaseEvent.logStartTranslationText( text = recognitionResult.result, fromLang = recognitionResult.langCode, @@ -325,7 +312,6 @@ class StateOperatorImpl @Inject constructor( } catch (e: Exception) { logger.warn(t = e) FirebaseEvent.logException(e) - action.emit(StateOperatorAction.ResultViewBackToIdle) showError(e.message ?: "Unknown error found while translating") } } @@ -341,7 +327,6 @@ class StateOperatorImpl @Inject constructor( when (translationResult) { TranslationResult.OuterTranslatorLaunched -> { FirebaseEvent.logTranslationTextFinished(translator) - action.emit(StateOperatorAction.ResultViewBackToIdle) backToIdle() } @@ -349,27 +334,13 @@ class StateOperatorImpl @Inject constructor( FirebaseEvent.logTranslationSourceLangNotSupport( translator, recognitionResult.langCode, ) - val result = Result.SourceLangNotSupport( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - providerType = translationResult.type, - ) - action.emit( - StateOperatorAction.ResultViewTextTranslated(result) - ) + showError(context.getString(R.string.msg_translator_provider_does_not_support_the_ocr_lang)) } TranslationResult.OCROnlyResult -> { FirebaseEvent.logTranslationTextFinished(translator) - action.emit( - StateOperatorAction.ResultViewTextTranslated( - Result.OCROnly( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - ) - ) - ) + stateNavigator.updateState( NavState.TextTranslated( parentRect = parentRect, @@ -383,16 +354,7 @@ class StateOperatorImpl @Inject constructor( is TranslationResult.TranslatedResult -> { FirebaseEvent.logTranslationTextFinished(translator) - action.emit( - StateOperatorAction.ResultViewTextTranslated( - Result.Translated( - ocrText = recognitionResult.result, - boundingBoxes = recognitionResult.boundingBoxes, - translatedText = translationResult.result, - providerType = translationResult.type, - ) - ) - ) + stateNavigator.updateState( NavState.TextTranslated( parentRect = parentRect, @@ -415,8 +377,6 @@ class StateOperatorImpl @Inject constructor( FirebaseEvent.logMicrosoftTranslationError(error) } - action.emit(StateOperatorAction.ResultViewBackToIdle) - if (error is IOException) { showError(context.getString(R.string.error_can_not_connect_to_translation_server)) } else { @@ -440,6 +400,8 @@ class StateOperatorImpl @Inject constructor( if (currentNavState != NavState.Idle) stateNavigator.updateState(NavState.Idle) + action.emit(StateOperatorAction.HideResultView) + if (showMainBar) action.emit(StateOperatorAction.ShowMainBar) @@ -456,28 +418,8 @@ sealed interface StateOperatorAction { data object ShowMainBar : StateOperatorAction data object ShowScreenCirclingView : StateOperatorAction data object HideScreenCirclingView : StateOperatorAction - data object ResultViewStartRecognition : StateOperatorAction //TODO subscribe state in view - - @Deprecated("subscribe state in view") - data class ResultViewSetRecognized( - val result: RecognitionResult, - val parentRect: Rect, - val selectedRect: Rect, - val croppedBitmap: Bitmap, - ) : StateOperatorAction //TODO subscribe state in view - - @Deprecated("subscribe state in view") - data class ResultViewStartTranslation( - val translationProviderType: TranslationProviderType, - ) : StateOperatorAction //TODO subscribe state in view - - @Deprecated("subscribe state in view") - data class ResultViewTextTranslated(val result: Result) : - StateOperatorAction //TODO subscribe state in view - - @Deprecated("subscribe state in view") - data object ResultViewBackToIdle : StateOperatorAction //TODO subscribe state in view - + data object ShowResultView : StateOperatorAction + data object HideResultView : StateOperatorAction data class ShowErrorDialog(val error: String) : StateOperatorAction } From 99a5b4af12595f5242468275a5fb24847c55cc45 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 15:12:52 +0900 Subject: [PATCH 023/121] Update animation on main bar and result view --- .../compose/base/ComposeMovableFloatingView.kt | 2 +- .../compose/resultview/ResultViewContent.kt | 8 +++++--- .../compose/resultview/ResultViewModel.kt | 15 +++++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt index 1ac45706..b93db8e2 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeMovableFloatingView.kt @@ -80,7 +80,7 @@ abstract class ComposeMovableFloatingView(context: Context) : ComposeFloatingVie //region Moving to edge fun moveToEdgeIfEnabled() { - rootView.post { if (moveToEdgeAfterMoved) moveToEdge() } + rootView.postDelayed({ if (moveToEdgeAfterMoved) moveToEdge() }, 100L) } private fun moveToEdge() { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 81dcee89..39115c0a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -2,6 +2,7 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.res.Configuration import android.graphics.Rect +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -85,8 +86,8 @@ fun ResultViewContent( ) } - val xOffset = remember { mutableStateOf(0) } - val yOffset = remember { mutableStateOf(0) } + val xOffset = remember { mutableStateOf(state.highlightUnion.left) } + val yOffset = remember { mutableStateOf(state.highlightUnion.top) } ResultPanel( modifier = Modifier @@ -99,7 +100,8 @@ fun ResultViewContent( .offset( xOffset.value.pxToDp(), yOffset.value.pxToDp(), - ), + ) + .animateContentSize(), viewModel = viewModel, textSearchEnabled = state.textSearchEnabled, fontSize = state.fontSize, diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 9a397a82..aced089b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -147,8 +147,15 @@ class ResultViewModelImpl @Inject constructor( when (navState) { is NavState.TextRecognizing -> state.update { + val (textAreas, unionArea) = calculateTextAreas( + listOf(navState.selectedRect), + navState.parentRect, + navState.selectedRect, + ) + it.copy( - highlightArea = listOf(navState.selectedRect), + highlightArea = textAreas, + highlightUnion = unionArea, ocrState = it.ocrState.copy( showProcessing = true, ) @@ -161,7 +168,7 @@ class ResultViewModelImpl @Inject constructor( val needTranslate = !navState.translationProviderType.nonTranslation val (textAreas, unionArea) = calculateTextAreas( - navState.recognitionResult, + navState.recognitionResult.boundingBoxes, navState.parentRect, navState.selectedRect, ) @@ -249,13 +256,13 @@ class ResultViewModelImpl @Inject constructor( } private fun calculateTextAreas( - result: RecognitionResult, + boundingBoxes: List, parent: Rect, selected: Rect, ): Pair, Rect> { val topOffset = parent.top + selected.top - rootViewYOffset val leftOffset = parent.left + selected.left - rootViewXOffset - val textAreas = result.boundingBoxes.map { + val textAreas = boundingBoxes.map { Rect( it.left + leftOffset, it.top + topOffset, From 2ff47c2a7619e01ebc9d16793926371923947b9b Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 16:22:10 +0900 Subject: [PATCH 024/121] Add [All] selection to WordSelectionText --- .../compose/wigets/WordSelectionText.kt | 50 +++++++++++++------ main/src/main/res/values/strings.xml | 3 +- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt index 3a69a4c8..9c98d28b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt @@ -7,12 +7,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.utils.WordBoundary import java.util.Locale @@ -27,7 +29,8 @@ fun WordSelectionText( ) { var selectedStart by remember { mutableStateOf(-1) } val annotatedString = buildText( - text = text, + fullText = text, + textAll = stringResource(id = R.string.text_all_text), locale = locale, selectedStart = selectedStart, selectedSpanStyle = selectedSpanStyle, @@ -40,24 +43,23 @@ fun WordSelectionText( val clicked = annotatedString.getStringAnnotations(offset, offset) .firstOrNull() if (clicked != null) { - if (selectedStart == clicked.start) { - selectedStart = -1 - } else { - selectedStart = clicked.start - onTextSelected.invoke(clicked.item) - } + selectedStart = clicked.start + onTextSelected.invoke(clicked.tag) } }, ) } private fun buildText( - text: String, + fullText: String, + textAll: String, locale: Locale, selectedStart: Int, selectedSpanStyle: SpanStyle ) = buildAnnotatedString { - if (text.isEmpty()) return@buildAnnotatedString + if (fullText.isEmpty()) return@buildAnnotatedString + + val text = "$textAll $fullText" val boundaries = WordBoundary.breakWords(text = text, locale = locale) if (boundaries.isEmpty()) { @@ -83,13 +85,31 @@ private fun buildText( append(text.substring(textStart until nextBoundary.start)) textStart = nextBoundary.start } else if (textStart == nextBoundary.start) { - val style = if (textStart == selectedStart) selectedStyle else unselectedStyle - val word = text.substring(nextBoundary.start until nextBoundary.end) - withStyle(style = style) { - pushStringAnnotation(tag = word, annotation = word) - append(word) + val style = if (textStart == selectedStart) + selectedStyle else unselectedStyle + + if (nextBoundary.start < textAll.length) { + while (true) { + val next = boundaries.getOrNull(index + 1) + if (next == null || next.start >= textAll.length) { + break + } + index++ + } + + withStyle(style = style) { + pushStringAnnotation(tag = fullText, annotation = textAll) + append(textAll) + } + textStart = textAll.length + } else { + val word = text.substring(nextBoundary.start until nextBoundary.end) + withStyle(style = style) { + pushStringAnnotation(tag = word, annotation = word) + append(word) + } + textStart = nextBoundary.end } - textStart = nextBoundary.end index++ } } diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 2c6cc7e2..723ce1e2 100644 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -74,6 +74,7 @@ Translation language selection is not available for OCR only mode Request permission Open in Browser + [All] EverTranslator needs the [Display over other apps] permission to show a floating window on the screen. EverTranslator needs the [Capture Screen] permission to find the text from the screen for you. @@ -117,4 +118,4 @@ None Keep MediaProjection resources - \ No newline at end of file + From 988e88565574bb11c56263cc2a4013196701222c Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 16:26:48 +0900 Subject: [PATCH 025/121] Adjust result view layout --- .../floatings/compose/resultview/ResultViewContent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 39115c0a..1e191fa8 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -200,6 +200,7 @@ private fun ResultPanel( } if (translationState.showTranslationArea) { + Spacer(modifier = Modifier.size(2.dp)) TranslationToolBar( onCopyClicked = { viewModel.onCopyClicked(TextType.TranslationResult) }, onGoogleTranslateClicked = { viewModel.onGoogleTranslateClicked(TextType.TranslationResult) } From 75d5337dc3accc684f59b82ba9c69ba9dff6d1cb Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 18:27:51 +0900 Subject: [PATCH 026/121] Fix result view size and position --- .../compose/resultview/ResultViewContent.kt | 70 ++++++++++++------- .../compose/resultview/ResultViewModel.kt | 10 +-- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 1e191fa8..336ecfc3 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -34,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -53,10 +51,9 @@ import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme import tw.firemaples.onscreenocr.floatings.compose.base.FontSize +import tw.firemaples.onscreenocr.floatings.compose.base.dpToPx import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp import tw.firemaples.onscreenocr.floatings.compose.wigets.WordSelectionText -import tw.firemaples.onscreenocr.utils.UIUtils -import tw.firemaples.onscreenocr.utils.dpToPx import java.util.Locale @Composable @@ -91,11 +88,13 @@ fun ResultViewContent( ResultPanel( modifier = Modifier + .padding(16.dp) .calculateOffset( - requestRootLocationOnScreen = requestRootLocationOnScreen, highlightUnion = state.highlightUnion, xOffset = xOffset, yOffset = yOffset, + padding = 16.dp.dpToPx(), + verticalSpacing = 4.dp.dpToPx(), ) .offset( xOffset.value.pxToDp(), @@ -112,33 +111,55 @@ fun ResultViewContent( } private fun Modifier.calculateOffset( - requestRootLocationOnScreen: () -> Rect, highlightUnion: Rect, xOffset: MutableState, yOffset: MutableState, + padding: Float, + verticalSpacing: Float, ): Modifier = onGloballyPositioned { coordinates -> - val parentRect = requestRootLocationOnScreen.invoke() - val anchorRect = Rect(highlightUnion).apply { - top += parentRect.top - left += parentRect.left - bottom += parentRect.top - right += parentRect.left + val parent = coordinates.parentLayoutCoordinates?.size ?: return@onGloballyPositioned + val current = coordinates.size + + val leftAnchor = maxOf(highlightUnion.left, padding.toInt()) + val rightAnchor = minOf(highlightUnion.right, parent.width - padding.toInt()) + + when { + leftAnchor + current.width + padding < parent.width -> { + // Align left + xOffset.value = highlightUnion.left - padding.toInt() + } + + rightAnchor - current.width - padding >= 0 -> { + // Align right + xOffset.value = rightAnchor - current.width - padding.toInt() + } + + else -> { + // No horizontal alignment + xOffset.value = 0 + } } - val bounds = coordinates.boundsInRoot() - val left = parentRect.left + bounds.left - val top = parentRect.top + bounds.top - val right = parentRect.left + bounds.right - val bottom = parentRect.top + bounds.bottom - val windowRect = Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) + val topAnchor = highlightUnion.bottom + verticalSpacing + val bottomAnchor = highlightUnion.top - verticalSpacing - val (leftMargin, topMargin) = UIUtils.countViewPosition( - anchorRect, parentRect, - windowRect.width(), windowRect.height(), 2.dpToPx(), - ) + when { + topAnchor + current.height + padding < parent.height -> { + // Display at bottom + yOffset.value = (topAnchor - padding).toInt() + } - xOffset.value = leftMargin - yOffset.value = topMargin + bottomAnchor - current.height - padding >= 0 -> { + // Display at top + yOffset.value = (bottomAnchor - current.height - padding).toInt() + } + + else -> { + // Display middle vertically + val middleAnchor = (parent.height - current.height) / 2 + yOffset.value = (middleAnchor - padding).toInt() + } + } } @Composable @@ -172,7 +193,6 @@ private fun ResultPanel( ) { Column( modifier = modifier - .widthIn(max = 300.dp) .background( color = AppColorScheme.background, shape = RoundedCornerShape(8.dp), diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index aced089b..7875c302 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -147,15 +147,9 @@ class ResultViewModelImpl @Inject constructor( when (navState) { is NavState.TextRecognizing -> state.update { - val (textAreas, unionArea) = calculateTextAreas( - listOf(navState.selectedRect), - navState.parentRect, - navState.selectedRect, - ) - it.copy( - highlightArea = textAreas, - highlightUnion = unionArea, + highlightArea = listOf(navState.selectedRect), + highlightUnion = navState.selectedRect, ocrState = it.ocrState.copy( showProcessing = true, ) From b81d86bae267dfe6e8abb0c57e5f32cba14f0fa4 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 18:46:50 +0900 Subject: [PATCH 027/121] Add result panel position animation --- .../compose/resultview/ResultViewContent.kt | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 336ecfc3..365417a6 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -3,6 +3,7 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.res.Configuration import android.graphics.Rect import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateIntOffsetAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -41,6 +42,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.flow.MutableSharedFlow @@ -83,23 +85,25 @@ fun ResultViewContent( ) } - val xOffset = remember { mutableStateOf(state.highlightUnion.left) } - val yOffset = remember { mutableStateOf(state.highlightUnion.top) } + val targetOffset = remember { + mutableStateOf(IntOffset(state.highlightUnion.left, state.highlightUnion.top)) + } + + val animOffset by animateIntOffsetAsState( + targetValue = targetOffset.value, + label = "result panel position", + ) ResultPanel( modifier = Modifier .padding(16.dp) .calculateOffset( highlightUnion = state.highlightUnion, - xOffset = xOffset, - yOffset = yOffset, + offset = targetOffset, padding = 16.dp.dpToPx(), verticalSpacing = 4.dp.dpToPx(), ) - .offset( - xOffset.value.pxToDp(), - yOffset.value.pxToDp(), - ) + .offset { animOffset } .animateContentSize(), viewModel = viewModel, textSearchEnabled = state.textSearchEnabled, @@ -112,8 +116,7 @@ fun ResultViewContent( private fun Modifier.calculateOffset( highlightUnion: Rect, - xOffset: MutableState, - yOffset: MutableState, + offset: MutableState, padding: Float, verticalSpacing: Float, ): Modifier = onGloballyPositioned { coordinates -> @@ -123,43 +126,45 @@ private fun Modifier.calculateOffset( val leftAnchor = maxOf(highlightUnion.left, padding.toInt()) val rightAnchor = minOf(highlightUnion.right, parent.width - padding.toInt()) - when { + val x = when { leftAnchor + current.width + padding < parent.width -> { // Align left - xOffset.value = highlightUnion.left - padding.toInt() + highlightUnion.left - padding.toInt() } rightAnchor - current.width - padding >= 0 -> { // Align right - xOffset.value = rightAnchor - current.width - padding.toInt() + rightAnchor - current.width - padding.toInt() } else -> { // No horizontal alignment - xOffset.value = 0 + 0 } } val topAnchor = highlightUnion.bottom + verticalSpacing val bottomAnchor = highlightUnion.top - verticalSpacing - when { + val y = when { topAnchor + current.height + padding < parent.height -> { // Display at bottom - yOffset.value = (topAnchor - padding).toInt() + (topAnchor - padding).toInt() } bottomAnchor - current.height - padding >= 0 -> { // Display at top - yOffset.value = (bottomAnchor - current.height - padding).toInt() + (bottomAnchor - current.height - padding).toInt() } else -> { // Display middle vertically val middleAnchor = (parent.height - current.height) / 2 - yOffset.value = (middleAnchor - padding).toInt() + (middleAnchor - padding).toInt() } } + + offset.value = IntOffset(x, y) } @Composable From a0a0cbada232b5c1050dccbe6040dcd3645fdcc3 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 21 Jan 2024 18:54:37 +0900 Subject: [PATCH 028/121] Round the selected area --- .../floatings/compose/resultview/ResultViewContent.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 365417a6..78a7c126 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -183,7 +183,10 @@ private fun TextHighlightBox(highlightArea: Rect) { .height() .pxToDp(), ) - .background(colorResource(id = R.color.resultView_recognizedBoundingBoxes)) + .background( + color = colorResource(id = R.color.resultView_recognizedBoundingBoxes), + shape = RoundedCornerShape(2.dp), + ) ) } From 7a62a9cdfec8dfabac7bae8da05bc3a0343e0cfe Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 28 Jan 2024 22:19:15 +0900 Subject: [PATCH 029/121] Fix Google Translate language in text search view --- .../textInfoSearch/TextInfoSearchViewModel.kt | 9 ++++++++- .../java/tw/firemaples/onscreenocr/utils/Utils.kt | 13 +++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt index 83058606..b96d6a1d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/textInfoSearch/TextInfoSearchViewModel.kt @@ -1,5 +1,6 @@ package tw.firemaples.onscreenocr.floatings.textInfoSearch +import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope @@ -7,6 +8,7 @@ import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel import tw.firemaples.onscreenocr.pref.AppPref import tw.firemaples.onscreenocr.utils.Constants +import tw.firemaples.onscreenocr.utils.toGoogleTranslateLang import java.net.URLEncoder class TextInfoSearchViewModel( @@ -100,7 +102,12 @@ class TextInfoSearchViewModel( class GoogleTranslate(text: String, sourceLang: String, val targetLang: String) : Page(text, sourceLang, PageType.GoogleTranslate) { override val url: String - get() = "https://translate.google.com/?sl=$sourceLang&tl=$targetLang&text=$encodedText&op=translate" + get() = Uri.parse("https://translate.google.com/?op=translate") + .buildUpon() + .appendQueryParameter("sl", sourceLang.toGoogleTranslateLang()) + .appendQueryParameter("tl", targetLang.toGoogleTranslateLang()) + .appendQueryParameter("text", text) + .toString() } class Wikipedia(text: String, sourceLang: String) : diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/Utils.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/Utils.kt index 5b2504af..cb7afbb7 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/Utils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/Utils.kt @@ -107,6 +107,19 @@ object Utils { fun String.firstPart(): String = split(":")[0].split("-")[0] +private val googleTranslateLang = mapOf( + "zh-TW" to setOf("zh-tw", "zh-hant", "zh"), + "zh-CN" to setOf("zh-cn", "zh-hans"), +) + +fun String.toGoogleTranslateLang(): String { + val target = this + googleTranslateLang.entries.forEach { (lang, set) -> + if (set.contains(target.lowercase())) return lang + } + return target.firstPart() +} + fun Context.getThemedLayoutInflater(theme: Int = R.style.Theme_EverTranslator): LayoutInflater = LayoutInflater.from(this) .cloneInContext(ContextThemeWrapper(this, theme)) From b5b1534ad5ac69766333816599e96d42981c4d58 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 3 Feb 2024 22:44:01 +0900 Subject: [PATCH 030/121] Add missing end blank lines --- detekt.yml | 2 +- .../data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt | 2 +- .../onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt | 2 +- .../tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt | 2 +- .../firemaples/onscreenocr/floatings/compose/base/AppTheme.kt | 2 +- .../onscreenocr/floatings/compose/mainbar/MainBarContent.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/detekt.yml b/detekt.yml index c7f9f196..63c721c0 100644 --- a/detekt.yml +++ b/detekt.yml @@ -82,4 +82,4 @@ Compose: ViewModelInjection: active: true # -- You can optionally add your own ViewModel factories here - # viewModelFactories: hiltViewModel,potatoViewModel \ No newline at end of file + # viewModelFactories: hiltViewModel,potatoViewModel diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt index e7461d65..32e8cadc 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetCurrentOCRDisplayLangCodeUseCase.kt @@ -13,4 +13,4 @@ class GetCurrentOCRDisplayLangCodeUseCase @Inject constructor( .combine(recognitionRepository.ocrLanguage) { provider, lang -> TextRecognizer.getRecognizer(provider).parseToDisplayLangCode(lang) } -} \ No newline at end of file +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt index de439f52..c4ce9282 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SaveLastMainBarPositionUseCase.kt @@ -8,4 +8,4 @@ class SaveLastMainBarPositionUseCase @Inject constructor( ) { operator fun invoke(x: Int, y: Int) = preferenceRepository.saveLastMainBarPosition(x = x, y = y) -} \ No newline at end of file +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt index 46268bf5..c22fccae 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/di/CoroutinesDispatchersModule.kt @@ -49,4 +49,4 @@ object CoroutinesDispatchersModule { fun provideDefaultCoroutineScope( @DefaultDispatcher dispatcher: CoroutineDispatcher, ): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) -} \ No newline at end of file +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt index f61442f1..7c386f42 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt @@ -25,4 +25,4 @@ fun AppTheme( object FontSize { val Small = 14.sp -} \ No newline at end of file +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index d6892ff4..7d2b5bb2 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -238,4 +238,4 @@ private fun MainBarContentPreview( onDrag = { change, dragAmount -> }) } -} \ No newline at end of file +} From 1abfb872636a82c87a514ded0a98ee6279181b29 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 3 Feb 2024 22:54:27 +0900 Subject: [PATCH 031/121] Comment out old result view --- .../floatings/result/ResultView.kt | 532 +++++++++--------- .../floatings/result/ResultViewModel.kt | 470 ++++++++-------- 2 files changed, 501 insertions(+), 501 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt index 354f42c2..3345d53d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultView.kt @@ -1,266 +1,266 @@ -package tw.firemaples.onscreenocr.floatings.result - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Rect -import android.text.method.ScrollingMovementMethod -import android.util.TypedValue -import android.view.View -import android.view.WindowManager -import android.widget.RelativeLayout -import androidx.core.content.ContextCompat -import dagger.hilt.android.qualifiers.ApplicationContext -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.databinding.FloatingResultViewBinding -import tw.firemaples.onscreenocr.databinding.ViewResultPanelBinding -import tw.firemaples.onscreenocr.floatings.base.FloatingView -import tw.firemaples.onscreenocr.floatings.manager.Result -import tw.firemaples.onscreenocr.floatings.recognizedTextEditor.RecognizedTextEditor -import tw.firemaples.onscreenocr.floatings.textInfoSearch.TextInfoSearchView -import tw.firemaples.onscreenocr.recognition.RecognitionResult -import tw.firemaples.onscreenocr.translator.TranslationProviderType -import tw.firemaples.onscreenocr.translator.utils.GoogleTranslateUtils -import tw.firemaples.onscreenocr.utils.Logger -import tw.firemaples.onscreenocr.utils.UIUtils -import tw.firemaples.onscreenocr.utils.Utils -import tw.firemaples.onscreenocr.utils.clickOnce -import tw.firemaples.onscreenocr.utils.dpToPx -import tw.firemaples.onscreenocr.utils.getViewRect -import tw.firemaples.onscreenocr.utils.setReusable -import tw.firemaples.onscreenocr.utils.setTextOrGone -import tw.firemaples.onscreenocr.utils.showOrHide -import java.util.Locale -import javax.inject.Inject - -class ResultView @Inject constructor( - @ApplicationContext context: Context, - private val viewModel: ResultViewModel, -) : FloatingView(context) { - companion object { - private const val LABEL_RECOGNIZED_TEXT = "Recognized text" - private const val LABEL_TRANSLATED_TEXT = "Translated text" - } - - private val logger: Logger by lazy { Logger(ResultView::class) } - - override val layoutId: Int - get() = R.layout.floating_result_view - - override val layoutWidth: Int - get() = WindowManager.LayoutParams.MATCH_PARENT - - override val layoutHeight: Int - get() = WindowManager.LayoutParams.MATCH_PARENT - - override val enableHomeButtonWatcher: Boolean - get() = true - - private val binding: FloatingResultViewBinding = FloatingResultViewBinding.bind(rootLayout) - - private val viewRoot: RelativeLayout = binding.viewRoot - - var onUserDismiss: (() -> Unit)? = null - - private val viewResultWindow: View = binding.viewResultWindow - - private var unionRect: Rect = Rect() - - private var croppedBitmap: Bitmap? = null - - init { - binding.resultPanel.setViews() - } - - private fun ViewResultPanelBinding.setViews() { - viewModel.displayOCROperationProgress.observe(lifecycleOwner) { - pbOcrOperating.showOrHide(it) - } - viewModel.displayTranslationProgress.observe(lifecycleOwner) { - pbTranslationOperating.showOrHide(it) - } - viewModel.displaySelectableText.observe(lifecycleOwner) { - textSelectable.isChecked = it - tvOcrText.showOrHide(!it) - tvWordBreakOcrText.showOrHide(it) - } - viewModel.ocrText.observe(lifecycleOwner) { - tvWordBreakOcrText.setContent(it?.text(), it?.locale() ?: Locale.getDefault()) - tvOcrText.text = it?.text() - } - viewModel.translatedText.observe(lifecycleOwner) { - if (it == null) { - tvTranslatedText.text = null - } else { - val (text, color) = it - tvTranslatedText.text = text - tvTranslatedText.setTextColor(ContextCompat.getColor(context, color)) - } - - reposition() - } - - viewModel.displayRecognitionBlock.observe(lifecycleOwner) { - groupRecognitionViews.showOrHide(it) - } - viewModel.displayTranslatedBlock.observe(lifecycleOwner) { - groupTranslationViews.showOrHide(it) - } - - viewModel.translationProviderText.observe(lifecycleOwner) { - tvTranslationProvider.setTextOrGone(it) - } - viewModel.displayTranslatedByGoogle.observe(lifecycleOwner) { - ivTranslatedByGoogle.showOrHide(it) - } - - viewModel.displayRecognizedTextAreas.observe(lifecycleOwner) { - val (boundingBoxes, unionRect) = it - binding.viewTextBoundingBoxView.boundingBoxes = boundingBoxes - updateSelectedAreas(unionRect) - } - - viewModel.copyRecognizedText.observe(lifecycleOwner) { - Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, it) - } - - viewModel.fontSize.observe(lifecycleOwner) { - tvOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) - tvWordBreakOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) - tvTranslatedText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) - } - - viewModel.displayTextInfoSearchView.observe(lifecycleOwner) { - TextInfoSearchView(context, it.text, it.sourceLang, it.targetLang) - .attachToScreen() - } - - textSelectable.setOnCheckedChangeListener { _, checked -> - viewModel.onTextSelectableChecked(checked) - } - tvWordBreakOcrText.onWordClicked = { word -> - if (word != null) { - viewModel.onWordSelected(word) - tvWordBreakOcrText.clearSelection() - } - } - tvOcrText.movementMethod = ScrollingMovementMethod() - tvTranslatedText.movementMethod = ScrollingMovementMethod() - viewRoot.clickOnce { onUserDismiss?.invoke() } - btEditOCRText.clickOnce { - showRecognizedTextEditor(viewModel.ocrText.value?.text() ?: "") - } - btCopyOCRText.clickOnce { - Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, viewModel.ocrText.value?.text() ?: "") - } - btCopyTranslatedText.clickOnce { - Utils.copyToClipboard(LABEL_TRANSLATED_TEXT, tvTranslatedText.text.toString()) - } - btTranslateOCRTextWithGoogleTranslate.clickOnce { - GoogleTranslateUtils.launchTranslator(viewModel.ocrText.value?.text() ?: "") - onUserDismiss?.invoke() - } - btTranslateTranslatedTextWithGoogleTranslate.clickOnce { - GoogleTranslateUtils.launchTranslator(tvTranslatedText.text.toString()) - onUserDismiss?.invoke() - } - btShareOCRText.clickOnce { - val ocrText = viewModel.ocrText.value?.text() ?: return@clickOnce - Utils.shareText(ocrText) - onUserDismiss?.invoke() - } - btAdjustFontSize.clickOnce { - FontSizeAdjuster(context).attachToScreen() - } - } - - private fun showRecognizedTextEditor(recognizedText: String) { - RecognizedTextEditor( - context = context, - review = croppedBitmap, - text = recognizedText, - onSubmit = { - if (it.isNotBlank() && it.trim() != recognizedText) { - viewModel.onOCRTextEdited(it.trim()) - } - }, - ).attachToScreen() - } - - override fun onAttachedToScreen() { - super.onAttachedToScreen() - viewResultWindow.visibility = View.INVISIBLE - } - - override fun onDetachedFromScreen() { - super.onDetachedFromScreen() - this.croppedBitmap?.setReusable() - this.croppedBitmap = null - } - - override fun onHomeButtonPressed() { - super.onHomeButtonPressed() - onUserDismiss?.invoke() - } - - fun startRecognition() { - attachToScreen() - viewModel.startRecognition() - } - - fun textRecognized( - result: RecognitionResult, - parent: Rect, - selected: Rect, - croppedBitmap: Bitmap - ) { - this.croppedBitmap = croppedBitmap - viewModel.textRecognized(result, parent, selected, rootView.getViewRect()) - } - - fun startTranslation(translationProviderType: TranslationProviderType) { - viewModel.startTranslation(translationProviderType) - } - - fun textTranslated(result: Result) { - viewModel.textTranslated(result) - } - - fun backToIdle() { - detachFromScreen() - } - - private fun updateSelectedAreas(unionRect: Rect) { - this.unionRect = unionRect - reposition() - } - - private fun reposition() { - rootView.post { - val parentRect = viewRoot.getViewRect() - val anchorRect = Rect(unionRect).apply { - top += parentRect.top - left += parentRect.left - bottom += parentRect.top - right += parentRect.left - } - val windowRect = viewResultWindow.getViewRect() - - val (leftMargin, topMargin) = UIUtils.countViewPosition( - anchorRect, parentRect, - windowRect.width(), windowRect.height(), 2.dpToPx(), - ) - - val layoutParams = - (viewResultWindow.layoutParams as RelativeLayout.LayoutParams).apply { - this.leftMargin = leftMargin - this.topMargin = topMargin - } - - viewRoot.updateViewLayout(viewResultWindow, layoutParams) - - viewRoot.post { - viewResultWindow.visibility = View.VISIBLE - } - } - } -} +//package tw.firemaples.onscreenocr.floatings.result +// +//import android.content.Context +//import android.graphics.Bitmap +//import android.graphics.Rect +//import android.text.method.ScrollingMovementMethod +//import android.util.TypedValue +//import android.view.View +//import android.view.WindowManager +//import android.widget.RelativeLayout +//import androidx.core.content.ContextCompat +//import dagger.hilt.android.qualifiers.ApplicationContext +//import tw.firemaples.onscreenocr.R +//import tw.firemaples.onscreenocr.databinding.FloatingResultViewBinding +//import tw.firemaples.onscreenocr.databinding.ViewResultPanelBinding +//import tw.firemaples.onscreenocr.floatings.base.FloatingView +//import tw.firemaples.onscreenocr.floatings.manager.Result +//import tw.firemaples.onscreenocr.floatings.recognizedTextEditor.RecognizedTextEditor +//import tw.firemaples.onscreenocr.floatings.textInfoSearch.TextInfoSearchView +//import tw.firemaples.onscreenocr.recognition.RecognitionResult +//import tw.firemaples.onscreenocr.translator.TranslationProviderType +//import tw.firemaples.onscreenocr.translator.utils.GoogleTranslateUtils +//import tw.firemaples.onscreenocr.utils.Logger +//import tw.firemaples.onscreenocr.utils.UIUtils +//import tw.firemaples.onscreenocr.utils.Utils +//import tw.firemaples.onscreenocr.utils.clickOnce +//import tw.firemaples.onscreenocr.utils.dpToPx +//import tw.firemaples.onscreenocr.utils.getViewRect +//import tw.firemaples.onscreenocr.utils.setReusable +//import tw.firemaples.onscreenocr.utils.setTextOrGone +//import tw.firemaples.onscreenocr.utils.showOrHide +//import java.util.Locale +//import javax.inject.Inject +// +//class ResultView @Inject constructor( +// @ApplicationContext context: Context, +// private val viewModel: ResultViewModel, +//) : FloatingView(context) { +// companion object { +// private const val LABEL_RECOGNIZED_TEXT = "Recognized text" +// private const val LABEL_TRANSLATED_TEXT = "Translated text" +// } +// +// private val logger: Logger by lazy { Logger(ResultView::class) } +// +// override val layoutId: Int +// get() = R.layout.floating_result_view +// +// override val layoutWidth: Int +// get() = WindowManager.LayoutParams.MATCH_PARENT +// +// override val layoutHeight: Int +// get() = WindowManager.LayoutParams.MATCH_PARENT +// +// override val enableHomeButtonWatcher: Boolean +// get() = true +// +// private val binding: FloatingResultViewBinding = FloatingResultViewBinding.bind(rootLayout) +// +// private val viewRoot: RelativeLayout = binding.viewRoot +// +// var onUserDismiss: (() -> Unit)? = null +// +// private val viewResultWindow: View = binding.viewResultWindow +// +// private var unionRect: Rect = Rect() +// +// private var croppedBitmap: Bitmap? = null +// +// init { +// binding.resultPanel.setViews() +// } +// +// private fun ViewResultPanelBinding.setViews() { +// viewModel.displayOCROperationProgress.observe(lifecycleOwner) { +// pbOcrOperating.showOrHide(it) +// } +// viewModel.displayTranslationProgress.observe(lifecycleOwner) { +// pbTranslationOperating.showOrHide(it) +// } +// viewModel.displaySelectableText.observe(lifecycleOwner) { +// textSelectable.isChecked = it +// tvOcrText.showOrHide(!it) +// tvWordBreakOcrText.showOrHide(it) +// } +// viewModel.ocrText.observe(lifecycleOwner) { +// tvWordBreakOcrText.setContent(it?.text(), it?.locale() ?: Locale.getDefault()) +// tvOcrText.text = it?.text() +// } +// viewModel.translatedText.observe(lifecycleOwner) { +// if (it == null) { +// tvTranslatedText.text = null +// } else { +// val (text, color) = it +// tvTranslatedText.text = text +// tvTranslatedText.setTextColor(ContextCompat.getColor(context, color)) +// } +// +// reposition() +// } +// +// viewModel.displayRecognitionBlock.observe(lifecycleOwner) { +// groupRecognitionViews.showOrHide(it) +// } +// viewModel.displayTranslatedBlock.observe(lifecycleOwner) { +// groupTranslationViews.showOrHide(it) +// } +// +// viewModel.translationProviderText.observe(lifecycleOwner) { +// tvTranslationProvider.setTextOrGone(it) +// } +// viewModel.displayTranslatedByGoogle.observe(lifecycleOwner) { +// ivTranslatedByGoogle.showOrHide(it) +// } +// +// viewModel.displayRecognizedTextAreas.observe(lifecycleOwner) { +// val (boundingBoxes, unionRect) = it +// binding.viewTextBoundingBoxView.boundingBoxes = boundingBoxes +// updateSelectedAreas(unionRect) +// } +// +// viewModel.copyRecognizedText.observe(lifecycleOwner) { +// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, it) +// } +// +// viewModel.fontSize.observe(lifecycleOwner) { +// tvOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// tvWordBreakOcrText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// tvTranslatedText.setTextSize(TypedValue.COMPLEX_UNIT_SP, it) +// } +// +// viewModel.displayTextInfoSearchView.observe(lifecycleOwner) { +// TextInfoSearchView(context, it.text, it.sourceLang, it.targetLang) +// .attachToScreen() +// } +// +// textSelectable.setOnCheckedChangeListener { _, checked -> +// viewModel.onTextSelectableChecked(checked) +// } +// tvWordBreakOcrText.onWordClicked = { word -> +// if (word != null) { +// viewModel.onWordSelected(word) +// tvWordBreakOcrText.clearSelection() +// } +// } +// tvOcrText.movementMethod = ScrollingMovementMethod() +// tvTranslatedText.movementMethod = ScrollingMovementMethod() +// viewRoot.clickOnce { onUserDismiss?.invoke() } +// btEditOCRText.clickOnce { +// showRecognizedTextEditor(viewModel.ocrText.value?.text() ?: "") +// } +// btCopyOCRText.clickOnce { +// Utils.copyToClipboard(LABEL_RECOGNIZED_TEXT, viewModel.ocrText.value?.text() ?: "") +// } +// btCopyTranslatedText.clickOnce { +// Utils.copyToClipboard(LABEL_TRANSLATED_TEXT, tvTranslatedText.text.toString()) +// } +// btTranslateOCRTextWithGoogleTranslate.clickOnce { +// GoogleTranslateUtils.launchTranslator(viewModel.ocrText.value?.text() ?: "") +// onUserDismiss?.invoke() +// } +// btTranslateTranslatedTextWithGoogleTranslate.clickOnce { +// GoogleTranslateUtils.launchTranslator(tvTranslatedText.text.toString()) +// onUserDismiss?.invoke() +// } +// btShareOCRText.clickOnce { +// val ocrText = viewModel.ocrText.value?.text() ?: return@clickOnce +// Utils.shareText(ocrText) +// onUserDismiss?.invoke() +// } +// btAdjustFontSize.clickOnce { +// FontSizeAdjuster(context).attachToScreen() +// } +// } +// +// private fun showRecognizedTextEditor(recognizedText: String) { +// RecognizedTextEditor( +// context = context, +// review = croppedBitmap, +// text = recognizedText, +// onSubmit = { +// if (it.isNotBlank() && it.trim() != recognizedText) { +// viewModel.onOCRTextEdited(it.trim()) +// } +// }, +// ).attachToScreen() +// } +// +// override fun onAttachedToScreen() { +// super.onAttachedToScreen() +// viewResultWindow.visibility = View.INVISIBLE +// } +// +// override fun onDetachedFromScreen() { +// super.onDetachedFromScreen() +// this.croppedBitmap?.setReusable() +// this.croppedBitmap = null +// } +// +// override fun onHomeButtonPressed() { +// super.onHomeButtonPressed() +// onUserDismiss?.invoke() +// } +// +// fun startRecognition() { +// attachToScreen() +// viewModel.startRecognition() +// } +// +// fun textRecognized( +// result: RecognitionResult, +// parent: Rect, +// selected: Rect, +// croppedBitmap: Bitmap +// ) { +// this.croppedBitmap = croppedBitmap +// viewModel.textRecognized(result, parent, selected, rootView.getViewRect()) +// } +// +// fun startTranslation(translationProviderType: TranslationProviderType) { +// viewModel.startTranslation(translationProviderType) +// } +// +// fun textTranslated(result: Result) { +// viewModel.textTranslated(result) +// } +// +// fun backToIdle() { +// detachFromScreen() +// } +// +// private fun updateSelectedAreas(unionRect: Rect) { +// this.unionRect = unionRect +// reposition() +// } +// +// private fun reposition() { +// rootView.post { +// val parentRect = viewRoot.getViewRect() +// val anchorRect = Rect(unionRect).apply { +// top += parentRect.top +// left += parentRect.left +// bottom += parentRect.top +// right += parentRect.left +// } +// val windowRect = viewResultWindow.getViewRect() +// +// val (leftMargin, topMargin) = UIUtils.countViewPosition( +// anchorRect, parentRect, +// windowRect.width(), windowRect.height(), 2.dpToPx(), +// ) +// +// val layoutParams = +// (viewResultWindow.layoutParams as RelativeLayout.LayoutParams).apply { +// this.leftMargin = leftMargin +// this.topMargin = topMargin +// } +// +// viewRoot.updateViewLayout(viewResultWindow, layoutParams) +// +// viewRoot.post { +// viewResultWindow.visibility = View.VISIBLE +// } +// } +// } +//} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt index bc3e6e69..3008f4bd 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/result/ResultViewModel.kt @@ -1,236 +1,236 @@ -package tw.firemaples.onscreenocr.floatings.result - -import android.content.Context -import android.graphics.Rect -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.chibatching.kotpref.livedata.asLiveData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel -import tw.firemaples.onscreenocr.floatings.manager.NavigationAction -import tw.firemaples.onscreenocr.floatings.manager.Result -import tw.firemaples.onscreenocr.floatings.manager.StateNavigator -import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope -import tw.firemaples.onscreenocr.pref.AppPref -import tw.firemaples.onscreenocr.recognition.RecognitionResult -import tw.firemaples.onscreenocr.repo.GeneralRepository -import tw.firemaples.onscreenocr.translator.TranslationProviderType -import tw.firemaples.onscreenocr.utils.Constants -import tw.firemaples.onscreenocr.utils.Logger -import tw.firemaples.onscreenocr.utils.SingleLiveEvent -import tw.firemaples.onscreenocr.utils.Utils -import java.util.Locale -import javax.inject.Inject - -typealias OCRText = Pair - -fun OCRText.text(): String = this.first -fun OCRText.locale(): Locale = Locale.forLanguageTag(this.second) -fun OCRText.langCode(): String = this.second - -class ResultViewModel @Inject constructor( - @MainImmediateCoroutineScope viewScope: CoroutineScope, - private val stateNavigator: StateNavigator, -) : FloatingViewModel(viewScope) { - private val _displayOCROperationProgress = MutableLiveData() - val displayOCROperationProgress: LiveData = _displayOCROperationProgress - - private val _displayTranslationProgress = MutableLiveData() - val displayTranslationProgress: LiveData = _displayTranslationProgress - - private val _ocrText = MutableLiveData() - val ocrText: LiveData = _ocrText - - private val _translatedText = MutableLiveData?>() - val translatedText: LiveData?> = _translatedText - - val displaySelectableText: LiveData = - AppPref.asLiveData(AppPref::displaySelectedTextOnResultWindow) - - private val _displayRecognitionBlock = MutableLiveData() - val displayRecognitionBlock: LiveData = _displayRecognitionBlock - - private val _displayTranslationBlock = MutableLiveData() - val displayTranslatedBlock: LiveData = _displayTranslationBlock - - private val _translationProviderText = MutableLiveData() - val translationProviderText: LiveData = _translationProviderText - - private val _displayTranslatedByGoogle = MutableLiveData() - val displayTranslatedByGoogle: LiveData = _displayTranslatedByGoogle - - private val _displayRecognizedTextAreas = MutableLiveData, Rect>>() - val displayRecognizedTextAreas: LiveData, Rect>> = _displayRecognizedTextAreas - - private val _copyRecognizedText = SingleLiveEvent() - val copyRecognizedText: LiveData = _copyRecognizedText - - private val _displayTextInfoSearchView = SingleLiveEvent() - val displayTextInfoSearchView: LiveData = _displayTextInfoSearchView - - val fontSize: LiveData = AppPref.asLiveData(AppPref::resultWindowFontSize) - - private val logger: Logger by lazy { Logger(ResultViewModel::class) } - - private val context: Context by lazy { Utils.context } - - private val repo: GeneralRepository by lazy { GeneralRepository() } - - private var lastLangCode: String = Constants.DEFAULT_OCR_LANG - private var lastTextBoundingBoxes: List = listOf() - -// companion object { -// private const val STATE_RECOGNIZING = 0 -// private const val STATE_RECOGNIZED = 0 -// private const val STATE_TRANSLATING = 0 -// private const val STATE_TRANSLATED = 0 +//package tw.firemaples.onscreenocr.floatings.result +// +//import android.content.Context +//import android.graphics.Rect +//import androidx.lifecycle.LiveData +//import androidx.lifecycle.MutableLiveData +//import com.chibatching.kotpref.livedata.asLiveData +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.flow.first +//import kotlinx.coroutines.launch +//import tw.firemaples.onscreenocr.R +//import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel +//import tw.firemaples.onscreenocr.floatings.manager.NavigationAction +//import tw.firemaples.onscreenocr.floatings.manager.Result +//import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +//import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +//import tw.firemaples.onscreenocr.pref.AppPref +//import tw.firemaples.onscreenocr.recognition.RecognitionResult +//import tw.firemaples.onscreenocr.repo.GeneralRepository +//import tw.firemaples.onscreenocr.translator.TranslationProviderType +//import tw.firemaples.onscreenocr.utils.Constants +//import tw.firemaples.onscreenocr.utils.Logger +//import tw.firemaples.onscreenocr.utils.SingleLiveEvent +//import tw.firemaples.onscreenocr.utils.Utils +//import java.util.Locale +//import javax.inject.Inject +// +//typealias OCRText = Pair +// +//fun OCRText.text(): String = this.first +//fun OCRText.locale(): Locale = Locale.forLanguageTag(this.second) +//fun OCRText.langCode(): String = this.second +// +//class ResultViewModel @Inject constructor( +// @MainImmediateCoroutineScope viewScope: CoroutineScope, +// private val stateNavigator: StateNavigator, +//) : FloatingViewModel(viewScope) { +// private val _displayOCROperationProgress = MutableLiveData() +// val displayOCROperationProgress: LiveData = _displayOCROperationProgress +// +// private val _displayTranslationProgress = MutableLiveData() +// val displayTranslationProgress: LiveData = _displayTranslationProgress +// +// private val _ocrText = MutableLiveData() +// val ocrText: LiveData = _ocrText +// +// private val _translatedText = MutableLiveData?>() +// val translatedText: LiveData?> = _translatedText +// +// val displaySelectableText: LiveData = +// AppPref.asLiveData(AppPref::displaySelectedTextOnResultWindow) +// +// private val _displayRecognitionBlock = MutableLiveData() +// val displayRecognitionBlock: LiveData = _displayRecognitionBlock +// +// private val _displayTranslationBlock = MutableLiveData() +// val displayTranslatedBlock: LiveData = _displayTranslationBlock +// +// private val _translationProviderText = MutableLiveData() +// val translationProviderText: LiveData = _translationProviderText +// +// private val _displayTranslatedByGoogle = MutableLiveData() +// val displayTranslatedByGoogle: LiveData = _displayTranslatedByGoogle +// +// private val _displayRecognizedTextAreas = MutableLiveData, Rect>>() +// val displayRecognizedTextAreas: LiveData, Rect>> = _displayRecognizedTextAreas +// +// private val _copyRecognizedText = SingleLiveEvent() +// val copyRecognizedText: LiveData = _copyRecognizedText +// +// private val _displayTextInfoSearchView = SingleLiveEvent() +// val displayTextInfoSearchView: LiveData = _displayTextInfoSearchView +// +// val fontSize: LiveData = AppPref.asLiveData(AppPref::resultWindowFontSize) +// +// private val logger: Logger by lazy { Logger(ResultViewModel::class) } +// +// private val context: Context by lazy { Utils.context } +// +// private val repo: GeneralRepository by lazy { GeneralRepository() } +// +// private var lastLangCode: String = Constants.DEFAULT_OCR_LANG +// private var lastTextBoundingBoxes: List = listOf() +// +//// companion object { +//// private const val STATE_RECOGNIZING = 0 +//// private const val STATE_RECOGNIZED = 0 +//// private const val STATE_TRANSLATING = 0 +//// private const val STATE_TRANSLATED = 0 +//// } +// +// fun startRecognition() { +// viewScope.launch { +// _displayRecognizedTextAreas.value = emptyList() to Rect() +// +// _displayOCROperationProgress.value = true +// _displayTranslationProgress.value = false +// +// _ocrText.value = null +// _translatedText.value = null +// +// _displayRecognitionBlock.value = true +// _displayTranslationBlock.value = false +// _translationProviderText.value = null +// _displayTranslatedByGoogle.value = false +// } // } - - fun startRecognition() { - viewScope.launch { - _displayRecognizedTextAreas.value = emptyList() to Rect() - - _displayOCROperationProgress.value = true - _displayTranslationProgress.value = false - - _ocrText.value = null - _translatedText.value = null - - _displayRecognitionBlock.value = true - _displayTranslationBlock.value = false - _translationProviderText.value = null - _displayTranslatedByGoogle.value = false - } - } - - fun textRecognized(result: RecognitionResult, parent: Rect, selected: Rect, viewRect: Rect) { - viewScope.launch { - this@ResultViewModel.lastLangCode = result.langCode - - _displayOCROperationProgress.value = false - _ocrText.value = result.result to result.langCode - - val topOffset = parent.top + selected.top - viewRect.top - val leftOffset = parent.left + selected.left - viewRect.left - this@ResultViewModel.lastTextBoundingBoxes = result.boundingBoxes.toList() - val textAreas = result.boundingBoxes.map { - Rect( - it.left + leftOffset, - it.top + topOffset, - it.right + leftOffset, - it.bottom + topOffset - ) - } - val unionRect = Rect() - textAreas.forEach { unionRect.union(it) } - _displayRecognizedTextAreas.value = textAreas to unionRect - - if (repo.isAutoCopyOCRResult().first()) { - _copyRecognizedText.value = result.result - } - } - } - - fun startTranslation(translationProviderType: TranslationProviderType) { - viewScope.launch { - if (!translationProviderType.nonTranslation) { - _displayTranslationBlock.value = true - _displayTranslationProgress.value = true - _displayTranslatedByGoogle.value = false - _translationProviderText.value = null - } - - when (translationProviderType) { - TranslationProviderType.MicrosoftAzure, - TranslationProviderType.MyMemory -> - _translationProviderText.value = - "${context.getString(R.string.text_translated_by)} " + - context.getString(translationProviderType.nameRes) - - TranslationProviderType.GoogleMLKit -> - _displayTranslatedByGoogle.value = true - - TranslationProviderType.GoogleTranslateApp, - TranslationProviderType.BingTranslateApp, - TranslationProviderType.PapagoTranslateApp, - TranslationProviderType.YandexTranslateApp, - TranslationProviderType.OtherTranslateApp, - TranslationProviderType.OCROnly -> { - } - } - } - } - - fun textTranslated(result: Result) { - viewScope.launch { - _displayTranslationProgress.value = false - - when (result) { - is Result.Translated -> { - _translatedText.value = result.translatedText to R.color.foregroundSecond - - if (repo.hideRecognizedTextAfterTranslated().first()) { - _displayRecognitionBlock.value = false - } - } - - is Result.SourceLangNotSupport -> { - _translatedText.value = - context.getString(R.string.msg_translator_provider_does_not_support_the_ocr_lang) to R.color.alert - } - - is Result.OCROnly -> { - } - } - } - } - - fun onOCRTextEdited(text: String) { - viewScope.launch { - val langCode = _ocrText.value!!.langCode() - _ocrText.value = text to langCode - -// val langCode = try { -// LanguageIdentify.identifyLanguage(text) -// } catch (e: Exception) { -// logger.debug(t = e) -// null -// } ?: lastLangCode - - stateNavigator.navigate( - NavigationAction.NavigateToStartTranslation( - RecognitionResult( - langCode = langCode, - result = text, - boundingBoxes = lastTextBoundingBoxes, - ) - ) - ) - } - } - - fun onTextSelectableChecked(checked: Boolean) { - viewScope.launch { - AppPref.displaySelectedTextOnResultWindow = checked - } - } - - fun onWordSelected(word: String) { - viewScope.launch { - _displayTextInfoSearchView.value = TextInfoSearchViewData( - text = word, - sourceLang = AppPref.selectedOCRLang, - targetLang = AppPref.selectedTranslationLang, - ) - } - } - - data class TextInfoSearchViewData( - val text: String, - val sourceLang: String, - val targetLang: String, - ) -} +// +// fun textRecognized(result: RecognitionResult, parent: Rect, selected: Rect, viewRect: Rect) { +// viewScope.launch { +// this@ResultViewModel.lastLangCode = result.langCode +// +// _displayOCROperationProgress.value = false +// _ocrText.value = result.result to result.langCode +// +// val topOffset = parent.top + selected.top - viewRect.top +// val leftOffset = parent.left + selected.left - viewRect.left +// this@ResultViewModel.lastTextBoundingBoxes = result.boundingBoxes.toList() +// val textAreas = result.boundingBoxes.map { +// Rect( +// it.left + leftOffset, +// it.top + topOffset, +// it.right + leftOffset, +// it.bottom + topOffset +// ) +// } +// val unionRect = Rect() +// textAreas.forEach { unionRect.union(it) } +// _displayRecognizedTextAreas.value = textAreas to unionRect +// +// if (repo.isAutoCopyOCRResult().first()) { +// _copyRecognizedText.value = result.result +// } +// } +// } +// +// fun startTranslation(translationProviderType: TranslationProviderType) { +// viewScope.launch { +// if (!translationProviderType.nonTranslation) { +// _displayTranslationBlock.value = true +// _displayTranslationProgress.value = true +// _displayTranslatedByGoogle.value = false +// _translationProviderText.value = null +// } +// +// when (translationProviderType) { +// TranslationProviderType.MicrosoftAzure, +// TranslationProviderType.MyMemory -> +// _translationProviderText.value = +// "${context.getString(R.string.text_translated_by)} " + +// context.getString(translationProviderType.nameRes) +// +// TranslationProviderType.GoogleMLKit -> +// _displayTranslatedByGoogle.value = true +// +// TranslationProviderType.GoogleTranslateApp, +// TranslationProviderType.BingTranslateApp, +// TranslationProviderType.PapagoTranslateApp, +// TranslationProviderType.YandexTranslateApp, +// TranslationProviderType.OtherTranslateApp, +// TranslationProviderType.OCROnly -> { +// } +// } +// } +// } +// +// fun textTranslated(result: Result) { +// viewScope.launch { +// _displayTranslationProgress.value = false +// +// when (result) { +// is Result.Translated -> { +// _translatedText.value = result.translatedText to R.color.foregroundSecond +// +// if (repo.hideRecognizedTextAfterTranslated().first()) { +// _displayRecognitionBlock.value = false +// } +// } +// +// is Result.SourceLangNotSupport -> { +// _translatedText.value = +// context.getString(R.string.msg_translator_provider_does_not_support_the_ocr_lang) to R.color.alert +// } +// +// is Result.OCROnly -> { +// } +// } +// } +// } +// +// fun onOCRTextEdited(text: String) { +// viewScope.launch { +// val langCode = _ocrText.value!!.langCode() +// _ocrText.value = text to langCode +// +//// val langCode = try { +//// LanguageIdentify.identifyLanguage(text) +//// } catch (e: Exception) { +//// logger.debug(t = e) +//// null +//// } ?: lastLangCode +// +// stateNavigator.navigate( +// NavigationAction.NavigateToStartTranslation( +// RecognitionResult( +// langCode = langCode, +// result = text, +// boundingBoxes = lastTextBoundingBoxes, +// ) +// ) +// ) +// } +// } +// +// fun onTextSelectableChecked(checked: Boolean) { +// viewScope.launch { +// AppPref.displaySelectedTextOnResultWindow = checked +// } +// } +// +// fun onWordSelected(word: String) { +// viewScope.launch { +// _displayTextInfoSearchView.value = TextInfoSearchViewData( +// text = word, +// sourceLang = AppPref.selectedOCRLang, +// targetLang = AppPref.selectedTranslationLang, +// ) +// } +// } +// +// data class TextInfoSearchViewData( +// val text: String, +// val sourceLang: String, +// val targetLang: String, +// ) +//} From 853fd95e020e4292634f7d5bb1aa13b336376859 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 3 Feb 2024 23:13:40 +0900 Subject: [PATCH 032/121] Make state navigation centralized --- .../floatings/manager/StateNavigator.kt | 20 +++-- .../floatings/manager/StateOperator.kt | 86 ++++++++++++------- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 28a43c27..3bf87d67 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -10,6 +10,8 @@ import tw.firemaples.onscreenocr.floatings.compose.base.awaitForSubscriber import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.recognition.TextRecognitionProviderType import tw.firemaples.onscreenocr.translator.TranslationProviderType +import tw.firemaples.onscreenocr.translator.TranslationResult +import tw.firemaples.onscreenocr.translator.Translator import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -100,14 +102,17 @@ sealed interface NavigationAction { ) : NavigationAction data class NavigateToTextRecognition( + val ocrLang: String, + val ocrProvider: TextRecognitionProviderType, val croppedBitmap: Bitmap, - val parent: Rect, - val selected: Rect, + val parentRect: Rect, + val selectedRect: Rect, ) : NavigationAction - //TODO remove - @Deprecated("Replaced by ReStartTranslation") data class NavigateToStartTranslation( + val croppedBitmap: Bitmap, + val parentRect: Rect, + val selectedRect: Rect, val recognitionResult: RecognitionResult, ) : NavigationAction @@ -119,7 +124,12 @@ sealed interface NavigationAction { ) : NavigationAction data class NavigateToTranslated( - val result: Result, + val croppedBitmap: Bitmap, + val parentRect: Rect, + val selectedRect: Rect, + val recognitionResult: RecognitionResult, + val translator: Translator, + val translationResult: TranslationResult, ) : NavigationAction data class ShowError( diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index ec109fdc..62c72eb4 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -79,16 +79,6 @@ class StateOperatorImpl @Inject constructor( ocrProvider = action.ocrProvider, ) - is NavigationAction.NavigateToStartTranslation -> { - //TODO remove -// val croppedBitmap = currentNavState.getBitmap() -// ?: throw IllegalStateException("Navigate to StartTranslation failed, bitmap is null: $currentNavState") -// startTranslation( -// croppedBitmap = croppedBitmap, -// recognitionResult = action.recognitionResult, -// ) - } - is NavigationAction.ReStartTranslation -> { startTranslation( croppedBitmap = action.croppedBitmap, @@ -101,9 +91,35 @@ class StateOperatorImpl @Inject constructor( is NavigationAction.NavigateToIdle -> backToIdle(showMainBar = action.showMainBar) - is NavigationAction.NavigateToTextRecognition -> TODO() - is NavigationAction.NavigateToTranslated -> TODO() - is NavigationAction.ShowError -> TODO() + is NavigationAction.NavigateToTextRecognition -> + startRecognition( + ocrLang = action.ocrLang, + ocrProvider = action.ocrProvider, + croppedBitmap = action.croppedBitmap, + parentRect = action.parentRect, + selectedRect = action.selectedRect, + ) + + is NavigationAction.NavigateToStartTranslation -> + startTranslation( + croppedBitmap = action.croppedBitmap, + parentRect = action.parentRect, + selectedRect = action.selectedRect, + recognitionResult = action.recognitionResult, + ) + + is NavigationAction.NavigateToTranslated -> + onTranslated( + croppedBitmap = action.croppedBitmap, + parentRect = action.parentRect, + selectedRect = action.selectedRect, + recognitionResult = action.recognitionResult, + translator = action.translator, + translationResult = action.translationResult, + ) + + is NavigationAction.ShowError -> + showError(action.error) } }.launchIn(scope) } @@ -183,12 +199,14 @@ class StateOperatorImpl @Inject constructor( action.emit(StateOperatorAction.ShowMainBar) - startRecognition( - ocrLang = ocrLang, - ocrProvider = ocrProvider, - croppedBitmap = croppedBitmap, - parentRect = parentRect, - selectedRect = selectedRect, + stateNavigator.navigate( + NavigationAction.NavigateToTextRecognition( + ocrLang = ocrLang, + ocrProvider = ocrProvider, + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + ) ) } catch (t: TimeoutCancellationException) { logger.debug(t = t) @@ -249,11 +267,13 @@ class StateOperatorImpl @Inject constructor( FirebaseEvent.logOCRFinished(recognizer.name) - startTranslation( - croppedBitmap = croppedBitmap, - parentRect = parentRect, - selectedRect = selectedRect, - recognitionResult = result, + stateNavigator.navigate( + NavigationAction.NavigateToStartTranslation( + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + recognitionResult = result, + ) ) } catch (e: Exception) { val error = @@ -301,13 +321,15 @@ class StateOperatorImpl @Inject constructor( sourceLangCode = recognitionResult.langCode, ) - onTranslated( - croppedBitmap = croppedBitmap, - parentRect = parentRect, - selectedRect = selectedRect, - recognitionResult = recognitionResult, - translator = translator, - translationResult = translationResult, + stateNavigator.navigate( + NavigationAction.NavigateToTranslated( + croppedBitmap = croppedBitmap, + parentRect = parentRect, + selectedRect = selectedRect, + recognitionResult = recognitionResult, + translator = translator, + translationResult = translationResult, + ) ) } catch (e: Exception) { logger.warn(t = e) @@ -316,7 +338,7 @@ class StateOperatorImpl @Inject constructor( } } - private suspend fun onTranslated( + private fun onTranslated( croppedBitmap: Bitmap, parentRect: Rect, selectedRect: Rect, From ae80024692df8889a7410d36e4614734916437c6 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 3 Feb 2024 23:17:31 +0900 Subject: [PATCH 033/121] Correct use case naming --- ...nslated.kt => GetHidingOCRAreaAfterTranslatedUseCase.kt} | 2 +- .../floatings/compose/resultview/ResultViewModel.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename main/src/main/java/tw/firemaples/onscreenocr/data/usecase/{GetHidingOCRAreaAfterTranslated.kt => GetHidingOCRAreaAfterTranslatedUseCase.kt} (80%) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslatedUseCase.kt similarity index 80% rename from main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt rename to main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslatedUseCase.kt index 450b6ad1..46aca954 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslated.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetHidingOCRAreaAfterTranslatedUseCase.kt @@ -3,7 +3,7 @@ package tw.firemaples.onscreenocr.data.usecase import tw.firemaples.onscreenocr.data.repo.SettingRepository import javax.inject.Inject -class GetHidingOCRAreaAfterTranslated @Inject constructor( +class GetHidingOCRAreaAfterTranslatedUseCase @Inject constructor( private val settingRepository: SettingRepository, ) { operator fun invoke() = settingRepository.hideOCRAreaAfterTranslated() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 7875c302..95c5eca3 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase -import tw.firemaples.onscreenocr.data.usecase.GetHidingOCRAreaAfterTranslated +import tw.firemaples.onscreenocr.data.usecase.GetHidingOCRAreaAfterTranslatedUseCase import tw.firemaples.onscreenocr.data.usecase.GetResultViewFontSizeUseCase import tw.firemaples.onscreenocr.data.usecase.GetShowTextSelectorOnResultViewUseCase import tw.firemaples.onscreenocr.data.usecase.SetShowTextSelectorOnResultViewUseCase @@ -98,7 +98,7 @@ class ResultViewModelImpl @Inject constructor( private val setShowTextSelectorOnResultViewUseCase: SetShowTextSelectorOnResultViewUseCase, getResultViewFontSizeUseCase: GetResultViewFontSizeUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, - private val getHidingOCRAreaAfterTranslated: GetHidingOCRAreaAfterTranslated, + private val getHidingOCRAreaAfterTranslatedUseCase: GetHidingOCRAreaAfterTranslatedUseCase, ) : ResultViewModel { private val logger by lazy { Logger(this::class) } @@ -208,7 +208,7 @@ class ResultViewModelImpl @Inject constructor( val providerName = context.getString(providerType.nameRes) "${context.getString(R.string.text_translated_by)} $providerName" } else null - val showRecognitionArea = getHidingOCRAreaAfterTranslated.invoke().not() + val showRecognitionArea = getHidingOCRAreaAfterTranslatedUseCase.invoke().not() state.update { it.copy( From 8998a64cb95ca08a8c606e34bc48c4f72f5b1d5d Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 4 Feb 2024 00:11:31 +0900 Subject: [PATCH 034/121] Request post notification permission --- main/src/main/AndroidManifest.xml | 3 +- .../PermissionCaptureScreenFragment.kt | 57 ++++++++++++++++++- main/src/main/res/values-zh-rCN/strings.xml | 4 +- main/src/main/res/values-zh-rTW/strings.xml | 4 +- main/src/main/res/values/strings.xml | 4 +- 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index 8282b884..e18ade1f 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> + @@ -70,4 +71,4 @@ - \ No newline at end of file + diff --git a/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/permissions/PermissionCaptureScreenFragment.kt b/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/permissions/PermissionCaptureScreenFragment.kt index 9a1a4637..cc63b4a9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/permissions/PermissionCaptureScreenFragment.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/permissions/PermissionCaptureScreenFragment.kt @@ -1,14 +1,22 @@ package tw.firemaples.onscreenocr.pages.launch.permissions +import android.Manifest import android.app.Activity import android.content.Context +import android.content.pm.PackageManager import android.media.projection.MediaProjectionManager +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.databinding.PermissionCaptureScreenFragmentBinding import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.pages.setting.SettingManager @@ -35,7 +43,7 @@ class PermissionCaptureScreenFragment : Fragment() { setViews() if (ScreenExtractor.isGranted) { - startService() + requestNotificationPermissionOrStartService() } } @@ -59,9 +67,56 @@ class PermissionCaptureScreenFragment : Fragment() { intent = intent, keepMediaProjection = SettingManager.keepMediaProjectionResources, ) + requestNotificationPermissionOrStartService() + } + } + + private fun requestNotificationPermissionOrStartService() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + startService() + } else { + requestNotificationPermission { startService() } } + } + + private val notificationResultLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ -> + startService() + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestNotificationPermission( + onResult: () -> Unit, + ) { + val permission = Manifest.permission.POST_NOTIFICATIONS + when { + ContextCompat.checkSelfPermission( + requireContext(), permission + ) == PackageManager.PERMISSION_GRANTED -> { + onResult.invoke() + } + + ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), permission) -> { + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.msg_grant_posting_notification_permission_rationale)) + .setPositiveButton(getString(R.string.btn_request_again)) { _, _ -> + notificationResultLauncher.launch(permission) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + onResult.invoke() + } + .show() + } + + else -> { + // You can directly ask for the permission. + // The registered ActivityResultCallback gets the result of this request. + notificationResultLauncher.launch(permission) + } + } + } private fun startService() { ViewHolderService.showViews(requireActivity()) diff --git a/main/src/main/res/values-zh-rCN/strings.xml b/main/src/main/res/values-zh-rCN/strings.xml index 4465ca4d..cd11c22d 100644 --- a/main/src/main/res/values-zh-rCN/strings.xml +++ b/main/src/main/res/values-zh-rCN/strings.xml @@ -66,6 +66,8 @@ 发现暂时性错误,请再试一次(1) (Image reader format) 此翻译引擎不支援选择的辨识语言 + 授予通知权限以快速显示/隐藏浮动视窗 + 重新请求 版本资讯 错误 使用说明 @@ -112,4 +114,4 @@ 维持相关资源直到重启程式 每次使用后释放相关资源 萤幕截图 - \ No newline at end of file + diff --git a/main/src/main/res/values-zh-rTW/strings.xml b/main/src/main/res/values-zh-rTW/strings.xml index ede4603a..9e504b5a 100644 --- a/main/src/main/res/values-zh-rTW/strings.xml +++ b/main/src/main/res/values-zh-rTW/strings.xml @@ -66,6 +66,8 @@ 發現暫時性錯誤,請再試一次(1) (Image reader format) 此翻譯引擎不支援選擇的辨識語言 + 授予通知權限以快速顯示/隱藏浮動視窗 + 重新請求 版本資訊 錯誤 使用說明 @@ -112,4 +114,4 @@ 維持相關資源直到重啟程式 每次使用後釋放相關資源 螢幕截圖 - \ No newline at end of file + diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 723ce1e2..7dfac8a1 100644 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Screenshot Keep the resources until restart Release the resources after used + Keep MediaProjection resources Setting Privacy Policy @@ -86,6 +87,8 @@ Found temporary error, please try it again. (Image reader format) This translation provider does not support the selected recognition language. Please restart the app to take effect + Grant posting notification permission to restore/hide the app easier + Request Again Version History Error @@ -117,5 +120,4 @@ Space None - Keep MediaProjection resources From d43cfa8727368fa9142d4e933d98fb4a1dcdffd1 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 4 Feb 2024 00:34:24 +0900 Subject: [PATCH 035/121] Bump to v4.0.0 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index 38369b70..ad639676 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 33 - versionCode 117 - versionName "3.1.25" + versionCode 118 + versionName "4.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From a65e9f1f612f48b68159119e487f06daffb125a3 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 4 Feb 2024 22:50:13 +0900 Subject: [PATCH 036/121] Use Compose Material Theme --- .../floatings/compose/base/AppTheme.kt | 28 ------ .../compose/base/ComposeFloatingView.kt | 1 + .../compose/mainbar/MainBarContent.kt | 17 ++-- .../compose/resultview/ResultViewContent.kt | 28 +++--- .../tw/firemaples/onscreenocr/theme/Color.kt | 67 ++++++++++++++ .../tw/firemaples/onscreenocr/theme/Font.kt | 8 ++ .../tw/firemaples/onscreenocr/theme/Theme.kt | 90 +++++++++++++++++++ 7 files changed, 188 insertions(+), 51 deletions(-) delete mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/theme/Color.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/theme/Font.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt deleted file mode 100644 index 7c386f42..00000000 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/AppTheme.kt +++ /dev/null @@ -1,28 +0,0 @@ -package tw.firemaples.onscreenocr.floatings.compose.base - - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.sp - -lateinit var AppColorScheme: ColorScheme - -@Composable -fun AppTheme( - content: @Composable () -> Unit, -) { - AppColorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() - - MaterialTheme( - colorScheme = AppColorScheme, - content = content, - ) -} - -object FontSize { - val Small = 14.sp -} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 14f97702..510a0cc9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -29,6 +29,7 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import tw.firemaples.onscreenocr.theme.AppTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 7d2b5bb2..60cb6139 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -25,7 +26,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -38,8 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme -import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme +import tw.firemaples.onscreenocr.theme.AppTheme @Composable fun MainBarContent( @@ -54,7 +53,7 @@ fun MainBarContent( Box( modifier = Modifier .background( - color = AppColorScheme.background, + color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp), ), ) { @@ -115,7 +114,7 @@ private fun LanguageBlock( .height(32.dp) .border( width = 2.dp, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, shape = RoundedCornerShape(4.dp), ) .clickable(onClick = onClick) @@ -126,13 +125,13 @@ private fun LanguageBlock( text = langText, fontSize = 12.sp, fontWeight = FontWeight.Bold, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, ) if (translatorIcon != null) { Image( painter = painterResource(id = translatorIcon), contentDescription = "", - colorFilter = ColorFilter.tint(AppColorScheme.onBackground), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), ) } } @@ -148,7 +147,7 @@ private fun MainBarButton( modifier = Modifier .size(32.dp) .clickable(onClick = onClick) - .background(colorResource(id = R.color.md_blue_800), shape = RoundedCornerShape(4.dp)) + .background(color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(4.dp)) .padding(4.dp), painter = painterResource(id = icon), contentDescription = "", @@ -177,7 +176,7 @@ private fun MenuButton( .clickable(onClick = onClick) .padding(2.dp), painter = painterResource(id = R.drawable.ic_menu_move), - colorFilter = ColorFilter.tint(AppColorScheme.onBackground), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), contentDescription = "", ) } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 78a7c126..0b5f6f01 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,12 +51,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.floatings.compose.base.AppColorScheme -import tw.firemaples.onscreenocr.floatings.compose.base.AppTheme -import tw.firemaples.onscreenocr.floatings.compose.base.FontSize import tw.firemaples.onscreenocr.floatings.compose.base.dpToPx import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp import tw.firemaples.onscreenocr.floatings.compose.wigets.WordSelectionText +import tw.firemaples.onscreenocr.theme.AppTheme +import tw.firemaples.onscreenocr.theme.FontSize import java.util.Locale @Composable @@ -202,7 +202,7 @@ private fun ResultPanel( Column( modifier = modifier .background( - color = AppColorScheme.background, + color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp), ) .clickable { } @@ -263,7 +263,7 @@ private fun OCRToolBar( text = stringResource(id = R.string.text_ocr_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, ) // Image(painter = painterResource(id = R.drawable.ic_play), contentDescription = "") @@ -271,8 +271,8 @@ private fun OCRToolBar( Spacer(modifier = Modifier.size(4.dp)) val textSearchTintColor = if (textSearchEnabled) - colorResource(id = R.color.md_blue_800) - else AppColorScheme.onBackground + MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface Image( modifier = Modifier.clickable(onClick = onSearchClicked), painter = painterResource(id = R.drawable.ic_text_search), @@ -341,12 +341,12 @@ private fun OCRTextArea( text = ocrText, locale = Locale.US, textStyle = TextStyle( - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, ), selectedSpanStyle = SpanStyle( - color = AppColorScheme.onSecondary, - background = AppColorScheme.secondary, + color = MaterialTheme.colorScheme.onSecondary, + background = MaterialTheme.colorScheme.secondary, ), onTextSelected = onTextSelected, ) @@ -356,7 +356,7 @@ private fun OCRTextArea( .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), text = ocrText, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, ) } @@ -376,7 +376,7 @@ private fun TranslationToolBar( text = stringResource(id = R.string.text_translated_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, ) Spacer(modifier = Modifier.size(4.dp)) @@ -412,7 +412,7 @@ private fun TranslationTextArea( .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), text = translatedText, - color = AppColorScheme.onBackground, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, ) } @@ -432,7 +432,7 @@ private fun ColumnScope.TranslationProviderBar( Text( modifier = Modifier.align(Alignment.End), text = translationProviderText, - color = AppColorScheme.secondary, + color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, maxLines = 1, ) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/theme/Color.kt b/main/src/main/java/tw/firemaples/onscreenocr/theme/Color.kt new file mode 100644 index 00000000..a98efc17 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/theme/Color.kt @@ -0,0 +1,67 @@ +package tw.firemaples.onscreenocr.theme +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFF365AB0) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFDAE2FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001848) +val md_theme_light_secondary = Color(0xFF375CA8) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFD9E2FF) +val md_theme_light_onSecondaryContainer = Color(0xFF001945) +val md_theme_light_tertiary = Color(0xFF874589) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFD6FA) +val md_theme_light_onTertiaryContainer = Color(0xFF37003C) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFEFBFF) +val md_theme_light_onBackground = Color(0xFF1B1B1F) +val md_theme_light_surface = Color(0xFFFEFBFF) +val md_theme_light_onSurface = Color(0xFF1B1B1F) +val md_theme_light_surfaceVariant = Color(0xFFE1E2EC) +val md_theme_light_onSurfaceVariant = Color(0xFF45464F) +val md_theme_light_outline = Color(0xFF757780) +val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4) +val md_theme_light_inverseSurface = Color(0xFF303034) +val md_theme_light_inversePrimary = Color(0xFFB2C5FF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF365AB0) +val md_theme_light_outlineVariant = Color(0xFFC5C6D0) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFFB2C5FF) +val md_theme_dark_onPrimary = Color(0xFF002B74) +val md_theme_dark_primaryContainer = Color(0xFF174197) +val md_theme_dark_onPrimaryContainer = Color(0xFFDAE2FF) +val md_theme_dark_secondary = Color(0xFFB0C6FF) +val md_theme_dark_onSecondary = Color(0xFF002D6F) +val md_theme_dark_secondaryContainer = Color(0xFF19438F) +val md_theme_dark_onSecondaryContainer = Color(0xFFD9E2FF) +val md_theme_dark_tertiary = Color(0xFFFBACF8) +val md_theme_dark_onTertiary = Color(0xFF521457) +val md_theme_dark_tertiaryContainer = Color(0xFF6C2D70) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFD6FA) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF1B1B1F) +val md_theme_dark_onBackground = Color(0xFFE4E2E6) +val md_theme_dark_surface = Color(0xFF1B1B1F) +val md_theme_dark_onSurface = Color(0xFFE4E2E6) +val md_theme_dark_surfaceVariant = Color(0xFF45464F) +val md_theme_dark_onSurfaceVariant = Color(0xFFC5C6D0) +val md_theme_dark_outline = Color(0xFF8F909A) +val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F) +val md_theme_dark_inverseSurface = Color(0xFFE4E2E6) +val md_theme_dark_inversePrimary = Color(0xFF365AB0) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFFB2C5FF) +val md_theme_dark_outlineVariant = Color(0xFF45464F) +val md_theme_dark_scrim = Color(0xFF000000) + + +val seed = Color(0xFF365AB0) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/theme/Font.kt b/main/src/main/java/tw/firemaples/onscreenocr/theme/Font.kt new file mode 100644 index 00000000..909ffab1 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/theme/Font.kt @@ -0,0 +1,8 @@ +package tw.firemaples.onscreenocr.theme + + +import androidx.compose.ui.unit.sp + +object FontSize { + val Small = 14.sp +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt b/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt new file mode 100644 index 00000000..7d9e9e87 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt @@ -0,0 +1,90 @@ +package tw.firemaples.onscreenocr.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun AppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable() () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} From 1969d8c26e7d7380604e57c9cc600404deac4aa6 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 4 Feb 2024 23:09:57 +0900 Subject: [PATCH 037/121] Update MainBar button tint color --- .../onscreenocr/floatings/compose/mainbar/MainBarContent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 60cb6139..e349c3a0 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -151,6 +151,7 @@ private fun MainBarButton( .padding(4.dp), painter = painterResource(id = icon), contentDescription = "", + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), ) } From 60e06528e7e20a6f2fbff8669e0c196006085050 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 00:29:43 +0900 Subject: [PATCH 038/121] Add option for limit the max width of result panel --- .../onscreenocr/data/repo/SettingRepository.kt | 3 +++ .../usecase/GetLimitResultViewMaxWidthUseCase.kt | 10 ++++++++++ .../compose/resultview/ResultViewContent.kt | 11 +++++++++-- .../floatings/compose/resultview/ResultViewModel.kt | 12 +++++++++++- .../onscreenocr/pages/setting/SettingManager.kt | 4 ++++ main/src/main/res/values-zh-rCN/strings.xml | 3 +++ main/src/main/res/values-zh-rTW/strings.xml | 3 +++ main/src/main/res/values/strings.xml | 3 +++ main/src/main/res/xml/perference.xml | 8 +++++++- 9 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLimitResultViewMaxWidthUseCase.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt index 48e3e093..9e07c037 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt @@ -9,4 +9,7 @@ class SettingRepository @Inject constructor() { fun hideOCRAreaAfterTranslated(): Boolean = SettingManager.hideRecognizedResultAfterTranslated + + fun limitResultViewMaxWidth(): Boolean = + SettingManager.limitResultViewMaxWidth } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLimitResultViewMaxWidthUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLimitResultViewMaxWidthUseCase.kt new file mode 100644 index 00000000..24aa9a34 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLimitResultViewMaxWidthUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.SettingRepository +import javax.inject.Inject + +class GetLimitResultViewMaxWidthUseCase @Inject constructor( + private val settingRepository: SettingRepository, +) { + operator fun invoke(): Boolean = settingRepository.limitResultViewMaxWidth() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 0b5f6f01..52c71873 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -97,6 +98,11 @@ fun ResultViewContent( ResultPanel( modifier = Modifier .padding(16.dp) + .run { + if (state.limitMaxWidth) + widthIn(max = 300.dp) + else this + } .calculateOffset( highlightUnion = state.highlightUnion, offset = targetOffset, @@ -462,10 +468,11 @@ private fun ResultViewContentPreview() { val state = ResultViewState( highlightArea = listOf(areaRect), highlightUnion = areaRect, + limitMaxWidth = true, textSearchEnabled = true, ocrState = OCRState( - showProcessing = true, - ocrText = "Test OCR text", + showProcessing = false, + ocrText = "Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text Test OCR text ", ), translationState = TranslationState( showTranslationArea = true, diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index 95c5eca3..fb2af61c 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.data.usecase.GetCurrentTranslationLangUseCase import tw.firemaples.onscreenocr.data.usecase.GetHidingOCRAreaAfterTranslatedUseCase +import tw.firemaples.onscreenocr.data.usecase.GetLimitResultViewMaxWidthUseCase import tw.firemaples.onscreenocr.data.usecase.GetResultViewFontSizeUseCase import tw.firemaples.onscreenocr.data.usecase.GetShowTextSelectorOnResultViewUseCase import tw.firemaples.onscreenocr.data.usecase.SetShowTextSelectorOnResultViewUseCase @@ -50,6 +51,7 @@ interface ResultViewModel { } data class ResultViewState( + val limitMaxWidth: Boolean = true, val textSearchEnabled: Boolean = false, val fontSize: Float = Constants.DEFAULT_RESULT_WINDOW_FONT_SIZE, val highlightArea: List = listOf(), @@ -97,6 +99,7 @@ class ResultViewModelImpl @Inject constructor( getShowTextSelectorOnResultViewUseCase: GetShowTextSelectorOnResultViewUseCase, private val setShowTextSelectorOnResultViewUseCase: SetShowTextSelectorOnResultViewUseCase, getResultViewFontSizeUseCase: GetResultViewFontSizeUseCase, + private val getLimitResultViewMaxWidthUseCase: GetLimitResultViewMaxWidthUseCase, private val getCurrentTranslationLangUseCase: GetCurrentTranslationLangUseCase, private val getHidingOCRAreaAfterTranslatedUseCase: GetHidingOCRAreaAfterTranslatedUseCase, ) : ResultViewModel { @@ -144,6 +147,12 @@ class ResultViewModelImpl @Inject constructor( this@ResultViewModelImpl.croppedBitmap = navState.bitmap } + state.update { + it.copy( + limitMaxWidth = getLimitResultViewMaxWidthUseCase.invoke(), + ) + } + when (navState) { is NavState.TextRecognizing -> state.update { @@ -208,7 +217,8 @@ class ResultViewModelImpl @Inject constructor( val providerName = context.getString(providerType.nameRes) "${context.getString(R.string.text_translated_by)} $providerName" } else null - val showRecognitionArea = getHidingOCRAreaAfterTranslatedUseCase.invoke().not() + val showRecognitionArea = + getHidingOCRAreaAfterTranslatedUseCase.invoke().not() state.update { it.copy( diff --git a/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingManager.kt b/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingManager.kt index ad8b1f97..eca7d0b5 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingManager.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingManager.kt @@ -33,6 +33,7 @@ object SettingManager { private const val PREF_AUTO_COPY_OCR_RESULT = "pref_auto_copy_ocr_result" private const val PREF_HIDE_RECOGNIZED_RESULT_AFTER_TRANSLATED = "pref_hide_recognized_result_after_translated" + private const val PREF_LIMIT_RESULT_VIEW_MAX_WIDTH = "pref_limit_result_view_max_width" private const val PREF_SAVE_LAST_SELECTION_AREA = "pref_save_last_selection_area" private const val PREF_EXIT_APP_WHILE_SPEN_INSERTED = "pref_exit_app_while_spen_inserted" @@ -108,6 +109,9 @@ object SettingManager { val hideRecognizedResultAfterTranslated: Boolean get() = preferences.getBoolean(PREF_HIDE_RECOGNIZED_RESULT_AFTER_TRANSLATED, false) + val limitResultViewMaxWidth: Boolean + get() = preferences.getBoolean(PREF_LIMIT_RESULT_VIEW_MAX_WIDTH, true) + val saveLastSelectionArea: Boolean get() = preferences.getBoolean(PREF_SAVE_LAST_SELECTION_AREA, true) diff --git a/main/src/main/res/values-zh-rCN/strings.xml b/main/src/main/res/values-zh-rCN/strings.xml index cd11c22d..1fb69413 100644 --- a/main/src/main/res/values-zh-rCN/strings.xml +++ b/main/src/main/res/values-zh-rCN/strings.xml @@ -114,4 +114,7 @@ 维持相关资源直到重启程式 每次使用后释放相关资源 萤幕截图 + 限制最大宽度 + 限制最大宽度至 300dp + 依内容自动调整宽度 diff --git a/main/src/main/res/values-zh-rTW/strings.xml b/main/src/main/res/values-zh-rTW/strings.xml index 9e504b5a..e76f2b9a 100644 --- a/main/src/main/res/values-zh-rTW/strings.xml +++ b/main/src/main/res/values-zh-rTW/strings.xml @@ -114,4 +114,7 @@ 維持相關資源直到重啟程式 每次使用後釋放相關資源 螢幕截圖 + 限制最大寬度 + 限制最大寬度至 300dp + 不限制寬度 diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 7dfac8a1..17732f1a 100644 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -51,6 +51,9 @@ Keep the resources until restart Release the resources after used Keep MediaProjection resources + Limit the max width + Limit the max width to 300dp + Dynamic width based on the content Setting Privacy Policy diff --git a/main/src/main/res/xml/perference.xml b/main/src/main/res/xml/perference.xml index 69fed756..4a620d6b 100644 --- a/main/src/main/res/xml/perference.xml +++ b/main/src/main/res/xml/perference.xml @@ -96,6 +96,12 @@ android:defaultValue="false" android:key="pref_hide_recognized_result_after_translated" android:title="@string/pref_hide_recognized_result_after_translated" /> + @@ -112,4 +118,4 @@ android:key="pref_exit_app_while_spen_inserted" android:title="@string/pref_exit_app_while_spen_inserted" /> - \ No newline at end of file + From f19ccf3bde8180c32ca6150eb0f2c9de7f2df1ec Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 01:05:21 +0900 Subject: [PATCH 039/121] Mark Lifecycle on destroy before detach floating view --- .../floatings/compose/base/ComposeFloatingView.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 510a0cc9..09c81c91 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -29,12 +29,12 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import tw.firemaples.onscreenocr.theme.AppTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren +import tw.firemaples.onscreenocr.theme.AppTheme import tw.firemaples.onscreenocr.utils.Logger import tw.firemaples.onscreenocr.utils.PermissionUtil import tw.firemaples.onscreenocr.utils.UIUtils @@ -214,6 +214,7 @@ abstract class ComposeFloatingView(protected val context: Context) { windowManager.addView(rootView, params) with(lifecycleOwner) { + handleLifecycleEvent(Lifecycle.Event.ON_CREATE) handleLifecycleEvent(Lifecycle.Event.ON_START) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } @@ -240,13 +241,14 @@ abstract class ComposeFloatingView(protected val context: Context) { viewScope.coroutineContext.cancelChildren() - windowManager.removeView(rootView) - with(lifecycleOwner) { handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) handleLifecycleEvent(Lifecycle.Event.ON_STOP) + handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } + windowManager.removeView(rootView) + attachedFloatingViews.remove(this) if (enableDeviceDirectionTracker) @@ -309,7 +311,6 @@ abstract class ComposeFloatingView(protected val context: Context) { protected val lifecycleOwner: FloatingViewLifecycleOwner = FloatingViewLifecycleOwner().apply { performRestore(null) - handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } // private val tasks = mutableListOf>() From aa899b05a5c8f245215775cee0cc0ee525d1463f Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 01:10:44 +0900 Subject: [PATCH 040/121] Make the translator icon smaller --- .../floatings/compose/resultview/ResultViewContent.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 52c71873..d2d75d4a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -446,7 +447,9 @@ private fun ColumnScope.TranslationProviderBar( if (translationProviderIcon != null) { Image( - modifier = Modifier.align(Alignment.End), + modifier = Modifier + .align(Alignment.End) + .height(16.dp), painter = painterResource(id = translationProviderIcon), contentDescription = "", ) From 7d5ec698a27c62006c9f928ad99604014c71a722 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 01:41:23 +0900 Subject: [PATCH 041/121] Bump to v4.0.1 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index ad639676..6d0a0033 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 33 - versionCode 118 - versionName "4.0.0" + versionCode 119 + versionName "4.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 60480adcf4ea170d944ab8a7e497cf09bd262fd1 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 22:44:43 +0900 Subject: [PATCH 042/121] Make MainBar holding its visible state --- .../compose/base/ComposeFloatingView.kt | 3 +-- .../compose/mainbar/MainBarContent.kt | 2 ++ .../compose/mainbar/MainBarViewModel.kt | 10 ++++++++++ .../compose/resultview/ResultViewModel.kt | 8 ++++---- .../manager/FloatingViewCoordinator.kt | 4 +--- .../floatings/manager/StateNavigator.kt | 2 +- .../floatings/manager/StateOperator.kt | 18 +++++------------- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 09c81c91..0eb4c42a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -214,7 +214,6 @@ abstract class ComposeFloatingView(protected val context: Context) { windowManager.addView(rootView, params) with(lifecycleOwner) { - handleLifecycleEvent(Lifecycle.Event.ON_CREATE) handleLifecycleEvent(Lifecycle.Event.ON_START) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } @@ -244,7 +243,6 @@ abstract class ComposeFloatingView(protected val context: Context) { with(lifecycleOwner) { handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) handleLifecycleEvent(Lifecycle.Event.ON_STOP) - handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) } windowManager.removeView(rootView) @@ -311,6 +309,7 @@ abstract class ComposeFloatingView(protected val context: Context) { protected val lifecycleOwner: FloatingViewLifecycleOwner = FloatingViewLifecycleOwner().apply { performRestore(null) + handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } // private val tasks = mutableListOf>() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index e349c3a0..982f1b21 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.pointer.PointerInputChange @@ -52,6 +53,7 @@ fun MainBarContent( Box( modifier = Modifier + .alpha(if (state.drawMainBar) 1f else 0f) .background( color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp), diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index b7bac95e..e9f9ea92 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -49,6 +49,7 @@ interface MainBarViewModel { } data class MainBarState( + val drawMainBar: Boolean = true, val langText: String = "", val translatorIcon: Int? = null, val displaySelectButton: Boolean = false, @@ -95,7 +96,16 @@ class MainBarViewModelImpl @Inject constructor( private suspend fun onNavigationStateChanges(navState: NavState) { state.update { + val drawMainBar = when (navState) { + NavState.Idle, + NavState.ScreenCircling, + is NavState.ScreenCircled -> true + + else -> false + } + it.copy( + drawMainBar = drawMainBar, displaySelectButton = navState == NavState.Idle, displayTranslateButton = navState is NavState.ScreenCircled, displayCloseButton = diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index fb2af61c..ca45315d 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -287,13 +287,13 @@ class ResultViewModelImpl @Inject constructor( override fun onDialogOutsideClicked() { scope.launch { - stateNavigator.navigate(NavigationAction.NavigateToIdle()) + stateNavigator.navigate(NavigationAction.NavigateToIdle) } } override fun onHomeButtonPressed() { scope.launch { - stateNavigator.navigate(NavigationAction.NavigateToIdle()) + stateNavigator.navigate(NavigationAction.NavigateToIdle) } } @@ -371,14 +371,14 @@ class ResultViewModelImpl @Inject constructor( override fun onGoogleTranslateClicked(textType: TextType) { scope.launch { action.emit(ResultViewAction.LaunchGoogleTranslator(textType.getTargetText())) - stateNavigator.navigate(NavigationAction.NavigateToIdle()) + stateNavigator.navigate(NavigationAction.NavigateToIdle) } } override fun onShareOCRTextClicked() { scope.launch { action.emit(ResultViewAction.ShareText(state.value.ocrState.ocrText)) - stateNavigator.navigate(NavigationAction.NavigateToIdle()) + stateNavigator.navigate(NavigationAction.NavigateToIdle) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index e863adda..b2d68057 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -52,8 +52,6 @@ class FloatingViewCoordinator @Inject constructor( .onEach { action -> when (action) { StateOperatorAction.TopMainBar -> arrangeMainBarToTop() - StateOperatorAction.HideMainBar -> hideMainBar() - StateOperatorAction.ShowMainBar -> showMainBar() StateOperatorAction.ShowScreenCirclingView -> screenCirclingView.attachToScreen() @@ -97,7 +95,7 @@ class FloatingViewCoordinator @Inject constructor( fun detachAllViews() { scope.launch { - stateNavigator.navigate(NavigationAction.NavigateToIdle(showMainBar = false)) + stateNavigator.navigate(NavigationAction.NavigateToIdle) hideMainBar() FloatingView.detachAllFloatingViews() } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt index 3bf87d67..1807f265 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateNavigator.kt @@ -85,7 +85,7 @@ class StateNavigatorImpl @Inject constructor() : StateNavigator { } sealed interface NavigationAction { - data class NavigateToIdle(val showMainBar: Boolean = true) : NavigationAction + data object NavigateToIdle : NavigationAction data object NavigateToScreenCircling : NavigationAction diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt index 62c72eb4..4e5182c8 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/StateOperator.kt @@ -38,7 +38,7 @@ interface StateOperator { val action: SharedFlow companion object { - const val SCREENSHOT_DELAY = 100L + const val SCREENSHOT_DELAY = 200L } } @@ -89,7 +89,7 @@ class StateOperatorImpl @Inject constructor( } is NavigationAction.NavigateToIdle -> - backToIdle(showMainBar = action.showMainBar) + backToIdle() is NavigationAction.NavigateToTextRecognition -> startRecognition( @@ -182,7 +182,6 @@ class StateOperatorImpl @Inject constructor( stateNavigator.updateState(NavState.ScreenCapturing) action.emit(StateOperatorAction.HideScreenCirclingView) - action.emit(StateOperatorAction.HideMainBar) delay(SCREENSHOT_DELAY) @@ -197,8 +196,6 @@ class StateOperatorImpl @Inject constructor( } FirebaseEvent.logCaptureScreenFinished() - action.emit(StateOperatorAction.ShowMainBar) - stateNavigator.navigate( NavigationAction.NavigateToTextRecognition( ocrLang = ocrLang, @@ -210,15 +207,15 @@ class StateOperatorImpl @Inject constructor( ) } catch (t: TimeoutCancellationException) { logger.debug(t = t) - showError(context.getString(R.string.error_capture_screen_timeout)) FirebaseEvent.logCaptureScreenFailed(t) + showError(context.getString(R.string.error_capture_screen_timeout)) bitmap?.setReusable() } catch (t: Throwable) { logger.debug(t = t) + FirebaseEvent.logCaptureScreenFailed(t) val errorMsg = t.message ?: context.getString(R.string.error_unknown_error_capturing_screen) showError(errorMsg) - FirebaseEvent.logCaptureScreenFailed(t) bitmap?.setReusable() } } @@ -418,15 +415,12 @@ class StateOperatorImpl @Inject constructor( action.emit(StateOperatorAction.ShowErrorDialog(error)) } - private fun backToIdle(showMainBar: Boolean = true) = scope.launch { + private fun backToIdle() = scope.launch { if (currentNavState != NavState.Idle) stateNavigator.updateState(NavState.Idle) action.emit(StateOperatorAction.HideResultView) - if (showMainBar) - action.emit(StateOperatorAction.ShowMainBar) - currentNavState.getBitmap()?.setReusable() } @@ -436,8 +430,6 @@ class StateOperatorImpl @Inject constructor( sealed interface StateOperatorAction { data object TopMainBar : StateOperatorAction - data object HideMainBar : StateOperatorAction - data object ShowMainBar : StateOperatorAction data object ShowScreenCirclingView : StateOperatorAction data object HideScreenCirclingView : StateOperatorAction data object ShowResultView : StateOperatorAction From 082ede4cf46da58d9199dc3b679a2c842056ee59 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 23:03:01 +0900 Subject: [PATCH 043/121] Bump to v4.0.2 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index 6d0a0033..e416301a 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 33 - versionCode 119 - versionName "4.0.1" + versionCode 120 + versionName "4.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 27a4d7d366c7769751960e122693a5426a922e2f Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 17 Feb 2024 21:19:30 +0900 Subject: [PATCH 044/121] Temporarily remove the animations on the result view --- .../compose/resultview/ResultViewContent.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index d2d75d4a..058cbd61 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -2,8 +2,6 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.res.Configuration import android.graphics.Rect -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.animateIntOffsetAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -91,10 +89,10 @@ fun ResultViewContent( mutableStateOf(IntOffset(state.highlightUnion.left, state.highlightUnion.top)) } - val animOffset by animateIntOffsetAsState( - targetValue = targetOffset.value, - label = "result panel position", - ) +// val animOffset by animateIntOffsetAsState( +// targetValue = targetOffset.value, +// label = "result panel position", +// ) ResultPanel( modifier = Modifier @@ -110,8 +108,9 @@ fun ResultViewContent( padding = 16.dp.dpToPx(), verticalSpacing = 4.dp.dpToPx(), ) - .offset { animOffset } - .animateContentSize(), + .offset { targetOffset.value }, +// .offset { animOffset } +// .animateContentSize(), viewModel = viewModel, textSearchEnabled = state.textSearchEnabled, fontSize = state.fontSize, From 565c3edce963bd9184eacfc8c876dc9b87b487f0 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 17 Feb 2024 21:55:42 +0900 Subject: [PATCH 045/121] Bump to v4.0.3 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index e416301a..d1ed9f3a 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 33 - versionCode 120 - versionName "4.0.2" + versionCode 121 + versionName "4.0.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From d5fe0c0b2b07ab40f57060aad6cf8028e30ddd48 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 Feb 2024 23:13:02 +0900 Subject: [PATCH 046/121] Upgrade target version to 34 --- main/build.gradle | 6 +++--- main/src/main/AndroidManifest.xml | 1 + .../pages/launch/LaunchActivity.kt | 9 +++++++++ .../onscreenocr/screenshot/ScreenExtractor.kt | 20 +++++++++++++++++++ .../onscreenocr/utils/QuickTileService.kt | 12 +++++++++-- .../utils/SamsungSpenInsertedReceiver.kt | 9 ++++++--- .../onscreenocr/wigets/HomeButtonWatcher.kt | 8 +++++++- 7 files changed, 56 insertions(+), 9 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index d1ed9f3a..47ea4fcb 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -22,13 +22,13 @@ task ensureFiles { } android { - compileSdk 33 - buildToolsVersion = "33.0.1" + compileSdk 34 + buildToolsVersion = "34.0.0" defaultConfig { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 121 versionName "4.0.3" diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index e18ade1f..41a11c3c 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startActivityAndCollapse(LaunchActivity.getLaunchPendingIntent(this)) + } else { + startActivityAndCollapse(LaunchActivity.getLaunchIntent(this)) + } } } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/SamsungSpenInsertedReceiver.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/SamsungSpenInsertedReceiver.kt index dcb792b6..3b4f2d62 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/SamsungSpenInsertedReceiver.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/SamsungSpenInsertedReceiver.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import androidx.core.content.ContextCompat import tw.firemaples.onscreenocr.CoreApplication import tw.firemaples.onscreenocr.floatings.ViewHolderService @@ -28,9 +29,11 @@ class SamsungSpenInsertedReceiver : BroadcastReceiver() { receiver = SamsungSpenInsertedReceiver() } - context.registerReceiver( + ContextCompat.registerReceiver( + context, receiver, - IntentFilter(ACTION_SAMSUNG_SPEN_INSERT) + IntentFilter(ACTION_SAMSUNG_SPEN_INSERT), + ContextCompat.RECEIVER_EXPORTED, ) isRegistered = true @@ -68,4 +71,4 @@ class SamsungSpenInsertedReceiver : BroadcastReceiver() { } } } -} \ No newline at end of file +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/wigets/HomeButtonWatcher.kt b/main/src/main/java/tw/firemaples/onscreenocr/wigets/HomeButtonWatcher.kt index a89b234b..1dbb7c3a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/wigets/HomeButtonWatcher.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/wigets/HomeButtonWatcher.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import androidx.core.content.ContextCompat import tw.firemaples.onscreenocr.utils.Logger /** @@ -30,7 +31,12 @@ class HomeButtonWatcher( fun startWatch() { if (watching) return - context.registerReceiver(receiver, filter) + ContextCompat.registerReceiver( + context, + receiver, + filter, + ContextCompat.RECEIVER_NOT_EXPORTED, + ) watching = true } From fbae4a527357e23e83500b919615a7b1013d2886 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 15:26:34 +0900 Subject: [PATCH 047/121] Bump compose version to 2024.02.02 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 47ea4fcb..9003b0f5 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -152,7 +152,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.8.1' // Compose - def composeBom = platform('androidx.compose:compose-bom:2023.06.01') + def composeBom = platform('androidx.compose:compose-bom:2024.02.02') implementation composeBom androidTestImplementation composeBom // Material Design 3 From 530de193ece650e3cd35b04b8ab21ef568ccaf88 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 15:30:04 +0900 Subject: [PATCH 048/121] Fix compose view not responding issue --- .../floatings/compose/base/ComposeFloatingView.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt index 0eb4c42a..974b5c64 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeFloatingView.kt @@ -8,6 +8,7 @@ import android.os.Bundle import android.os.Looper import android.view.Gravity import android.view.OrientationEventListener +import android.view.View import android.view.WindowManager import androidx.annotation.CallSuper import androidx.annotation.MainThread @@ -140,7 +141,8 @@ abstract class ComposeFloatingView(protected val context: Context) { // ) // } // } - protected val rootView by lazy { + protected lateinit var rootView: View + private fun createView(): View = ComposeView(context).apply { setOnKeyListener { v, keyCode, event -> //TODO check or remove logger.debug("setOnKeyListener, keyCode: $keyCode, event: $event") @@ -179,7 +181,6 @@ abstract class ComposeFloatingView(protected val context: Context) { setViewTreeViewModelStoreOwner(viewModelStoreOwner) } - } private var lastScreenWidth: Int = -1 open val enableDeviceDirectionTracker: Boolean = false @@ -211,6 +212,7 @@ abstract class ComposeFloatingView(protected val context: Context) { return } + rootView = createView() windowManager.addView(rootView, params) with(lifecycleOwner) { From 25658dbebe78079eded9ffbe446b699858a986e4 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 15:59:41 +0900 Subject: [PATCH 049/121] Trigger CI on refactor branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd30f27a..333547f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [ "master", "dep/**" ] pull_request: - branches: [ "master", "dep/**" ] + branches: [ "master", "dep/**", "refactor/**" ] jobs: build: From 597db4b8d4a98a406acea3fdaff1f4480a02560d Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 16:19:04 +0900 Subject: [PATCH 050/121] Refactor screen circling view to compose --- .../data/repo/PreferenceRepository.kt | 7 + .../data/repo/SettingRepository.kt | 3 + .../usecase/GetLastSelectedAreaUseCase.kt | 10 + .../GetRememberLastSelectionAreaUseCase.kt | 10 + .../usecase/SetLastSelectedAreaUseCase.kt | 13 ++ .../screencircling/ScreenCirclingContent.kt | 205 ++++++++++++++++++ .../ScreenCirclingFloatingView.kt | 34 +++ .../screencircling/ScreenCirclingModule.kt | 13 ++ .../screencircling/ScreenCirclingViewModel.kt | 125 +++++++++++ .../manager/FloatingViewCoordinator.kt | 35 +-- .../screenCircling/ScreenCirclingView.kt | 132 +++++------ .../screenCircling/ScreenCirclingViewModel.kt | 68 +++--- .../tw/firemaples/onscreenocr/utils/Logger.kt | 46 ++-- 13 files changed, 564 insertions(+), 137 deletions(-) create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLastSelectedAreaUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetRememberLastSelectionAreaUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetLastSelectedAreaUseCase.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingContent.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingModule.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingViewModel.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt index b397f3b0..62217e6a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/PreferenceRepository.kt @@ -1,6 +1,7 @@ package tw.firemaples.onscreenocr.data.repo import android.graphics.Point +import android.graphics.Rect import androidx.lifecycle.asFlow import com.chibatching.kotpref.livedata.asLiveData import tw.firemaples.onscreenocr.pref.AppPref @@ -27,4 +28,10 @@ class PreferenceRepository @Inject constructor() { fun setResultViewFontSize(fontSize: Float) { AppPref.resultWindowFontSize = fontSize } + + fun getLastSelectedArea() = AppPref.lastSelectionArea + + fun setLastSelectedArea(rect: Rect) { + AppPref.lastSelectionArea = rect + } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt index 9e07c037..d3c4fdb1 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/repo/SettingRepository.kt @@ -12,4 +12,7 @@ class SettingRepository @Inject constructor() { fun limitResultViewMaxWidth(): Boolean = SettingManager.limitResultViewMaxWidth + + fun rememberLastSelectionArea(): Boolean = + SettingManager.saveLastSelectionArea } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLastSelectedAreaUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLastSelectedAreaUseCase.kt new file mode 100644 index 00000000..584fd596 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetLastSelectedAreaUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class GetLastSelectedAreaUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke() = preferenceRepository.getLastSelectedArea() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetRememberLastSelectionAreaUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetRememberLastSelectionAreaUseCase.kt new file mode 100644 index 00000000..f9daea7d --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/GetRememberLastSelectionAreaUseCase.kt @@ -0,0 +1,10 @@ +package tw.firemaples.onscreenocr.data.usecase + +import tw.firemaples.onscreenocr.data.repo.SettingRepository +import javax.inject.Inject + +class GetRememberLastSelectionAreaUseCase @Inject constructor( + private val settingRepository: SettingRepository, +) { + operator fun invoke() = settingRepository.rememberLastSelectionArea() +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetLastSelectedAreaUseCase.kt b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetLastSelectedAreaUseCase.kt new file mode 100644 index 00000000..9df40678 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/data/usecase/SetLastSelectedAreaUseCase.kt @@ -0,0 +1,13 @@ +package tw.firemaples.onscreenocr.data.usecase + +import android.graphics.Rect +import tw.firemaples.onscreenocr.data.repo.PreferenceRepository +import javax.inject.Inject + +class SetLastSelectedAreaUseCase @Inject constructor( + private val preferenceRepository: PreferenceRepository, +) { + operator fun invoke(selectedRect: Rect) { + preferenceRepository.setLastSelectedArea(selectedRect) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingContent.kt new file mode 100644 index 00000000..8cf93d4f --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingContent.kt @@ -0,0 +1,205 @@ +package tw.firemaples.onscreenocr.floatings.compose.screencircling + +import android.content.res.Configuration +import android.graphics.Rect +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.flow.MutableStateFlow +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.screenCircling.CirclingView +import tw.firemaples.onscreenocr.floatings.screenCircling.HelperTextView +import tw.firemaples.onscreenocr.floatings.screenCircling.ProgressBorderView +import tw.firemaples.onscreenocr.theme.AppTheme +import tw.firemaples.onscreenocr.utils.getViewRect +import tw.firemaples.onscreenocr.utils.onViewPrepared + +@Composable +fun ScreenCirclingContent(viewModel: ScreenCirclingViewModel) { + val state by viewModel.state.collectAsState() + val helperTextView = remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + viewModel.onViewDisplayed() + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(colorResource(id = R.color.dialogOutside)) + ) { + ProgressBorderView() + + CirclingView( + selectedArea = state.selectedArea, + helperTextView = helperTextView, + onViewPrepared = viewModel::onCirclingViewPrepared, + onAreaSelected = viewModel::onAreaSelected, + ) + + HelperTextView(helperTextView = helperTextView) + } +} + +@Composable +private fun ProgressBorderView() { + var run by remember { mutableStateOf(true) } + + DisposableEffect(Unit) { + onDispose { + run = false + } + } + + // TODO refactor to compose + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + ProgressBorderView(context = context).also { view -> + if (run) { + view.start() + } + } + }, + update = { view -> + if (!run) { + view.stop() + } + } + ) +} + +@Composable +private fun CirclingView( + selectedArea: Rect?, + helperTextView: MutableState, + onViewPrepared: (viewRect: Rect) -> Unit, + onAreaSelected: (selected: Rect) -> Unit, +) { + // TODO refactor to compose + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + CirclingView( + context = context, + ).apply { + this.selectedBox = selectedArea + + this.onAreaSelected = { selected -> + onAreaSelected.invoke(selected) + } + + onViewPrepared { + onViewPrepared.invoke(getViewRect()) + } + } + }, + update = { view -> + view.helperTextView = helperTextView.value + }, + ) +} + +@Composable +private fun HelperTextView(helperTextView: MutableState) { + // TODO refactor to compose + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + HelperTextView(context = context).also { view -> + helperTextView.value = view + } + }, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ScreenCircleContentPreview() { + val viewModel = object : ScreenCirclingViewModel { + override val state = MutableStateFlow(ScreenCirclingState()) + override fun onViewDisplayed() = Unit + override fun onTranslateClicked() = Unit + override fun onCloseClick() = Unit + override fun onCirclingViewPrepared(viewRect: Rect) = Unit + override fun onAreaSelected(selected: Rect) = Unit + } + + AppTheme { + ScreenCirclingContent( + viewModel = viewModel, + ) + } +} + +//@Composable +//private fun CirclingView() { +// var startPoint: Offset? by remember { mutableStateOf(null) } +// var endPoint: Offset by remember { mutableStateOf(Offset(0f, 0f)) } +// +// Box( +// modifier = Modifier +// .fillMaxSize() +// .pointerInput(Unit) { +// detectDragGestures( +// onDragStart = { offset: Offset -> +// composeDebug("onDragStart()") +// startPoint = offset +// endPoint = offset +// }, +// onDragEnd = {}, +// onDragCancel = {}, +// onDrag = { change: PointerInputChange, dragAmount: Offset -> +// endPoint += dragAmount +// } +// ) +//// detectTransformGestures( +//// onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float -> +//// composeDebug("onGesture(), centroid: $centroid, pan: $pan, zoom: $zoom, rotation: $rotation") +//// var start = startPoint +//// if (start == null) { +//// startPoint = centroid +//// return@detectTransformGestures +//// } +////// var start = startPoint ?: return@detectTransformGestures +//// var end = centroid +//// +//// start += pan +//// end += pan +//// +//// startPoint = start +//// endPoint = end +//// } +//// ) +// } +// .drawBehind { +// val start = startPoint ?: return@drawBehind +// val end = endPoint +// val left = min(start.x, end.x) +// val top = min(start.y, end.y) +// val width = abs(start.x - end.x) +// val height = abs(start.y - end.y) +// +// drawRect( +// color = Color.Green, +// topLeft = Offset(left, top), +// size = Size(width, height), +// style = Stroke(width = 2.dp.toPx()), +// ) +// } +// ) +//} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingFloatingView.kt new file mode 100644 index 00000000..4330bd1d --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingFloatingView.kt @@ -0,0 +1,34 @@ +package tw.firemaples.onscreenocr.floatings.compose.screencircling + +import android.content.Context +import android.view.WindowManager +import androidx.compose.runtime.Composable +import dagger.hilt.android.qualifiers.ApplicationContext +import tw.firemaples.onscreenocr.floatings.compose.base.ComposeFloatingView +import javax.inject.Inject + + +class ScreenCirclingFloatingView @Inject constructor( + @ApplicationContext context: Context, + private val viewModel: ScreenCirclingViewModel, +) : ComposeFloatingView(context) { + + override val fullscreenMode: Boolean + get() = true + + override val layoutWidth: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + override val layoutHeight: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + override val enableHomeButtonWatcher: Boolean + get() = true + + @Composable + override fun RootContent() { + ScreenCirclingContent( + viewModel = viewModel, + ) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingModule.kt new file mode 100644 index 00000000..3a05ce02 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingModule.kt @@ -0,0 +1,13 @@ +package tw.firemaples.onscreenocr.floatings.compose.screencircling + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface ScreenCirclingModule { + @Binds + fun bindScreenCirclingViewModel(impl: ScreenCirclingViewModelImpl): ScreenCirclingViewModel +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingViewModel.kt new file mode 100644 index 00000000..d09e5e90 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/screencircling/ScreenCirclingViewModel.kt @@ -0,0 +1,125 @@ +package tw.firemaples.onscreenocr.floatings.compose.screencircling + +import android.graphics.Rect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import tw.firemaples.onscreenocr.data.usecase.GetCurrentOCRLangUseCase +import tw.firemaples.onscreenocr.data.usecase.GetLastSelectedAreaUseCase +import tw.firemaples.onscreenocr.data.usecase.GetRememberLastSelectionAreaUseCase +import tw.firemaples.onscreenocr.data.usecase.SetLastSelectedAreaUseCase +import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope +import tw.firemaples.onscreenocr.floatings.manager.NavigationAction +import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.utils.Logger +import javax.inject.Inject + +interface ScreenCirclingViewModel { + val state: StateFlow + fun onViewDisplayed() + fun onTranslateClicked() + fun onCloseClick() + fun onCirclingViewPrepared(viewRect: Rect) + fun onAreaSelected(selected: Rect) +} + +data class ScreenCirclingState( + val selectedArea: Rect? = null, +) + +class ScreenCirclingViewModelImpl @Inject constructor( + @MainImmediateCoroutineScope + private val scope: CoroutineScope, + private val stateNavigator: StateNavigator, + private val getRememberLastSelectionAreaUseCase: GetRememberLastSelectionAreaUseCase, + private val getCurrentOCRLangUseCase: GetCurrentOCRLangUseCase, + private val getLastSelectedAreaUseCase: GetLastSelectedAreaUseCase, + private val setLastSelectedAreaUseCase: SetLastSelectedAreaUseCase, +) : ScreenCirclingViewModel { + private val logger = Logger(ScreenCirclingViewModel::class) + + override val state = MutableStateFlow(ScreenCirclingState()) + + private var viewRectFlow = MutableSharedFlow() + private var selectedRectFlow = MutableSharedFlow() + + init { + combine( + flow = viewRectFlow, + flow2 = selectedRectFlow, + transform = ::onScreenCircled, + ).launchIn(scope) + } + + override fun onViewDisplayed() { + logger.debug("onViewDisplayed()") + if (getRememberLastSelectionAreaUseCase.invoke()) { + val lastSelectedArea = getLastSelectedAreaUseCase.invoke() + state.update { + it.copy( + selectedArea = lastSelectedArea, + ) + } + + if (lastSelectedArea != null) { + scope.launch { + selectedRectFlow.emit(lastSelectedArea) + } + } + } + } + + override fun onTranslateClicked() { + scope.launch { + val (ocrProvider, ocrLang) = getCurrentOCRLangUseCase.invoke().first() + stateNavigator.navigate( + NavigationAction.NavigateToScreenCapturing( + ocrLang = ocrLang, + ocrProvider = ocrProvider, + ) + ) + } + } + + override fun onCloseClick() { + scope.launch { + stateNavigator.navigate(NavigationAction.CancelScreenCircling) + } + } + + override fun onCirclingViewPrepared(viewRect: Rect) { + logger.debug("onCirclingViewPrepared(), $viewRect") + scope.launch { + viewRectFlow.emit(viewRect) + } + } + + override fun onAreaSelected(selected: Rect) { + logger.debug("onAreaSelected(), $selected") + scope.launch { + setLastSelectedAreaUseCase.invoke(selected) + selectedRectFlow.emit(selected) + state.update { + it.copy(selectedArea = selected) + } + } + } + + private fun onScreenCircled(viewRect: Rect, selectedRect: Rect) { + logger.debug("onScreenCircled(), parent: $viewRect, selected: $selectedRect") + scope.launch { + stateNavigator.navigate( + NavigationAction.NavigateToScreenCircled( + parentRect = viewRect, + selectedRect = selectedRect, + ) + ) + } + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt index b2d68057..08008dc1 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/manager/FloatingViewCoordinator.kt @@ -11,8 +11,8 @@ import tw.firemaples.onscreenocr.di.MainImmediateCoroutineScope import tw.firemaples.onscreenocr.floatings.base.FloatingView import tw.firemaples.onscreenocr.floatings.compose.mainbar.MainBarFloatingView import tw.firemaples.onscreenocr.floatings.compose.resultview.ResultViewFloatingView +import tw.firemaples.onscreenocr.floatings.compose.screencircling.ScreenCirclingFloatingView import tw.firemaples.onscreenocr.floatings.dialog.showErrorDialog -import tw.firemaples.onscreenocr.floatings.screenCircling.ScreenCirclingView import tw.firemaples.onscreenocr.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -24,24 +24,25 @@ class FloatingViewCoordinator @Inject constructor( private val stateNavigator: StateNavigator, stateOperator: StateOperator, private val mainBar: MainBarFloatingView, + private val screenCirclingFloatingView: ScreenCirclingFloatingView, private val resultView: ResultViewFloatingView, ) { private val logger: Logger by lazy { Logger(FloatingViewCoordinator::class) } - private val screenCirclingView: ScreenCirclingView by lazy { - ScreenCirclingView(context).apply { - onAreaSelected = { parent, selected -> - scope.launch { - stateNavigator.navigate( - NavigationAction.NavigateToScreenCircled( - parentRect = parent, - selectedRect = selected, - ) - ) - } - } - } - } +// private val screenCirclingView: ScreenCirclingView by lazy { +// ScreenCirclingView(context).apply { +// onAreaSelected = { parent, selected -> +// scope.launch { +// stateNavigator.navigate( +// NavigationAction.NavigateToScreenCircled( +// parentRect = parent, +// selectedRect = selected, +// ) +// ) +// } +// } +// } +// } val showingStateChangedFlow = MutableStateFlow(false) val isMainBarAttached: Boolean @@ -54,10 +55,10 @@ class FloatingViewCoordinator @Inject constructor( StateOperatorAction.TopMainBar -> arrangeMainBarToTop() StateOperatorAction.ShowScreenCirclingView -> - screenCirclingView.attachToScreen() + screenCirclingFloatingView.attachToScreen() StateOperatorAction.HideScreenCirclingView -> - screenCirclingView.detachFromScreen() + screenCirclingFloatingView.detachFromScreen() StateOperatorAction.ShowResultView -> resultView.attachToScreen() diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingView.kt index fcd06985..ba1bcc3f 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingView.kt @@ -1,66 +1,66 @@ -package tw.firemaples.onscreenocr.floatings.screenCircling - -import android.content.Context -import android.graphics.Rect -import android.view.WindowManager -import tw.firemaples.onscreenocr.R -import tw.firemaples.onscreenocr.floatings.base.FloatingView -import tw.firemaples.onscreenocr.utils.getViewRect - -class ScreenCirclingView(context: Context) : FloatingView(context) { - override val layoutId: Int - get() = R.layout.floating_screen_circling - - //TODO check this - override val fullscreenMode: Boolean - get() = true - - override val layoutWidth: Int - get() = WindowManager.LayoutParams.MATCH_PARENT - override val layoutHeight: Int - get() = WindowManager.LayoutParams.MATCH_PARENT - - private val circlingView: CirclingView = rootView.findViewById(R.id.view_circlingView) - private val progressBorderView: ProgressBorderView = - rootView.findViewById(R.id.view_progressBorder) - private val helperTextView: HelperTextView = rootView.findViewById(R.id.view_helperText) - - private val viewModel: ScreenCirclingViewModel by lazy { ScreenCirclingViewModel(viewScope) } - - var onAreaSelected: ((parent: Rect, selected: Rect) -> Unit)? = null - - init { - setViews() - } - - private fun setViews() { - circlingView.helperTextView = helperTextView - circlingView.onAreaSelected = { selected -> - viewModel.onAreaSelected(selected) - onAreaSelected?.invoke(circlingView.getViewRect(), selected) - } - - viewModel.lastSelectedArea.observe(lifecycleOwner) { selected -> - selected ?: return@observe - if (circlingView.getViewRect().contains(selected)) { - circlingView.selectedBox = selected - onAreaSelected?.invoke(circlingView.getViewRect(), selected) - } - } - } - - override fun attachToScreen() { - super.attachToScreen() - - progressBorderView.start() - - viewModel.onAttached() - } - - override fun detachFromScreen() { - super.detachFromScreen() - - circlingView.clear() - progressBorderView.stop() - } -} +//package tw.firemaples.onscreenocr.floatings.screenCircling +// +//import android.content.Context +//import android.graphics.Rect +//import android.view.WindowManager +//import tw.firemaples.onscreenocr.R +//import tw.firemaples.onscreenocr.floatings.base.FloatingView +//import tw.firemaples.onscreenocr.utils.getViewRect +// +//class ScreenCirclingView(context: Context) : FloatingView(context) { +// override val layoutId: Int +// get() = R.layout.floating_screen_circling +// +// //TODO check this +// override val fullscreenMode: Boolean +// get() = true +// +// override val layoutWidth: Int +// get() = WindowManager.LayoutParams.MATCH_PARENT +// override val layoutHeight: Int +// get() = WindowManager.LayoutParams.MATCH_PARENT +// +// private val circlingView: CirclingView = rootView.findViewById(R.id.view_circlingView) +// private val progressBorderView: ProgressBorderView = +// rootView.findViewById(R.id.view_progressBorder) +// private val helperTextView: HelperTextView = rootView.findViewById(R.id.view_helperText) +// +// private val viewModel: ScreenCirclingViewModel by lazy { ScreenCirclingViewModel(viewScope) } +// +// var onAreaSelected: ((parent: Rect, selected: Rect) -> Unit)? = null +// +// init { +// setViews() +// } +// +// private fun setViews() { +// circlingView.helperTextView = helperTextView +// circlingView.onAreaSelected = { selected -> +// viewModel.onAreaSelected(selected) +// onAreaSelected?.invoke(circlingView.getViewRect(), selected) +// } +// +// viewModel.lastSelectedArea.observe(lifecycleOwner) { selected -> +// selected ?: return@observe +// if (circlingView.getViewRect().contains(selected)) { +// circlingView.selectedBox = selected +// onAreaSelected?.invoke(circlingView.getViewRect(), selected) +// } +// } +// } +// +// override fun attachToScreen() { +// super.attachToScreen() +// +// progressBorderView.start() +// +// viewModel.onAttached() +// } +// +// override fun detachFromScreen() { +// super.detachFromScreen() +// +// circlingView.clear() +// progressBorderView.stop() +// } +//} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingViewModel.kt index d81744a6..03d0f7c4 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/screenCircling/ScreenCirclingViewModel.kt @@ -1,34 +1,34 @@ -package tw.firemaples.onscreenocr.floatings.screenCircling - -import android.graphics.Rect -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel -import tw.firemaples.onscreenocr.repo.GeneralRepository - -class ScreenCirclingViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) { - private val generalRepo = GeneralRepository() - - private val _lastSelectedArea = MutableLiveData() - val lastSelectedArea: LiveData = _lastSelectedArea - - fun onAttached() { - viewScope.launch { - if (generalRepo.isRememberLastSelection().first()) { - val lastSelection = generalRepo.getLastRememberedSelectionArea().first() - if (lastSelection != null) { - _lastSelectedArea.value = lastSelection - } - } - } - } - - fun onAreaSelected(selected: Rect) { - viewScope.launch { - generalRepo.setLastRememberedSelectionArea(selected) - } - } -} +//package tw.firemaples.onscreenocr.floatings.screenCircling +// +//import android.graphics.Rect +//import androidx.lifecycle.LiveData +//import androidx.lifecycle.MutableLiveData +//import kotlinx.coroutines.CoroutineScope +//import kotlinx.coroutines.flow.first +//import kotlinx.coroutines.launch +//import tw.firemaples.onscreenocr.floatings.base.FloatingViewModel +//import tw.firemaples.onscreenocr.repo.GeneralRepository +// +//class ScreenCirclingViewModel(viewScope: CoroutineScope) : FloatingViewModel(viewScope) { +// private val generalRepo = GeneralRepository() +// +// private val _lastSelectedArea = MutableLiveData() +// val lastSelectedArea: LiveData = _lastSelectedArea +// +// fun onAttached() { +// viewScope.launch { +// if (generalRepo.isRememberLastSelection().first()) { +// val lastSelection = generalRepo.getLastRememberedSelectionArea().first() +// if (lastSelection != null) { +// _lastSelectedArea.value = lastSelection +// } +// } +// } +// } +// +// fun onAreaSelected(selected: Rect) { +// viewScope.launch { +// generalRepo.setLastRememberedSelectionArea(selected) +// } +// } +//} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/Logger.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/Logger.kt index 3d93cc58..25f0175f 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/Logger.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/Logger.kt @@ -4,32 +4,38 @@ import android.util.Log import tw.firemaples.onscreenocr.BuildConfig import kotlin.reflect.KClass +fun composeDebug(msg: String? = null, t: Throwable? = null) { + Logger.log(Logger.COMPOSE_LOGGER, Logger.DEBUG, msg, t) +} + class Logger(clazz: KClass<*>) { companion object { - private const val DEBUG = 0 - private const val INFO = 1 - private const val WARN = 2 - private const val ERROR = 3 - } - - private val tag: String = clazz.java.simpleName + const val DEBUG = 0 + const val INFO = 1 + const val WARN = 2 + const val ERROR = 3 - fun debug(msg: String? = null, t: Throwable? = null) = log(DEBUG, msg, t) - fun info(msg: String? = null, t: Throwable? = null) = log(INFO, msg, t) - fun warn(msg: String? = null, t: Throwable? = null) = log(WARN, msg, t) - fun error(msg: String? = null, t: Throwable? = null) = log(ERROR, msg, t) + const val COMPOSE_LOGGER = "ComposeLogger" - private fun log(level: Int, msg: String?, t: Throwable?) { - if (BuildConfig.DISABLE_LOGGING) return + fun log(tag: String, level: Int, msg: String?, t: Throwable?) { + if (BuildConfig.DISABLE_LOGGING) return - val threadName = Thread.currentThread().name - val message = if (msg != null) "[$threadName] $msg" else "[$threadName]" + val threadName = Thread.currentThread().name + val message = if (msg != null) "[$threadName] $msg" else "[$threadName]" - when (level) { - DEBUG -> Log.d(tag, message, t) - INFO -> Log.i(tag, message, t) - WARN -> Log.w(tag, message, t) - ERROR -> Log.e(tag, message, t) + when (level) { + DEBUG -> Log.d(tag, message, t) + INFO -> Log.i(tag, message, t) + WARN -> Log.w(tag, message, t) + ERROR -> Log.e(tag, message, t) + } } } + + private val tag: String = clazz.java.simpleName + + fun debug(msg: String? = null, t: Throwable? = null) = log(tag, DEBUG, msg, t) + fun info(msg: String? = null, t: Throwable? = null) = log(tag, INFO, msg, t) + fun warn(msg: String? = null, t: Throwable? = null) = log(tag, WARN, msg, t) + fun error(msg: String? = null, t: Throwable? = null) = log(tag, ERROR, msg, t) } From 894a9484cbfd74d0271bb567617a9b071d6ee0d0 Mon Sep 17 00:00:00 2001 From: omar Date: Sat, 17 Feb 2024 17:41:07 +0300 Subject: [PATCH 051/121] Update ResultViewContent.kt add textDirection = TextDirection.Content --- .../floatings/compose/resultview/ResultViewContent.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 058cbd61..21cfd589 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -269,6 +270,9 @@ private fun OCRToolBar( text = stringResource(id = R.string.text_ocr_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, + style = TextStyle(textDirection = TextDirection.Content), + + color = MaterialTheme.colorScheme.onSurface, ) @@ -347,6 +351,8 @@ private fun OCRTextArea( text = ocrText, locale = Locale.US, textStyle = TextStyle( + textDirection = TextDirection.Content, + color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, ), @@ -361,6 +367,7 @@ private fun OCRTextArea( modifier = Modifier .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), + style = TextStyle(textDirection = TextDirection.Content), text = ocrText, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, @@ -417,6 +424,8 @@ private fun TranslationTextArea( modifier = Modifier .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), + style = TextStyle( textDirection = TextDirection.Content, + ), text = translatedText, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, @@ -438,6 +447,7 @@ private fun ColumnScope.TranslationProviderBar( Text( modifier = Modifier.align(Alignment.End), text = translationProviderText, + style = TextStyle(textDirection = TextDirection.Content), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, maxLines = 1, From 2e686b55a8db55002cfae5a8fe684118dd930cc5 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 22:33:56 +0900 Subject: [PATCH 052/121] Set default TextStyle to align the text direction with text content --- .../compose/resultview/ResultViewContent.kt | 15 ++------- .../tw/firemaples/onscreenocr/theme/Theme.kt | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 21cfd589..da8b1e10 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,9 +41,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -270,9 +269,6 @@ private fun OCRToolBar( text = stringResource(id = R.string.text_ocr_text), fontSize = FontSize.Small, fontWeight = FontWeight.Bold, - style = TextStyle(textDirection = TextDirection.Content), - - color = MaterialTheme.colorScheme.onSurface, ) @@ -350,9 +346,7 @@ private fun OCRTextArea( .verticalScroll(rememberScrollState()), text = ocrText, locale = Locale.US, - textStyle = TextStyle( - textDirection = TextDirection.Content, - + textStyle = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, ), @@ -367,7 +361,6 @@ private fun OCRTextArea( modifier = Modifier .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), - style = TextStyle(textDirection = TextDirection.Content), text = ocrText, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, @@ -416,7 +409,6 @@ private fun TranslationTextArea( translatedText: String, fontSize: Float, ) { - if (showProcessing) { ProgressIndicator() } else { @@ -424,8 +416,6 @@ private fun TranslationTextArea( modifier = Modifier .sizeIn(maxHeight = 150.dp) .verticalScroll(rememberScrollState()), - style = TextStyle( textDirection = TextDirection.Content, - ), text = translatedText, color = MaterialTheme.colorScheme.onSurface, fontSize = fontSize.sp, @@ -447,7 +437,6 @@ private fun ColumnScope.TranslationProviderBar( Text( modifier = Modifier.align(Alignment.End), text = translationProviderText, - style = TextStyle(textDirection = TextDirection.Content), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, maxLines = 1, diff --git a/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt b/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt index 7d9e9e87..a955c0c6 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/theme/Theme.kt @@ -2,9 +2,12 @@ package tw.firemaples.onscreenocr.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDirection private val LightColors = lightColorScheme( @@ -74,17 +77,23 @@ private val DarkColors = darkColorScheme( @Composable fun AppTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable() () -> Unit + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit ) { - val colors = if (!useDarkTheme) { - LightColors - } else { - DarkColors - } + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } - MaterialTheme( - colorScheme = colors, - content = content - ) + MaterialTheme( + colorScheme = colors, + ) { + val textStyle = TextStyle( + textDirection = TextDirection.Content, + ) + ProvideTextStyle(value = textStyle) { + content() + } + } } From 738a2c521f119d0dccf5a32f141028c4cc8085b8 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 22:46:22 +0900 Subject: [PATCH 053/121] Use LocalTextStyle instead of TextStyle.Default --- .../onscreenocr/floatings/compose/wigets/WordSelectionText.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt index 9c98d28b..36417870 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/wigets/WordSelectionText.kt @@ -1,6 +1,7 @@ package tw.firemaples.onscreenocr.floatings.compose.wigets import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,7 +24,7 @@ fun WordSelectionText( modifier: Modifier = Modifier, text: String, locale: Locale, - textStyle: TextStyle = TextStyle.Default, + textStyle: TextStyle = LocalTextStyle.current, selectedSpanStyle: SpanStyle = SpanStyle(), onTextSelected: (String) -> Unit ) { From 5673da620ce47d284081b9cb20e367a05c078d92 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 20 Mar 2024 23:33:58 +0900 Subject: [PATCH 054/121] Bump to v4.0.4 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index 9003b0f5..d4a1db08 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 34 - versionCode 121 - versionName "4.0.3" + versionCode 122 + versionName "4.0.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From f4cf81e05695e3c234602f73559b5dd1da1317e3 Mon Sep 17 00:00:00 2001 From: firemaples Date: Thu, 28 Mar 2024 01:27:15 +0900 Subject: [PATCH 055/121] Remove the default ripple effect on the result window background --- .../floatings/compose/base/ComposeUtils.kt | 14 ++++++++++++++ .../compose/resultview/ResultViewContent.kt | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt index 47fbd088..e6dc8000 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt @@ -1,7 +1,10 @@ package tw.firemaples.onscreenocr.floatings.compose.base +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.Dp @@ -30,3 +33,14 @@ fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() } @Composable fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() } + +fun Modifier.clickableWithoutRipple( + interactionSource: MutableInteractionSource, + onClick: () -> Unit +) = then( + Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = { onClick() } + ) +) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index da8b1e10..a124dbc9 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -5,6 +5,7 @@ import android.graphics.Rect import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -51,6 +52,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.compose.base.clickableWithoutRipple import tw.firemaples.onscreenocr.floatings.compose.base.dpToPx import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp import tw.firemaples.onscreenocr.floatings.compose.wigets.WordSelectionText @@ -64,6 +66,7 @@ fun ResultViewContent( requestRootLocationOnScreen: () -> Rect, ) { val state by viewModel.state.collectAsState() + val emptyInteractionSource = remember { MutableInteractionSource() } LaunchedEffect(Unit) { val rootLocation = requestRootLocationOnScreen.invoke() @@ -77,7 +80,10 @@ fun ResultViewContent( modifier = Modifier .fillMaxSize() .background(colorResource(id = R.color.dialogOutside)) - .clickable(onClick = viewModel::onDialogOutsideClicked), + .clickableWithoutRipple( + interactionSource = emptyInteractionSource, + onClick = viewModel::onDialogOutsideClicked, + ), ) { state.highlightArea.forEach { TextHighlightBox( From 4ba63d37a34283205e99b73d9ab1931c0a35519a Mon Sep 17 00:00:00 2001 From: firemaples Date: Thu, 28 Mar 2024 03:16:13 +0900 Subject: [PATCH 056/121] Bump to v4.0.5 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index d4a1db08..d00a3acf 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 34 - versionCode 122 - versionName "4.0.4" + versionCode 123 + versionName "4.0.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 68588400e077540eeeaac957ea54db398d4c20f3 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 7 Apr 2024 00:24:54 +0900 Subject: [PATCH 057/121] Add temporarily removed animation on result view back --- .../compose/resultview/ResultViewContent.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index a124dbc9..23538bcc 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -2,6 +2,8 @@ package tw.firemaples.onscreenocr.floatings.compose.resultview import android.content.res.Configuration import android.graphics.Rect +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateIntOffsetAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -95,10 +97,10 @@ fun ResultViewContent( mutableStateOf(IntOffset(state.highlightUnion.left, state.highlightUnion.top)) } -// val animOffset by animateIntOffsetAsState( -// targetValue = targetOffset.value, -// label = "result panel position", -// ) + val animOffset by animateIntOffsetAsState( + targetValue = targetOffset.value, + label = "result panel position", + ) ResultPanel( modifier = Modifier @@ -114,9 +116,8 @@ fun ResultViewContent( padding = 16.dp.dpToPx(), verticalSpacing = 4.dp.dpToPx(), ) - .offset { targetOffset.value }, -// .offset { animOffset } -// .animateContentSize(), + .offset { animOffset } + .animateContentSize(), viewModel = viewModel, textSearchEnabled = state.textSearchEnabled, fontSize = state.fontSize, From edcb75560834284ab8932cf86b1dd8df18e0cf48 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 7 Apr 2024 00:41:56 +0900 Subject: [PATCH 058/121] Reschedule main bar fade out after closing the menu --- .../onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index e9f9ea92..30842dc0 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -256,6 +256,8 @@ class MainBarViewModelImpl @Inject constructor( ) } + action.emit(MainBarAction.RescheduleFadeOut) + when (mainBarMenuOption) { MainBarMenuOption.SETTING -> action.emit(MainBarAction.OpenSettings) From 241159c6104a396e9ff4d226cc35d05dedb3aafe Mon Sep 17 00:00:00 2001 From: firemaples Date: Mon, 8 Apr 2024 23:30:14 +0900 Subject: [PATCH 059/121] Use customized compose menu list --- .../floatings/compose/base/ComposeUtils.kt | 73 ++++++++++ .../compose/mainbar/MainBarContent.kt | 13 +- .../compose/mainbar/MainBarFloatingView.kt | 53 +++++++ .../floatings/compose/mainbar/MainBarMenu.kt | 42 ------ .../compose/mainbar/MainBarMenuConst.kt | 11 ++ .../compose/mainbar/MainBarViewModel.kt | 33 ++--- .../floatings/compose/menu/MenuContent.kt | 130 ++++++++++++++++++ .../compose/menu/MenuFloatingView.kt | 28 ++++ .../floatings/compose/menu/MenuModule.kt | 13 ++ .../floatings/compose/menu/MenuViewModel.kt | 61 ++++++++ .../compose/resultview/ResultViewContent.kt | 67 +-------- 11 files changed, 395 insertions(+), 129 deletions(-) delete mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenuConst.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuContent.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuFloatingView.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuModule.kt create mode 100644 main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuViewModel.kt diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt index e6dc8000..8de11767 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/base/ComposeUtils.kt @@ -1,13 +1,19 @@ package tw.firemaples.onscreenocr.floatings.compose.base +import android.content.res.Configuration +import android.graphics.Rect import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow @@ -44,3 +50,70 @@ fun Modifier.clickableWithoutRipple( onClick = { onClick() } ) ) + +fun Modifier.calculateOffset( + anchor: Rect, + offset: MutableState, + viewPadding: Float = 0f, + verticalSpacing: Float = 0f, +): Modifier = onGloballyPositioned { coordinates -> + val parent = coordinates.parentLayoutCoordinates?.size ?: return@onGloballyPositioned + val current = coordinates.size + + val leftAnchor = maxOf(anchor.left, viewPadding.toInt()) + val rightAnchor = minOf(anchor.right, parent.width - viewPadding.toInt()) + + val x = when { + leftAnchor + current.width + viewPadding < parent.width -> { + // Align left + anchor.left - viewPadding.toInt() + } + + rightAnchor - current.width - viewPadding >= 0 -> { + // Align right + rightAnchor - current.width - viewPadding.toInt() + } + + else -> { + // No horizontal alignment + 0 + } + } + + val topAnchor = anchor.bottom + verticalSpacing + val bottomAnchor = anchor.top - verticalSpacing + + val y = when { + topAnchor + current.height + viewPadding < parent.height -> { + // Display at bottom + (topAnchor - viewPadding).toInt() + } + + bottomAnchor - current.height - viewPadding >= 0 -> { + // Display at top + (bottomAnchor - current.height - viewPadding).toInt() + } + + else -> { + // Display middle vertically + val middleAnchor = (parent.height - current.height) / 2 + (middleAnchor - viewPadding).toInt() + } + } + + offset.value = IntOffset(x, y) +} + +/** + * A MultiPreview annotation for desplaying a @[Composable] method using light and dark themes. + * + * Note that the app theme should support dark and light modes for these previews to be different. + */ +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.FUNCTION +) +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +annotation class PreviewThemes diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt index 982f1b21..75aeeb79 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarContent.kt @@ -97,10 +97,6 @@ fun MainBarContent( onDragCancel = onDragCancel, onDrag = onDrag, ) - MainBarMenu( - expanded = state.displayMainBarMenu, - onMenuOptionSelected = viewModel::onMenuOptionSelected, - ) } } } @@ -221,7 +217,7 @@ private fun MainBarContentPreview( override fun getFadeOutAfterMoved(): Boolean = false override fun getFadeOutDelay(): Long = 0L override fun getFadeOutDestinationAlpha(): Float = 0f - override fun onMenuItemClicked(key: String) = Unit + override fun onMenuItemClicked(key: String?) = Unit override fun onSelectClicked() = Unit override fun onTranslateClicked() = Unit override fun onCloseClicked() = Unit @@ -229,15 +225,16 @@ private fun MainBarContentPreview( override fun onAttachedToScreen() = Unit override fun onDragEnd(x: Int, y: Int) = Unit override fun onLanguageBlockClicked() = Unit - override fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) = Unit } AppTheme { - MainBarContent(viewModel = viewModel, + MainBarContent( + viewModel = viewModel, onDragStart = { offset -> }, onDragEnd = {}, onDragCancel = {}, - onDrag = { change, dragAmount -> }) + onDrag = { change, dragAmount -> }, + ) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt index f58c0e6d..c89db9b2 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarFloatingView.kt @@ -2,11 +2,16 @@ package tw.firemaples.onscreenocr.floatings.compose.mainbar import android.content.Context import android.graphics.Point +import android.graphics.Rect import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import dagger.hilt.android.qualifiers.ApplicationContext +import tw.firemaples.onscreenocr.R import tw.firemaples.onscreenocr.floatings.ViewHolderService import tw.firemaples.onscreenocr.floatings.compose.base.ComposeMovableFloatingView import tw.firemaples.onscreenocr.floatings.compose.base.collectOnLifecycleResumed +import tw.firemaples.onscreenocr.floatings.compose.menu.MenuFloatingView +import tw.firemaples.onscreenocr.floatings.compose.menu.MenuItem import tw.firemaples.onscreenocr.floatings.history.VersionHistoryView import tw.firemaples.onscreenocr.floatings.readme.ReadmeView import tw.firemaples.onscreenocr.floatings.translationSelectPanel.TranslationSelectPanel @@ -17,6 +22,7 @@ import javax.inject.Inject class MainBarFloatingView @Inject constructor( @ApplicationContext context: Context, private val viewModel: MainBarViewModel, + private val menuFloatingView: MenuFloatingView, ) : ComposeMovableFloatingView(context) { override val initialPosition: Point @@ -61,6 +67,53 @@ class MainBarFloatingView @Inject constructor( MainBarAction.ExitApp -> // TODO wait to be refactored ViewHolderService.exit(context) + + MainBarAction.ShowMenu ->{ + val anchor = + Rect(params.x, params.y, params.x + rootView.width, params.y + rootView.height) + menuFloatingView.getMenuViewDelegate().setAnchor(anchor) + menuFloatingView.attachToScreen() + } + + MainBarAction.HideMenu -> + menuFloatingView.detachFromScreen() + } + } + + val menuItems: List = listOf( + MenuItem( + key = MainBarMenuConst.MENU_SETTING, + text = stringResource(id = R.string.menu_setting), + ), + MenuItem( + key = MainBarMenuConst.MENU_PRIVACY_POLICY, + text = stringResource(id = R.string.menu_privacy_policy), + ), + MenuItem( + key = MainBarMenuConst.MENU_ABOUT, + text = stringResource(id = R.string.menu_about), + ), + MenuItem( + key = MainBarMenuConst.MENU_VERSION_HISTORY, + text = stringResource(id = R.string.menu_version_history), + ), + MenuItem( + key = MainBarMenuConst.MENU_README, + text = stringResource(id = R.string.menu_readme), + ), + MenuItem( + key = MainBarMenuConst.MENU_HIDE, + text = stringResource(id = R.string.menu_hide), + ), + MenuItem( + key = MainBarMenuConst.MENU_EXIT, + text = stringResource(id = R.string.menu_exit), + ), + ) + with(menuFloatingView.getMenuViewDelegate()) { + setMenuData(menuItems) + setOnMenuItemClickedListener { key -> + viewModel.onMenuItemClicked(key) } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt deleted file mode 100644 index 061654fd..00000000 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenu.kt +++ /dev/null @@ -1,42 +0,0 @@ -package tw.firemaples.onscreenocr.floatings.compose.mainbar - -import androidx.annotation.StringRes -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import tw.firemaples.onscreenocr.R - -@Composable -fun MainBarMenu( - expanded: Boolean, - onMenuOptionSelected: (MainBarMenuOption?) -> Unit, -) { - DropdownMenu( - expanded = expanded, - onDismissRequest = { onMenuOptionSelected.invoke(null) }, - ) { - MainBarMenuOption.entries.forEach { option -> - DropdownMenuItem( - text = { - Text(text = stringResource(id = option.text)) - }, - onClick = { onMenuOptionSelected.invoke(option) } - ) - } - } -} - -enum class MainBarMenuOption( - @StringRes - val text: Int, -) { - SETTING(R.string.menu_setting), - PRIVACY_POLICY(R.string.menu_privacy_policy), - ABOUT(R.string.menu_about), - VERSION_HISTORY(R.string.menu_version_history), - README(R.string.menu_readme), - HIDE(R.string.menu_hide), - EXIT(R.string.menu_exit), -} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenuConst.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenuConst.kt new file mode 100644 index 00000000..2415f0d8 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarMenuConst.kt @@ -0,0 +1,11 @@ +package tw.firemaples.onscreenocr.floatings.compose.mainbar + +object MainBarMenuConst { + const val MENU_SETTING = "SETTING" + const val MENU_PRIVACY_POLICY = "PRIVACY_POLICY" + const val MENU_ABOUT = "ABOUT" + const val MENU_VERSION_HISTORY = "VERSION_HISTORY" + const val MENU_README = "README" + const val MENU_HIDE = "HIDE" + const val MENU_EXIT = "EXIT" +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt index 30842dc0..1801af4a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/mainbar/MainBarViewModel.kt @@ -37,7 +37,6 @@ interface MainBarViewModel { fun getFadeOutAfterMoved(): Boolean fun getFadeOutDelay(): Long fun getFadeOutDestinationAlpha(): Float - fun onMenuItemClicked(key: String) fun onSelectClicked() fun onTranslateClicked() fun onCloseClicked() @@ -45,7 +44,7 @@ interface MainBarViewModel { fun onAttachedToScreen() fun onDragEnd(x: Int, y: Int) fun onLanguageBlockClicked() - fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) + fun onMenuItemClicked(key: String?) } data class MainBarState( @@ -68,6 +67,8 @@ sealed interface MainBarAction { data object OpenReadme : MainBarAction data object HideMainBar : MainBarAction data object ExitApp : MainBarAction + data object ShowMenu : MainBarAction + data object HideMenu : MainBarAction } @Suppress("LongParameterList", "TooManyFunctions") @@ -185,12 +186,6 @@ class MainBarViewModelImpl @Inject constructor( override fun getFadeOutDestinationAlpha(): Float = SettingManager.opaquePercentageToFadeOut //TODO move logic - override fun onMenuItemClicked(key: String) { - scope.launch { - action.emit(MainBarAction.RescheduleFadeOut) - } - } - override fun onSelectClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) @@ -221,6 +216,7 @@ class MainBarViewModelImpl @Inject constructor( override fun onMenuButtonClicked() { scope.launch { action.emit(MainBarAction.RescheduleFadeOut) + action.emit(MainBarAction.ShowMenu) state.update { it.copy( displayMainBarMenu = true, @@ -248,7 +244,7 @@ class MainBarViewModelImpl @Inject constructor( } } - override fun onMenuOptionSelected(mainBarMenuOption: MainBarMenuOption?) { + override fun onMenuItemClicked(key: String?) { scope.launch { state.update { it.copy( @@ -256,31 +252,30 @@ class MainBarViewModelImpl @Inject constructor( ) } + action.emit(MainBarAction.HideMenu) action.emit(MainBarAction.RescheduleFadeOut) - when (mainBarMenuOption) { - MainBarMenuOption.SETTING -> + when (key) { + MainBarMenuConst.MENU_SETTING -> action.emit(MainBarAction.OpenSettings) - MainBarMenuOption.PRIVACY_POLICY -> + MainBarMenuConst.MENU_PRIVACY_POLICY -> action.emit(MainBarAction.OpenBrowser(RemoteConfigManager.privacyPolicyUrl)) - MainBarMenuOption.ABOUT -> + MainBarMenuConst.MENU_ABOUT -> action.emit(MainBarAction.OpenBrowser(RemoteConfigManager.aboutUrl)) - MainBarMenuOption.VERSION_HISTORY -> + MainBarMenuConst.MENU_VERSION_HISTORY -> action.emit(MainBarAction.OpenVersionHistory) - MainBarMenuOption.README -> + MainBarMenuConst.MENU_README -> action.emit(MainBarAction.OpenReadme) - MainBarMenuOption.HIDE -> + MainBarMenuConst.MENU_HIDE -> action.emit(MainBarAction.HideMainBar) - MainBarMenuOption.EXIT -> + MainBarMenuConst.MENU_EXIT -> action.emit(MainBarAction.ExitApp) - - null -> {} } } } diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuContent.kt new file mode 100644 index 00000000..668c04d9 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuContent.kt @@ -0,0 +1,130 @@ +package tw.firemaples.onscreenocr.floatings.compose.menu + +import android.graphics.Rect +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.MutableStateFlow +import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.compose.base.PreviewThemes +import tw.firemaples.onscreenocr.floatings.compose.base.calculateOffset +import tw.firemaples.onscreenocr.floatings.compose.base.clickableWithoutRipple +import tw.firemaples.onscreenocr.floatings.compose.base.dpToPx +import tw.firemaples.onscreenocr.theme.AppTheme + +@Composable +fun MenuContent(viewModel: MenuViewModel) { + val state by viewModel.state.collectAsState() + val emptyInteractionSource = remember { MutableInteractionSource() } + + val offset = remember { + mutableStateOf(IntOffset(20, 0)) + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(color = colorResource(id = R.color.dialogOutside)) + .clickableWithoutRipple( + interactionSource = emptyInteractionSource, + onClick = viewModel::onMenuOutsideClicked, + ), + ) { + Column( + modifier = Modifier + .calculateOffset( + anchor = state.anchor, + offset = offset, + verticalSpacing = 4.dp.dpToPx(), + ) + .offset { offset.value } + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp), + ) + .width(IntrinsicSize.Max) + .animateContentSize(), + ) { + state.menuItems.forEach { item -> + Row( + modifier = Modifier + .clickable(onClick = { viewModel.onMenuClicked(item.key) }) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (item.selected) { + Icon( + painter = painterResource(id = R.drawable.ic_check), + contentDescription = "", + tint = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.size(2.dp)) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = item.text, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } + } +} + +@PreviewThemes +@Composable +private fun MenuContentPreview() { + val state = MenuState( + menuItems = listOf( + MenuItem( + key = "1", text = "Menu Item 1", selected = true, + ), + MenuItem( + key = "2", text = "Menu Item 2", selected = false, + ), + MenuItem( + key = "3", text = "Menu Item 3", selected = false, + ), + ) + ) + + val viewModel = object : MenuViewModel { + override val state = MutableStateFlow(state) + override fun onMenuClicked(key: String) = Unit + override fun onMenuOutsideClicked() = Unit + override fun setOnMenuItemClickedListener(onClicked: (key: String?) -> Unit) = Unit + override fun setMenuData(menuItems: List) = Unit + override fun setAnchor(anchor: Rect) = Unit + } + + AppTheme { + MenuContent(viewModel = viewModel) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuFloatingView.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuFloatingView.kt new file mode 100644 index 00000000..0584343e --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuFloatingView.kt @@ -0,0 +1,28 @@ +package tw.firemaples.onscreenocr.floatings.compose.menu + +import android.content.Context +import android.view.WindowManager +import androidx.compose.runtime.Composable +import dagger.hilt.android.qualifiers.ApplicationContext +import tw.firemaples.onscreenocr.floatings.compose.base.ComposeFloatingView +import javax.inject.Inject + +class MenuFloatingView @Inject constructor( + @ApplicationContext context: Context, + private val viewModel: MenuViewModel, +) : ComposeFloatingView(context) { + + override val layoutWidth: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + override val layoutHeight: Int + get() = WindowManager.LayoutParams.MATCH_PARENT + + fun getMenuViewDelegate(): MenuViewDelegate = + this.viewModel + + @Composable + override fun RootContent() { + MenuContent(viewModel = viewModel) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuModule.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuModule.kt new file mode 100644 index 00000000..963021d0 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuModule.kt @@ -0,0 +1,13 @@ +package tw.firemaples.onscreenocr.floatings.compose.menu + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface MenuModule { + @Binds + fun bindMenuViewModel(menuViewModelImpl: MenuViewModelImpl): MenuViewModel +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuViewModel.kt new file mode 100644 index 00000000..18c00973 --- /dev/null +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/menu/MenuViewModel.kt @@ -0,0 +1,61 @@ +package tw.firemaples.onscreenocr.floatings.compose.menu + +import android.graphics.Rect +import androidx.compose.runtime.Stable +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +interface MenuViewModel : MenuViewDelegate { + val state: StateFlow + fun onMenuClicked(key: String) + fun onMenuOutsideClicked() +} + +interface MenuViewDelegate { + fun setOnMenuItemClickedListener(onClicked: (key: String?) -> Unit) + fun setMenuData(menuItems: List) + fun setAnchor(anchor: Rect) +} + +data class MenuItem(val key: String, val text: String, val selected: Boolean = false) + +@Stable +data class MenuState( + val anchor: Rect = Rect(), + val menuItems: List = listOf(), +) + +class MenuViewModelImpl @Inject constructor() : MenuViewModel { + + override val state = MutableStateFlow(MenuState()) + + private var onMenuItemClicked: ((key: String?) -> Unit)? = null + + override fun setOnMenuItemClickedListener(onClicked: (key: String?) -> Unit) { + this.onMenuItemClicked = onClicked + } + + override fun setMenuData(menuItems: List) { + state.update { + it.copy( + menuItems = menuItems, + ) + } + } + + override fun setAnchor(anchor: Rect) { + state.update { + it.copy(anchor = anchor) + } + } + + override fun onMenuClicked(key: String) { + onMenuItemClicked?.invoke(key) + } + + override fun onMenuOutsideClicked() { + onMenuItemClicked?.invoke(null) + } +} diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt index 23538bcc..8f00350a 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewContent.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -31,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,7 +37,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -54,6 +51,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import tw.firemaples.onscreenocr.R +import tw.firemaples.onscreenocr.floatings.compose.base.calculateOffset import tw.firemaples.onscreenocr.floatings.compose.base.clickableWithoutRipple import tw.firemaples.onscreenocr.floatings.compose.base.dpToPx import tw.firemaples.onscreenocr.floatings.compose.base.pxToDp @@ -78,7 +76,7 @@ fun ResultViewContent( ) } - BoxWithConstraints( + Box( modifier = Modifier .fillMaxSize() .background(colorResource(id = R.color.dialogOutside)) @@ -102,18 +100,20 @@ fun ResultViewContent( label = "result panel position", ) + val panelPadding = 16.dp + ResultPanel( modifier = Modifier - .padding(16.dp) + .padding(panelPadding) .run { if (state.limitMaxWidth) widthIn(max = 300.dp) else this } .calculateOffset( - highlightUnion = state.highlightUnion, + anchor = state.highlightUnion, offset = targetOffset, - padding = 16.dp.dpToPx(), + viewPadding = panelPadding.dpToPx(), verticalSpacing = 4.dp.dpToPx(), ) .offset { animOffset } @@ -127,59 +127,6 @@ fun ResultViewContent( } } -private fun Modifier.calculateOffset( - highlightUnion: Rect, - offset: MutableState, - padding: Float, - verticalSpacing: Float, -): Modifier = onGloballyPositioned { coordinates -> - val parent = coordinates.parentLayoutCoordinates?.size ?: return@onGloballyPositioned - val current = coordinates.size - - val leftAnchor = maxOf(highlightUnion.left, padding.toInt()) - val rightAnchor = minOf(highlightUnion.right, parent.width - padding.toInt()) - - val x = when { - leftAnchor + current.width + padding < parent.width -> { - // Align left - highlightUnion.left - padding.toInt() - } - - rightAnchor - current.width - padding >= 0 -> { - // Align right - rightAnchor - current.width - padding.toInt() - } - - else -> { - // No horizontal alignment - 0 - } - } - - val topAnchor = highlightUnion.bottom + verticalSpacing - val bottomAnchor = highlightUnion.top - verticalSpacing - - val y = when { - topAnchor + current.height + padding < parent.height -> { - // Display at bottom - (topAnchor - padding).toInt() - } - - bottomAnchor - current.height - padding >= 0 -> { - // Display at top - (bottomAnchor - current.height - padding).toInt() - } - - else -> { - // Display middle vertically - val middleAnchor = (parent.height - current.height) / 2 - (middleAnchor - padding).toInt() - } - } - - offset.value = IntOffset(x, y) -} - @Composable private fun TextHighlightBox(highlightArea: Rect) { Box( From 7571d92749cb6e22e86c2b6a71a8458f84f77d70 Mon Sep 17 00:00:00 2001 From: firemaples Date: Wed, 10 Apr 2024 09:53:09 +0900 Subject: [PATCH 060/121] Bump to v4.0.6 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index d00a3acf..efb97475 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 34 - versionCode 123 - versionName "4.0.5" + versionCode 124 + versionName "4.0.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From e82ba872fa9180c515c96599457998ea7136210e Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 29 Mar 2025 20:07:19 +0900 Subject: [PATCH 061/121] Bump AGP version to 8.9.1 --- .gitignore | 1 + build.gradle | 5 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- main/build.gradle | 14 +++++--------- main/src/main/AndroidManifest.xml | 5 +++++ 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index d093678d..1abf2ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ google-services.json !main/src/dev/google-services.json config_ad.xml !main/src/main/res/values/config_ad.xml +.kotlin/ diff --git a/build.gradle b/build.gradle index 74841978..11c0c64b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,12 +14,12 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' + classpath 'com.android.tools.build:gradle:8.9.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.gms:google-services:4.4.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20" classpath 'com.google.firebase:perf-plugin:1.4.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' @@ -33,6 +33,7 @@ buildscript { plugins { id 'com.google.dagger.hilt.android' version '2.48' apply false + id 'com.google.devtools.ksp' version '2.1.20-1.0.32' apply false } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e..e2847c82 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/main/build.gradle b/main/build.gradle index efb97475..588e75f6 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -1,13 +1,14 @@ plugins { id 'com.android.application' id 'kotlin-android' - id 'kotlin-kapt' + id 'com.google.devtools.ksp' id 'androidx.navigation.safeargs.kotlin' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.google.firebase.firebase-perf' id 'com.google.dagger.hilt.android' id "io.gitlab.arturbosch.detekt" + id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" } def buildParams = getGradle().getStartParameter().toString().toLowerCase() @@ -23,7 +24,6 @@ task ensureFiles { android { compileSdk 34 - buildToolsVersion = "34.0.0" defaultConfig { applicationId "tw.firemaples.onscreenocr" @@ -97,7 +97,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion = "1.5.4" + kotlinCompilerExtensionVersion = "1.5.15" } packagingOptions { jniLibs { @@ -106,10 +106,6 @@ android { } } -kapt { - correctErrorTypes true -} - dependencies { detektPlugins "io.nlopez.compose.rules:detekt:0.3.9" @@ -165,8 +161,8 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' // Hilt - implementation "com.google.dagger:hilt-android:2.48" - kapt "com.google.dagger:hilt-compiler:2.48" + implementation "com.google.dagger:hilt-android:2.53" + ksp "com.google.dagger:hilt-compiler:2.53" // Firebase implementation platform('com.google.firebase:firebase-bom:32.6.0') diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index 41a11c3c..256c14fb 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -32,6 +32,11 @@ android:supportsRtl="true" android:theme="@style/Theme.EverTranslator"> + + From 1d09c2070c27fc9141e418dc65f0956e97311d77 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 29 Mar 2025 20:12:40 +0900 Subject: [PATCH 062/121] Fix CI script --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 333547f0..3cf94228 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Build with Gradle run: ./gradlew assembleDevDebug - name: Upload debug APK - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4 with: name: main-dev-debug.apk path: main/build/outputs/apk/dev/debug/main-dev-debug.apk From 3385f058394ce14797ad09548a6ba821183117a5 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 29 Mar 2025 20:19:59 +0900 Subject: [PATCH 063/121] empty commit From 6af4fde7db5fa573e379a0a7fe039943a42f9598 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sat, 10 May 2025 22:08:02 +0900 Subject: [PATCH 064/121] Auto copy after text is recognized --- .../compose/resultview/ResultViewModel.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt index ca45315d..385da715 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/compose/resultview/ResultViewModel.kt @@ -27,6 +27,7 @@ import tw.firemaples.onscreenocr.floatings.manager.NavState import tw.firemaples.onscreenocr.floatings.manager.NavigationAction import tw.firemaples.onscreenocr.floatings.manager.ResultInfo import tw.firemaples.onscreenocr.floatings.manager.StateNavigator +import tw.firemaples.onscreenocr.pages.setting.SettingManager import tw.firemaples.onscreenocr.recognition.RecognitionResult import tw.firemaples.onscreenocr.translator.TranslationProviderType import tw.firemaples.onscreenocr.utils.Constants @@ -165,7 +166,7 @@ class ResultViewModelImpl @Inject constructor( ) } - is NavState.TextTranslating -> + is NavState.TextTranslating -> { state.update { this@ResultViewModelImpl.lastRecognitionResult = navState.recognitionResult @@ -191,6 +192,18 @@ class ResultViewModelImpl @Inject constructor( ) } + if (SettingManager.autoCopyOCRResult) { + val recognizedText = navState.recognitionResult.result + scope.launch { + Utils.copyToClipboard( + label = LABEL_RECOGNIZED_TEXT, + text = recognizedText, + ) + } + } + } + + is NavState.TextTranslated -> { when (val resultInfo = navState.resultInfo) { is ResultInfo.Error -> From dfe0a3895fb987c01f4cc1f3e77c85bf487ca303 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:20:17 +0000 Subject: [PATCH 065/121] Update dependency com.google.gms:google-services to v4.4.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 11c0c64b..6bb0c4d4 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files - classpath 'com.google.gms:google-services:4.4.0' + classpath 'com.google.gms:google-services:4.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20" classpath 'com.google.firebase:perf-plugin:1.4.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' From 4092ca2ae44f7d1ed8dcd1e20dd20278d3f5c80a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:20:21 +0000 Subject: [PATCH 066/121] Update dependency com.google.mlkit:language-id to v17.0.6 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 588e75f6..867c50b1 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -188,7 +188,7 @@ dependencies { implementation 'com.google.mlkit:translate:17.0.2' // Google MLKit - Identify Languages - implementation 'com.google.mlkit:language-id:17.0.4' + implementation 'com.google.mlkit:language-id:17.0.6' // Kotpref // core From eab79e781f153232d54133a5f20ef39a8d451a78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:20:33 +0000 Subject: [PATCH 067/121] Update dependency androidx.test.ext:junit to v1.2.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 588e75f6..1f7b8d35 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -123,7 +123,7 @@ dependencies { implementation 'androidx.webkit:webkit:1.7.0' //noinspection GradleDynamicVersion testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' From 6ac5cacb51ed4d7c25b1215d4bda2b92a4db018c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:30:54 +0000 Subject: [PATCH 068/121] Update dependency com.google.mlkit:text-recognition to v16.0.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index cf3b7f40..fa6ef489 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -174,7 +174,7 @@ dependencies { // Google MLKit - Text recognition v2 // To recognize Latin script - implementation 'com.google.mlkit:text-recognition:16.0.0' + implementation 'com.google.mlkit:text-recognition:16.0.1' // To recognize Chinese script implementation 'com.google.mlkit:text-recognition-chinese:16.0.0' // To recognize Devanagari script From 72f1a4ae4ec38299e08c3d9ff9fc90eeebcf36e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:30:56 +0000 Subject: [PATCH 069/121] Update dependency com.google.mlkit:text-recognition-chinese to v16.0.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index cf3b7f40..d2c94ca6 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -176,7 +176,7 @@ dependencies { // To recognize Latin script implementation 'com.google.mlkit:text-recognition:16.0.0' // To recognize Chinese script - implementation 'com.google.mlkit:text-recognition-chinese:16.0.0' + implementation 'com.google.mlkit:text-recognition-chinese:16.0.1' // To recognize Devanagari script implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0' // To recognize Japanese script From 994e9bafdff007c1f9061fdd49ff8a875c338b01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:30:59 +0000 Subject: [PATCH 070/121] Update dependency com.google.mlkit:text-recognition-devanagari to v16.0.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index cf3b7f40..9143ffd6 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -178,7 +178,7 @@ dependencies { // To recognize Chinese script implementation 'com.google.mlkit:text-recognition-chinese:16.0.0' // To recognize Devanagari script - implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0' + implementation 'com.google.mlkit:text-recognition-devanagari:16.0.1' // To recognize Japanese script implementation 'com.google.mlkit:text-recognition-japanese:16.0.0' // To recognize Korean script From 87fb17886145219db63ca44390028f6d1a042c5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:37:07 +0000 Subject: [PATCH 071/121] Update dependency com.google.mlkit:text-recognition-japanese to v16.0.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 5d39967c..e7d426eb 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -180,7 +180,7 @@ dependencies { // To recognize Devanagari script implementation 'com.google.mlkit:text-recognition-devanagari:16.0.1' // To recognize Japanese script - implementation 'com.google.mlkit:text-recognition-japanese:16.0.0' + implementation 'com.google.mlkit:text-recognition-japanese:16.0.1' // To recognize Korean script implementation 'com.google.mlkit:text-recognition-korean:16.0.0' From 942f2edb83e6160c14ed8dc95b47e35dbc9bb5da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:37:11 +0000 Subject: [PATCH 072/121] Update dependency com.google.mlkit:text-recognition-korean to v16.0.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 5d39967c..cdd2a827 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -182,7 +182,7 @@ dependencies { // To recognize Japanese script implementation 'com.google.mlkit:text-recognition-japanese:16.0.0' // To recognize Korean script - implementation 'com.google.mlkit:text-recognition-korean:16.0.0' + implementation 'com.google.mlkit:text-recognition-korean:16.0.1' // Google MLKit - Text translate implementation 'com.google.mlkit:translate:17.0.2' From 1a1104d29cf38b96cf257c9c82c19ded749a45ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:37:14 +0000 Subject: [PATCH 073/121] Update dependency com.google.mlkit:translate to v17.0.3 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 5d39967c..07186652 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -185,7 +185,7 @@ dependencies { implementation 'com.google.mlkit:text-recognition-korean:16.0.0' // Google MLKit - Text translate - implementation 'com.google.mlkit:translate:17.0.2' + implementation 'com.google.mlkit:translate:17.0.3' // Google MLKit - Identify Languages implementation 'com.google.mlkit:language-id:17.0.6' From bdf0aeef1704dbd086b3c6b2281824a5b2fb79ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:44:15 +0000 Subject: [PATCH 074/121] Update dependency androidx.compose:compose-bom to v2024.12.01 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index acd2a85f..34588605 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -148,7 +148,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.8.1' // Compose - def composeBom = platform('androidx.compose:compose-bom:2024.02.02') + def composeBom = platform('androidx.compose:compose-bom:2024.12.01') implementation composeBom androidTestImplementation composeBom // Material Design 3 From 71860580bae8a142ee8363d8380c3aa52e0dc345 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:50:27 +0000 Subject: [PATCH 075/121] Update dependency androidx.test.espresso:espresso-core to v3.6.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 34588605..c1f96fe5 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -124,7 +124,7 @@ dependencies { //noinspection GradleDynamicVersion testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' // For using coroutines in tests From e8fd88ae257059fb5483576407a6ffc6e5e1950c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:50:29 +0000 Subject: [PATCH 076/121] Update dependency androidx.webkit:webkit to v1.13.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 34588605..41b05ffd 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -120,7 +120,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.2' implementation "androidx.preference:preference-ktx:1.2.1" - implementation 'androidx.webkit:webkit:1.7.0' + implementation 'androidx.webkit:webkit:1.13.0' //noinspection GradleDynamicVersion testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' From 5a82660f56cfaf70d5a0c14b14ffabb9d8b46f28 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:50:32 +0000 Subject: [PATCH 077/121] Update dependency com.google.android.material:material to v1.12.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 34588605..5df7ef9b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -113,7 +113,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' From 63115375ccb7b1975275a34eb516273383e6bbc4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 13:50:34 +0000 Subject: [PATCH 078/121] Update dependency com.google.code.gson:gson to v2.13.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 34588605..e2124315 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -199,7 +199,7 @@ dependencies { implementation 'com.chibatching.kotpref:enum-support:2.13.2' // optional, support saving json string through Gson implementation 'com.chibatching.kotpref:gson-support:2.13.2' - implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.google.code.gson:gson:2.13.1' // optional, support LiveData observable preference implementation 'com.chibatching.kotpref:livedata-support:2.13.2' // implementation 'androidx.lifecycle:lifecycle-livedata:2.2.0' From f2138c7b5b648e2cd702897ba23e83642581337b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:03:54 +0000 Subject: [PATCH 079/121] Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6bb0c4d4..7c42e501 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ buildscript { classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" // Detekt - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.3" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8" } } From bca7fafaa06bdca12d9c5119d8c184fb06d87cb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:04:13 +0000 Subject: [PATCH 080/121] Update dependency androidx.datastore:datastore-preferences to v1.1.6 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 1ff72ed7..d3429cc3 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -134,7 +134,7 @@ dependencies { testImplementation "androidx.arch.core:core-testing:2.2.0" // Data store - preference - implementation "androidx.datastore:datastore-preferences:1.0.0" + implementation "androidx.datastore:datastore-preferences:1.1.6" // Navigation component implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" From 6c0e7bf8b71035d8f1a94e300c0a60bb0db1da17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:11:22 +0000 Subject: [PATCH 081/121] Update dependency pl.droidsonroids.gif:android-gif-drawable to v1.2.29 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index d3429cc3..3529615f 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -216,7 +216,7 @@ dependencies { // implementation 'cz.adaptech:tesseract4android:4.1.0' // koral--/android-gif-drawable - implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.28' + implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.29' // // Ad network mediation - Mopub // implementation('com.mopub:mopub-sdk:+@aar') { From 81b5261ce3ee7a9311202bec8abcd42a91fb85d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:11:28 +0000 Subject: [PATCH 082/121] Update dependency androidx.appcompat:appcompat to v1.7.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index d3429cc3..a1707c68 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -112,7 +112,7 @@ dependencies { implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation 'androidx.core:core-ktx:1.10.1' - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" From 40a249b14a07a67f177874e90215e0a70333de8b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:11:30 +0000 Subject: [PATCH 083/121] Update dependency androidx.constraintlayout:constraintlayout to v2.2.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index d3429cc3..15187691 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -114,7 +114,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' From cfbb743a58f8dfae9227dbfe578e0db762559b9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:20:08 +0000 Subject: [PATCH 084/121] Update dependency com.github.MikeOrtiz:TouchImageView to v3.7.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 90a0672a..51d1868d 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -247,5 +247,5 @@ dependencies { implementation 'com.facebook.infer.annotation:infer-annotation:0.18.0' // TouchImageView - implementation 'com.github.MikeOrtiz:TouchImageView:3.6' + implementation 'com.github.MikeOrtiz:TouchImageView:3.7.1' } From b904fb783d20aefb9f12f47c986f81914126bd3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:31:27 +0000 Subject: [PATCH 085/121] Update dependency com.google.android.gms:play-services-ads to v22.6.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 51d1868d..e9ea8dd0 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -236,7 +236,7 @@ dependencies { // implementation 'com.mopub.mediation:adcolony:4.5.0.1' // Admob - implementation 'com.google.android.gms:play-services-ads:22.5.0' + implementation 'com.google.android.gms:play-services-ads:22.6.0' // Facebook Ad network implementation 'com.google.ads.mediation:facebook:6.16.0.0' // Adcolony Ad network From 3775804602304f7a961fc9abf1a27e8fb78cf332 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:33:27 +0000 Subject: [PATCH 086/121] Update dependency com.google.ads.mediation:facebook to v6.20.0.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index e9ea8dd0..a4d701f3 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -238,7 +238,7 @@ dependencies { // Admob implementation 'com.google.android.gms:play-services-ads:22.6.0' // Facebook Ad network - implementation 'com.google.ads.mediation:facebook:6.16.0.0' + implementation 'com.google.ads.mediation:facebook:6.20.0.0' // Adcolony Ad network implementation 'com.google.ads.mediation:adcolony:4.8.0.2' // To fix build failed with issue From cc0f3b99142a0d1f34c56d3a8c004e8aefcd72a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:33:33 +0000 Subject: [PATCH 087/121] Update dependency com.google.dagger:hilt-compiler to v2.56.2 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index e9ea8dd0..5f57c1dc 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -162,7 +162,7 @@ dependencies { // Hilt implementation "com.google.dagger:hilt-android:2.53" - ksp "com.google.dagger:hilt-compiler:2.53" + ksp "com.google.dagger:hilt-compiler:2.56.2" // Firebase implementation platform('com.google.firebase:firebase-bom:32.6.0') From 31a3a3ea43b797bb457ae00cd887acf62d78f3f6 Mon Sep 17 00:00:00 2001 From: Louis Chen Date: Sun, 11 May 2025 00:47:11 +0900 Subject: [PATCH 088/121] Update build.gradle --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index a4d701f3..4086c24b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -238,7 +238,7 @@ dependencies { // Admob implementation 'com.google.android.gms:play-services-ads:22.6.0' // Facebook Ad network - implementation 'com.google.ads.mediation:facebook:6.20.0.0' + implementation 'com.google.ads.mediation:facebook:6.19.0.0' // Adcolony Ad network implementation 'com.google.ads.mediation:adcolony:4.8.0.2' // To fix build failed with issue From eec08ed97b8baebfca93abbea81ba304f3be4771 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 15:48:49 +0000 Subject: [PATCH 089/121] Update dependency com.google.dagger:hilt-android to v2.56.2 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 5f57c1dc..6a11d910 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -161,7 +161,7 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' // Hilt - implementation "com.google.dagger:hilt-android:2.53" + implementation "com.google.dagger:hilt-android:2.56.2" ksp "com.google.dagger:hilt-compiler:2.56.2" // Firebase From 77e24948d0ab2ba75809dbc8fe472ea1bcae1171 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 11 May 2025 01:14:33 +0900 Subject: [PATCH 090/121] Unify dependency versions --- build.gradle | 7 +++++++ main/build.gradle | 40 ++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 7c42e501..97879dff 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,13 @@ buildscript { ext.nav_version = "2.6.0" // require SDK 34 from 2.7.0 + ext.hilt_version = "2.56.2" + ext.coroutines_version = "1.7.3" + ext.lifecycle_version = "2.6.2" + ext.text_recognition_version = "16.0.1" + ext.kotpref_version = "2.13.2" + ext.retrofit2_version = "2.9.0" + repositories { google() gradlePluginPortal() diff --git a/main/build.gradle b/main/build.gradle index 6a11d910..53c8407d 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -116,9 +116,9 @@ dependencies { implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' - implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.2' + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" implementation "androidx.preference:preference-ktx:1.2.1" implementation 'androidx.webkit:webkit:1.13.0' //noinspection GradleDynamicVersion @@ -126,10 +126,10 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" // For using coroutines in tests // For runBlockingTest, CoroutineDispatcher etc. - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" // For InstantTaskExecutorRule testImplementation "androidx.arch.core:core-testing:2.2.0" @@ -161,8 +161,8 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' // Hilt - implementation "com.google.dagger:hilt-android:2.56.2" - ksp "com.google.dagger:hilt-compiler:2.56.2" + implementation "com.google.dagger:hilt-android:$hilt_version" + ksp "com.google.dagger:hilt-compiler:$hilt_version" // Firebase implementation platform('com.google.firebase:firebase-bom:32.6.0') @@ -174,15 +174,15 @@ dependencies { // Google MLKit - Text recognition v2 // To recognize Latin script - implementation 'com.google.mlkit:text-recognition:16.0.1' + implementation "com.google.mlkit:text-recognition:$text_recognition_version" // To recognize Chinese script - implementation 'com.google.mlkit:text-recognition-chinese:16.0.1' + implementation "com.google.mlkit:text-recognition-chinese:$text_recognition_version" // To recognize Devanagari script - implementation 'com.google.mlkit:text-recognition-devanagari:16.0.1' + implementation "com.google.mlkit:text-recognition-devanagari:$text_recognition_version" // To recognize Japanese script - implementation 'com.google.mlkit:text-recognition-japanese:16.0.1' + implementation "com.google.mlkit:text-recognition-japanese:$text_recognition_version" // To recognize Korean script - implementation 'com.google.mlkit:text-recognition-korean:16.0.1' + implementation "com.google.mlkit:text-recognition-korean:$text_recognition_version" // Google MLKit - Text translate implementation 'com.google.mlkit:translate:17.0.3' @@ -192,24 +192,24 @@ dependencies { // Kotpref // core - implementation 'com.chibatching.kotpref:kotpref:2.13.2' + implementation "com.chibatching.kotpref:kotpref:$kotpref_version" // optional, auto initialization module - implementation 'com.chibatching.kotpref:initializer:2.13.2' + implementation "com.chibatching.kotpref:initializer:$kotpref_version" // optional, support saving enum value and ordinal - implementation 'com.chibatching.kotpref:enum-support:2.13.2' + implementation "com.chibatching.kotpref:enum-support:$kotpref_version" // optional, support saving json string through Gson - implementation 'com.chibatching.kotpref:gson-support:2.13.2' + implementation "com.chibatching.kotpref:gson-support:$kotpref_version" implementation 'com.google.code.gson:gson:2.13.1' // optional, support LiveData observable preference - implementation 'com.chibatching.kotpref:livedata-support:2.13.2' + implementation "com.chibatching.kotpref:livedata-support:$kotpref_version" // implementation 'androidx.lifecycle:lifecycle-livedata:2.2.0' // // experimental, preference screen build dsl // implementation 'com.chibatching.kotpref:preference-screen-dsl:2.13.1' // Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' + implementation "com.squareup.retrofit2:retrofit:$retrofit2_version" + implementation "com.squareup.retrofit2:converter-moshi:$retrofit2_version" + implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" // Tesseract implementation 'cz.adaptech.tesseract4android:tesseract4android:4.5.0' From 34ce6fa5e7087fd1ab9d2361a04c5f41812ff0c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 16:16:44 +0000 Subject: [PATCH 091/121] Update dependency com.google.firebase:firebase-bom to v32.8.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 2a3297a6..0acc765d 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -165,7 +165,7 @@ dependencies { ksp "com.google.dagger:hilt-compiler:$hilt_version" // Firebase - implementation platform('com.google.firebase:firebase-bom:32.6.0') + implementation platform('com.google.firebase:firebase-bom:32.8.1') implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-analytics' // implementation 'com.google.firebase:firebase-core' From bf35445d37b13bbb32a1e3031fcd79df2e291412 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 16:16:50 +0000 Subject: [PATCH 092/121] Update dependency io.nlopez.compose.rules:detekt to v0.4.22 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 2a3297a6..69a0839e 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -107,7 +107,7 @@ android { } dependencies { - detektPlugins "io.nlopez.compose.rules:detekt:0.3.9" + detektPlugins "io.nlopez.compose.rules:detekt:0.4.22" implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') From 8edad06112243e9e36f253fd5e158a911bb580a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 13:56:30 +0000 Subject: [PATCH 093/121] Update kotlinx-coroutines monorepo to v1.10.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 97879dff..64310adb 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { ext.nav_version = "2.6.0" // require SDK 34 from 2.7.0 ext.hilt_version = "2.56.2" - ext.coroutines_version = "1.7.3" + ext.coroutines_version = "1.10.2" ext.lifecycle_version = "2.6.2" ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" From 9dbc716447f8d84d9fe2f7c472e9bb1c903eb9ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 13:56:41 +0000 Subject: [PATCH 094/121] Update plugin com.google.dagger.hilt.android to v2.56.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 97879dff..af7f99bf 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ buildscript { } plugins { - id 'com.google.dagger.hilt.android' version '2.48' apply false + id 'com.google.dagger.hilt.android' version '2.56.2' apply false id 'com.google.devtools.ksp' version '2.1.20-1.0.32' apply false } From e410600681514f8466274bc06f471a7a691eb613 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 13:56:44 +0000 Subject: [PATCH 095/121] Update retrofit monorepo to v2.11.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 97879dff..e883d73b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.lifecycle_version = "2.6.2" ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" - ext.retrofit2_version = "2.9.0" + ext.retrofit2_version = "2.11.0" repositories { google() From 75caead9184b65fd23b9693bb2cb9c4cb554d7cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 14:15:45 +0000 Subject: [PATCH 096/121] Update plugin com.google.devtools.ksp to v2.1.20-2.0.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 383254cc..ee22f2fe 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ buildscript { plugins { id 'com.google.dagger.hilt.android' version '2.56.2' apply false - id 'com.google.devtools.ksp' version '2.1.20-1.0.32' apply false + id 'com.google.devtools.ksp' version '2.1.20-2.0.1' apply false } allprojects { From 6a4bf1e8651cd1533c4fc914e2f47666b81c267c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 15:41:44 +0000 Subject: [PATCH 097/121] Update actions/setup-java action to v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cf94228..0d6399c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' From 7f87fde03dc4dc12e22bd422a3d480f2c63bbdf0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 15:41:51 +0000 Subject: [PATCH 098/121] Update dependency com.google.firebase:firebase-bom to v33 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index cc90c862..9ccb1b90 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -165,7 +165,7 @@ dependencies { ksp "com.google.dagger:hilt-compiler:$hilt_version" // Firebase - implementation platform('com.google.firebase:firebase-bom:32.8.1') + implementation platform('com.google.firebase:firebase-bom:33.13.0') implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-analytics' // implementation 'com.google.firebase:firebase-core' From 6ed48a73270e184565be632a80c622acbc69b981 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 11 May 2025 15:59:38 +0000 Subject: [PATCH 099/121] Update dependency com.google.firebase:firebase-crashlytics-gradle to v3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ee22f2fe..d4718d4b 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ buildscript { classpath 'com.google.gms:google-services:4.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20" classpath 'com.google.firebase:perf-plugin:1.4.2' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' + classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3' // Refactor classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" From 8d7870802dc49f24f248aaaaad7b884cfaef1bd6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 11:31:44 +0000 Subject: [PATCH 100/121] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v2.1.21 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d4718d4b..653b3ba4 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.gms:google-services:4.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21" classpath 'com.google.firebase:perf-plugin:1.4.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3' From c02090809f15de72421328eff186f18c60d465d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 11:31:49 +0000 Subject: [PATCH 101/121] Update plugin org.jetbrains.kotlin.plugin.compose to v2.1.21 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 9ccb1b90..ef14fd98 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -8,7 +8,7 @@ plugins { id 'com.google.firebase.firebase-perf' id 'com.google.dagger.hilt.android' id "io.gitlab.arturbosch.detekt" - id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" + id("org.jetbrains.kotlin.plugin.compose") version "2.1.21" } def buildParams = getGradle().getStartParameter().toString().toLowerCase() From d6ed86d51c8ffe90744904142b43ec39d29d4890 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 07:54:49 +0000 Subject: [PATCH 102/121] Update plugin com.google.devtools.ksp to v2.1.21-2.0.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 653b3ba4..e25f79fc 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ buildscript { plugins { id 'com.google.dagger.hilt.android' version '2.56.2' apply false - id 'com.google.devtools.ksp' version '2.1.20-2.0.1' apply false + id 'com.google.devtools.ksp' version '2.1.21-2.0.1' apply false } allprojects { From 88cc3cc0a6f1121fdea5603d197d00264727fe35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 07:55:11 +0000 Subject: [PATCH 103/121] Update retrofit monorepo to v2.12.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 653b3ba4..716f5db5 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.lifecycle_version = "2.6.2" ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" - ext.retrofit2_version = "2.11.0" + ext.retrofit2_version = "2.12.0" repositories { google() From 05a81a450b417594eb3744d7c31fbf58d8d1a126 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 08:51:17 +0000 Subject: [PATCH 104/121] Update retrofit monorepo to v3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2d368942..86c91d55 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.lifecycle_version = "2.6.2" ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" - ext.retrofit2_version = "2.12.0" + ext.retrofit2_version = "3.0.0" repositories { google() From 3c04e036ea8fee9988803e4f7aa6ecf6e6160196 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 08:51:55 +0000 Subject: [PATCH 105/121] Update dependency cz.adaptech.tesseract4android:tesseract4android to v4.8.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index ef14fd98..3b258b4c 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -212,7 +212,7 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" // Tesseract - implementation 'cz.adaptech.tesseract4android:tesseract4android:4.5.0' + implementation 'cz.adaptech.tesseract4android:tesseract4android:4.8.0' // implementation 'cz.adaptech:tesseract4android:4.1.0' // koral--/android-gif-drawable From 51ff1a3c25598c2b6cd06212b792e9233174fab8 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 18:31:21 +0900 Subject: [PATCH 106/121] Fix Moshi converter --- build.gradle | 1 + main/build.gradle | 3 +++ .../java/tw/firemaples/onscreenocr/api/ApiHub.kt | 14 +++++++++++++- .../translator/azure/MicrosoftAzureAPIService.kt | 5 +++++ .../translator/mymemory/MyMemoryAPIService.kt | 3 +++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 86c91d55..37c36224 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" ext.retrofit2_version = "3.0.0" + ext.moshi_version = "1.15.2" repositories { google() diff --git a/main/build.gradle b/main/build.gradle index 3b258b4c..62228632 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -210,6 +210,9 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofit2_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit2_version" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" + // Moshi + ksp("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version") + implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" // Tesseract implementation 'cz.adaptech.tesseract4android:tesseract4android:4.8.0' diff --git a/main/src/main/java/tw/firemaples/onscreenocr/api/ApiHub.kt b/main/src/main/java/tw/firemaples/onscreenocr/api/ApiHub.kt index 3cd23db9..46ac5b0b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/api/ApiHub.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/api/ApiHub.kt @@ -1,15 +1,27 @@ package tw.firemaples.onscreenocr.api import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory import tw.firemaples.onscreenocr.utils.Utils import java.io.File object ApiHub { private val context: Context by lazy { Utils.context } + private val moshi: Moshi by lazy { + Moshi.Builder() + .addLast(KotlinJsonAdapterFactory()) + .build() + } + private val retrofit: Retrofit by lazy { - Retrofit.Builder().baseUrl("http://localhost/").build() + Retrofit.Builder() + .baseUrl("http://localhost/") + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() } val tessDataTempFile: File by lazy { diff --git a/main/src/main/java/tw/firemaples/onscreenocr/translator/azure/MicrosoftAzureAPIService.kt b/main/src/main/java/tw/firemaples/onscreenocr/translator/azure/MicrosoftAzureAPIService.kt index b8fb2470..427889ea 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/translator/azure/MicrosoftAzureAPIService.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/translator/azure/MicrosoftAzureAPIService.kt @@ -2,6 +2,7 @@ package tw.firemaples.onscreenocr.translator.azure import androidx.annotation.Keep import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Header @@ -30,6 +31,7 @@ interface MicrosoftAzureAPIService { ): Response } +@JsonClass(generateAdapter = true) @Keep data class TranslateRequest( @Json(name = "Text") @@ -40,6 +42,7 @@ data class TranslateRequest( } } +@JsonClass(generateAdapter = true) @Keep data class TranslateResponse( @Json(name = "detectedLanguage") @@ -48,6 +51,7 @@ data class TranslateResponse( val translations: List, ) +@JsonClass(generateAdapter = true) @Keep data class DetectedLanguage( @Json(name = "language") @@ -56,6 +60,7 @@ data class DetectedLanguage( val score: Float, ) +@JsonClass(generateAdapter = true) @Keep data class Translation( @Json(name = "text") diff --git a/main/src/main/java/tw/firemaples/onscreenocr/translator/mymemory/MyMemoryAPIService.kt b/main/src/main/java/tw/firemaples/onscreenocr/translator/mymemory/MyMemoryAPIService.kt index bce8581f..35da571b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/translator/mymemory/MyMemoryAPIService.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/translator/mymemory/MyMemoryAPIService.kt @@ -2,6 +2,7 @@ package tw.firemaples.onscreenocr.translator.mymemory import androidx.annotation.Keep import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query @@ -15,6 +16,7 @@ interface MyMemoryAPIService { ): Response } +@JsonClass(generateAdapter = true) @Keep data class TranslateResponse( @Json(name = "responseData") @@ -36,6 +38,7 @@ data class TranslateResponse( fun isSuccess(): Boolean = responseStatus.toString().toDoubleOrNull() == 200.0 } +@JsonClass(generateAdapter = true) @Keep data class ResponseData( @Json(name = "translatedText") From daf462db40b27ea17649691dff5cc1e69466860a Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 19:20:54 +0900 Subject: [PATCH 107/121] Add R8 rules to proguard-rules.pro --- main/proguard-rules.pro | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main/proguard-rules.pro b/main/proguard-rules.pro index 0ada610a..d220f7b1 100644 --- a/main/proguard-rules.pro +++ b/main/proguard-rules.pro @@ -74,3 +74,8 @@ -if interface * { @retrofit2.http.* public *** *(...); } -keep,allowoptimization,allowshrinking,allowobfuscation class <3> ##### Proguard2 End + +##### Proguard R8 +-dontwarn android.media.LoudnessCodecController$OnLoudnessCodecUpdateListener +-dontwarn android.media.LoudnessCodecController +##### From 49e4edbce6bb2cec2cd57b2d905b5089104e0d23 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 19:25:32 +0900 Subject: [PATCH 108/121] Fix Android lint error --- .../translationSelectPanel/TranslationSelectPanelViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/floatings/translationSelectPanel/TranslationSelectPanelViewModel.kt b/main/src/main/java/tw/firemaples/onscreenocr/floatings/translationSelectPanel/TranslationSelectPanelViewModel.kt index 7bcbd17b..e46c7da4 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/floatings/translationSelectPanel/TranslationSelectPanelViewModel.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/floatings/translationSelectPanel/TranslationSelectPanelViewModel.kt @@ -216,7 +216,7 @@ class TranslationSelectPanelViewModel(viewScope: CoroutineScope) : fun onTranslationProviderClicked() { viewScope.launch { - _displayTranslationProviders.value = translationRepo.getAllProviders().first() + _displayTranslationProviders.value = translationRepo.getAllProviders().first()!! } } From 6a5d210f077daf357af412f16c314cc5f972a3c7 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 19:37:16 +0900 Subject: [PATCH 109/121] Update the target SDK to 35 --- main/build.gradle | 4 ++-- .../main/java/tw/firemaples/onscreenocr/utils/BitmapCache.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index 62228632..7099c5f0 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -23,12 +23,12 @@ task ensureFiles { } android { - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 124 versionName "4.0.6" diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/BitmapCache.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/BitmapCache.kt index 7f8d949a..2ee33a5b 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/BitmapCache.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/BitmapCache.kt @@ -83,7 +83,7 @@ object BitmapCache { return false } - val targetConfig = config ?: this.config + val targetConfig = config ?: this.config ?: return false val byteCount = width * height * targetConfig.getBytesPerPixel() return byteCount <= this.allocationByteCount } From 13866e948f5dd4434964e4bad1e7cb73abc6a7ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:40:54 +0000 Subject: [PATCH 110/121] Update dependency androidx.core:core-ktx to v1.16.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 7099c5f0..2377b83b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -111,7 +111,7 @@ dependencies { implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') - implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.core:core-ktx:1.16.0' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' From 73d417e5a7e205f934e708c7ee1a13d735bf102b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:40:59 +0000 Subject: [PATCH 111/121] Update dependency androidx.work:work-runtime-ktx to v2.10.1 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 7099c5f0..c5cd8f78 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -145,7 +145,7 @@ dependencies { androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" // required to avoid crash on Android 12 API 31 - implementation 'androidx.work:work-runtime-ktx:2.8.1' + implementation 'androidx.work:work-runtime-ktx:2.10.1' // Compose def composeBom = platform('androidx.compose:compose-bom:2024.12.01') From d526a2d064c5b3719224411b99186f8a3feb463b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:41:09 +0000 Subject: [PATCH 112/121] Update lifecycle_version to v2.9.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 37c36224..77b0a3d2 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { ext.nav_version = "2.6.0" // require SDK 34 from 2.7.0 ext.hilt_version = "2.56.2" ext.coroutines_version = "1.10.2" - ext.lifecycle_version = "2.6.2" + ext.lifecycle_version = "2.9.0" ext.text_recognition_version = "16.0.1" ext.kotpref_version = "2.13.2" ext.retrofit2_version = "3.0.0" From bcc631f225b7d0ea52034663cd87e9b48ef332bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:41:14 +0000 Subject: [PATCH 113/121] Update nav_version to v2.9.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 37c36224..3d2243f3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.nav_version = "2.6.0" // require SDK 34 from 2.7.0 + ext.nav_version = "2.9.0" // require SDK 34 from 2.7.0 ext.hilt_version = "2.56.2" ext.coroutines_version = "1.10.2" ext.lifecycle_version = "2.6.2" From f1b284318af26e16b574c076b0bc10bf1969fbaa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 May 2025 10:41:19 +0000 Subject: [PATCH 114/121] Update dependency androidx.compose:compose-bom to v2025 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 7099c5f0..e722ccf4 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -148,7 +148,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.8.1' // Compose - def composeBom = platform('androidx.compose:compose-bom:2024.12.01') + def composeBom = platform('androidx.compose:compose-bom:2025.05.00') implementation composeBom androidTestImplementation composeBom // Material Design 3 From b4eebce4d950c9e628df90525be78cce71d6429e Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 19:45:39 +0900 Subject: [PATCH 115/121] Remove unused rules after upgrading target SDK to 35 --- main/proguard-rules.pro | 5 ----- 1 file changed, 5 deletions(-) diff --git a/main/proguard-rules.pro b/main/proguard-rules.pro index d220f7b1..0ada610a 100644 --- a/main/proguard-rules.pro +++ b/main/proguard-rules.pro @@ -74,8 +74,3 @@ -if interface * { @retrofit2.http.* public *** *(...); } -keep,allowoptimization,allowshrinking,allowobfuscation class <3> ##### Proguard2 End - -##### Proguard R8 --dontwarn android.media.LoudnessCodecController$OnLoudnessCodecUpdateListener --dontwarn android.media.LoudnessCodecController -##### From 8faffa7afaaae217263a42c72de04f7c1801f628 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 20:07:19 +0900 Subject: [PATCH 116/121] Bump com.google.android.gms:play-services-ads to 23.6.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index 0d3e14fb..ad2ea37c 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -239,7 +239,7 @@ dependencies { // implementation 'com.mopub.mediation:adcolony:4.5.0.1' // Admob - implementation 'com.google.android.gms:play-services-ads:22.6.0' + implementation 'com.google.android.gms:play-services-ads:23.6.0' // Facebook Ad network implementation 'com.google.ads.mediation:facebook:6.19.0.0' // Adcolony Ad network From 71860c6cfd7eb5832c2cc69e55a9db79de56eb4d Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 21:17:01 +0900 Subject: [PATCH 117/121] Support edge-to-edge --- .../onscreenocr/pages/launch/LaunchActivity.kt | 5 +++++ .../pages/setting/SettingActivity.kt | 5 +++++ .../tw/firemaples/onscreenocr/utils/UIUtils.kt | 17 +++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/LaunchActivity.kt b/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/LaunchActivity.kt index 7a6cd2a2..ff0f1f50 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/LaunchActivity.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/pages/launch/LaunchActivity.kt @@ -4,11 +4,13 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import tw.firemaples.onscreenocr.databinding.ActivityLaunchBinding import tw.firemaples.onscreenocr.remoteconfig.RemoteConfigManager import tw.firemaples.onscreenocr.utils.AdManager import tw.firemaples.onscreenocr.utils.DeviceInfoChecker +import tw.firemaples.onscreenocr.utils.fitCutoutInsets class LaunchActivity : AppCompatActivity() { @@ -30,10 +32,13 @@ class LaunchActivity : AppCompatActivity() { private lateinit var binding: ActivityLaunchBinding override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) binding = ActivityLaunchBinding.inflate(layoutInflater) setContentView(binding.root) + binding.root.fitCutoutInsets() + AdManager.loadBanner(binding.admobAd.root) // MoPubAdManager.loadPermissionPageBanner(this, findViewById(R.id.ad_permissionPage)) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingActivity.kt b/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingActivity.kt index c2f34177..24901cfd 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingActivity.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/pages/setting/SettingActivity.kt @@ -3,9 +3,11 @@ package tw.firemaples.onscreenocr.pages.setting import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import tw.firemaples.onscreenocr.databinding.ActivitySettingBinding import tw.firemaples.onscreenocr.utils.AdManager +import tw.firemaples.onscreenocr.utils.fitCutoutInsets class SettingActivity : AppCompatActivity() { companion object { @@ -20,10 +22,13 @@ class SettingActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingBinding override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) binding = ActivitySettingBinding.inflate(layoutInflater) setContentView(binding.root) + binding.root.fitCutoutInsets() + AdManager.loadBanner(binding.admobAd) // MoPubAdManager.loadSettingPageBanner(this, findViewById(R.id.ad_settingPage)) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/UIUtils.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/UIUtils.kt index 8ae4f437..a5b90855 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/UIUtils.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/UIUtils.kt @@ -13,6 +13,7 @@ import android.view.WindowManager import android.widget.TextView import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding object UIUtils { private val context by lazy { Utils.context } @@ -187,3 +188,19 @@ fun View.showKeyboard() = fun View.hideKeyboard() = ViewCompat.getWindowInsetsController(this) ?.hide(WindowInsetsCompat.Type.ime()) + +fun View.fitCutoutInsets() { + ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets -> + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout() + ) + v.updatePadding( + left = bars.left, + top = bars.top, + right = bars.right, + bottom = bars.bottom, + ) + WindowInsetsCompat.CONSUMED + } +} From 51da5aca6170f104ccd75c38a80630ce3d5193e6 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 21:31:40 +0900 Subject: [PATCH 118/121] Downgrade androidx.datastore:datastore-preferences back to 1.0.0 --- main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/build.gradle b/main/build.gradle index ad2ea37c..ff6c167b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -134,7 +134,7 @@ dependencies { testImplementation "androidx.arch.core:core-testing:2.2.0" // Data store - preference - implementation "androidx.datastore:datastore-preferences:1.1.6" + implementation "androidx.datastore:datastore-preferences:1.0.0" // Navigation component implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" From 1505397c2c542808718ebd4fb662a00ad639a8da Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 21:38:51 +0900 Subject: [PATCH 119/121] Bump to 4.0.9 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index ff6c167b..44245ca6 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 35 - versionCode 124 - versionName "4.0.6" + versionCode 127 + versionName "4.0.9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From 62423379a72bcfa26a080b5470b790561990f772 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 21:55:22 +0900 Subject: [PATCH 120/121] Remove usage of removeLast() based on PlayStore consoles checker --- .../java/tw/firemaples/onscreenocr/utils/WordBoundary.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/src/main/java/tw/firemaples/onscreenocr/utils/WordBoundary.kt b/main/src/main/java/tw/firemaples/onscreenocr/utils/WordBoundary.kt index dace567f..975945c5 100644 --- a/main/src/main/java/tw/firemaples/onscreenocr/utils/WordBoundary.kt +++ b/main/src/main/java/tw/firemaples/onscreenocr/utils/WordBoundary.kt @@ -16,14 +16,14 @@ object WordBoundary { // Skip punctuations and blanks } else { if (boundaries.size >= 2 && boundaries.last().word.isDash()) { - val sb = StringBuilder(boundaries.removeLast().word) - val lastWord = boundaries.removeLast() + val sb = StringBuilder(boundaries.removeAt(boundaries.lastIndex).word) + val lastWord = boundaries.removeAt(boundaries.lastIndex) sb.insert(0, lastWord.word) sb.append(word) boundaries.add(Boundary(sb.toString(), lastWord.start, end)) } else { if (boundaries.isNotEmpty() && boundaries.last().word.isDash()) { - boundaries.removeLast() + boundaries.removeAt(boundaries.lastIndex) } if (word.isDash() && previousWord?.isBlank() == true) { // Skip dash following blank pattern. " -" From b2514d0fa477860d69206a531379cb2cac611cf7 Mon Sep 17 00:00:00 2001 From: firemaples Date: Sun, 18 May 2025 22:06:29 +0900 Subject: [PATCH 121/121] Bump to v4.0.10 --- main/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/build.gradle b/main/build.gradle index 44245ca6..ecc2bc91 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -29,8 +29,8 @@ android { applicationId "tw.firemaples.onscreenocr" minSdkVersion 21 targetSdkVersion 35 - versionCode 127 - versionName "4.0.9" + versionCode 128 + versionName "4.0.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"