Skip to content

Commit

Permalink
💄 Add device refresh button (#295)
Browse files Browse the repository at this point in the history
  • Loading branch information
guiyanakuang authored Feb 5, 2024
1 parent f305c15 commit ef3e0bc
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.clipevery.net

interface ClientHandlerManager {

fun start()

fun addHandler(id: String)

fun removeHandler(id: String)

fun stop()

suspend fun checkConnects(checkAction: CheckAction)

suspend fun checkConnect(id: String, checkAction: CheckAction): Boolean
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.clipevery.net

import androidx.compose.runtime.State

interface DeviceRefresher {
suspend fun refresh(checkAction: CheckAction)

val isRefreshing: State<Boolean>

fun refresh(checkAction: CheckAction)
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,123 @@
package com.clipevery.ui.devices

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import com.clipevery.LocalKoinApplication
import com.clipevery.dao.sync.SyncRuntimeInfo
import com.clipevery.dao.sync.SyncRuntimeInfoDao
import com.clipevery.net.CheckAction
import com.clipevery.net.DeviceRefresher
import com.clipevery.ui.PageViewContext
import com.clipevery.ui.base.ClipIconButton
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.toList

@Composable
fun DevicesView(currentPageViewContext: MutableState<PageViewContext>) {
Box(contentAlignment = Alignment.TopCenter) {
DevicesListView(currentPageViewContext)
DevicesRefreshView()
}
}

@Composable
fun DevicesListView(currentPageViewContext: MutableState<PageViewContext>) {
val current = LocalKoinApplication.current
val syncRuntimeInfoDao = current.koin.get<SyncRuntimeInfoDao>()
val syncRuntimeInfosFlow = syncRuntimeInfoDao.getAllSyncRuntimeInfos().asFlow()

var rememberSyncRuntimeInfos by remember { mutableStateOf(emptyList<SyncRuntimeInfo>()) }

LaunchedEffect(syncRuntimeInfosFlow) {
rememberSyncRuntimeInfos = syncRuntimeInfosFlow.toList()
}
Column {
for ((index, syncRuntimeInfo) in rememberSyncRuntimeInfos.withIndex()) {
DeviceItemView(syncRuntimeInfo, currentPageViewContext)
if (index != rememberSyncRuntimeInfos.size - 1) {
Divider(modifier = Modifier.fillMaxWidth())
}
}
}
}

for ((index, syncRuntimeInfo) in rememberSyncRuntimeInfos.withIndex()) {
DeviceItemView(syncRuntimeInfo, currentPageViewContext)
if (index != rememberSyncRuntimeInfos.size - 1) {
Divider(modifier = Modifier.fillMaxWidth())
@Composable
fun DevicesRefreshView() {
val current = LocalKoinApplication.current
val deviceRefresher = current.koin.get<DeviceRefresher>()

val isRefreshing by deviceRefresher.isRefreshing

val rotationDegrees = remember { Animatable(0f) }

LaunchedEffect(isRefreshing) {
while (isRefreshing) {
rotationDegrees.animateTo(
targetValue = rotationDegrees.value + 360,
animationSpec = tween(
durationMillis = 1000,
easing = LinearEasing
)
)
}
rotationDegrees.snapTo(0f)
}

Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom) {
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End) {
ClipIconButton(
radius = 18.dp,
onClick = {
if (!isRefreshing) {
deviceRefresher.refresh(CheckAction.CheckAll)
}
},
modifier = Modifier
.padding(30.dp)
.background(
MaterialTheme.colors.primary,
CircleShape
)
) {
Icon(
Icons.Outlined.Refresh,
contentDescription = "info",
modifier = Modifier.padding(3.dp)
.size(25.dp)
.graphicsLayer(rotationZ = rotationDegrees.value ),
tint = Color.White
)
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.clipevery.net
import com.clipevery.app.AppInfo
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.timeout
import io.ktor.client.request.get
import io.ktor.client.request.header
Expand All @@ -13,7 +14,11 @@ import io.ktor.util.InternalAPI

class DesktopClipClient(private val appInfo: AppInfo): ClipClient {

private val client: HttpClient = HttpClient(CIO)
private val client: HttpClient = HttpClient(CIO) {
install(HttpTimeout) {
requestTimeoutMillis = 1000
}
}

@OptIn(InternalAPI::class)
override suspend fun post(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
package com.clipevery.net

import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import com.clipevery.utils.ioDispatcher
import kotlinx.coroutines.withContext
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class DesktopDeviceRefresher(private val clientHandlerManager: ClientHandlerManager): DeviceRefresher {

override suspend fun refresh(checkAction: CheckAction) {
withContext(ioDispatcher) {
clientHandlerManager.checkConnects(checkAction)
val logger = KotlinLogging.logger {}

private var _refreshing = mutableStateOf(false)

override val isRefreshing: State<Boolean> get() = _refreshing

override fun refresh(checkAction: CheckAction) {
_refreshing.value = true
CoroutineScope(ioDispatcher).launch {
logger.info { "start launch" }
try {
clientHandlerManager.checkConnects(checkAction)
} catch (e: Exception) {
logger.error(e) { "checkConnects error" }
}
delay(1000)
logger.info { "set refreshing false" }
_refreshing.value = false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.clipevery.utils
import com.clipevery.dao.sync.HostInfo
import com.clipevery.net.ClipClient
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.http.URLProtocol
import io.ktor.http.path
import kotlinx.coroutines.async
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
Expand All @@ -24,14 +26,12 @@ class TelnetUtils(private val clipClient: ClipClient) {
}

var result: HostInfo? = null
while (deferredArray.isNotEmpty() && result == null) {
select {
deferredArray.forEach { deferred ->
deferred.onAwait { hostInfo ->
if (hostInfo != null) {
result = hostInfo
deferredArray.forEach { it.cancel() }
}
select {
deferredArray.forEach { deferred ->
deferred.onAwait { hostInfo ->
if (hostInfo != null) {
result = hostInfo
deferredArray.forEach { it.cancel() }
}
}
}
Expand All @@ -42,9 +42,13 @@ class TelnetUtils(private val clipClient: ClipClient) {
private suspend fun telnet(hostInfo: HostInfo, port: Int, timeout: Long): Boolean {
return try {
val httpResponse = clipClient.get(timeout = timeout) { urlBuilder ->
urlBuilder.protocol = URLProtocol.HTTP
urlBuilder.port = port
urlBuilder.host = hostInfo.hostAddress
urlBuilder.path("sync", "telnet")
}
logger.info { "httpResponse.status = ${httpResponse.status.value} ${hostInfo.hostAddress}:$port" }

httpResponse.status.value == 200
} catch (e: Exception) {
logger.debug(e) { "telnet $hostInfo fail" }
Expand Down

0 comments on commit ef3e0bc

Please sign in to comment.