() {
+// override fun createIntent(context: Context, input: String): Intent {
+// return QrScanActivity.forScanResultIntent(context)
+// }
+//
+// override fun parseResult(resultCode: Int, intent: Intent?): String {
+// intent?.let {
+// return it.getStringExtra(QrScanActivity.DataKey).toString()
+// }
+// return ""
+// }
+//}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/store/SettingStore.kt b/android/app/src/main/java/com/pushdeer/os/store/SettingStore.kt
new file mode 100644
index 0000000..508ac8c
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/store/SettingStore.kt
@@ -0,0 +1,21 @@
+package com.pushdeer.os.store
+
+import android.content.Context
+import com.wh.common.store.Store
+
+class SettingStore(context:Context) {
+ val store = Store.create(context,"setting")
+
+ var userToken by store.string("user-token","")
+ var deviceName by store.string("device-name","My Dear Deer")
+ var useRecv by store.boolean("use-recv",false) // 启用接收
+ var useSend by store.boolean("use-send",false)
+ var useSendNotification by store.boolean("use-send-notification",false)
+ var notificationPackages by store.stringSet("notification-packages", emptySet())
+ var useSendMissedCall by store.boolean("use-send=missed-call",false)
+ var useSendSMS by store.boolean("use-send-sms",false)
+
+ var showMessageSender by store.boolean("show-message-sender",true)
+ var thisPushSdk by store.string("this-push-sdk","mi-push")
+ var thisDeviceId by store.string("this-device-id","")
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/typeExt/Flow.kt b/android/app/src/main/java/com/pushdeer/os/typeExt/Flow.kt
new file mode 100644
index 0000000..cf17130
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/typeExt/Flow.kt
@@ -0,0 +1,8 @@
+package com.pushdeer.os.typeExt
+
+import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/Item.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/Item.kt
new file mode 100644
index 0000000..d0fa3dd
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/Item.kt
@@ -0,0 +1,146 @@
+package com.pushdeer.os.ui.compose.componment
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.R
+import com.pushdeer.os.ui.theme.MBlue
+import com.pushdeer.os.ui.theme.MainBlue
+
+@ExperimentalMaterialApi
+@Composable
+fun CardItemSingleLineWithIcon(
+ onClick: () -> Unit = {},
+ @DrawableRes resId: Int = R.drawable.iphone2x,
+ text: String = "Easy's iPhone"
+) {
+ Card(
+ onClick = onClick,
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier
+ .border(
+ width = 1.dp,
+ color = MainBlue,
+ shape = RoundedCornerShape(4.dp)
+ ),
+ elevation = 5.dp
+
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp)
+ .padding(start = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = resId),
+ contentDescription = "",
+ colorFilter = ColorFilter.tint(color = MaterialTheme.colors.MBlue),
+ modifier = Modifier.size(28.dp)
+ )
+ Text(
+ text = text,
+ color = MainBlue,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun CardItemMultiLine(
+ onClick: () -> Unit = {},
+ @DrawableRes resId: Int = R.drawable.iphone2x,
+ text: String = "Easy's iPhone"
+) {
+ Card(
+ onClick = onClick,
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier
+// .padding(bottom = 16.dp)
+ .border(
+ width = 1.dp,
+ color = MainBlue,
+ shape = RoundedCornerShape(4.dp)
+ ),
+ elevation = 5.dp
+
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp)
+ .padding(start = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = resId),
+ contentDescription = "",
+ colorFilter = ColorFilter.tint(color = MaterialTheme.colors.MBlue),
+ modifier = Modifier.size(28.dp)
+ )
+ Text(
+ text = text,
+ color = MainBlue,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun CardItemWithContent(onClick: () -> Unit = {}, content: @Composable () -> Unit = {}) {
+ Card(
+ onClick = onClick,
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier
+ .border(
+ width = 1.dp,
+ color = MainBlue,
+ shape = RoundedCornerShape(4.dp)
+ ),
+ content = content,
+ elevation = 5.dp
+ )
+}
+
+@Composable
+fun ListBottomBlankItem() {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.Top
+ ) {
+// Text(text = "End of List",color = Color.Gray)
+ }
+}
+
+@ExperimentalMaterialApi
+@Preview(showBackground = true)
+@Composable
+fun IP() {
+ CardItemWithContent {
+ Text(text = "aaa")
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/KeyItem.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/KeyItem.kt
new file mode 100644
index 0000000..ae2d65f
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/KeyItem.kt
@@ -0,0 +1,134 @@
+package com.pushdeer.os.ui.compose.componment
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.pushdeer.os.R
+import com.pushdeer.os.data.api.data.response.PushKey
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.theme.MBlue
+import com.wh.common.util.TimeUtils
+
+@ExperimentalMaterialApi
+@Composable
+fun KeyItem(key: PushKey, requestHolder: RequestHolder) {
+ CardItemWithContent {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Bottom,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ ) {
+ Row(verticalAlignment = Alignment.Bottom) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_deer_head_with_mail),
+ contentDescription = "",
+ modifier = Modifier.size(36.dp)
+ )
+ Text(
+ text = key.name,
+ fontSize = 16.sp,
+ color = MaterialTheme.colors.MBlue,
+ modifier = Modifier.padding(start = 10.dp)
+ )
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ imageVector = Icons.Default.DateRange,
+ contentDescription = "",
+ tint = Color.Gray,
+ modifier = Modifier
+ .size(20.dp)
+ .padding(end = 4.dp)
+ )
+ Text(
+ text = TimeUtils.getFormattedTime(
+ TimeUtils.utcTS2ms(
+ key.created_at,
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
+ ), "MM/dd HH:mm"
+ ),
+ color = Color.Gray,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ fontSize = 12.sp
+ )
+ }
+ }
+ Text(
+ text = key.key,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = Color.Gray,
+ fontSize = 14.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 14.dp)
+ .border(
+ width = 1.dp,
+ color = Color.Gray,
+ shape = RoundedCornerShape(4.dp)
+ )
+ .padding(horizontal = 14.dp, vertical = 8.dp)
+ )
+// Canvas(modifier = Modifier
+// .fillMaxWidth()
+// .height(16.dp), onDraw = {
+// val linePath = Path()
+// val linePaint = Paint()
+// linePaint.pathEffect = PathEffect.dashPathEffect(FloatArray(10),10F)
+// drawIntoCanvas {
+// it.drawPath(linePath, linePaint)
+// }
+// })
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ OutlinedButton(
+ onClick = { requestHolder.keyRegen(key.id) },
+ colors = ButtonDefaults.outlinedButtonColors(
+ backgroundColor = Color.Transparent,
+ contentColor = MaterialTheme.colors.MBlue
+ ),
+ border = BorderStroke(1.dp, MaterialTheme.colors.MBlue),
+ shape = RoundedCornerShape(6.dp)
+ ) {
+ Text(text = "Reset")
+ }
+ Button(
+ onClick = {
+ requestHolder.copyPlainString(key.key)
+ },
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MaterialTheme.colors.MBlue,
+ contentColor = Color.White
+ ),
+ shape = RoundedCornerShape(6.dp)
+ ) {
+ Text(text = "Copy")
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/MessageItem.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/MessageItem.kt
new file mode 100644
index 0000000..a2a5d1e
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/MessageItem.kt
@@ -0,0 +1,168 @@
+package com.pushdeer.os.ui.compose.componment
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.pushdeer.os.R
+import com.pushdeer.os.data.database.entity.MessageEntity
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.theme.MBlue
+import com.pushdeer.os.util.CurrentTimeUtil
+import com.pushdeer.os.values.ConstValues
+
+
+@ExperimentalMaterialApi
+@Composable
+fun PlainTextMessageItem(message: MessageEntity) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colors.surface)
+ ) {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_deer_head_with_mail),
+ contentDescription = "",
+ modifier = Modifier.size(40.dp)
+ )
+ Text(
+ text = "${message.text}·${
+ CurrentTimeUtil.resolveUTCTimeAndNow(
+ message.created_at,
+ System.currentTimeMillis()
+ )
+ }"
+ )
+ }
+
+ CardItemWithContent() {
+ Text(
+ text = message.desp,
+ overflow = TextOverflow.Visible,
+ lineHeight = 24.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ )
+ }
+ }
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun ImageMessageItem(message: MessageEntity) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(4.dp))
+ .background (color = MaterialTheme.colors.surface)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = ConstValues.MainPageSidePadding)
+ .padding(bottom = 12.dp),
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_deer_head_with_mail),
+ contentDescription = "",
+ modifier = Modifier.size(40.dp)
+ )
+ Text(
+ text = "${message.text}·${
+ CurrentTimeUtil.resolveUTCTimeAndNow(
+ message.created_at,
+ System.currentTimeMillis()
+ )
+ }"
+ )
+ }
+ Card(modifier = Modifier.fillMaxWidth(), onClick = {}) {
+ Image(
+ painter = painterResource(id = R.drawable.logo_com_x2),
+ contentDescription = "",
+ contentScale = ContentScale.FillWidth
+ )
+ }
+ }
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(4.dp))
+ .background(color = MaterialTheme.colors.surface)
+ ) {
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp), verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box {
+ Image(
+ painter = painterResource(id = R.drawable.ic_deer_head_with_mail),
+ contentDescription = "",
+ modifier = Modifier.size(40.dp)
+ )
+ Icon(
+ painter = painterResource(id = R.drawable.ic_markdown),
+ contentDescription = "",
+ tint = MaterialTheme.colors.MBlue,
+ modifier = Modifier
+ .size(20.dp)
+ .align(alignment = Alignment.BottomCenter)
+ )
+ }
+
+ Text(
+ text = "${message.text}·${
+ CurrentTimeUtil.resolveUTCTimeAndNow(
+ message.created_at,
+ System.currentTimeMillis()
+ )
+ }"
+ )
+ }
+
+ CardItemWithContent {
+ AndroidView(
+ factory = { ctx ->
+ android.widget.TextView(ctx).apply {
+ this.post {
+// requestHolder.markdown.configuration().theme().
+ requestHolder.markdown.setMarkdown(this, message.desp)
+ }
+ }
+
+ }, modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SettingItem.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SettingItem.kt
new file mode 100644
index 0000000..05a8498
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SettingItem.kt
@@ -0,0 +1,54 @@
+package com.pushdeer.os.ui.compose.componment
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.pushdeer.os.ui.theme.MBlue
+
+@ExperimentalMaterialApi
+@Composable
+fun SettingItem(text: String, buttonString: String, onClick: () -> Unit) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ ) {
+ CardItemWithContent() {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+// .padding(vertical = 10.dp)
+ .padding(start = 20.dp, end = 16.dp, top = 10.dp, bottom = 10.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = text,
+ fontSize = 16.sp,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ )
+ Button(
+ onClick = onClick,
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MaterialTheme.colors.MBlue,
+ contentColor = Color.White
+ )
+ ) {
+ Text(
+ text = buttonString,
+ fontSize = 15.sp
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SwipeToDismissItem.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SwipeToDismissItem.kt
new file mode 100644
index 0000000..6e910d0
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/componment/SwipeToDismissItem.kt
@@ -0,0 +1,80 @@
+package com.pushdeer.os.ui.compose.componment
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.values.ConstValues
+
+@ExperimentalMaterialApi
+@Composable
+fun SwipeToDismissItem(
+ onDismiss: () -> Unit,
+ sidePadding: Boolean = false,
+ content: @Composable RowScope.() -> Unit
+) {
+ val dismissState = rememberDismissState()
+ if (dismissState.isDismissed(DismissDirection.EndToStart)) {
+ onDismiss()
+ }
+ Column(modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)) {
+ SwipeToDismiss(
+ state = dismissState,
+ background = {
+ val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
+
+ val color by animateColorAsState(
+ when (dismissState.targetValue) {
+ DismissValue.DismissedToEnd -> Color.Green
+ DismissValue.DismissedToStart -> Color.Red
+ else -> Color.Gray
+ }
+ )
+
+ val alignment = when (direction) {
+ DismissDirection.StartToEnd -> Alignment.CenterStart
+ DismissDirection.EndToStart -> Alignment.CenterEnd
+ }
+
+ val icon = when (direction) {
+ DismissDirection.StartToEnd -> Icons.Default.Done
+ DismissDirection.EndToStart -> Icons.Default.Delete
+ }
+
+ Box(
+ contentAlignment = alignment,
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(4.dp))
+ .background(color)
+ .padding(end = 32.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = "",
+// tint = Color.Red
+ )
+ }
+ },
+ directions = setOf(DismissDirection.EndToStart, DismissDirection.EndToStart),
+ dismissThresholds = { direction ->
+ FractionalThreshold(if (direction == DismissDirection.EndToStart) 0.45f else 0.57f)
+ },
+ dismissContent = content,
+ modifier = Modifier.padding(horizontal = if (sidePadding) ConstValues.MainPageSidePadding else 0.dp)
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LogDaoPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LogDaoPage.kt
new file mode 100644
index 0000000..9fba3f5
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LogDaoPage.kt
@@ -0,0 +1,75 @@
+package com.pushdeer.os.ui.compose.page
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+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.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.R
+import com.pushdeer.os.data.database.entity.LogDog
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.compose.page.main.MainPageFrame
+
+@ExperimentalMaterialApi
+@Composable
+fun LogDaoPage(requestHolder: RequestHolder) {
+ MainPageFrame(
+ titleStringId = R.string.global_logdog,
+ sideIcon = Icons.Default.Delete,
+ onSideIconClick = {
+ requestHolder.clearLogDog()
+ }) {
+ val logDogs by requestHolder.logDogViewModel.all.collectAsState(initial = emptyList())
+
+ Scaffold(modifier = Modifier.fillMaxSize()) {
+ if (logDogs.isEmpty()) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.logo_half),
+ contentDescription = "",
+ )
+ }
+ } else {
+ LazyColumn(
+ content = {
+ items(logDogs, key = { item: LogDog -> item.id }) { logDog: LogDog ->
+ Card(
+ onClick = { /*TODO*/ },
+ elevation = 5.dp,
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(text = logDog.toString())
+ }
+ }
+ }
+ },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LoginPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LoginPage.kt
new file mode 100644
index 0000000..187047f
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/LoginPage.kt
@@ -0,0 +1,76 @@
+package com.pushdeer.os.ui.compose.page
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.R
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.theme.MainBlue
+import com.pushdeer.os.ui.theme.MainGreen
+
+@ExperimentalMaterialApi
+@Composable
+fun LoginPage(requestHolder: RequestHolder) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = painterResource(R.drawable.logo_com_x2),
+ contentDescription = "big push deer logo"
+ )
+ Card(
+ onClick = { /*TODO*/ },
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier
+ .padding(bottom = 16.dp)
+ .border(
+ width = 1.dp,
+ color = MainBlue,
+ shape = RoundedCornerShape(4.dp)
+ )
+ ) {
+ Text(
+ text = "Sign in with Apple",
+ color = MainBlue,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ .fillMaxWidth(0.6F)
+
+ )
+ }
+ Card(
+ onClick = {},
+ shape = RoundedCornerShape(4.dp),
+ modifier = Modifier.border(
+ width = 1.dp,
+ color = MainGreen,
+ shape = RoundedCornerShape(4.dp)
+ )
+ ) {
+ Text(
+ text = "Sign in with WeChat",
+ color = MainGreen,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .padding(vertical = 16.dp)
+ .fillMaxWidth(0.6F)
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/DeviceListPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/DeviceListPage.kt
new file mode 100644
index 0000000..5e1f9a7
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/DeviceListPage.kt
@@ -0,0 +1,109 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import android.os.Build
+import android.text.TextUtils
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Card
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.R
+import com.pushdeer.os.data.api.data.request.DeviceInfo
+import com.pushdeer.os.data.api.data.response.UserInfo
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.compose.componment.CardItemSingleLineWithIcon
+import com.pushdeer.os.ui.compose.componment.ListBottomBlankItem
+import com.pushdeer.os.ui.compose.componment.SwipeToDismissItem
+import com.pushdeer.os.ui.navigation.Page
+import com.pushdeer.os.util.SystemUtil
+
+
+@ExperimentalMaterialApi
+@Composable
+fun DeviceListPage(requestHolder: RequestHolder) {
+ MainPageFrame(
+ titleStringId = Page.Devices.labelStringId,
+ onSideIconClick = {
+ requestHolder.deviceReg(
+ deviceInfo = DeviceInfo().apply {
+ name = System.currentTimeMillis().toString()
+ device_id = "sdsdf"
+ is_clip = 0
+ }
+ )
+ }
+ ) {
+ val state = rememberLazyListState()
+ LazyColumn(state = state) {
+ items(
+ items = requestHolder.pushDeerViewModel.deviceList,
+ key = { item: DeviceInfo -> item.id }) { deviceInfo: DeviceInfo ->
+ SwipeToDismissItem(onDismiss = { requestHolder.deviceRemove(deviceInfo) }) {
+ CardItemSingleLineWithIcon(
+ onClick = {},
+ resId = R.drawable.ipad_landscape2x,
+ text = if (deviceInfo.device_id == requestHolder.settingStore.thisDeviceId) "${deviceInfo.name} (this device)" else deviceInfo.name
+ )
+ }
+ }
+ item {
+ ListBottomBlankItem()
+ }
+ }
+ }
+}
+
+@ExperimentalMaterialApi
+@Composable
+fun DeviceListPage(userInfo: UserInfo, token: String, regid: String) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 8.dp)
+ .padding(top = 8.dp)
+ ) {
+ item {
+ Card(elevation = 5.dp, onClick = {}, modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) {
+ Text(text = "当前版本 Android ${SystemUtil.getSystemVersion()}")
+ Text(text = "本机品牌 ${SystemUtil.getDeviceBrand()}")
+ Text(text = "本机型号 ${SystemUtil.getDeviceModel()}")
+ Text(text = "产品名称 ${Build.PRODUCT}")
+ MiuiVersion()
+ }
+ }
+ }
+ item {
+ Card(elevation = 5.dp, onClick = {}, modifier = Modifier.fillMaxWidth()) {
+ Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) {
+ Text(text = "id ${userInfo.id}")
+ Text(text = "name ${userInfo.name}")
+ Text(text = "email ${userInfo.email}")
+ Text(text = "app_id ${userInfo.app_id}")
+ Text(text = "wechat_id ${userInfo.wechat_id}")
+ Text(text = "created_at ${userInfo.created_at}")
+ Text(text = "updated_at ${userInfo.updated_at}")
+ Text(text = "level ${userInfo.level}")
+ Text(text = "token $token")
+ Text(text = "regid $regid")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MiuiVersion() {
+ val v = SystemUtil.getMiuiVersion()
+ if (!TextUtils.isEmpty(v)) {
+ Text(text = "Miui 版本 ${v}")
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/KeyListPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/KeyListPage.kt
new file mode 100644
index 0000000..39429a1
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/KeyListPage.kt
@@ -0,0 +1,37 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.pushdeer.os.data.api.data.response.PushKey
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.compose.componment.KeyItem
+import com.pushdeer.os.ui.compose.componment.ListBottomBlankItem
+import com.pushdeer.os.ui.compose.componment.SwipeToDismissItem
+import com.pushdeer.os.ui.navigation.Page
+
+@ExperimentalMaterialApi
+@Composable
+fun KeyListPage(requestHolder: RequestHolder) {
+ MainPageFrame(
+ titleStringId = Page.Keys.labelStringId,
+ onSideIconClick = { requestHolder.keyGen() }
+ ) {
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
+ items(
+ requestHolder.pushDeerViewModel.keyList,
+ key = { item: PushKey -> item.id }) { pushKey: PushKey ->
+ SwipeToDismissItem(onDismiss = { requestHolder.keyRemove(pushKey) }
+ ) {
+ KeyItem(key = pushKey, requestHolder = requestHolder)
+ }
+ }
+ item {
+ ListBottomBlankItem()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPage.kt
new file mode 100644
index 0000000..50c1f5d
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPage.kt
@@ -0,0 +1,109 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.pushdeer.os.R
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.navigation.Page
+import com.pushdeer.os.ui.navigation.pageList
+import com.pushdeer.os.ui.theme.mainBottomBtn
+
+@ExperimentalAnimationApi
+@ExperimentalMaterialApi
+@Composable
+fun MainPage(requestHolder: RequestHolder) {
+
+ var titleStringId by remember {
+ mutableStateOf(Page.Devices.labelStringId)
+ }
+ val navController = rememberNavController()
+ Scaffold(
+ scaffoldState = rememberScaffoldState(),
+ bottomBar = {
+ BottomNavigation(backgroundColor = MaterialTheme.colors.surface) {
+ val navBackStackEntry by requestHolder.globalNavController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+ pageList.forEach { page ->
+ val selected = page.labelStringId == titleStringId
+ BottomNavigationItem(
+ icon = {
+ Icon(
+ painter = painterResource(id = page.id),
+ contentDescription = stringResource(id = titleStringId),
+ modifier = Modifier.size(23.dp),
+ tint = MaterialTheme.colors.mainBottomBtn(selected = selected)
+ )
+ },
+ label = {
+ Text(
+ stringResource(id = page.labelStringId),
+ color = MaterialTheme.colors.mainBottomBtn(selected = selected)
+ )
+ },
+ selected = currentDestination?.hierarchy?.any { it.route == page.route } == true,
+ onClick = {
+ navController.navigate(page.route) {
+ titleStringId = page.labelStringId
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ )
+ }
+ }
+ },
+ snackbarHost = { },
+ content = {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_logo_svg_1),
+ contentDescription = "",
+ alpha = 0.05F, colorFilter = ColorFilter.tint(color = Color.Gray),
+ modifier = Modifier
+ .align(alignment = Alignment.BottomCenter)
+ .size(500.dp)
+ .offset(x = (-180).dp, y = 140.dp),
+ )
+ NavHost(
+ navController = navController,
+ startDestination = Page.Devices.route,
+ ) {
+ composable(Page.Devices.route) {
+ DeviceListPage(requestHolder = requestHolder)
+ }
+ composable(Page.Keys.route) {
+ KeyListPage(requestHolder = requestHolder)
+ }
+ composable(Page.Messages.route) {
+ MessageListPage(requestHolder = requestHolder)
+ }
+ composable(Page.Settings.route) {
+ SettingPage(requestHolder = requestHolder)
+ }
+ }
+ }
+ },
+ )
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPageFrame.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPageFrame.kt
new file mode 100644
index 0000000..40af90b
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MainPageFrame.kt
@@ -0,0 +1,70 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.pushdeer.os.values.ConstValues
+
+@Composable
+fun MainPageFrame(
+ titleStringId: Int,
+ showSideIcon: Boolean = true,
+ sideIcon: ImageVector = Icons.Default.Add,
+ onSideIconClick: () -> Unit = {},
+ sidePadding: Boolean = true,
+ content: @Composable BoxScope.() -> Unit
+) {
+// val sizePaddingValue = if (sidePadding) PaddingValues()
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+// .padding(horizontal = if (sidePadding) 37.dp else 0.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp, top = 27.dp)
+ .padding(horizontal = ConstValues.MainPageSidePadding)
+ .background(color = Color.Transparent),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(id = titleStringId), fontSize = 32.sp,
+ fontWeight = FontWeight.W400,
+ )
+// if (showSideIcon) {
+ IconButton(onClick = onSideIconClick,modifier = Modifier.alpha(if (showSideIcon)1F else 0F)) {
+ Icon(
+ imageVector = sideIcon,
+ contentDescription = "",
+ tint = Color.LightGray
+ )
+ }
+// }
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = if (sidePadding) ConstValues.MainPageSidePadding else 0.dp)
+ ) {
+ content()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MessageListPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MessageListPage.kt
new file mode 100644
index 0000000..3f3658c
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/MessageListPage.kt
@@ -0,0 +1,113 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.pushdeer.os.data.database.entity.MessageEntity
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.compose.componment.*
+import com.pushdeer.os.ui.navigation.Page
+import com.pushdeer.os.ui.theme.MBlue
+import com.pushdeer.os.values.ConstValues
+
+
+@ExperimentalAnimationApi
+@ExperimentalMaterialApi
+@Composable
+fun MessageListPage(requestHolder: RequestHolder) {
+ MainPageFrame(
+ titleStringId = Page.Messages.labelStringId,
+ sideIcon = if (requestHolder.uiViewModel.showMessageSender) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
+ onSideIconClick = { requestHolder.toggleMessageSender() },
+ sidePadding = false
+ ) {
+ var s by remember {
+ mutableStateOf("")
+ }
+ val messageList by requestHolder.messageViewModel.all.collectAsState(initial = emptyList())
+
+ LazyColumn(content = {
+ item {
+ AnimatedVisibility(
+ visible = requestHolder.uiViewModel.showMessageSender,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ .padding(horizontal = ConstValues.MainPageSidePadding)
+
+ ) {
+ OutlinedTextField(
+ value = s,
+ onValueChange = { s = it },
+ shape = RoundedCornerShape(4.dp),
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ backgroundColor = Color.Transparent,
+ focusedBorderColor = MaterialTheme.colors.MBlue,
+ focusedLabelColor = Color.Transparent,
+ unfocusedBorderColor = MaterialTheme.colors.MBlue,
+ unfocusedLabelColor = Color.Transparent,
+ ),
+ maxLines = 5,
+ singleLine = false,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ )
+ Button(
+ onClick = {
+ requestHolder.messagePushTest(s)
+ },
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MaterialTheme.colors.MBlue,
+ contentColor = Color.White
+ ),
+ ) {
+ Text(text = "Send")
+ }
+ }
+ }
+ }
+ items(
+ items = messageList,
+ key = { item: MessageEntity -> item.id }) { message: MessageEntity ->
+ SwipeToDismissItem(
+ onDismiss = {
+ requestHolder.messageRemove(message.toMessage(), onDone = {
+ requestHolder.messageViewModel.delete(message)
+ })
+ },
+// sidePadding = false
+ sidePadding = message.type != "image"
+ ) {
+// ImageMessageItem(message)
+ when (message.type) {
+ "markdown" -> MarkdownMessageItem(message, requestHolder)
+ "text" -> PlainTextMessageItem(message)
+ "image" -> ImageMessageItem(message)
+ }
+ }
+ }
+
+ item {
+ ListBottomBlankItem()
+ }
+ })
+
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/SettingPage.kt b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/SettingPage.kt
new file mode 100644
index 0000000..77baab1
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/compose/page/main/SettingPage.kt
@@ -0,0 +1,72 @@
+package com.pushdeer.os.ui.compose.page.main
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.pushdeer.os.holder.RequestHolder
+import com.pushdeer.os.ui.compose.componment.SettingItem
+import com.pushdeer.os.ui.navigation.Page
+
+@ExperimentalMaterialApi
+@Composable
+fun SettingPage(requestHolder: RequestHolder) {
+ MainPageFrame(
+ titleStringId = Page.Settings.labelStringId,
+ showSideIcon = false
+ ) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ SettingItem(
+ text = "Hi ${requestHolder.pushDeerViewModel.userInfo.name} !",
+ buttonString = "Logout"
+ ) {
+ requestHolder.settingStore.userToken = ""
+ // logout 操作:
+ // 从服务器删除本设备
+ // 删除保存的 token
+ }
+ }
+ item {
+ SettingItem(
+ text = "Customize Server",
+ buttonString = "Scan QR"
+ ) {
+ requestHolder.startQrScanActivity()
+ }
+ }
+ item {
+ SettingItem(
+ text = "Do you like PushDeer ?",
+ buttonString = "Like"
+ ) {
+ }
+ }
+
+ item {
+ SettingItem(
+ text = "LogDog",
+ buttonString = "Open"
+ ) {
+ requestHolder.globalNavController.navigate("logdog")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TogglePreferenceItem(label: String) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Text(text = label)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/navigation/Page.kt b/android/app/src/main/java/com/pushdeer/os/ui/navigation/Page.kt
new file mode 100644
index 0000000..b30da0f
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/navigation/Page.kt
@@ -0,0 +1,16 @@
+package com.pushdeer.os.ui.navigation
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.pushdeer.os.R
+
+sealed class Page(val route: String, @StringRes val labelStringId:Int, @DrawableRes val id : Int) {
+ object Devices : Page("device",R.string.main_device, R.drawable.ipad_and_iphon2x)
+ object Keys:Page("key",R.string.main_key,R.drawable.key2x)
+ object Messages:Page("message",R.string.main_message,R.drawable.message2x)
+ object Settings:Page("setting",R.string.main_setting,R.drawable.gearshape2x)
+}
+
+val pageList = listOf(
+ Page.Devices,Page.Keys,Page.Messages,Page.Settings
+)
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/theme/Color.kt b/android/app/src/main/java/com/pushdeer/os/ui/theme/Color.kt
new file mode 100644
index 0000000..b55e96a
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/theme/Color.kt
@@ -0,0 +1,12 @@
+package com.pushdeer.os.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
+
+val MainBlue = Color(0xFF3B4789)
+val MainGreen = Color(0xFF296C05)
+val MainBottomBtn = Color(0xFF8E8E8E)
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/theme/Shape.kt b/android/app/src/main/java/com/pushdeer/os/ui/theme/Shape.kt
new file mode 100644
index 0000000..ba2e23b
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.pushdeer.os.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/theme/Theme.kt b/android/app/src/main/java/com/pushdeer/os/ui/theme/Theme.kt
new file mode 100644
index 0000000..659c72e
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/theme/Theme.kt
@@ -0,0 +1,71 @@
+package com.pushdeer.os.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.Colors
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+private val DarkColorPalette = darkColors(
+ primary = Purple200,
+ primaryVariant = Purple700,
+ secondary = Teal200
+)
+
+private val LightColorPalette = lightColors(
+ primary = Purple500,
+ primaryVariant = Purple700,
+ secondary = Teal200
+
+ /* Other default colors to override
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+ */
+)
+
+val Colors.MBlue: Color
+ @Composable get() = if (isLight) MainBlue else MainBlue
+
+val Colors.MBottomBtn: Color
+ @Composable get() = MainBottomBtn
+
+val Colors.MBottomBarBgc: Color
+ @Composable get() = if (isLight) Color.White else Color.White
+
+//val Colors.thingNormal: Color
+// @Composable get() = if (isLight) Green400 else Green700
+//
+//val Colors.thingLost: Color
+// @Composable get() = if (isLight) Red400 else Red700
+//
+//val Colors.thingEnd: Color
+// @Composable get() = if (isLight) Color.LightGray else Color.DarkGray
+
+@Composable
+fun Colors.mainBottomBtn(selected: Boolean): Color {
+ return if (selected) MBlue else MBottomBtn
+}
+
+@Composable
+fun PushDeerTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ LightColorPalette
+
+ MaterialTheme(
+ colors = colors,
+ typography = Typography,
+ shapes = Shapes,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/ui/theme/Type.kt b/android/app/src/main/java/com/pushdeer/os/ui/theme/Type.kt
new file mode 100644
index 0000000..14ded5a
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.pushdeer.os.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/util/CurrentTimeUtil.kt b/android/app/src/main/java/com/pushdeer/os/util/CurrentTimeUtil.kt
new file mode 100644
index 0000000..cc90e26
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/util/CurrentTimeUtil.kt
@@ -0,0 +1,47 @@
+package com.pushdeer.os.util
+
+import android.annotation.SuppressLint
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.math.abs
+
+object CurrentTimeUtil {
+ @SuppressLint("SimpleDateFormat")
+ val ymdFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+
+ @SuppressLint("SimpleDateFormat")
+ val ymdthmssFmt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
+
+ private val currentTimeZone: TimeZone = TimeZone.getDefault()
+
+ private val tz2utcMSOffset = currentTimeZone.getOffset(System.currentTimeMillis())
+
+ fun utcTS2ms(utcTS: String): Long {
+ val calendar = Calendar.getInstance(currentTimeZone)
+ val date = ymdthmssFmt.parse(utcTS)!!
+ calendar.time = date
+ return calendar.time.time + tz2utcMSOffset
+ }
+
+ fun msTSDis(now: Long, then: Long): String {
+ val dis = abs(now - then)
+ return when {
+ dis < 60_000 -> {
+ (dis / 1_000).toString() + "s ago"
+ }
+ dis < 3_600_000 -> {
+ (dis / 60_000).toString() + "min ago"
+ }
+ dis < 86_400_000 -> {
+ (dis / 3_600_000).toString() + "h ago"
+ }
+ else -> {
+ ymdFmt.format(Date(then))
+ }
+ }
+ }
+
+ fun resolveUTCTimeAndNow(utcTS: String, now: Long): String {
+ return msTSDis(now, utcTS2ms(utcTS))
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/util/StatusBarUtils.java b/android/app/src/main/java/com/pushdeer/os/util/StatusBarUtils.java
new file mode 100644
index 0000000..beb9a56
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/util/StatusBarUtils.java
@@ -0,0 +1,329 @@
+package com.pushdeer.os.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Created zmm
+ *
+ * Functions: 设置状态栏透明并改变状态栏颜色为深色工具类
+ */
+
+public class StatusBarUtils {
+
+ public static int getStatusBarHeight(Resources resources,Context context) {
+ int statusBarHeight = 0;
+ int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ statusBarHeight = resources.getDimensionPixelSize(resourceId);
+ }
+ Log.d("CompatToolbar", "状态栏高度:" + px2dp(statusBarHeight,context) + "dp");
+ return statusBarHeight;
+ }
+
+ public static float px2dp(float pxVal, Context context) {
+ final float scale = context.getResources().getDisplayMetrics().density;
+ return (pxVal / scale);
+ }
+
+
+ public static void setStatusBarFontIconDark(Window window,boolean dark) {
+ // 小米MIUI
+ try {
+ Class clazz = window.getClass();
+ Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
+ Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
+ int darkModeFlag = field.getInt(layoutParams);
+ Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
+ if (dark) { //状态栏亮色且黑色字体
+ extraFlagField.invoke(window, darkModeFlag, darkModeFlag);
+ } else { //清除黑色字体
+ extraFlagField.invoke(window, 0, darkModeFlag);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ // 魅族FlymeUI
+ try {
+ WindowManager.LayoutParams lp = window.getAttributes();
+ Field darkFlag = WindowManager.LayoutParams.class.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
+ Field meizuFlags = WindowManager.LayoutParams.class.getDeclaredField("meizuFlags");
+ darkFlag.setAccessible(true);
+ meizuFlags.setAccessible(true);
+ int bit = darkFlag.getInt(null);
+ int value = meizuFlags.getInt(lp);
+ if (dark) {
+ value |= bit;
+ } else {
+ value &= ~bit;
+ }
+ meizuFlags.setInt(lp, value);
+ window.setAttributes(lp);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ // android6.0+系统
+ // 这个设置和在xml的style文件中用这个- true
属性是一样的
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (dark) {
+ window.getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+ }
+
+
+ /**
+ * 设置魅族手机状态栏图标颜色风格
+ *
+ * 可以用来判断是否为Flyme用户
+ *
+ * @param window 需要设置的窗口
+ * @param dark 是否把状态栏字体及图标颜色设置为深色
+ * @return boolean 成功执行返回true
+ */
+
+ public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) {
+
+ boolean result = false;
+
+ if (window != null) {
+
+ try {
+
+ WindowManager.LayoutParams lp = window.getAttributes();
+
+ Field darkFlag = WindowManager.LayoutParams.class
+
+ .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
+
+ Field meizuFlags = WindowManager.LayoutParams.class
+
+ .getDeclaredField("meizuFlags");
+
+ darkFlag.setAccessible(true);
+
+ meizuFlags.setAccessible(true);
+
+ int bit = darkFlag.getInt(null);
+
+ int value = meizuFlags.getInt(lp);
+
+ if (dark) {
+
+ value |= bit;
+
+ } else {
+
+ value &= ~bit;
+
+ }
+
+ meizuFlags.setInt(lp, value);
+
+ window.setAttributes(lp);
+
+ result = true;
+
+ } catch (Exception e) {
+
+ }
+
+ }
+
+ return result;
+
+ }
+
+ /**
+ * 设置小米手机状态栏字体图标颜色模式,需要MIUIV6以上
+ *
+ * @param window 需要设置的窗口
+ * @param dark 是否把状态栏字体及图标颜色设置为深色
+ * @return boolean 成功执行返回true
+ */
+
+ public static boolean MIUISetStatusBarLightMode(Window window, boolean dark) {
+
+ boolean result = false;
+
+ if (window != null) {
+
+ Class clazz = window.getClass();
+
+ try {
+
+ int darkModeFlag = 0;
+
+ Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
+
+ Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
+
+ darkModeFlag = field.getInt(layoutParams);
+
+ Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
+
+ if (dark) {//状态栏透明且黑色字体
+
+ extraFlagField.invoke(window, darkModeFlag, darkModeFlag);
+
+ } else {//清除黑色字体
+
+ extraFlagField.invoke(window, 0, darkModeFlag);
+
+ }
+
+ result = true;
+
+ } catch (Exception e) {
+
+ }
+
+ }
+
+ return result;
+
+ }
+
+ /**
+ * 在不知道手机系统的情况下尝试设置状态栏字体模式为深色
+ *
+ * 也可以根据此方法判断手机系统类型
+ *
+ * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
+ *
+ * @param activity
+ * @return 1:MIUUI 2:Flyme 3:android6.0 0:设置失败
+ */
+
+ public static void statusBarLightMode(Activity activity) {
+
+ int result = 0;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+
+ if (MIUISetStatusBarLightMode(activity.getWindow(), true)) {
+
+//result = 1;
+
+ StatusBarLightMode(activity, 1);
+
+ } else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) {
+
+//result = 2;
+
+ StatusBarLightMode(activity, 2);
+
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+
+//activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+
+//result = 3;
+
+ StatusBarLightMode(activity, 3);
+
+ }
+
+ }
+
+ }
+
+ /**
+ * 已知系统类型时,设置状态栏字体图标为深色。
+ *
+ * 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
+ *
+ * @param activity
+ * @param type 1:MIUUI 2:Flyme 3:android6.0
+ */
+
+ public static void StatusBarLightMode(Activity activity, int type) {
+
+ if (type == 1) {
+
+ MIUISetStatusBarLightMode(activity.getWindow(), true);
+
+ } else if (type == 2) {
+
+ FlymeSetStatusBarLightMode(activity.getWindow(), true);
+
+ } else if (type == 3) {
+
+ Window window = activity.getWindow();
+
+ window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+
+ }
+
+ }
+
+ /**
+ * 已知系统类型时,清除MIUI或flyme或6.0以上版本状态栏字体深色模式
+ *
+ * @param activity
+ * @param type 1:MIUUI 2:Flyme 3:android6.0
+ */
+
+ public static void StatusBarDarkMode(Activity activity, int type) {
+
+ if (type == 1) {
+
+ MIUISetStatusBarLightMode(activity.getWindow(), false);
+
+ } else if (type == 2) {
+
+ FlymeSetStatusBarLightMode(activity.getWindow(), false);
+
+ } else if (type == 3) {
+
+ activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+
+ }
+
+ }
+
+ /**
+ * 状态栏背景透明
+ *
+ * @param activity
+ */
+
+ public static void StatusBarTransport(Activity activity) {
+
+ Window window = activity.getWindow();
+
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+
+ window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+
+ window.setStatusBarColor(Color.TRANSPARENT);
+
+ window.setNavigationBarColor(Color.TRANSPARENT);
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/util/SystemUtil.java b/android/app/src/main/java/com/pushdeer/os/util/SystemUtil.java
new file mode 100644
index 0000000..05d8aab
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/util/SystemUtil.java
@@ -0,0 +1,97 @@
+package com.pushdeer.os.util;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Locale;
+
+public class SystemUtil {
+
+ /**
+ * 获取当前手机系统语言。
+ *
+ * @return 返回当前系统语言。例如:当前设置的是“中文-中国”,则返回“zh-CN”
+ */
+ public static String getSystemLanguage() {
+ return Locale.getDefault().getLanguage();
+ }
+
+ /**
+ * 获取当前系统上的语言列表(Locale列表)
+ *
+ * @return 语言列表
+ */
+ public static Locale[] getSystemLanguageList() {
+ return Locale.getAvailableLocales();
+ }
+
+ /**
+ * 获取当前手机系统版本号
+ *
+ * @return 系统版本号
+ */
+ public static String getSystemVersion() {
+ return android.os.Build.VERSION.RELEASE;
+ }
+
+ /**
+ * 获取手机型号
+ *
+ * @return 手机型号
+ */
+ public static String getDeviceModel() {
+ return android.os.Build.MODEL;
+ }
+
+ /**
+ * 获取手机厂商
+ *
+ * @return 手机厂商
+ */
+ public static String getDeviceBrand() {
+ return android.os.Build.BRAND;
+ }
+
+ public static String getSystemProperty(String propName){
+ String line;
+ BufferedReader input = null;
+ try
+ {
+ Process p = Runtime.getRuntime().exec("getprop " + propName);
+ input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
+ line = input.readLine();
+ input.close();
+ }
+ catch (IOException ex)
+ {
+ Log.e("WH_", "Unable to read sysprop " + propName, ex);
+ return null;
+ }
+ finally
+ {
+ if(input != null)
+ {
+ try
+ {
+ input.close();
+ }
+ catch (IOException e)
+ {
+ Log.e("WH_", "Exception while closing InputStream", e);
+ }
+ }
+ }
+ return line;
+ }
+
+ public static String getMiuiVersion(){
+ return getSystemProperty("ro.miui.ui.version.name");
+ }
+
+ public static boolean isMiui(){
+ return !TextUtils.isEmpty(getMiuiVersion());
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/values/ConstValues.kt b/android/app/src/main/java/com/pushdeer/os/values/ConstValues.kt
new file mode 100644
index 0000000..33fc4fe
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/values/ConstValues.kt
@@ -0,0 +1,12 @@
+package com.pushdeer.os.values
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
+
+object ConstValues {
+ val MainPageSidePadding = 37.dp
+ val MainPageSidePaddings = PaddingValues(horizontal = 37.dp)
+
+ val bigRoundCorner = RoundedCornerShape(8.dp)
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/viewmodel/LogDogViewModel.kt b/android/app/src/main/java/com/pushdeer/os/viewmodel/LogDogViewModel.kt
new file mode 100644
index 0000000..c373db0
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/viewmodel/LogDogViewModel.kt
@@ -0,0 +1,12 @@
+package com.pushdeer.os.viewmodel
+
+import androidx.lifecycle.ViewModel
+import com.pushdeer.os.data.repository.LogDogRepository
+
+class LogDogViewModel(private val logDogRepository: LogDogRepository): ViewModel() {
+ val all = logDogRepository.all
+
+ suspend fun clear(){
+ logDogRepository.clear()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/viewmodel/MessageViewModel.kt b/android/app/src/main/java/com/pushdeer/os/viewmodel/MessageViewModel.kt
new file mode 100644
index 0000000..987674e
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/viewmodel/MessageViewModel.kt
@@ -0,0 +1,37 @@
+package com.pushdeer.os.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.pushdeer.os.data.api.PushDeerApi
+import com.pushdeer.os.data.api.data.response.Message
+import com.pushdeer.os.data.database.entity.MessageEntity
+import com.pushdeer.os.data.repository.MessageRepository
+import com.pushdeer.os.store.SettingStore
+import kotlinx.coroutines.launch
+
+
+class MessageViewModel(
+ private val messageRepository: MessageRepository,
+ private val settingStore: SettingStore,
+ private val pushDeerService: PushDeerApi
+) : ViewModel() {
+ val all = messageRepository.all
+
+ fun insert(vararg message: Message) {
+ viewModelScope.launch {
+ messageRepository.insert(*message)
+ }
+ }
+
+ fun delete(vararg message: Message) {
+ viewModelScope.launch {
+ messageRepository.delete(*message)
+ }
+ }
+
+ fun delete(vararg messageEntity: MessageEntity){
+ viewModelScope.launch {
+ messageRepository.delete(*messageEntity)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/viewmodel/PushDeerViewModel.kt b/android/app/src/main/java/com/pushdeer/os/viewmodel/PushDeerViewModel.kt
new file mode 100644
index 0000000..8645b01
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/viewmodel/PushDeerViewModel.kt
@@ -0,0 +1,236 @@
+package com.pushdeer.os.viewmodel
+
+import android.util.Log
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import com.pushdeer.os.data.api.PushDeerApi
+import com.pushdeer.os.data.api.data.request.DeviceInfo
+import com.pushdeer.os.data.api.data.response.PushKey
+import com.pushdeer.os.data.api.data.response.UserInfo
+import com.pushdeer.os.data.repository.LogDogRepository
+import com.pushdeer.os.data.repository.MessageRepository
+import com.pushdeer.os.store.SettingStore
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class PushDeerViewModel(
+ private val settingStore: SettingStore,
+ private val logDogRepository: LogDogRepository,
+ private val pushDeerService:PushDeerApi,
+ private val messageRepository: MessageRepository
+) : ViewModel() {
+ private val TAG = "WH_"
+
+ var token: String by mutableStateOf(settingStore.userToken)
+ var userInfo: UserInfo by mutableStateOf(UserInfo())
+ var deviceList = mutableStateListOf()
+ val keyList = mutableStateListOf()
+// var messageList = mutableStateListOf()
+
+ suspend fun login(onReturn: (String) -> Unit = {}) {
+ withContext(Dispatchers.IO) {
+ if (token == "") {
+ try {
+ pushDeerService.fakeLogin().let {
+ it.content?.let { tokenOnly ->
+ settingStore.userToken = tokenOnly.token
+ token = tokenOnly.token
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "login: ${e.localizedMessage}")
+ logDogRepository.loge("login", "", e.toString())
+ return@withContext
+ }
+ logDogRepository.logi("login","normally","nothing happened")
+ }
+// Log.d(TAG, "login: token $token")
+ }
+ }
+
+ suspend fun userInfo(onReturn: (UserInfo) -> Unit = {}) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.userInfo(token).let {
+ it.content?.let { ita ->
+ userInfo = ita
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "userInfo: ${e.localizedMessage}")
+ logDogRepository.loge("userInfo", "", e.toString())
+ }
+ }
+ }
+
+ suspend fun deviceReg(deviceInfo: DeviceInfo, onReturn: (DeviceInfo) -> Unit = {}) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.deviceReg(deviceInfo.toRequestMap(token)).let {
+ it.content?.let {
+ deviceList.clear()
+ deviceList.addAll(it.devices)
+ deviceList.filter {
+ it.device_id == deviceInfo.device_id
+ }.let { dis ->
+ if (dis.isNotEmpty()) {
+ withContext(Dispatchers.Default) {
+ onReturn(dis.first())
+ }
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "deviceReg: ${e.localizedMessage}")
+ logDogRepository.loge("deviceReg", "", e.toString())
+ }
+ }
+ }
+
+ suspend fun deviceList(onReturn: (List) -> Unit = {}) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.deviceList(token).let {
+ it.content?.let {
+ deviceList.clear()
+ deviceList.addAll(it.devices)
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "deviceList: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ fun shouldRegDevice(): Boolean {
+// Log.d(TAG, "isDeviceReged: current device id ${settingStore.thisDeviceId}")
+ return deviceList.none { it.device_id == settingStore.thisDeviceId }
+ }
+
+ suspend fun deviceRemove(deviceId: Int) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.deviceRemove(token, deviceId).let {
+ deviceList()
+ Log.d(TAG, "deviceRemove: $it")
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "deviceRemove: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun keyGen() {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.keyGen(token).let {
+ it.content?.let {
+ keyList.clear()
+ keyList.addAll(it.keys)
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "keyGen: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun keyRegen(keyId: String) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.keyRegen(
+ mapOf(
+ "token" to token,
+ "id" to keyId
+ )
+ ).let {
+ keyList()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "keyRegen: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun keyList() {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.keyList(token).let {
+ it.content?.let {
+ keyList.clear()
+ keyList.addAll(it.keys)
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "keyList: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun keyRemove(keyId: String) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.keyRemove(mapOf("token" to token, "id" to keyId)).let {
+ keyList()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "keyRemove: ${e.localizedMessage}")
+ }
+ }
+
+ }
+
+ suspend fun messagePush(
+ text: String,
+ desp: String,
+ type: String = "markdown",
+ pushkey: String
+ ) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.messagePush(
+ mapOf(
+ "pushkey" to pushkey,
+ "text" to text,
+ "desp" to desp,
+ "type" to type
+ )
+ ).let {
+ messageList()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "messagePush: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun messageList() {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.messageList(token).let {
+ it.content?.let {
+ messageRepository.insert(*(it.messages.toTypedArray()))
+ }
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "messageList: ${e.localizedMessage}")
+ }
+ }
+ }
+
+ suspend fun messageRemove(messageId: Int) {
+ withContext(Dispatchers.IO) {
+ try {
+ pushDeerService.messageRemove(token, messageId).let {
+ messageList()
+ }
+ } catch (e: Exception) {
+ Log.d(TAG, "keyRemove: ${e.localizedMessage}")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/pushdeer/os/viewmodel/UiViewModel.kt b/android/app/src/main/java/com/pushdeer/os/viewmodel/UiViewModel.kt
new file mode 100644
index 0000000..b6c459b
--- /dev/null
+++ b/android/app/src/main/java/com/pushdeer/os/viewmodel/UiViewModel.kt
@@ -0,0 +1,11 @@
+package com.pushdeer.os.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import com.pushdeer.os.store.SettingStore
+
+class UiViewModel(private val settingStore: SettingStore): ViewModel() {
+ var showMessageSender by mutableStateOf(settingStore.showMessageSender)
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/deer_placeholder.png b/android/app/src/main/res/drawable/deer_placeholder.png
new file mode 100644
index 0000000..ed7df69
Binary files /dev/null and b/android/app/src/main/res/drawable/deer_placeholder.png differ
diff --git a/android/app/src/main/res/drawable/gearshape2x.png b/android/app/src/main/res/drawable/gearshape2x.png
new file mode 100644
index 0000000..4f91229
Binary files /dev/null and b/android/app/src/main/res/drawable/gearshape2x.png differ
diff --git a/android/app/src/main/res/drawable/ic_deer_head_with_mail.xml b/android/app/src/main/res/drawable/ic_deer_head_with_mail.xml
new file mode 100644
index 0000000..1ee908d
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_deer_head_with_mail.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_logo_svg_1.xml b/android/app/src/main/res/drawable/ic_logo_svg_1.xml
new file mode 100644
index 0000000..e22ce72
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_logo_svg_1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_markdown.xml b/android/app/src/main/res/drawable/ic_markdown.xml
new file mode 100644
index 0000000..054618a
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_markdown.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ipad_and_iphon2x.png b/android/app/src/main/res/drawable/ipad_and_iphon2x.png
new file mode 100644
index 0000000..edc9ba3
Binary files /dev/null and b/android/app/src/main/res/drawable/ipad_and_iphon2x.png differ
diff --git a/android/app/src/main/res/drawable/ipad_landscape2x.png b/android/app/src/main/res/drawable/ipad_landscape2x.png
new file mode 100644
index 0000000..a6c9d0a
Binary files /dev/null and b/android/app/src/main/res/drawable/ipad_landscape2x.png differ
diff --git a/android/app/src/main/res/drawable/iphone2x.png b/android/app/src/main/res/drawable/iphone2x.png
new file mode 100644
index 0000000..bab68a1
Binary files /dev/null and b/android/app/src/main/res/drawable/iphone2x.png differ
diff --git a/android/app/src/main/res/drawable/key2x.png b/android/app/src/main/res/drawable/key2x.png
new file mode 100644
index 0000000..9cc3c7e
Binary files /dev/null and b/android/app/src/main/res/drawable/key2x.png differ
diff --git a/android/app/src/main/res/drawable/logo_com_x2.png b/android/app/src/main/res/drawable/logo_com_x2.png
new file mode 100644
index 0000000..ec37ebc
Binary files /dev/null and b/android/app/src/main/res/drawable/logo_com_x2.png differ
diff --git a/android/app/src/main/res/drawable/logo_half.png b/android/app/src/main/res/drawable/logo_half.png
new file mode 100644
index 0000000..a4118b2
Binary files /dev/null and b/android/app/src/main/res/drawable/logo_half.png differ
diff --git a/android/app/src/main/res/drawable/message2x.png b/android/app/src/main/res/drawable/message2x.png
new file mode 100644
index 0000000..13b1582
Binary files /dev/null and b/android/app/src/main/res/drawable/message2x.png differ
diff --git a/android/app/src/main/res/layout/activity_qr_scan.xml b/android/app/src/main/res/layout/activity_qr_scan.xml
new file mode 100644
index 0000000..9594e0c
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_qr_scan.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..a1d6fd0
--- /dev/null
+++ b/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..8867171
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,14 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+ #3B4789
+ #296C05
+ #8E8E8E
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..65eca7d
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+ PushDeer
+ Device
+ Key
+ Message
+ Setting
+ LogDog
+
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..0767fa7
--- /dev/null
+++ b/android/app/src/main/res/values/themes.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/test/java/com/pushdeer/os/ExampleUnitTest.kt b/android/app/src/test/java/com/pushdeer/os/ExampleUnitTest.kt
new file mode 100644
index 0000000..5867e1a
--- /dev/null
+++ b/android/app/src/test/java/com/pushdeer/os/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.pushdeer.os
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..885ba20
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,18 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext {
+ compose_version = '1.0.5'
+ }
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.0.4"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/android/common/.gitignore b/android/common/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/android/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/common/build.gradle b/android/common/build.gradle
new file mode 100644
index 0000000..ea139a6
--- /dev/null
+++ b/android/common/build.gradle
@@ -0,0 +1,51 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 31
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ compileSdkVersion 31
+ buildToolsVersion '31.0.0'
+ ndkVersion '23.0.7599858'
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.0'
+ implementation 'com.google.android.material:material:1.4.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ // https://mvnrepository.com/artifact/com.alibaba/fastjson
+ implementation 'com.alibaba:fastjson:1.2.78'
+
+ implementation 'androidx.preference:preference-ktx:1.1.1'
+
+ implementation("com.squareup.okhttp3:okhttp:4.9.2")
+ implementation 'com.google.code.gson:gson:2.8.9'
+}
\ No newline at end of file
diff --git a/android/common/consumer-rules.pro b/android/common/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/android/common/proguard-rules.pro b/android/common/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/android/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/common/src/androidTest/java/com/wh/common/ExampleInstrumentedTest.kt b/android/common/src/androidTest/java/com/wh/common/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..e87d0f1
--- /dev/null
+++ b/android/common/src/androidTest/java/com/wh/common/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.wh.common
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.wh.common.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/AndroidManifest.xml b/android/common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ff551d0
--- /dev/null
+++ b/android/common/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/activity/ABaseActivity.kt b/android/common/src/main/java/com/wh/common/activity/ABaseActivity.kt
new file mode 100644
index 0000000..574a5a5
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/activity/ABaseActivity.kt
@@ -0,0 +1,7 @@
+package com.wh.common.activity
+
+import androidx.activity.ComponentActivity
+
+abstract class ABaseActivity : ComponentActivity() {
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/data/app/AppInfo.kt b/android/common/src/main/java/com/wh/common/data/app/AppInfo.kt
new file mode 100644
index 0000000..b17a129
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/data/app/AppInfo.kt
@@ -0,0 +1,13 @@
+package com.wh.common.data.app
+
+import android.graphics.drawable.Drawable
+
+data class AppInfo(
+ val packageName: String,
+ val icon: Drawable,
+ val label: String,
+// val installTime:Long,
+// val updateTime:Long
+) {
+ var selected: Boolean = false
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/store/AStoreFactory.kt b/android/common/src/main/java/com/wh/common/store/AStoreFactory.kt
new file mode 100644
index 0000000..cc44473
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/store/AStoreFactory.kt
@@ -0,0 +1,5 @@
+package com.wh.common.store
+
+abstract class AStoreFactory {
+ abstract fun create(modeClass: Class): T
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/store/IStore.kt b/android/common/src/main/java/com/wh/common/store/IStore.kt
new file mode 100644
index 0000000..3241618
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/store/IStore.kt
@@ -0,0 +1,4 @@
+package com.wh.common.store
+
+interface IStore {
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/store/Providers.kt b/android/common/src/main/java/com/wh/common/store/Providers.kt
new file mode 100644
index 0000000..816ae14
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/store/Providers.kt
@@ -0,0 +1,60 @@
+package com.wh.common.store
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+
+class SharedPreferenceProvider(private val preferences: SharedPreferences) : StoreProvider {
+ override fun getInt(key: String, defaultValue: Int): Int {
+ return preferences.getInt(key, defaultValue)
+ }
+
+ override fun setInt(key: String, value: Int) {
+ preferences.edit {
+ putInt(key, value)
+ }
+ }
+
+ override fun getLong(key: String, defaultValue: Long): Long {
+ return preferences.getLong(key, defaultValue)
+ }
+
+ override fun setLong(key: String, value: Long) {
+ preferences.edit {
+ putLong(key, value)
+ }
+ }
+
+ override fun getString(key: String, defaultValue: String): String {
+ return preferences.getString(key, defaultValue)!!
+ }
+
+ override fun setString(key: String, value: String) {
+ preferences.edit {
+ putString(key, value)
+ }
+ }
+
+ override fun getStringSet(key: String, defaultValue: Set): Set {
+ return preferences.getStringSet(key, defaultValue)!!
+ }
+
+ override fun setStringSet(key: String, value: Set) {
+ preferences.edit {
+ putStringSet(key, value)
+ }
+ }
+
+ override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
+ return preferences.getBoolean(key, defaultValue)
+ }
+
+ override fun setBoolean(key: String, value: Boolean) {
+ preferences.edit {
+ putBoolean(key, value)
+ }
+ }
+}
+
+fun SharedPreferences.asStoreProvider(): StoreProvider {
+ return SharedPreferenceProvider(this)
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/store/Store.kt b/android/common/src/main/java/com/wh/common/store/Store.kt
new file mode 100644
index 0000000..39cb3d6
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/store/Store.kt
@@ -0,0 +1,106 @@
+package com.wh.common.store
+
+import android.content.Context
+import kotlin.reflect.KProperty
+
+class Store(val provider: StoreProvider) {
+
+ companion object {
+ fun create(context: Context, name: String): Store {
+ return Store(context.getSharedPreferences(name, Context.MODE_PRIVATE).asStoreProvider())
+ }
+ }
+
+ interface Delegate {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)
+ }
+
+ fun int(key: String, defaultValue: Int): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
+ return provider.getInt(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
+ provider.setInt(key, value)
+ }
+ }
+ }
+
+ fun long(key: String, defaultValue: Long): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
+ return provider.getLong(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
+ provider.setLong(key, value)
+ }
+ }
+ }
+
+ fun string(key: String, defaultValue: String): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): String {
+ return provider.getString(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
+ provider.setString(key, value)
+ }
+ }
+ }
+
+ fun stringSet(key: String, defaultValue: Set): Delegate> {
+ return object : Delegate> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Set {
+ return provider.getStringSet(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) {
+ provider.setStringSet(key, value)
+ }
+ }
+ }
+
+ fun boolean(key: String, defaultValue: Boolean): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
+ return provider.getBoolean(key, defaultValue)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
+ provider.setBoolean(key, value)
+ }
+ }
+ }
+
+ fun > enum(key: String, defaultValue: T, values: Array): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+ val name = provider.getString(key, defaultValue.name)
+
+ return values.find { name == it.name } ?: defaultValue
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+ provider.setString(key, value.name)
+ }
+ }
+ }
+
+ fun typedString(key: String, from: (String) -> T?, to: (T?) -> String): Delegate {
+ return object : Delegate {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+ val value = provider.getString(key, to(null))
+
+ return from(value)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
+ provider.setString(key, to(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/store/StoreProvider.kt b/android/common/src/main/java/com/wh/common/store/StoreProvider.kt
new file mode 100644
index 0000000..8d80af6
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/store/StoreProvider.kt
@@ -0,0 +1,18 @@
+package com.wh.common.store
+
+interface StoreProvider {
+ fun getInt(key: String, defaultValue: Int): Int
+ fun setInt(key: String, value: Int)
+
+ fun getLong(key: String, defaultValue: Long): Long
+ fun setLong(key: String, value: Long)
+
+ fun getString(key: String, defaultValue: String): String
+ fun setString(key: String, value: String)
+
+ fun getStringSet(key: String, defaultValue: Set): Set
+ fun setStringSet(key: String, value: Set)
+
+ fun getBoolean(key: String, defaultValue: Boolean): Boolean
+ fun setBoolean(key: String, value: Boolean)
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/AdminPassword.kt b/android/common/src/main/java/com/wh/common/toy/portainer/data/AdminPassword.kt
new file mode 100644
index 0000000..16a553f
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/AdminPassword.kt
@@ -0,0 +1,15 @@
+package com.wh.common.toy.portainer.data
+
+import com.alibaba.fastjson.JSONObject
+import com.wh.common.type.Json
+
+
+class AdminPassword {
+ var username: String? = null
+ var password: String? = null
+
+ fun toJson(): Json {
+ return Json(JSONObject.toJSONString(this))
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Auth.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Auth.java
new file mode 100644
index 0000000..29067c4
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Auth.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 14:21:16
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Auth {
+
+ private String jwt;
+ private Long expireTimestamp;
+
+ public Auth() {
+ }
+
+ public void setJwt(String jwt) {
+ this.jwt = jwt;
+ }
+
+ public String getJwt() {
+ return jwt;
+ }
+
+ public Long getExpireTimestamp() {
+ return expireTimestamp;
+ }
+
+ public void setExpireTimestamp(Long expireTimestamp) {
+ this.expireTimestamp = expireTimestamp;
+ }
+
+ public void setExpireTimestamp() {
+ expireTimestamp = System.currentTimeMillis() + 3500 * 3;
+ }
+
+ public boolean isOutDate() {
+ return System.currentTimeMillis() >= expireTimestamp || jwt == null || jwt.equals("");
+ }
+
+ public Auth(String jwt, Long expireTimestamp) {
+ this.jwt = jwt;
+ this.expireTimestamp = expireTimestamp;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Bridge.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Bridge.java
new file mode 100644
index 0000000..332ce55
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Bridge.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Bridge {
+
+ private String Aliases;
+ private String DriverOpts;
+ private String EndpointID;
+ private String Gateway;
+ private String GlobalIPv6Address;
+ private int GlobalIPv6PrefixLen;
+ private String IPAMConfig;
+ private String IPAddress;
+ private int IPPrefixLen;
+ private String IPv6Gateway;
+ private String Links;
+ private String MacAddress;
+ private String NetworkID;
+ public void setAliases(String Aliases) {
+ this.Aliases = Aliases;
+ }
+ public String getAliases() {
+ return Aliases;
+ }
+
+ public void setDriverOpts(String DriverOpts) {
+ this.DriverOpts = DriverOpts;
+ }
+ public String getDriverOpts() {
+ return DriverOpts;
+ }
+
+ public void setEndpointID(String EndpointID) {
+ this.EndpointID = EndpointID;
+ }
+ public String getEndpointID() {
+ return EndpointID;
+ }
+
+ public void setGateway(String Gateway) {
+ this.Gateway = Gateway;
+ }
+ public String getGateway() {
+ return Gateway;
+ }
+
+ public void setGlobalIPv6Address(String GlobalIPv6Address) {
+ this.GlobalIPv6Address = GlobalIPv6Address;
+ }
+ public String getGlobalIPv6Address() {
+ return GlobalIPv6Address;
+ }
+
+ public void setGlobalIPv6PrefixLen(int GlobalIPv6PrefixLen) {
+ this.GlobalIPv6PrefixLen = GlobalIPv6PrefixLen;
+ }
+ public int getGlobalIPv6PrefixLen() {
+ return GlobalIPv6PrefixLen;
+ }
+
+ public void setIPAMConfig(String IPAMConfig) {
+ this.IPAMConfig = IPAMConfig;
+ }
+ public String getIPAMConfig() {
+ return IPAMConfig;
+ }
+
+ public void setIPAddress(String IPAddress) {
+ this.IPAddress = IPAddress;
+ }
+ public String getIPAddress() {
+ return IPAddress;
+ }
+
+ public void setIPPrefixLen(int IPPrefixLen) {
+ this.IPPrefixLen = IPPrefixLen;
+ }
+ public int getIPPrefixLen() {
+ return IPPrefixLen;
+ }
+
+ public void setIPv6Gateway(String IPv6Gateway) {
+ this.IPv6Gateway = IPv6Gateway;
+ }
+ public String getIPv6Gateway() {
+ return IPv6Gateway;
+ }
+
+ public void setLinks(String Links) {
+ this.Links = Links;
+ }
+ public String getLinks() {
+ return Links;
+ }
+
+ public void setMacAddress(String MacAddress) {
+ this.MacAddress = MacAddress;
+ }
+ public String getMacAddress() {
+ return MacAddress;
+ }
+
+ public void setNetworkID(String NetworkID) {
+ this.NetworkID = NetworkID;
+ }
+ public String getNetworkID() {
+ return NetworkID;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Endpoint.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Endpoint.java
new file mode 100644
index 0000000..26fd0bc
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Endpoint.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+import java.util.List;
+
+/**
+ * Auto-generated: 2021-12-07 23:38:6
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Endpoint {
+
+ private int Id;
+ private String Name;
+ private int Type;
+ private String URL;
+ private int GroupId;
+ private String PublicURL;
+ private List Extensions;
+ private List TagIds;
+ private int Status;
+ private String EdgeKey;
+ private int EdgeCheckinInterval;
+ private String ComposeSyntaxMaxVersion;
+ private int LastCheckInDate;
+ private String AuthorizedUsers;
+ private String AuthorizedTeams;
+ private String Tags;
+ public void setId(int Id) {
+ this.Id = Id;
+ }
+ public int getId() {
+ return Id;
+ }
+
+ public void setName(String Name) {
+ this.Name = Name;
+ }
+ public String getName() {
+ return Name;
+ }
+
+ public void setType(int Type) {
+ this.Type = Type;
+ }
+ public int getType() {
+ return Type;
+ }
+
+ public void setURL(String URL) {
+ this.URL = URL;
+ }
+ public String getURL() {
+ return URL;
+ }
+
+ public void setGroupId(int GroupId) {
+ this.GroupId = GroupId;
+ }
+ public int getGroupId() {
+ return GroupId;
+ }
+
+ public void setPublicURL(String PublicURL) {
+ this.PublicURL = PublicURL;
+ }
+ public String getPublicURL() {
+ return PublicURL;
+ }
+
+ public void setExtensions(List Extensions) {
+ this.Extensions = Extensions;
+ }
+ public List getExtensions() {
+ return Extensions;
+ }
+
+ public void setTagIds(List TagIds) {
+ this.TagIds = TagIds;
+ }
+ public List getTagIds() {
+ return TagIds;
+ }
+
+ public void setStatus(int Status) {
+ this.Status = Status;
+ }
+ public int getStatus() {
+ return Status;
+ }
+
+ public void setEdgeKey(String EdgeKey) {
+ this.EdgeKey = EdgeKey;
+ }
+ public String getEdgeKey() {
+ return EdgeKey;
+ }
+
+ public void setEdgeCheckinInterval(int EdgeCheckinInterval) {
+ this.EdgeCheckinInterval = EdgeCheckinInterval;
+ }
+ public int getEdgeCheckinInterval() {
+ return EdgeCheckinInterval;
+ }
+
+ public void setComposeSyntaxMaxVersion(String ComposeSyntaxMaxVersion) {
+ this.ComposeSyntaxMaxVersion = ComposeSyntaxMaxVersion;
+ }
+ public String getComposeSyntaxMaxVersion() {
+ return ComposeSyntaxMaxVersion;
+ }
+
+ public void setLastCheckInDate(int LastCheckInDate) {
+ this.LastCheckInDate = LastCheckInDate;
+ }
+ public int getLastCheckInDate() {
+ return LastCheckInDate;
+ }
+
+ public void setAuthorizedUsers(String AuthorizedUsers) {
+ this.AuthorizedUsers = AuthorizedUsers;
+ }
+ public String getAuthorizedUsers() {
+ return AuthorizedUsers;
+ }
+
+ public void setAuthorizedTeams(String AuthorizedTeams) {
+ this.AuthorizedTeams = AuthorizedTeams;
+ }
+ public String getAuthorizedTeams() {
+ return AuthorizedTeams;
+ }
+
+ public void setTags(String Tags) {
+ this.Tags = Tags;
+ }
+ public String getTags() {
+ return Tags;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/HostConfig.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/HostConfig.java
new file mode 100644
index 0000000..af46464
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/HostConfig.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class HostConfig {
+
+ private String NetworkMode;
+ public void setNetworkMode(String NetworkMode) {
+ this.NetworkMode = NetworkMode;
+ }
+ public String getNetworkMode() {
+ return NetworkMode;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Labels.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Labels.java
new file mode 100644
index 0000000..a5a8485
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Labels.java
@@ -0,0 +1,127 @@
+package com.wh.common.toy.portainer.data;///**
+// * Copyright 2021 json.cn
+// */
+//package com.wh.common.data.portainer;
+//import java.util.Date;
+//
+///**
+// * Auto-generated: 2021-12-07 13:3:57
+// *
+// * @author json.cn (i@json.cn)
+// * @website http://www.json.cn/java2pojo/
+// */
+//public class Labels {
+//
+// private String build_version;
+// private String maintainer;
+// private String org.opencontainers.image.authors;
+// private Date org.opencontainers.image.created;
+// private String org.opencontainers.image.description;
+// private String org.opencontainers.image.documentation;
+// private String org.opencontainers.image.licenses;
+// private String org.opencontainers.image.ref.name;
+// private String org.opencontainers.image.revision;
+// private String org.opencontainers.image.source;
+// private String org.opencontainers.image.title;
+// private String org.opencontainers.image.url;
+// private String org.opencontainers.image.vendor;
+// private String org.opencontainers.image.version;
+// public void setBuild_version(String build_version) {
+// this.build_version = build_version;
+// }
+// public String getBuild_version() {
+// return build_version;
+// }
+//
+// public void setMaintainer(String maintainer) {
+// this.maintainer = maintainer;
+// }
+// public String getMaintainer() {
+// return maintainer;
+// }
+//
+// public void setOrg.opencontainers.image.authors(String org.opencontainers.image.authors) {
+// this.org.opencontainers.image.authors = org.opencontainers.image.authors;
+// }
+// public String getOrg.opencontainers.image.authors() {
+// return org.opencontainers.image.authors;
+// }
+//
+// public void setOrg.opencontainers.image.created(Date org.opencontainers.image.created) {
+// this.org.opencontainers.image.created = org.opencontainers.image.created;
+// }
+// public Date getOrg.opencontainers.image.created() {
+// return org.opencontainers.image.created;
+// }
+//
+// public void setOrg.opencontainers.image.description(String org.opencontainers.image.description) {
+// this.org.opencontainers.image.description = org.opencontainers.image.description;
+// }
+// public String getOrg.opencontainers.image.description() {
+// return org.opencontainers.image.description;
+// }
+//
+// public void setOrg.opencontainers.image.documentation(String org.opencontainers.image.documentation) {
+// this.org.opencontainers.image.documentation = org.opencontainers.image.documentation;
+// }
+// public String getOrg.opencontainers.image.documentation() {
+// return org.opencontainers.image.documentation;
+// }
+//
+// public void setOrg.opencontainers.image.licenses(String org.opencontainers.image.licenses) {
+// this.org.opencontainers.image.licenses = org.opencontainers.image.licenses;
+// }
+// public String getOrg.opencontainers.image.licenses() {
+// return org.opencontainers.image.licenses;
+// }
+//
+// public void setOrg.opencontainers.image.ref.name(String org.opencontainers.image.ref.name) {
+// this.org.opencontainers.image.ref.name = org.opencontainers.image.ref.name;
+// }
+// public String getOrg.opencontainers.image.ref.name() {
+// return org.opencontainers.image.ref.name;
+// }
+//
+// public void setOrg.opencontainers.image.revision(String org.opencontainers.image.revision) {
+// this.org.opencontainers.image.revision = org.opencontainers.image.revision;
+// }
+// public String getOrg.opencontainers.image.revision() {
+// return org.opencontainers.image.revision;
+// }
+//
+// public void setOrg.opencontainers.image.source(String org.opencontainers.image.source) {
+// this.org.opencontainers.image.source = org.opencontainers.image.source;
+// }
+// public String getOrg.opencontainers.image.source() {
+// return org.opencontainers.image.source;
+// }
+//
+// public void setOrg.opencontainers.image.title(String org.opencontainers.image.title) {
+// this.org.opencontainers.image.title = org.opencontainers.image.title;
+// }
+// public String getOrg.opencontainers.image.title() {
+// return org.opencontainers.image.title;
+// }
+//
+// public void setOrg.opencontainers.image.url(String org.opencontainers.image.url) {
+// this.org.opencontainers.image.url = org.opencontainers.image.url;
+// }
+// public String getOrg.opencontainers.image.url() {
+// return org.opencontainers.image.url;
+// }
+//
+// public void setOrg.opencontainers.image.vendor(String org.opencontainers.image.vendor) {
+// this.org.opencontainers.image.vendor = org.opencontainers.image.vendor;
+// }
+// public String getOrg.opencontainers.image.vendor() {
+// return org.opencontainers.image.vendor;
+// }
+//
+// public void setOrg.opencontainers.image.version(String org.opencontainers.image.version) {
+// this.org.opencontainers.image.version = org.opencontainers.image.version;
+// }
+// public String getOrg.opencontainers.image.version() {
+// return org.opencontainers.image.version;
+// }
+//
+//}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Mounts.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Mounts.java
new file mode 100644
index 0000000..5fc62c2
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Mounts.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Mounts {
+
+ private String Destination;
+ private String Mode;
+ private String Propagation;
+ private boolean RW;
+ private String Source;
+ private String Type;
+ public void setDestination(String Destination) {
+ this.Destination = Destination;
+ }
+ public String getDestination() {
+ return Destination;
+ }
+
+ public void setMode(String Mode) {
+ this.Mode = Mode;
+ }
+ public String getMode() {
+ return Mode;
+ }
+
+ public void setPropagation(String Propagation) {
+ this.Propagation = Propagation;
+ }
+ public String getPropagation() {
+ return Propagation;
+ }
+
+ public void setRW(boolean RW) {
+ this.RW = RW;
+ }
+ public boolean getRW() {
+ return RW;
+ }
+
+ public void setSource(String Source) {
+ this.Source = Source;
+ }
+ public String getSource() {
+ return Source;
+ }
+
+ public void setType(String Type) {
+ this.Type = Type;
+ }
+ public String getType() {
+ return Type;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/NetworkSettings.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/NetworkSettings.java
new file mode 100644
index 0000000..caf5111
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/NetworkSettings.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class NetworkSettings {
+
+ private Networks Networks;
+ public void setNetworks(Networks Networks) {
+ this.Networks = Networks;
+ }
+ public Networks getNetworks() {
+ return Networks;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Networks.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Networks.java
new file mode 100644
index 0000000..4fcc199
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Networks.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Networks {
+
+ private Bridge bridge;
+ public void setBridge(Bridge bridge) {
+ this.bridge = bridge;
+ }
+ public Bridge getBridge() {
+ return bridge;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Portainer.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Portainer.java
new file mode 100644
index 0000000..53ac586
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Portainer.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Portainer {
+
+ @NonNull
+ public String toString() {
+ return getFirstName().replace("/", "") + " " + getState();
+ }
+
+ public String getFirstName() {
+ return getNames().get(0);
+ }
+
+ // private String Command;
+ private long Created;
+ // private HostConfig HostConfig;
+// private String Id;
+ private String Image;
+ // private String ImageID;
+ // private Labels Labels;
+// private List Mounts;
+ private List Names;
+ // private NetworkSettings NetworkSettings;
+// private List Ports;
+ private String State;
+ private String Status;
+
+// public void setCommand(String Command) {
+// this.Command = Command;
+// }
+
+// public String getCommand() {
+// return Command;
+// }
+
+ public void setCreated(long Created) {
+ this.Created = Created;
+ }
+
+ public long getCreated() {
+ return Created;
+ }
+
+// public void setHostConfig(HostConfig HostConfig) {
+// this.HostConfig = HostConfig;
+// }
+
+// public HostConfig getHostConfig() {
+// return HostConfig;
+// }
+
+// public void setId(String Id) {
+// this.Id = Id;
+// }
+
+// public String getId() {
+// return Id;
+// }
+
+ public void setImage(String Image) {
+ this.Image = Image;
+ }
+
+ public String getImage() {
+ return Image;
+ }
+
+// public void setImageID(String ImageID) {
+// this.ImageID = ImageID;
+// }
+
+// public String getImageID() {
+// return ImageID;
+// }
+
+// public void setLabels(Labels Labels) {
+// this.Labels = Labels;
+// }
+// public Labels getLabels() {
+// return Labels;
+// }
+
+// public void setMounts(List Mounts) {
+// this.Mounts = Mounts;
+// }
+
+// public List getMounts() {
+// return Mounts;
+// }
+
+ public void setNames(List Names) {
+ this.Names = Names;
+ }
+
+ public List getNames() {
+ return Names;
+ }
+
+// public void setNetworkSettings(NetworkSettings NetworkSettings) {
+// this.NetworkSettings = NetworkSettings;
+// }
+
+// public NetworkSettings getNetworkSettings() {
+// return NetworkSettings;
+// }
+
+// public void setPorts(List Ports) {
+// this.Ports = Ports;
+// }
+
+// public List getPorts() {
+// return Ports;
+// }
+
+ public void setState(String State) {
+ this.State = State;
+ }
+
+ public String getState() {
+ return State;
+ }
+
+ public void setStatus(String Status) {
+ this.Status = Status;
+ }
+
+ public String getStatus() {
+ return Status;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/data/Ports.java b/android/common/src/main/java/com/wh/common/toy/portainer/data/Ports.java
new file mode 100644
index 0000000..5cd172a
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/data/Ports.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.portainer.data;
+
+/**
+ * Auto-generated: 2021-12-07 13:3:57
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Ports {
+
+ private String IP;
+ private int PrivatePort;
+ private int PublicPort;
+ private String Type;
+ public void setIP(String IP) {
+ this.IP = IP;
+ }
+ public String getIP() {
+ return IP;
+ }
+
+ public void setPrivatePort(int PrivatePort) {
+ this.PrivatePort = PrivatePort;
+ }
+ public int getPrivatePort() {
+ return PrivatePort;
+ }
+
+ public void setPublicPort(int PublicPort) {
+ this.PublicPort = PublicPort;
+ }
+ public int getPublicPort() {
+ return PublicPort;
+ }
+
+ public void setType(String Type) {
+ this.Type = Type;
+ }
+ public String getType() {
+ return Type;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/helper/PortainerHttpApiHelper.kt b/android/common/src/main/java/com/wh/common/toy/portainer/helper/PortainerHttpApiHelper.kt
new file mode 100644
index 0000000..3af0e1f
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/helper/PortainerHttpApiHelper.kt
@@ -0,0 +1,149 @@
+package com.wh.common.toy.portainer.helper
+
+import com.wh.common.toy.portainer.data.AdminPassword
+import com.wh.common.toy.portainer.data.Auth
+import com.wh.common.toy.portainer.data.Endpoint
+import com.wh.common.toy.portainer.data.Portainer
+import com.wh.common.typeExt.toJson
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.*
+import java.io.IOException
+import java.net.InetSocketAddress
+import java.net.Proxy
+import java.net.SocketException
+
+object PortainerHttpApiHelper {
+
+ fun requestWithAuth(
+ url: String,
+ token: String
+ ): Request {
+ return Request.Builder()
+ .url(url)
+ .header(
+ "Authorization",
+ "Bearer $token"
+ )
+ .get()
+ .build()
+ }
+
+ fun requestWithAdminPassword(url: String, username: String, password: String): Request {
+ return Request.Builder()
+ .post(
+ AdminPassword().apply {
+ this.username = username
+ this.password = password
+ }.toJson().toRequestBody()
+ )
+ .url(url)
+ .build()
+ }
+
+ fun clientWithProxy(
+ proxyType: Proxy.Type,
+ host: String,
+ port: Int
+ ): OkHttpClient {
+ return OkHttpClient.Builder()
+ .proxy(Proxy(proxyType, InetSocketAddress(host, port))).build()
+ }
+
+ suspend fun fetchPortainerAuth(
+ baseUrl: String,
+ username: String,
+ password: String,
+ proxyType: Proxy.Type,
+ proxyHost: String,
+ proxyPort: Int
+ ): Auth {
+ return withContext(Dispatchers.IO) {
+ val url = "${baseUrl}/api/auth"
+ val call = clientWithProxy(
+ proxyType,
+ proxyHost,
+ proxyPort
+ ).newCall(
+ requestWithAdminPassword(
+ url,
+ username,
+ password
+ )
+ )
+ val response = try {
+ call.execute()
+ } catch (e: SocketException) {
+ throw e
+ }
+ val body = response.body ?: throw IOException()
+ val s = body.string()
+ body.close()
+ return@withContext s.toJson().toObject(Auth::class.java).apply {
+ setExpireTimestamp()
+ }
+
+ }
+ }
+
+ //http://192.168.51.99:9000/api/endpoints
+
+ suspend fun fetchPortainerEndpoints(
+ baseUrl: String,
+ token: String,
+ proxyType: Proxy.Type,
+ proxyHost: String,
+ proxyPort: Int
+ ): List {
+ return withContext(Dispatchers.IO) {
+ val url = "${baseUrl}/api/endpoints"
+ val call = clientWithProxy(
+ proxyType,
+ proxyHost,
+ proxyPort
+ ).newCall(
+ requestWithAuth(
+ url,
+ token
+ )
+ )
+ val response = call.execute()
+ val body = response.body ?: throw IOException()
+ val s = body.string()
+ body.close()
+ return@withContext s.toJson().toList(Endpoint::class.java)
+ }
+ }
+
+
+ suspend fun fetchPortainerContainer(
+ baseUrl: String,
+ endpointId: Int,
+ token: String,
+ proxyType: Proxy.Type,
+ proxyHost: String,
+ proxyPort: Int
+ ): List {
+ return withContext(Dispatchers.IO) {
+ val url = "${baseUrl}/api/endpoints/${endpointId}/docker/containers/json"
+ val call = clientWithProxy(
+ proxyType,
+ proxyHost,
+ proxyPort
+ ).newCall(
+ requestWithAuth(
+ url,
+ token
+ )
+ )
+ val response = call.execute()
+ if (!response.isSuccessful) {
+ throw IOException("error in fetchPortainerContainer ${response.code} ${response.message}")
+ }
+ val body = response.body ?: throw IOException()
+ val s = body.string()
+ body.close()
+ return@withContext s.toJson().toList(Portainer::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/repository/PortainerRepository.kt b/android/common/src/main/java/com/wh/common/toy/portainer/repository/PortainerRepository.kt
new file mode 100644
index 0000000..e09859b
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/repository/PortainerRepository.kt
@@ -0,0 +1,67 @@
+package com.wh.common.toy.portainer.repository
+
+import androidx.lifecycle.MutableLiveData
+import com.wh.common.toy.portainer.data.Auth
+import com.wh.common.toy.portainer.data.Portainer
+import com.wh.common.toy.portainer.helper.PortainerHttpApiHelper
+import com.wh.common.toy.portainer.store.PortainerStore
+
+class PortainerRepository(private val portainerStore: PortainerStore) {
+ var auth = Auth(
+ portainerStore.authToken,
+ portainerStore.authExpireTimestamp
+ )
+
+ val portainers = MutableLiveData>(listOf())
+ private val portainersTmp = mutableListOf()
+
+ var isLoading = MutableLiveData(false)
+ var lastUpdateTimestamp = MutableLiveData(System.currentTimeMillis())
+
+ suspend fun refreshContainers() {
+ if (!portainerStore.enablePortainerMonitor) {
+ return
+ }
+ isLoading.postValue(true)
+ if (auth.isOutDate) {
+ auth = try {
+ PortainerHttpApiHelper.fetchPortainerAuth(
+ baseUrl = portainerStore.portainerBaseUrl,
+ username = portainerStore.adminUsername,
+ password = portainerStore.adminPassword,
+ proxyType = portainerStore.resolveProxyType(),
+ proxyHost = portainerStore.proxyHost,
+ proxyPort = portainerStore.proxyPort
+ ).also {
+ portainerStore.authToken = it.jwt
+ portainerStore.authExpireTimestamp = it.expireTimestamp
+ }
+ } catch (e: Exception) {
+ throw e
+ }
+ }
+ portainersTmp.clear()
+ PortainerHttpApiHelper.fetchPortainerEndpoints(
+ portainerStore.portainerBaseUrl,
+ auth.jwt,
+ portainerStore.resolveProxyType(),
+ portainerStore.proxyHost,
+ portainerStore.proxyPort
+ ).forEach {
+ PortainerHttpApiHelper.fetchPortainerContainer(
+ portainerStore.portainerBaseUrl,
+ it.id,
+ auth.jwt,
+ portainerStore.resolveProxyType(),
+ portainerStore.proxyHost,
+ portainerStore.proxyPort
+ )
+ .forEach { portainer ->
+ portainersTmp.add(portainer)
+ }
+ }
+ portainers.postValue(portainersTmp)
+ isLoading.postValue(false)
+ lastUpdateTimestamp.postValue(System.currentTimeMillis())
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/store/PortainerStore.kt b/android/common/src/main/java/com/wh/common/toy/portainer/store/PortainerStore.kt
new file mode 100644
index 0000000..9382970
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/store/PortainerStore.kt
@@ -0,0 +1,51 @@
+package com.wh.common.toy.portainer.store
+
+import android.content.Context
+import com.wh.common.store.IStore
+import com.wh.common.store.Store
+import com.wh.common.store.asStoreProvider
+import java.net.Proxy
+
+class PortainerStore(context: Context): IStore {
+ private val store = Store(
+ context.getSharedPreferences(
+ "portainer",
+ Context.MODE_PRIVATE
+ ).asStoreProvider()
+ )
+
+ companion object {
+ const val KEY_ENABLE_PORTAINER_MONITOR = "enable-portainer-monitor"
+
+ const val KEY_ADMIN_USERNAME = "admin-username"
+ const val KEY_ADMIN_PASSWORD = "admin-password"
+ const val KEY_PORTAINER_BASE_URL = "portainer-base-url"
+ const val KEY_USE_PROXY = "use-proxy"
+ const val KEY_PROXY_TYPE = "proxy-type"
+ const val KEY_PROXY_HOST = "proxy-host"
+ const val KEY_PROXY_PORT = "proxy-port"
+
+ const val KEY_AUTH_TOKEN = "auth-token"
+ const val KEY_AUTH_EXPIRE_TIMESTAMP = "auth-expire-timestamp"
+ }
+
+ var enablePortainerMonitor by store.boolean(KEY_ENABLE_PORTAINER_MONITOR, true)
+
+ var adminUsername by store.string(KEY_ADMIN_USERNAME, "admin")
+ var adminPassword by store.string(KEY_ADMIN_PASSWORD, "1qaz2wsx")
+ var portainerBaseUrl by store.string(KEY_PORTAINER_BASE_URL, "http://192.168.51.99:9000")
+ var useProxy by store.boolean(KEY_USE_PROXY, true)
+ var proxyType by store.int(KEY_PROXY_TYPE, 1)
+ var proxyHost by store.string(KEY_PROXY_HOST, "192.168.50.83")
+ var proxyPort by store.int(KEY_PROXY_PORT, 1080)
+ var authToken by store.string(KEY_AUTH_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTYzOTAwMjAxOH0.yM1WjR4esoaKGcJ00uGeVt5-eKgtb99DI5n2wjpP_zg")
+ var authExpireTimestamp by store.long(KEY_AUTH_EXPIRE_TIMESTAMP, 0L)
+
+ fun resolveProxyType(): Proxy.Type {
+ return when (proxyType) {
+ 1 -> Proxy.Type.SOCKS
+ 2 -> Proxy.Type.HTTP
+ else -> Proxy.Type.DIRECT
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/portainer/viewmodel/PortainerViewModel.kt b/android/common/src/main/java/com/wh/common/toy/portainer/viewmodel/PortainerViewModel.kt
new file mode 100644
index 0000000..769a4be
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/portainer/viewmodel/PortainerViewModel.kt
@@ -0,0 +1,26 @@
+package com.wh.common.toy.portainer.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wh.common.toy.portainer.repository.PortainerRepository
+import kotlinx.coroutines.launch
+
+class PortainerViewModel(private val portainerRepository: PortainerRepository) : ViewModel() {
+
+ val portainers = portainerRepository.portainers
+
+ var isLoading = portainerRepository.isLoading
+
+ var lastUpdateTimestamp = portainerRepository.lastUpdateTimestamp
+
+ fun refreshContainersAsync() {
+ viewModelScope.launch {
+ try {
+ portainerRepository.refreshContainers()
+ } catch (e: Exception) {
+ Log.d("WH_", "refreshContainersAsync: ${e.localizedMessage}")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/data/Now.java b/android/common/src/main/java/com/wh/common/toy/qweather/data/Now.java
new file mode 100644
index 0000000..84c7515
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/data/Now.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.qweather.data;
+import java.util.Date;
+
+/**
+ * Auto-generated: 2021-12-09 20:42:23
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Now {
+
+ private Date obsTime;
+ private String temp;
+ private String feelsLike;
+ private String icon;
+ private String text;
+ private String wind360;
+ private String windDir;
+ private String windScale;
+ private String windSpeed;
+ private String humidity;
+ private String precip;
+ private String pressure;
+ private String vis;
+ private String cloud;
+ private String dew;
+ public void setObsTime(Date obsTime) {
+ this.obsTime = obsTime;
+ }
+ public Date getObsTime() {
+ return obsTime;
+ }
+
+ public void setTemp(String temp) {
+ this.temp = temp;
+ }
+ public String getTemp() {
+ return temp;
+ }
+
+ public void setFeelsLike(String feelsLike) {
+ this.feelsLike = feelsLike;
+ }
+ public String getFeelsLike() {
+ return feelsLike;
+ }
+
+ public void setIcon(String icon) {
+ this.icon = icon;
+ }
+ public String getIcon() {
+ return icon;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+ public String getText() {
+ return text;
+ }
+
+ public void setWind360(String wind360) {
+ this.wind360 = wind360;
+ }
+ public String getWind360() {
+ return wind360;
+ }
+
+ public void setWindDir(String windDir) {
+ this.windDir = windDir;
+ }
+ public String getWindDir() {
+ return windDir;
+ }
+
+ public void setWindScale(String windScale) {
+ this.windScale = windScale;
+ }
+ public String getWindScale() {
+ return windScale;
+ }
+
+ public void setWindSpeed(String windSpeed) {
+ this.windSpeed = windSpeed;
+ }
+ public String getWindSpeed() {
+ return windSpeed;
+ }
+
+ public void setHumidity(String humidity) {
+ this.humidity = humidity;
+ }
+ public String getHumidity() {
+ return humidity;
+ }
+
+ public void setPrecip(String precip) {
+ this.precip = precip;
+ }
+ public String getPrecip() {
+ return precip;
+ }
+
+ public void setPressure(String pressure) {
+ this.pressure = pressure;
+ }
+ public String getPressure() {
+ return pressure;
+ }
+
+ public void setVis(String vis) {
+ this.vis = vis;
+ }
+ public String getVis() {
+ return vis;
+ }
+
+ public void setCloud(String cloud) {
+ this.cloud = cloud;
+ }
+ public String getCloud() {
+ return cloud;
+ }
+
+ public void setDew(String dew) {
+ this.dew = dew;
+ }
+ public String getDew() {
+ return dew;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/data/QWeather.java b/android/common/src/main/java/com/wh/common/toy/qweather/data/QWeather.java
new file mode 100644
index 0000000..17991fa
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/data/QWeather.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.qweather.data;
+import java.util.Date;
+
+/**
+ * Auto-generated: 2021-12-09 20:42:23
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class QWeather {
+
+ private String code;
+ private Date updateTime;
+ private String fxLink;
+ private Now now;
+ private Refer refer;
+ public void setCode(String code) {
+ this.code = code;
+ }
+ public String getCode() {
+ return code;
+ }
+
+ public void setUpdateTime(Date updateTime) {
+ this.updateTime = updateTime;
+ }
+ public Date getUpdateTime() {
+ return updateTime;
+ }
+
+ public void setFxLink(String fxLink) {
+ this.fxLink = fxLink;
+ }
+ public String getFxLink() {
+ return fxLink;
+ }
+
+ public void setNow(Now now) {
+ this.now = now;
+ }
+ public Now getNow() {
+ return now;
+ }
+
+ public void setRefer(Refer refer) {
+ this.refer = refer;
+ }
+ public Refer getRefer() {
+ return refer;
+ }
+
+}
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/data/Refer.java b/android/common/src/main/java/com/wh/common/toy/qweather/data/Refer.java
new file mode 100644
index 0000000..f2fb042
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/data/Refer.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2021 json.cn
+ */
+package com.wh.common.toy.qweather.data;
+import java.util.List;
+
+/**
+ * Auto-generated: 2021-12-09 20:42:23
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class Refer {
+
+ private List sources;
+ private List license;
+ public void setSources(List sources) {
+ this.sources = sources;
+ }
+ public List getSources() {
+ return sources;
+ }
+
+ public void setLicense(List license) {
+ this.license = license;
+ }
+ public List getLicense() {
+ return license;
+ }
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/helper/QWeatherHttpApiHelper.kt b/android/common/src/main/java/com/wh/common/toy/qweather/helper/QWeatherHttpApiHelper.kt
new file mode 100644
index 0000000..428da60
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/helper/QWeatherHttpApiHelper.kt
@@ -0,0 +1,41 @@
+package com.wh.common.toy.qweather.helper
+
+import com.wh.common.toy.qweather.data.QWeather
+import com.wh.common.typeExt.toJson
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.IOException
+
+object QWeatherHttpApiHelper {
+ // https://api.qweather.com/v7/weather/now?
+
+
+ private fun getClient(): OkHttpClient {
+ return OkHttpClient.Builder().build()
+ }
+
+ private fun getRequest(): Request {
+ val baseUrl = "https://devapi.qweather.com/v7/weather/now".toHttpUrl().newBuilder().apply {
+ addQueryParameter("key", "1dcaaf15508b4170adbbe5302b860200")
+ addQueryParameter("location", "39.91337035811204,116.40148390744017")
+ addQueryParameter("unit", "m")
+ }.build()
+ return Request.Builder().url(baseUrl).get().build()
+ }
+
+ suspend fun fetchRealTimeWeather(): QWeather {
+ return withContext(Dispatchers.IO) {
+ val response = getClient().newCall(getRequest()).execute()
+ if (!response.isSuccessful || response.code!=200) {
+ throw IOException("error in fetchRealTimeWeather error code ${response.code} ${response.message}")
+ }
+ val body = response.body ?: throw IOException()
+ val s = body.string()
+ body.close()
+ return@withContext s.toJson().toObject(QWeather::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/store/QWeatherStore.kt b/android/common/src/main/java/com/wh/common/toy/qweather/store/QWeatherStore.kt
new file mode 100644
index 0000000..3e09464
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/store/QWeatherStore.kt
@@ -0,0 +1,21 @@
+package com.wh.common.toy.qweather.store
+
+import android.content.Context
+import com.wh.common.store.IStore
+import com.wh.common.store.Store
+import com.wh.common.store.asStoreProvider
+
+class QWeatherStore(context: Context): IStore {
+ private val store = Store(
+ context.getSharedPreferences(
+ "q-weather",
+ Context.MODE_PRIVATE
+ ).asStoreProvider()
+ )
+
+ companion object{
+ const val KEY_SERVICE_KEY = "service-key"
+ }
+
+ var serviceKey by store.string(KEY_SERVICE_KEY,"")
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/toy/qweather/viewmodel/QWeatherViewModel.kt b/android/common/src/main/java/com/wh/common/toy/qweather/viewmodel/QWeatherViewModel.kt
new file mode 100644
index 0000000..d5d94fe
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/toy/qweather/viewmodel/QWeatherViewModel.kt
@@ -0,0 +1,20 @@
+package com.wh.common.toy.qweather.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.wh.common.toy.qweather.data.QWeather
+import com.wh.common.toy.qweather.helper.QWeatherHttpApiHelper
+import com.wh.common.toy.qweather.store.QWeatherStore
+import kotlinx.coroutines.launch
+
+class QWeatherViewModel(qWeatherStore: QWeatherStore) : ViewModel() {
+
+ var qWeather = MutableLiveData()
+
+ fun refreshQWeatherAsync() {
+ viewModelScope.launch {
+ qWeather.postValue(QWeatherHttpApiHelper.fetchRealTimeWeather())
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/type/Json.kt b/android/common/src/main/java/com/wh/common/type/Json.kt
new file mode 100644
index 0000000..23633cc
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/type/Json.kt
@@ -0,0 +1,22 @@
+package com.wh.common.type
+
+import com.alibaba.fastjson.JSONArray
+import com.alibaba.fastjson.JSONObject
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+
+class Json(private val v: String) {
+
+ fun toObject(modelClass: Class): T {
+ return JSONObject.parseObject(v, modelClass)
+ }
+
+ fun toList(modelClass: Class): List {
+ return JSONArray.parseArray(v, modelClass).toList()
+ }
+
+ fun toRequestBody(): RequestBody {
+ return v.toRequestBody("application/json".toMediaType())
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/Drawable.kt b/android/common/src/main/java/com/wh/common/typeExt/Drawable.kt
new file mode 100644
index 0000000..70e3e9c
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/Drawable.kt
@@ -0,0 +1,14 @@
+package com.wh.common.typeExt
+
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+
+fun Drawable.foreground():Drawable{
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+ this is AdaptiveIconDrawable && this.background == null
+ ) {
+ return this.foreground
+ }
+ return this
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/Float.kt b/android/common/src/main/java/com/wh/common/typeExt/Float.kt
new file mode 100644
index 0000000..e4ff8b2
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/Float.kt
@@ -0,0 +1,9 @@
+package com.wh.common.typeExt
+
+fun Float.toDot2NumStr(): String {
+ return String.format("%.2f",this)
+}
+
+fun Float.toDot2NumMoneyStr(): String {
+ return String.format("¥%.2f",this)
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/Int.kt b/android/common/src/main/java/com/wh/common/typeExt/Int.kt
new file mode 100644
index 0000000..d316212
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/Int.kt
@@ -0,0 +1,5 @@
+package com.wh.common.typeExt
+
+fun Int.daysToInterval(): Long {
+ return (this-1) * 86_400_000L
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/List.kt b/android/common/src/main/java/com/wh/common/typeExt/List.kt
new file mode 100644
index 0000000..0275c63
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/List.kt
@@ -0,0 +1,37 @@
+package com.wh.common.typeExt
+
+inline fun List.everyN(n: Int, onEach: (List) -> Unit) {
+ val times = this.size / n
+ var tmp = 0
+ while (times > tmp) {
+ onEach(this.getGroup(n, tmp))
+ tmp += 1
+ }
+ if (size % n != 0) {
+ onEach(getRest(times * n))
+ }
+}
+
+// endIndex is not included
+fun List.getRange(startIndex: Int, endIndex: Int): MutableList {
+ var startIndexAgent = startIndex
+ val tmp = mutableListOf()
+ while (startIndexAgent < endIndex) {
+ tmp.add(this[startIndexAgent])
+ startIndexAgent += 1
+ }
+ return tmp
+}
+
+// have no range side check
+// groupIndex start with 0
+fun List.getGroup(groupSize: Int, groupIndex: Int): MutableList {
+ val startIndex = groupIndex * groupSize
+ val endIndex = startIndex + groupSize
+ return getRange(startIndex, endIndex)
+}
+
+// have no check
+fun List.getRest(startIndex: Int): MutableList {
+ return getRange(startIndex, size)
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/Long.kt b/android/common/src/main/java/com/wh/common/typeExt/Long.kt
new file mode 100644
index 0000000..c94f732
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/Long.kt
@@ -0,0 +1,20 @@
+package com.wh.common.typeExt
+
+import com.wh.common.util.TimeUtils
+import kotlin.math.absoluteValue
+
+fun Long.toTimestamp(spf: String = "yyyy-MM-dd HH:mm:ss"): String {
+ return TimeUtils.getFormattedTime(this, spf)
+}
+
+fun Long.timestampMSToDayLevel(): Long {
+ return this / 86_400_000
+}
+
+fun Long.daysBetween(otherTime: Long): Int {
+ return (this.timestampMSToDayLevel() - otherTime.timestampMSToDayLevel()).absoluteValue.toInt() + 1
+}
+
+fun Long.daysBetween(): Int {
+ return (this.absoluteValue / 86_400_000).toInt() + 1
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/MutableList.kt b/android/common/src/main/java/com/wh/common/typeExt/MutableList.kt
new file mode 100644
index 0000000..77e62e3
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/MutableList.kt
@@ -0,0 +1,10 @@
+package com.wh.common.typeExt
+
+fun MutableList.addOrRemove(t: T): MutableList {
+ if (this.contains(t)) {
+ this.remove(t)
+ } else {
+ this.add(t)
+ }
+ return this
+}
diff --git a/android/common/src/main/java/com/wh/common/typeExt/PackageInfo.kt b/android/common/src/main/java/com/wh/common/typeExt/PackageInfo.kt
new file mode 100644
index 0000000..cb5f7f8
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/PackageInfo.kt
@@ -0,0 +1,22 @@
+package com.wh.common.typeExt
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import com.wh.common.data.app.AppInfo
+
+fun PackageInfo.toAppInfo(packageManager: PackageManager): AppInfo {
+ return AppInfo(
+ packageName = packageName,
+ icon = applicationInfo.loadIcon(packageManager).foreground(),
+ label = applicationInfo.loadLabel(packageManager).toString()
+ )
+}
+
+fun PackageInfo.isUserPackage(): Boolean {
+ return this.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
+}
+
+fun PackageInfo.isSystemPackage(): Boolean {
+ return this.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/Set.kt b/android/common/src/main/java/com/wh/common/typeExt/Set.kt
new file mode 100644
index 0000000..c71ad73
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/Set.kt
@@ -0,0 +1,11 @@
+package com.wh.common.typeExt
+
+fun Set.addOrRemove(t:T): Set {
+ val tmp = this.toMutableSet()
+ if (tmp.contains(t)){
+ tmp.remove(t)
+ }else{
+ tmp.add(t)
+ }
+ return tmp.toSet()
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/typeExt/String.kt b/android/common/src/main/java/com/wh/common/typeExt/String.kt
new file mode 100644
index 0000000..0cb304b
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/typeExt/String.kt
@@ -0,0 +1,21 @@
+package com.wh.common.typeExt
+
+import com.wh.common.type.Json
+import com.wh.common.util.Base64Utils
+import com.wh.common.util.TimeUtils
+
+fun String.timestampParse(spf:String = "yyyy-MM-dd HH:mm:ss"): Long {
+ return TimeUtils.getMSFromFormattedTime(spf)
+}
+
+fun String.base64Decode(): String {
+ return Base64Utils.decode(this)
+}
+
+fun String.base64Encode(): String {
+ return Base64Utils.encode(this).replace("\n","")
+}
+
+fun String.toJson(): Json {
+ return Json(this)
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/ui/componment/DatePicker.kt b/android/common/src/main/java/com/wh/common/ui/componment/DatePicker.kt
new file mode 100644
index 0000000..eb2f49b
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/ui/componment/DatePicker.kt
@@ -0,0 +1,24 @@
+package com.wh.common.ui.componment
+
+import android.util.Log
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.FragmentManager
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.wh.common.util.FragmentUtils
+
+fun showDatePicker(activity: AppCompatActivity, onOK: (Long) -> Unit) {
+ val picker = MaterialDatePicker.Builder.datePicker().setTitleText("aaa").build()
+ activity.let {
+ picker.show(it.supportFragmentManager, picker.toString())
+ picker.addOnPositiveButtonClickListener { it ->
+ onOK(it)
+ Log.d("WH_", "showDatePicker: $it")
+ }
+ }
+}
+
+fun showDatePicker(fragmentManager: FragmentManager,onOK: (Long) -> Unit){
+ val picker = MaterialDatePicker.Builder.datePicker().setTitleText("选择日期").build()
+ picker.addOnPositiveButtonClickListener(onOK)
+ FragmentUtils.showDialog(fragmentManager,picker,"date-picker")
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/Base64Utils.kt b/android/common/src/main/java/com/wh/common/util/Base64Utils.kt
new file mode 100644
index 0000000..0737c85
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/Base64Utils.kt
@@ -0,0 +1,14 @@
+package com.wh.common.util
+
+import android.util.Base64
+
+class Base64Utils {
+ companion object{
+ fun decode(input: String): String {
+ return String(Base64.decode(input,Base64.DEFAULT))
+ }
+ fun encode(input: String): String{
+ return Base64.encodeToString(input.toByteArray(),Base64.DEFAULT)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/ClipboardUtils.kt b/android/common/src/main/java/com/wh/common/util/ClipboardUtils.kt
new file mode 100644
index 0000000..9d226c1
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/ClipboardUtils.kt
@@ -0,0 +1,55 @@
+package com.wh.common.util
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.util.Log
+object ClipboardUtils {
+ fun getClipboardManager(context:Context):ClipboardManager{
+ return context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ }
+
+ fun copy(clipboardManager: ClipboardManager,label:String,text:String){
+ val clipData = ClipData(label, arrayOf("text/plain"),ClipData.Item(text))
+ clipboardManager.setPrimaryClip(clipData)
+ }
+
+ fun copy(context: Context,label:String,text:String){
+ val clipData = ClipData(label, arrayOf("text/plain"),ClipData.Item(text))
+ getClipboardManager(context = context).setPrimaryClip(clipData)
+ }
+
+ fun setupListener(clipboardManager: ClipboardManager,onChange:(String)->Unit){
+ clipboardManager.addPrimaryClipChangedListener {
+ clipboardManager.primaryClip?.let {
+ if(it.itemCount>0){
+ onChange(it.getItemAt(0).text.toString())
+ }
+ }
+ }
+ }
+
+ fun setupListener(context: Context,onChange:(String)->Unit): ClipboardManager {
+ val clipboardManager = getClipboardManager(context = context)
+ clipboardManager.addPrimaryClipChangedListener {
+ clipboardManager.primaryClip?.let {
+ if(it.description.label == "sbw-clip"){
+ Log.d("WH_", "setupListener: is from sbw-clip, skip listen")
+ return@addPrimaryClipChangedListener
+ }
+ if(it.itemCount>0){
+ onChange(it.getItemAt(0).text.toString())
+ }
+ }
+ }
+ return clipboardManager
+ }
+
+ fun setupListener(context: Context,l:ClipboardManager.OnPrimaryClipChangedListener):ClipboardManager{
+ val clipboardManager = getClipboardManager(context = context)
+ clipboardManager.addPrimaryClipChangedListener(l)
+ return clipboardManager
+ }
+
+
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/FragmentUtils.kt b/android/common/src/main/java/com/wh/common/util/FragmentUtils.kt
new file mode 100644
index 0000000..c286474
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/FragmentUtils.kt
@@ -0,0 +1,24 @@
+package com.wh.common.util
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+
+object FragmentUtils {
+
+ fun showDialog(
+ activity: AppCompatActivity,
+ fragment: DialogFragment,
+ tag: String? = null
+ ) {
+ showDialog(activity.supportFragmentManager, fragment, tag)
+ }
+
+ fun showDialog(
+ fragmentManager: FragmentManager,
+ fragment: DialogFragment,
+ tag: String? = null
+ ) {
+ fragment.show(fragmentManager, tag)
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/PackageUtils.kt b/android/common/src/main/java/com/wh/common/util/PackageUtils.kt
new file mode 100644
index 0000000..e907c22
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/PackageUtils.kt
@@ -0,0 +1,62 @@
+package com.wh.common.util
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.util.Log
+import com.wh.common.typeExt.isUserPackage
+
+
+object PackageUtils {
+
+ var manager: PackageManager? = null
+
+// @SuppressLint("QueryPermissionsNeeded")
+// fun getAllPackages(): Flow {
+// return manager!!.getInstalledPackages(0).asFlow()
+// }
+
+ fun getUserPackages(packageManager: PackageManager): MutableList {
+ val tmp = mutableListOf()
+ packageManager.getInstalledPackages(0).forEach {
+ if (it.isUserPackage()) {
+ tmp.add(it)
+
+ Log.d("WH_", "getUserPackages: ${it.packageName}")
+ }
+ }
+ return tmp
+ }
+
+// fun getSystemPackages(): Flow {
+// val tmp = mutableListOf()
+// manager!!.getInstalledPackages(0).forEach {
+// if (it.isSystemPackage()){
+// tmp.add(it)
+// }
+// }
+// return tmp.asFlow()
+// }
+
+ private fun getApplicationInfoByPackageName(packageName: String): ApplicationInfo {
+ return manager!!.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
+ }
+
+ fun getApplicationLabel(packageName: String): CharSequence {
+ return try {
+ manager!!.getApplicationLabel(getApplicationInfoByPackageName(packageName))
+ } catch (e: Exception) {
+ packageName
+ }.also {
+ Log.d("WH_", "getApplicationLabel: $packageName $it")
+ }
+// return getApplicationInfoByPackageName(packageName).loadLabel(manager!!).also {
+// Log.d("WH_", "getApplicationLabel: $it")
+// }
+ }
+
+ fun getApplicationIcon(pkgName: String): Drawable {
+ return getApplicationInfoByPackageName(pkgName).loadIcon(manager!!)
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/TimeUtils.java b/android/common/src/main/java/com/wh/common/util/TimeUtils.java
new file mode 100644
index 0000000..8f5176f
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/TimeUtils.java
@@ -0,0 +1,108 @@
+package com.wh.common.util;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+public class TimeUtils {
+ public static final String fullDateTimeFormat = "yyyy-MM-dd HH:mm:ss";
+ public static final String yMdTimeFormat = "yyyy-MM-dd";
+
+
+ /**
+ * @return 返回格式化后的字符串
+ * @describe 将长整型的时间戳转换成"yyyy-MM-dd"格式的时间
+ * @args MS long 长整型的毫秒时间戳
+ */
+ public static String getFormattedTime(Long MS) {
+ return genDateFormat(fullDateTimeFormat).format(new Date(MS));
+ }
+
+ /**
+ * @return 返回格式化后的字符串
+ * @describe 将长整型的时间戳转换成自定义的格式
+ * @args MS long 长整型的毫秒时间戳
+ * format String 格式
+ */
+ public static String getFormattedTime(long MS, String format) {
+ return genDateFormat(format).format(new Date(MS));
+ }
+
+ /**
+ * @return Long 返回转换后的长整型时间戳
+ * @describe 将格式化的时间戳字符串转换成长整型的毫秒时间戳
+ * @args formattedTime String 格式化的时间戳
+ */
+ public static long getMSFromFormattedTime(String formattedTime) throws ParseException {
+ Date date = genDateFormat(fullDateTimeFormat).parse(formattedTime);
+ if (date != null) {
+ return date.getTime();
+ } else {
+ return 0L;
+ }
+ }
+
+ public static String getTodayDateYMD() {
+ return getFormattedTime(System.currentTimeMillis(), "yyyy-MM-dd");
+ }
+
+ /**
+ * @return 返回格式数据结构
+ * @describe 输入格式字符串, 返回格式的数据结构
+ * @args stringFormat String 格式字符串
+ */
+ public static SimpleDateFormat genDateFormat(String stringFormat) {
+ return new SimpleDateFormat(stringFormat, Locale.getDefault());
+ }
+
+ public static long utcTS2ms(String utcTS, String utcFMT) throws ParseException {
+ @SuppressLint("SimpleDateFormat")
+ SimpleDateFormat sdf = new SimpleDateFormat(utcFMT);
+ Calendar calendar = Calendar.getInstance();
+ Date date = sdf.parse(utcTS);
+ assert date != null;
+ calendar.setTime(date);
+ calendar.set(Calendar.HOUR, calendar.get(Calendar.HOUR) + tz2utcMSOffset()/3600000);
+ return calendar.getTime().getTime();
+ }
+
+ public static String msTSDis(long now,long then){
+ long dis = Math.abs(now-then);
+ if (dis<60000){
+ return dis/1000+"s ago";
+ }else if (dis<3600000){
+ return dis/60000+"min ago";
+ }else if (dis<86400000){
+ return dis/3600000+"h ago";
+ }else {
+ return getFormattedTime(then,"yyyy-MM-dd HH:mm:ss");
+ }
+ }
+
+ public static int tz2utcMSOffset(){
+ return TimeZone.getDefault().getOffset(System.currentTimeMillis());
+ }
+
+
+ /**
+ * @return String 返回日期
+ * @describe 输入星期的日期 返回汉字
+ * @args num int 输入的日期
+ */
+ public static String num2Chinese(int num) {
+ String[] c = {"六", "日", "一", "二", "三", "四", "五"};
+ if (num > -1 && num < c.length) {
+ return c[num];
+ } else {
+ return c[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/UiUtils.kt b/android/common/src/main/java/com/wh/common/util/UiUtils.kt
new file mode 100644
index 0000000..a8022f0
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/UiUtils.kt
@@ -0,0 +1,25 @@
+package com.wh.common.util
+
+import android.os.Build
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+
+object UiUtils {
+ fun hideSystemUI(window: Window) {
+ var v = View.SYSTEM_UI_FLAG_IMMERSIVE or
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ v = v or WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+ }
+ window.decorView.systemUiVisibility = v
+ }
+
+ fun keepScreenOn(window: Window){
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+}
\ No newline at end of file
diff --git a/android/common/src/main/java/com/wh/common/util/VersionCodeUtil.java b/android/common/src/main/java/com/wh/common/util/VersionCodeUtil.java
new file mode 100644
index 0000000..a4b3c38
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/util/VersionCodeUtil.java
@@ -0,0 +1,26 @@
+package com.wh.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+public class VersionCodeUtil {
+ /**
+ * 获取版本号
+ *
+ * @param context Context
+ * @return 版本号
+ */
+ public static int getVersionCode(Context context) {
+ PackageInfo pi;
+ int code = -1;
+ PackageManager pm = context.getPackageManager();
+ try {
+ pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS);
+ code = pi.versionCode;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return code;
+ }
+}
diff --git a/android/common/src/main/java/com/wh/common/viewmodel/IViewModelFactory.kt b/android/common/src/main/java/com/wh/common/viewmodel/IViewModelFactory.kt
new file mode 100644
index 0000000..939a2f6
--- /dev/null
+++ b/android/common/src/main/java/com/wh/common/viewmodel/IViewModelFactory.kt
@@ -0,0 +1,35 @@
+package com.wh.common.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+abstract class IViewModelFactory : ViewModelProvider.Factory {
+
+ abstract override fun create(modelClass: Class): T
+
+// override fun create(modelClass: Class): T {
+// when {
+// modelClass.isAssignableFrom(PortainerViewModel::class.java) -> {
+// @Suppress("UNCHECKED_CAST")
+// return PortainerViewModel(portainerStore) as T
+// }
+// modelClass.isAssignableFrom(QWeatherViewModel::class.java) -> {
+// @Suppress("UNCHECKED_CAST")
+// return QWeatherViewModel(qWeatherStore) as T
+// }
+// modelClass.isAssignableFrom(LoggerViewModel::class.java) -> {
+// @Suppress("UNCHECKED_CAST")
+// return LoggerViewModel(repositoryFactory.create(LoggerRepository::class.java)) as T
+// }
+// modelClass.isAssignableFrom(PortainerViewModel::class.java)->{
+// @Suppress("UNCHECKED_CAST")
+// return PortainerViewModel(portainerStore) as T
+// }
+// modelClass.isAssignableFrom(AppInfoViewModel::class.java)->{
+// @Suppress("UNCHECKED_CAST")
+// return AppInfoViewModel(settingStore,pm) as T
+// }
+// }
+// throw IllegalArgumentException("Unknown ViewModel class")
+// }
+}
\ No newline at end of file
diff --git a/android/common/src/test/java/com/wh/common/ExampleUnitTest.kt b/android/common/src/test/java/com/wh/common/ExampleUnitTest.kt
new file mode 100644
index 0000000..fb8ee2e
--- /dev/null
+++ b/android/common/src/test/java/com/wh/common/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.wh.common
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/compose/.gitignore b/android/compose/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/android/compose/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/android/compose/build.gradle b/android/compose/build.gradle
new file mode 100644
index 0000000..1785839
--- /dev/null
+++ b/android/compose/build.gradle
@@ -0,0 +1,48 @@
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 31
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.0'
+ implementation 'com.google.android.material:material:1.4.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ implementation "androidx.compose.ui:ui:$compose_version"
+ implementation "androidx.compose.material:material:$compose_version"
+ implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
+ implementation 'androidx.activity:activity-compose:1.4.0'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
+ debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
+}
\ No newline at end of file
diff --git a/android/compose/consumer-rules.pro b/android/compose/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/android/compose/proguard-rules.pro b/android/compose/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/android/compose/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/android/compose/src/androidTest/java/com/wh/sbw/compose/ExampleInstrumentedTest.kt b/android/compose/src/androidTest/java/com/wh/sbw/compose/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..5247f1a
--- /dev/null
+++ b/android/compose/src/androidTest/java/com/wh/sbw/compose/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.wh.sbw.compose
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.wh.sbw.compose.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/android/compose/src/main/AndroidManifest.xml b/android/compose/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e9afbff
--- /dev/null
+++ b/android/compose/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/compose/src/main/java/com/wh/sbw/compose/typeExt/LiveData.kt b/android/compose/src/main/java/com/wh/sbw/compose/typeExt/LiveData.kt
new file mode 100644
index 0000000..eb3de0f
--- /dev/null
+++ b/android/compose/src/main/java/com/wh/sbw/compose/typeExt/LiveData.kt
@@ -0,0 +1,22 @@
+package com.wh.sbw.compose.typeExt
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+
+fun LiveData.asMutableState(lifecycleOwner: LifecycleOwner, default: T): MutableState {
+ val tmp = mutableStateOf(default)
+ this.observe(lifecycleOwner) {
+ tmp.value = it
+ }
+ return tmp
+}
+
+fun LiveData.asMutableState(lifecycleOwner: LifecycleOwner): MutableState {
+ val tmp = mutableStateOf(this.value)
+ this.observe(lifecycleOwner) {
+ tmp.value = it
+ }
+ return tmp
+}
\ No newline at end of file
diff --git a/android/compose/src/main/java/com/wh/sbw/compose/typeExt/SnapshotStateList.kt b/android/compose/src/main/java/com/wh/sbw/compose/typeExt/SnapshotStateList.kt
new file mode 100644
index 0000000..6ceb9e0
--- /dev/null
+++ b/android/compose/src/main/java/com/wh/sbw/compose/typeExt/SnapshotStateList.kt
@@ -0,0 +1,11 @@
+package com.wh.sbw.compose.typeExt
+
+import androidx.compose.runtime.snapshots.SnapshotStateList
+
+fun SnapshotStateList.addOrSkip(t:T){
+ if (this.contains(t)){
+ return
+ }else{
+ this.add(t)
+ }
+}
\ No newline at end of file
diff --git a/android/compose/src/main/java/com/wh/sbw/compose/ui/componment/IComposeFragment.kt b/android/compose/src/main/java/com/wh/sbw/compose/ui/componment/IComposeFragment.kt
new file mode 100644
index 0000000..9c40605
--- /dev/null
+++ b/android/compose/src/main/java/com/wh/sbw/compose/ui/componment/IComposeFragment.kt
@@ -0,0 +1,9 @@
+package com.wh.sbw.compose.ui.componment
+
+import androidx.compose.runtime.Composable
+
+interface IComposeFragment {
+
+ @Composable
+ fun ComposeView()
+}
\ No newline at end of file
diff --git a/android/compose/src/test/java/com/wh/sbw/compose/ExampleUnitTest.kt b/android/compose/src/test/java/com/wh/sbw/compose/ExampleUnitTest.kt
new file mode 100644
index 0000000..858da85
--- /dev/null
+++ b/android/compose/src/test/java/com/wh/sbw/compose/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.wh.sbw.compose
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..98bed16
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e6edfd2
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Dec 24 16:13:47 CST 2021
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/android/gradlew b/android/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/android/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/android/gradlew.bat b/android/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/android/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..f3fedc9
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,12 @@
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+// maven { url 'https://developer.huawei.com/repo/' }
+ }
+}
+rootProject.name = "PushDeer"
+include ':app'
+include ':common'
+include ':compose'