feat: Initial Close to system tray implementation #165

This commit is contained in:
Artem Chepurnyi 2024-02-17 23:37:48 +02:00
parent 84565fbcc8
commit 46f337189c
14 changed files with 277 additions and 42 deletions

View File

@ -85,6 +85,8 @@ interface SettingsReadRepository {
fun getUseExternalBrowser(): Flow<Boolean>
fun getCloseToTray(): Flow<Boolean>
fun getColors(): Flow<AppColors?>
fun getLocale(): Flow<String?>

View File

@ -157,6 +157,10 @@ interface SettingsReadWriteRepository : SettingsReadRepository {
useExternalBrowser: Boolean,
): IO<Unit>
fun setCloseToTray(
closeToTray: Boolean,
): IO<Unit>
fun setColors(
colors: AppColors?,
): IO<Unit>

View File

@ -67,6 +67,7 @@ class SettingsRepositoryImpl(
private const val KEY_TWO_PANEL_LAYOUT_LANDSCAPE = "two_panel_layout_landscape"
private const val KEY_TWO_PANEL_LAYOUT_PORTRAIT = "two_panel_layout_portrait"
private const val KEY_USE_EXTERNAL_BROWSER = "use_external_browser"
private const val KEY_CLOSE_TO_TRAY = "close_to_tray"
private const val KEY_FONT = "font"
private const val KEY_THEME = "theme"
private const val KEY_THEME_USE_AMOLED_DARK = "theme_use_amoled_dark"
@ -166,6 +167,9 @@ class SettingsRepositoryImpl(
private val useExternalBrowserPref =
store.getBoolean(KEY_USE_EXTERNAL_BROWSER, false)
private val closeToTrayPref =
store.getBoolean(KEY_CLOSE_TO_TRAY, false)
private val navAnimationPref =
store.getEnumNullable(KEY_NAV_ANIMATION, lens = NavAnimation::key)
@ -412,6 +416,13 @@ class SettingsRepositoryImpl(
override fun setUseExternalBrowser(useExternalBrowser: Boolean) = useExternalBrowserPref
.setAndCommit(useExternalBrowser)
override fun setCloseToTray(
closeToTray: Boolean,
) = closeToTrayPref
.setAndCommit(closeToTray)
override fun getCloseToTray() = closeToTrayPref
override fun setColors(colors: AppColors?) = colorsPref
.setAndCommit(colors)

View File

@ -0,0 +1,5 @@
package com.artemchep.keyguard.common.usecase
import kotlinx.coroutines.flow.Flow
interface GetCloseToTray : () -> Flow<Boolean>

View File

@ -0,0 +1,5 @@
package com.artemchep.keyguard.common.usecase
import com.artemchep.keyguard.common.io.IO
interface PutCloseToTray : (Boolean) -> IO<Unit>

View File

@ -0,0 +1,20 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.service.settings.SettingsReadRepository
import com.artemchep.keyguard.common.usecase.GetCloseToTray
import kotlinx.coroutines.flow.distinctUntilChanged
import org.kodein.di.DirectDI
import org.kodein.di.instance
class GetCloseToTrayImpl(
settingsReadRepository: SettingsReadRepository,
) : GetCloseToTray {
private val sharedFlow = settingsReadRepository.getCloseToTray()
.distinctUntilChanged()
constructor(directDI: DirectDI) : this(
settingsReadRepository = directDI.instance(),
)
override fun invoke() = sharedFlow
}

View File

@ -0,0 +1,18 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.service.settings.SettingsReadWriteRepository
import com.artemchep.keyguard.common.usecase.PutCloseToTray
import org.kodein.di.DirectDI
import org.kodein.di.instance
class PutCloseToTrayImpl(
private val settingsReadWriteRepository: SettingsReadWriteRepository,
) : PutCloseToTray {
constructor(directDI: DirectDI) : this(
settingsReadWriteRepository = directDI.instance(),
)
override fun invoke(closeToTray: Boolean): IO<Unit> = settingsReadWriteRepository
.setCloseToTray(closeToTray)
}

View File

@ -41,6 +41,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingClearCache
import com.artemchep.keyguard.feature.home.settings.component.settingClipboardAutoClearProvider
import com.artemchep.keyguard.feature.home.settings.component.settingClipboardAutoRefreshProvider
import com.artemchep.keyguard.feature.home.settings.component.settingClipboardNotificationSettingsProvider
import com.artemchep.keyguard.feature.home.settings.component.settingCloseToTrayProvider
import com.artemchep.keyguard.feature.home.settings.component.settingColorAccentProvider
import com.artemchep.keyguard.feature.home.settings.component.settingColorSchemeProvider
import com.artemchep.keyguard.feature.home.settings.component.settingConcealFieldsProvider
@ -166,6 +167,7 @@ object Setting {
const val TWO_PANEL_LAYOUT_LANDSCAPE = "two_panel_layout_landscape"
const val TWO_PANEL_LAYOUT_PORTRAIT = "two_panel_layout_portrait"
const val USE_EXTERNAL_BROWSER = "use_external_browser"
const val CLOSE_TO_TRAY = "close_to_tray"
const val APP_ICONS = "app_icons"
const val WEBSITE_ICONS = "website_icons"
const val CHECK_PWNED_PASSWORDS = "check_pwned_passwords"
@ -246,6 +248,7 @@ val hub = mapOf<String, (DirectDI) -> SettingComponent>(
Setting.TWO_PANEL_LAYOUT_LANDSCAPE to ::settingTwoPanelLayoutLandscapeProvider,
Setting.TWO_PANEL_LAYOUT_PORTRAIT to ::settingTwoPanelLayoutPortraitProvider,
Setting.USE_EXTERNAL_BROWSER to ::settingUseExternalBrowserProvider,
Setting.CLOSE_TO_TRAY to ::settingCloseToTrayProvider,
Setting.APP_ICONS to ::settingAppIconsProvider,
Setting.WEBSITE_ICONS to ::settingWebsiteIconsProvider,
Setting.CHECK_PWNED_PASSWORDS to ::settingCheckPwnedPasswordsProvider,

View File

@ -0,0 +1,91 @@
package com.artemchep.keyguard.feature.home.settings.component
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CloseFullscreen
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import arrow.core.partially1
import com.artemchep.keyguard.common.io.launchIn
import com.artemchep.keyguard.common.usecase.GetCloseToTray
import com.artemchep.keyguard.common.usecase.PutCloseToTray
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
import com.artemchep.keyguard.platform.CurrentPlatform
import com.artemchep.keyguard.platform.Platform
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.FlatItem
import com.artemchep.keyguard.ui.icons.icon
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.map
import org.kodein.di.DirectDI
import org.kodein.di.instance
fun settingCloseToTrayProvider(
directDI: DirectDI,
) = settingCloseToTrayProvider(
getCloseToTray = directDI.instance(),
putCloseToTray = directDI.instance(),
windowCoroutineScope = directDI.instance(),
)
fun settingCloseToTrayProvider(
getCloseToTray: GetCloseToTray,
putCloseToTray: PutCloseToTray,
windowCoroutineScope: WindowCoroutineScope,
): SettingComponent = getCloseToTray().map { closeToTray ->
val onCheckedChange = { shouldCloseToTray: Boolean ->
putCloseToTray(shouldCloseToTray)
.launchIn(windowCoroutineScope)
Unit
}
if (CurrentPlatform is Platform.Desktop) {
SettingIi(
search = SettingIi.Search(
group = "ux",
tokens = listOf(
"close",
"tray",
"taskbar",
),
),
) {
SettingCloseToTray(
checked = closeToTray,
onCheckedChange = onCheckedChange,
)
}
} else {
null
}
}
@Composable
private fun SettingCloseToTray(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
) {
FlatItem(
leading = icon<RowScope>(Icons.Outlined.CloseFullscreen),
trailing = {
CompositionLocalProvider(
LocalMinimumInteractiveComponentEnforcement provides false,
) {
Switch(
checked = checked,
enabled = onCheckedChange != null,
onCheckedChange = onCheckedChange,
)
}
},
title = {
Text(
text = stringResource(Res.strings.pref_item_close_to_tray_title),
)
},
onClick = onCheckedChange?.partially1(!checked),
)
}

View File

@ -42,6 +42,7 @@ fun UiSettingsScreen() {
list = listOf(
SettingPaneItem.Item(Setting.USE_EXTERNAL_BROWSER),
SettingPaneItem.Item(Setting.KEEP_SCREEN_ON),
SettingPaneItem.Item(Setting.CLOSE_TO_TRAY),
),
),
SettingPaneItem.Group(

View File

@ -896,6 +896,7 @@
<string name="pref_item_color_scheme_title">Theme</string>
<string name="pref_item_color_scheme_amoled_dark_title">Use contrast black theme</string>
<string name="pref_item_open_links_in_external_browser_title">Open links in external browser</string>
<string name="pref_item_close_to_tray_title">Close to system tray</string>
<string name="pref_item_conceal_fields_title">Conceal fields</string>
<string name="pref_item_conceal_fields_text">Conceal sensitive info, such as passwords and credit card numbers</string>
<!--

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -108,6 +108,7 @@ import com.artemchep.keyguard.common.usecase.GetClipboardAutoClear
import com.artemchep.keyguard.common.usecase.GetClipboardAutoClearVariants
import com.artemchep.keyguard.common.usecase.GetClipboardAutoRefresh
import com.artemchep.keyguard.common.usecase.GetClipboardAutoRefreshVariants
import com.artemchep.keyguard.common.usecase.GetCloseToTray
import com.artemchep.keyguard.common.usecase.GetColors
import com.artemchep.keyguard.common.usecase.GetColorsVariants
import com.artemchep.keyguard.common.usecase.GetConcealFields
@ -165,6 +166,7 @@ import com.artemchep.keyguard.common.usecase.PutCheckPwnedServices
import com.artemchep.keyguard.common.usecase.PutCheckTwoFA
import com.artemchep.keyguard.common.usecase.PutClipboardAutoClear
import com.artemchep.keyguard.common.usecase.PutClipboardAutoRefresh
import com.artemchep.keyguard.common.usecase.PutCloseToTray
import com.artemchep.keyguard.common.usecase.PutColors
import com.artemchep.keyguard.common.usecase.PutConcealFields
import com.artemchep.keyguard.common.usecase.PutDebugPremium
@ -228,6 +230,7 @@ import com.artemchep.keyguard.common.usecase.impl.GetClipboardAutoClearImpl
import com.artemchep.keyguard.common.usecase.impl.GetClipboardAutoClearVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetClipboardAutoRefreshImpl
import com.artemchep.keyguard.common.usecase.impl.GetClipboardAutoRefreshVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetCloseToTrayImpl
import com.artemchep.keyguard.common.usecase.impl.GetColorsImpl
import com.artemchep.keyguard.common.usecase.impl.GetColorsVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetConcealFieldsImpl
@ -282,6 +285,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedServicesImpl
import com.artemchep.keyguard.common.usecase.impl.PutCheckTwoFAImpl
import com.artemchep.keyguard.common.usecase.impl.PutClipboardAutoClearImpl
import com.artemchep.keyguard.common.usecase.impl.PutClipboardAutoRefreshImpl
import com.artemchep.keyguard.common.usecase.impl.PutCloseToTrayImpl
import com.artemchep.keyguard.common.usecase.impl.PutColorsImpl
import com.artemchep.keyguard.common.usecase.impl.PutConcealFieldsImpl
import com.artemchep.keyguard.common.usecase.impl.PutDebugPremiumImpl
@ -617,6 +621,16 @@ fun globalModuleJvm() = DI.Module(
directDI = this,
)
}
bindSingleton<GetCloseToTray> {
GetCloseToTrayImpl(
directDI = this,
)
}
bindSingleton<PutCloseToTray> {
PutCloseToTrayImpl(
directDI = this,
)
}
bindSingleton<PutAllowTwoPanelLayoutInLandscape> {
PutAllowTwoPanelLayoutInLandscapeImpl(
directDI = this,

View File

@ -6,11 +6,17 @@ import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.isTraySupported
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.window.rememberWindowState
import com.artemchep.keyguard.common.AppWorker
import com.artemchep.keyguard.common.io.bind
@ -19,6 +25,7 @@ import com.artemchep.keyguard.common.model.PersistedSession
import com.artemchep.keyguard.common.model.ToastMessage
import com.artemchep.keyguard.common.service.vault.KeyReadWriteRepository
import com.artemchep.keyguard.common.usecase.GetAccounts
import com.artemchep.keyguard.common.usecase.GetCloseToTray
import com.artemchep.keyguard.common.usecase.GetLocale
import com.artemchep.keyguard.common.usecase.GetVaultPersist
import com.artemchep.keyguard.common.usecase.GetVaultSession
@ -37,11 +44,12 @@ import com.artemchep.keyguard.feature.navigation.NavigationIntent
import com.artemchep.keyguard.feature.navigation.NavigationNode
import com.artemchep.keyguard.feature.navigation.NavigationRouterBackHandler
import com.artemchep.keyguard.platform.lifecycle.LeLifecycleState
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.LocalComposeWindow
import com.artemchep.keyguard.ui.surface.LocalSurfaceColor
import com.artemchep.keyguard.ui.theme.KeyguardTheme
import com.mayakapps.compose.windowstyler.WindowBackdrop
import com.mayakapps.compose.windowstyler.WindowStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import dev.icerock.moko.resources.desc.StringDesc
import io.kamel.core.config.KamelConfig
import io.kamel.core.config.takeFrom
@ -61,6 +69,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import org.kodein.di.DI
import org.kodein.di.bindSingleton
import org.kodein.di.compose.rememberInstance
import org.kodein.di.compose.withDI
import org.kodein.di.direct
@ -69,13 +78,16 @@ import java.util.Locale
import kotlin.reflect.KClass
fun main() {
val appDi = DI.invoke {
import(diFingerprintRepositoryModule())
}
val kamelConfig = KamelConfig {
this.takeFrom(KamelConfig.Default)
mapper(FaviconUrlMapper)
}
val appDi = DI.invoke {
import(diFingerprintRepositoryModule())
bindSingleton {
kamelConfig
}
}
val getVaultSession by appDi.di.instance<GetVaultSession>()
val getVaultPersist by appDi.di.instance<GetVaultPersist>()
@ -184,46 +196,94 @@ fun main() {
// }
// }
val getCloseToTray: GetCloseToTray = appDi.direct.instance()
application(exitProcessOnExit = true) {
withDI(appDi) {
Window(
::exitApplication,
state = rememberWindowState(),
title = "Keyguard",
) {
// this.window.addWindowStateListener {
// println("event $it")
// }
KeyguardTheme {
val containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
val contentColor = contentColorFor(containerColor)
val isWindowOpenState = remember {
mutableStateOf(true)
}
WindowStyle(
isDarkTheme = containerColor.luminance() < 0.5f,
backdropType = WindowBackdrop.Default,
)
Surface(
modifier = Modifier.semantics {
// Allows to use testTag() for UiAutomator's resource-id.
// It can be enabled high in the compose hierarchy,
// so that it's enabled for the whole subtree
// testTagsAsResourceId = true
},
color = containerColor,
contentColor = contentColor,
) {
CompositionLocalProvider(
LocalSurfaceColor provides containerColor,
LocalComposeWindow provides this.window,
LocalKamelConfig provides kamelConfig,
) {
Navigation(
exitApplication = ::exitApplication,
) {
Content()
}
// Show a tray icon and allow the app to be collapsed into
// the tray on supported platforms.
val getCloseToTrayState = if (isTraySupported) {
remember { getCloseToTray() }
.collectAsState(false)
} else {
// If the tray is not supported then we
// can never close to it.
remember {
mutableStateOf(false)
}
}
if (getCloseToTrayState.value) {
val trayState = rememberTrayState()
Tray(
icon = painterResource(Res.images.ic_keyguard),
state = trayState,
onAction = {
isWindowOpenState.value = true
},
menu = {
Item(
stringResource(Res.strings.close),
onClick = ::exitApplication,
)
},
)
} else {
isWindowOpenState.value = true
}
if (isWindowOpenState.value) {
KeyguardWindow(
onCloseRequest = {
val shouldCloseToTray = getCloseToTrayState.value
if (shouldCloseToTray) {
isWindowOpenState.value = false
} else {
exitApplication()
}
},
)
}
}
}
}
@Composable
private fun ApplicationScope.KeyguardWindow(
onCloseRequest: () -> Unit,
) {
val windowState = rememberWindowState()
Window(
onCloseRequest = onCloseRequest,
icon = painterResource(Res.images.ic_keyguard),
state = windowState,
title = "Keyguard",
) {
KeyguardTheme {
val containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
val contentColor = contentColorFor(containerColor)
Surface(
modifier = Modifier.semantics {
// Allows to use testTag() for UiAutomator's resource-id.
// It can be enabled high in the compose hierarchy,
// so that it's enabled for the whole subtree
// testTagsAsResourceId = true
},
color = containerColor,
contentColor = contentColor,
) {
val kamelConfig by rememberInstance<KamelConfig>()
CompositionLocalProvider(
LocalSurfaceColor provides containerColor,
LocalComposeWindow provides this.window,
LocalKamelConfig provides kamelConfig,
) {
Navigation(
exitApplication = ::exitApplication,
) {
Content()
}
}
}