Skip to content

Commit 64c3575

Browse files
committed
refactor(login): Optimize the login interface UI and operation logic
1 parent 47c35d3 commit 64c3575

File tree

13 files changed

+917
-770
lines changed

13 files changed

+917
-770
lines changed

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/data/model/LoginHistory.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ data class LoginHistory(
2323
@param:JsonProperty("isHttps")
2424
val isHttps: Boolean,
2525

26-
@get:JsonProperty("rememberMe")
27-
@param:JsonProperty("rememberMe")
28-
val rememberMe: Boolean,
26+
@get:JsonProperty("rememberPassword")
27+
@param:JsonProperty("rememberPassword")
28+
val rememberPassword: Boolean,
2929

3030
@get:JsonProperty("isNasLogin")
3131
@param:JsonProperty("isNasLogin")

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/data/network/impl/FnOfficialApiImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ class FnOfficialApiImpl() : FnOfficialApi {
319319
}
320320

321321
val authx = genAuthx(url, data = body)
322-
logger.i { "POST request, url: ${AccountDataCache.getFnOfficialBaseUrl()}$url, authx: $authx, body: $body" }
322+
logger.i { "POST request, url: ${AccountDataCache.getFnOfficialBaseUrl()}$url, authx: $authx, body: $body, cookie: ${AccountDataCache.cookieState}" }
323323
val response = fnOfficialClient.post("${AccountDataCache.getFnOfficialBaseUrl()}$url") {
324324
header(HttpHeaders.ContentType, "application/json; charset=utf-8")
325325
header("Authx", authx)

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/data/store/AccountDataCache.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ object AccountDataCache {
2626

2727
var isLoggedIn: Boolean = false
2828

29-
var rememberMe: Boolean = false
29+
var rememberPassword: Boolean = false
3030

3131
var isNasLogin: Boolean = false
3232

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/manager/LoginStateManager.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ object LoginStateManager {
7272
isHttps: Boolean,
7373
toastManager: ToastManager,
7474
loginViewModel: LoginViewModel,
75-
rememberMe: Boolean
75+
rememberPassword: Boolean
7676
) {
7777
// val loginState by loginViewModel.uiState.collectAsState()
7878
if (host.isBlank() || username.isBlank() || password.isBlank()) {
@@ -111,11 +111,12 @@ object LoginStateManager {
111111
AccountDataCache.userName = username
112112
val preferencesManager = PreferencesManager.getInstance()
113113
// 如果选择了记住账号,则保存账号密码和token
114-
if (rememberMe) {
114+
if (rememberPassword) {
115115
AccountDataCache.password = password
116-
AccountDataCache.rememberMe = true
116+
AccountDataCache.rememberPassword = true
117117
} else {
118-
AccountDataCache.rememberMe = false
118+
AccountDataCache.rememberPassword = false
119+
AccountDataCache.password = ""
119120
preferencesManager.clearLoginInfo()
120121
}
121122
preferencesManager.saveAllLoginInfo()

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/manager/PreferencesManager.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ class PreferencesManager private constructor() {
6161
AccountDataCache.parseCookie(cookie)
6262
AccountDataCache.refreshCookie()
6363
}
64-
AccountDataCache.rememberMe = settings.getBoolean("rememberMe", false)
64+
AccountDataCache.rememberPassword = settings.getBoolean("rememberPassword", false)
6565
AccountDataCache.isNasLogin = settings.getBoolean("isNasLogin", false)
6666
AccountDataCache.fnId = settings.getString("fnId", "")
67+
AccountDataCache.displayHost = settings.getString("displayHost", "")
6768
}
6869

6970
fun saveAllLoginInfo() {
@@ -76,9 +77,10 @@ class PreferencesManager private constructor() {
7677
settings.putBoolean("isLoggedIn", AccountDataCache.isLoggedIn)
7778
val cookie = AccountDataCache.cookieState
7879
settings.putString("cookie", cookie)
79-
settings.putBoolean("rememberMe", AccountDataCache.rememberMe)
80+
settings.putBoolean("rememberPassword", AccountDataCache.rememberPassword)
8081
settings.putBoolean("isNasLogin", AccountDataCache.isNasLogin)
8182
settings.putString("fnId", AccountDataCache.fnId)
83+
settings.putString("displayHost", AccountDataCache.displayHost)
8284
}
8385

8486
fun saveToken(token: String) {
@@ -88,15 +90,17 @@ class PreferencesManager private constructor() {
8890
}
8991

9092
fun clearLoginInfo() {
91-
settings.remove("username")
93+
// settings.remove("username")
9294
settings.remove("password")
93-
settings.remove("token")
94-
settings.remove("isHttps")
95-
settings.remove("host")
96-
settings.remove("port")
97-
settings.remove("cookie")
98-
settings.remove("isLoggedIn")
99-
settings.remove("rememberMe")
95+
// settings.remove("token")
96+
// settings.remove("isHttps")
97+
// settings.remove("host")
98+
// settings.remove("port")
99+
// settings.remove("cookie")
100+
// settings.remove("isLoggedIn")
101+
// settings.remove("rememberMe")
102+
// settings.remove("isNasLogin")
103+
// settings.remove("displayHost")
100104
}
101105

102106
fun hasSavedCredentials(): Boolean {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.jankinwu.fntv.client.processor
2+
3+
import co.touchlab.kermit.Logger
4+
import com.jankinwu.fntv.client.data.model.LoginHistory
5+
import com.jankinwu.fntv.client.data.model.request.AuthRequest
6+
import com.jankinwu.fntv.client.data.network.impl.FnOfficialApiImpl
7+
import com.jankinwu.fntv.client.data.store.AccountDataCache
8+
import com.jankinwu.fntv.client.manager.LoginStateManager
9+
import com.jankinwu.fntv.client.manager.PreferencesManager
10+
import com.jankinwu.fntv.client.ui.component.common.ToastManager
11+
import com.jankinwu.fntv.client.ui.component.common.ToastType
12+
import com.multiplatform.webview.cookie.Cookie
13+
import com.multiplatform.webview.web.WebViewNavigator
14+
import com.multiplatform.webview.web.WebViewState
15+
import kotlinx.serialization.json.Json
16+
import kotlinx.serialization.json.JsonObject
17+
import kotlinx.serialization.json.contentOrNull
18+
import kotlinx.serialization.json.jsonObject
19+
import kotlinx.serialization.json.jsonPrimitive
20+
21+
class NetworkMessageProcessor(
22+
private val fnOfficialApi: FnOfficialApiImpl,
23+
private val toastManager: ToastManager,
24+
private val webViewState: WebViewState,
25+
private val navigator: WebViewNavigator,
26+
private val onLoginSuccess: (LoginHistory) -> Unit,
27+
private val fnId: String,
28+
private val autoLoginUsername: String?
29+
) {
30+
private val logger = Logger.withTag("NetworkMessageProcessor")
31+
private var isAuthRequested = false
32+
private var isSysConfigInFlight = false
33+
private var isSysConfigLoaded = false
34+
35+
suspend fun process(
36+
params: String,
37+
baseUrl: String,
38+
onBaseUrlChange: (String) -> Unit,
39+
capturedUsername: String,
40+
capturedPassword: String,
41+
capturedRememberPassword: Boolean
42+
) {
43+
logger.i("Intercepted: $params")
44+
try {
45+
val json = Json.parseToJsonElement(params).jsonObject
46+
val type = json["type"]?.jsonPrimitive?.contentOrNull
47+
val url = json["url"]?.jsonPrimitive?.contentOrNull ?: ""
48+
49+
if (type == "XHR" && url.contains("/sac/rpcproxy/v1/new-user-guide/status")) {
50+
handleXhrMessage(json, baseUrl, onBaseUrlChange)
51+
} else if (type == "Response" && url.contains("/oauthapi/authorize")) {
52+
handleResponseMessage(json, baseUrl, capturedUsername, capturedPassword, capturedRememberPassword)
53+
}
54+
} catch (e: Exception) {
55+
logger.e("Handler error", e)
56+
}
57+
}
58+
59+
private suspend fun handleXhrMessage(
60+
json: JsonObject,
61+
baseUrl: String,
62+
onBaseUrlChange: (String) -> Unit
63+
) {
64+
val cookie = json["cookie"]?.jsonPrimitive?.contentOrNull
65+
logger.i("fnos cookie: $cookie")
66+
if (!cookie.isNullOrBlank()) {
67+
AccountDataCache.mergeCookieString(cookie)
68+
if (baseUrl.contains("5ddd.com")) {
69+
// 使用 FN Connect 外网访问必加此 Cookie 不然访问不了
70+
AccountDataCache.insertCookie("mode" to "relay")
71+
}
72+
if (!isSysConfigLoaded && !isSysConfigInFlight) {
73+
isSysConfigInFlight = true
74+
try {
75+
val config = fnOfficialApi.getSysConfig()
76+
logger.i("Got sys config: $config")
77+
val oauth = config.nasOauth
78+
var currentBaseUrl = baseUrl
79+
if (oauth.url.isNotBlank() && oauth.url != "://") {
80+
currentBaseUrl = oauth.url
81+
onBaseUrlChange(currentBaseUrl)
82+
}
83+
val appId = oauth.appId
84+
val redirectUri = "$currentBaseUrl/v/oauth/result"
85+
val targetUrl = "$currentBaseUrl/signin?client_id=$appId&redirect_uri=$redirectUri"
86+
87+
logger.i("Navigating to OAuth: $targetUrl")
88+
val domain = currentBaseUrl.substringAfter("://").substringBefore(":").substringBefore("/")
89+
cookie.split(";").forEach {
90+
val parts = it.trim().split("=", limit = 2)
91+
if (parts.size == 2) {
92+
val cookieObj = Cookie(
93+
name = parts[0],
94+
value = parts[1],
95+
domain = domain
96+
)
97+
webViewState.cookieManager.setCookie(currentBaseUrl, cookieObj)
98+
}
99+
}
100+
isSysConfigLoaded = true
101+
navigator.loadUrl(targetUrl)
102+
} catch (e: Exception) {
103+
isSysConfigInFlight = false
104+
logger.e("Failed to get sys config", e)
105+
toastManager.showToast("获取系统配置失败: ${e.message}", ToastType.Failed)
106+
}
107+
}
108+
}
109+
}
110+
111+
private suspend fun handleResponseMessage(
112+
json: JsonObject,
113+
baseUrl: String,
114+
capturedUsername: String,
115+
capturedPassword: String,
116+
capturedRememberPassword: Boolean
117+
) {
118+
if (!isAuthRequested) {
119+
val body = json["body"]?.jsonPrimitive?.contentOrNull
120+
if (!body.isNullOrBlank()) {
121+
try {
122+
val bodyJson = Json.parseToJsonElement(body).jsonObject
123+
val data = bodyJson["data"]?.jsonObject
124+
val code = data?.get("code")?.jsonPrimitive?.contentOrNull
125+
if (code != null) {
126+
isAuthRequested = true
127+
try {
128+
val response = fnOfficialApi.auth(AuthRequest("Trim-NAS", code))
129+
val token = response.token
130+
if (token.isNotBlank()) {
131+
AccountDataCache.authorization = token
132+
AccountDataCache.insertCookie("Trim-MC-token" to token)
133+
logger.i("cookie: ${AccountDataCache.cookieState}")
134+
LoginStateManager.updateLoginStatus(true)
135+
toastManager.showToast("登录成功", ToastType.Success)
136+
137+
val normalizedUsername = capturedUsername.trim()
138+
.ifBlank { autoLoginUsername?.trim().orEmpty() }
139+
if (normalizedUsername.isNotBlank()) {
140+
PreferencesManager.getInstance().addLoginUsernameHistory(normalizedUsername)
141+
}
142+
val shouldRemember = capturedRememberPassword && capturedPassword.isNotBlank()
143+
logger.i("Remember password: $capturedRememberPassword")
144+
val history = LoginHistory(
145+
host = "",
146+
port = 0,
147+
username = normalizedUsername,
148+
password = if (shouldRemember) capturedPassword else null,
149+
isHttps = baseUrl.startsWith("https"),
150+
rememberPassword = shouldRemember,
151+
isNasLogin = true,
152+
fnConnectUrl = baseUrl,
153+
fnId = fnId.trim()
154+
)
155+
onLoginSuccess(history)
156+
} else {
157+
isAuthRequested = false
158+
toastManager.showToast("登录失败: Token 为空", ToastType.Failed)
159+
}
160+
} catch (e: Exception) {
161+
isAuthRequested = false
162+
logger.e("OAuth result failed", e)
163+
toastManager.showToast("登录失败: ${e.message}", ToastType.Failed)
164+
}
165+
}
166+
} catch (e: Exception) {
167+
logger.e("Failed to parse OAuth response", e)
168+
}
169+
}
170+
}
171+
}
172+
}

composeApp/src/commonMain/kotlin/com/jankinwu/fntv/client/ui/component/common/dialog/Dialog.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.jankinwu.fntv.client.ui.component.common.dialog
22

3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.interaction.MutableInteractionSource
35
import androidx.compose.foundation.layout.Arrangement
46
import androidx.compose.foundation.layout.Box
57
import androidx.compose.foundation.layout.Column
@@ -15,18 +17,20 @@ import androidx.compose.material3.Button
1517
import androidx.compose.material3.ButtonDefaults
1618
import androidx.compose.material3.Icon
1719
import androidx.compose.material3.Surface
18-
import androidx.compose.material3.TextButton
1920
import androidx.compose.runtime.Composable
2021
import androidx.compose.runtime.CompositionLocalProvider
2122
import androidx.compose.runtime.getValue
2223
import androidx.compose.runtime.mutableStateOf
2324
import androidx.compose.runtime.remember
2425
import androidx.compose.runtime.setValue
2526
import androidx.compose.ui.Alignment
27+
import androidx.compose.ui.ExperimentalComposeUiApi
2628
import androidx.compose.ui.Modifier
2729
import androidx.compose.ui.graphics.Color
2830
import androidx.compose.ui.graphics.vector.ImageVector
31+
import androidx.compose.ui.input.pointer.PointerEventType
2932
import androidx.compose.ui.input.pointer.PointerIcon
33+
import androidx.compose.ui.input.pointer.onPointerEvent
3034
import androidx.compose.ui.input.pointer.pointerHoverIcon
3135
import androidx.compose.ui.platform.LocalUriHandler
3236
import androidx.compose.ui.text.font.FontWeight
@@ -53,6 +57,7 @@ import io.github.composefluent.component.DialogSize
5357
import io.github.composefluent.component.FluentDialog
5458
import io.github.composefluent.component.Text
5559

60+
@OptIn(ExperimentalComposeUiApi::class)
5661
@Composable
5762
fun ForgotPasswordDialog() {
5863
var displayDialog by remember { mutableStateOf(false) }
@@ -79,9 +84,20 @@ fun ForgotPasswordDialog() {
7984
}
8085
}
8186
}
82-
TextButton(onClick = { displayDialog = true }) {
83-
androidx.compose.material3.Text("忘记密码?", color = HintColor, fontSize = 14.sp)
84-
}
87+
var isHovered by remember { mutableStateOf(false) }
88+
Text(
89+
"忘记密码?",
90+
color = if (isHovered) Color.White else HintColor,
91+
fontSize = 14.sp,
92+
modifier = Modifier
93+
.onPointerEvent(PointerEventType.Enter) { isHovered = true }
94+
.onPointerEvent(PointerEventType.Exit) { isHovered = false }
95+
.clickable(
96+
interactionSource = remember { MutableInteractionSource() },
97+
indication = null,
98+
onClick = { displayDialog = true })
99+
.pointerHoverIcon(PointerIcon.Hand)
100+
)
85101
}
86102

87103
@Composable

0 commit comments

Comments
 (0)