|
|
@@ -29,12 +29,38 @@ import io.legado.app.utils.normalizeFileName
|
|
|
import io.legado.app.utils.toastOnUi
|
|
|
import kotlinx.coroutines.currentCoroutineContext
|
|
|
import kotlinx.coroutines.ensureActive
|
|
|
+import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
+import kotlinx.coroutines.flow.StateFlow
|
|
|
+import kotlinx.coroutines.flow.asStateFlow
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
import kotlinx.coroutines.sync.withLock
|
|
|
import splitties.init.appCtx
|
|
|
import org.koin.core.context.GlobalContext
|
|
|
import java.io.File
|
|
|
|
|
|
+/**
|
|
|
+ * WebDAV 配置就绪状态(本地配置 + 本次进程的网络初始化结果)。
|
|
|
+ *
|
|
|
+ * UI 只消费这个状态来开关「拉取/覆盖云端进度」这类入口,
|
|
|
+ * 不再直接读一次性的 [AppWebDav.authorization]。
|
|
|
+ */
|
|
|
+sealed interface WebDavConfigState {
|
|
|
+ /** 尚未取得过状态(App 刚启动,[AppWebDav.upConfig] 还没跑)。 */
|
|
|
+ data object Idle : WebDavConfigState
|
|
|
+
|
|
|
+ /** 本地没有账号或密码。 */
|
|
|
+ data object Unconfigured : WebDavConfigState
|
|
|
+
|
|
|
+ /** 有配置,正在做网络初始化(校验授权 + 建目录)。 */
|
|
|
+ data object Preparing : WebDavConfigState
|
|
|
+
|
|
|
+ /** 本次进程内初始化成功,可真正读写云端。 */
|
|
|
+ data object Ready : WebDavConfigState
|
|
|
+
|
|
|
+ /** 网络初始化失败(账号密码错、服务不支持 MKCOL、无网络等)。 */
|
|
|
+ data class Failed(val message: String?) : WebDavConfigState
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* webDav初始化会访问网络,不要放到主线程
|
|
|
*/
|
|
|
@@ -49,6 +75,15 @@ object AppWebDav {
|
|
|
private val configMutex = Mutex()
|
|
|
private var appliedConfig: AppliedWebDavConfig? = null
|
|
|
|
|
|
+ /**
|
|
|
+ * 云端配置(本地账号密码)的就绪状态,供 UI 观测。
|
|
|
+ *
|
|
|
+ * [upConfig] 会访问网络,成功与否不能靠「读一次 [isOk]」来判断:UI 的取值是快照,
|
|
|
+ * 初始化晚到或失败时菜单会永远消失。故把状态建模成 flow,由 UI 订阅。
|
|
|
+ */
|
|
|
+ private val _configState = MutableStateFlow<WebDavConfigState>(WebDavConfigState.Idle)
|
|
|
+ val configState: StateFlow<WebDavConfigState> = _configState.asStateFlow()
|
|
|
+
|
|
|
@Volatile
|
|
|
var authorization: Authorization? = null
|
|
|
private set
|
|
|
@@ -56,6 +91,19 @@ object AppWebDav {
|
|
|
@Volatile
|
|
|
var defaultBookWebDav: RemoteBookWebDav? = null
|
|
|
|
|
|
+ /**
|
|
|
+ * 是否已配置 WebDAV 账号(纯本地判断,不触发网络)。
|
|
|
+ *
|
|
|
+ * 与上游一致:菜单可见性只看「配置了没」,网络校验属于 [upConfig] 的职责。
|
|
|
+ * 此前这里返回 `authorization != null`,等于要求本次进程内已经完成过一次
|
|
|
+ * 成功的网络初始化 + 目录创建,初始化失败就会让整个进度同步入口永久消失。
|
|
|
+ */
|
|
|
+ val isConfigured: Boolean
|
|
|
+ get() = with(backupGateway.currentSettings) {
|
|
|
+ webDavAccount.trim().isNotEmpty() && webDavPassword.isNotEmpty()
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 本次进程内初始化是否已成功(能真正读写云端)。 */
|
|
|
val isOk get() = authorization != null
|
|
|
|
|
|
val isJianGuoYun get() = rootWebDavUrl.startsWith(defaultWebDavUrl, true)
|
|
|
@@ -81,27 +129,56 @@ object AppWebDav {
|
|
|
password = backupGateway.currentSettings.webDavPassword,
|
|
|
dir = backupGateway.currentSettings.webDavDir,
|
|
|
)
|
|
|
+ // 配置没变时状态只会是 Ready 或 Unconfigured,两者都无需重新网络初始化。
|
|
|
if (appliedConfig == config) return
|
|
|
|
|
|
+ if (config.account.isEmpty() || config.password.isEmpty()) {
|
|
|
+ appliedConfig = config
|
|
|
+ authorization = null
|
|
|
+ defaultBookWebDav = null
|
|
|
+ _configState.value = WebDavConfigState.Unconfigured
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ _configState.value = WebDavConfigState.Preparing
|
|
|
kotlin.runCatching {
|
|
|
+ val mAuthorization = Authorization(config.account, config.password)
|
|
|
+ checkAuthorization(mAuthorization)
|
|
|
+ WebDav(rootWebDavUrl, mAuthorization).makeAsDir()
|
|
|
+ WebDav(bookProgressUrl, mAuthorization).makeAsDir()
|
|
|
+ WebDav(exportsWebDavUrl, mAuthorization).makeAsDir()
|
|
|
+ WebDav(bgWebDavUrl, mAuthorization).makeAsDir()
|
|
|
+ val rootBooksUrl = "${rootWebDavUrl}books/"
|
|
|
+ defaultBookWebDav = RemoteBookWebDav(rootBooksUrl, mAuthorization)
|
|
|
+ authorization = mAuthorization
|
|
|
+ _configState.value = WebDavConfigState.Ready
|
|
|
+ }.onFailure { e ->
|
|
|
+ // 不再静默:此前异常被吞掉,UI 上表现为进度同步入口无声消失。
|
|
|
authorization = null
|
|
|
defaultBookWebDav = null
|
|
|
- if (config.account.isNotEmpty() && config.password.isNotEmpty()) {
|
|
|
- val mAuthorization = Authorization(config.account, config.password)
|
|
|
- checkAuthorization(mAuthorization)
|
|
|
- WebDav(rootWebDavUrl, mAuthorization).makeAsDir()
|
|
|
- WebDav(bookProgressUrl, mAuthorization).makeAsDir()
|
|
|
- WebDav(exportsWebDavUrl, mAuthorization).makeAsDir()
|
|
|
- WebDav(bgWebDavUrl, mAuthorization).makeAsDir()
|
|
|
- val rootBooksUrl = "${rootWebDavUrl}books/"
|
|
|
- defaultBookWebDav = RemoteBookWebDav(rootBooksUrl, mAuthorization)
|
|
|
- authorization = mAuthorization
|
|
|
- }
|
|
|
- appliedConfig = config
|
|
|
+ _configState.value = WebDavConfigState.Failed(e.localizedMessage)
|
|
|
+ AppLog.put("webDav初始化失败\n${e.localizedMessage}", e)
|
|
|
}
|
|
|
+ // appliedConfig 只在成功时推进,失败留白以便下次设置变化或显式调用时重试。
|
|
|
+ if (_configState.value == WebDavConfigState.Ready) appliedConfig = config
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 进度同步等入口在打开菜单时调用:配置齐备但本次进程尚未初始化成功时补一次。
|
|
|
+ *
|
|
|
+ * 等价于上游 `onPrepareOptionsMenu` 每次开菜单都重算 `AppWebDav.isOk` 的效果,
|
|
|
+ * 但把网络动作留在 IO,UI 只读 [configState]。
|
|
|
+ */
|
|
|
+ suspend fun ensureConfigured() {
|
|
|
+ if (isOk) return
|
|
|
+ if (!isConfigured) {
|
|
|
+ _configState.value = WebDavConfigState.Unconfigured
|
|
|
+ return
|
|
|
+ }
|
|
|
+ upConfig()
|
|
|
+ }
|
|
|
+
|
|
|
private data class AppliedWebDavConfig(
|
|
|
val url: String,
|
|
|
val account: String,
|
|
|
@@ -188,6 +265,10 @@ object AppWebDav {
|
|
|
val auth = Authorization(account, password)
|
|
|
checkAuthorization(auth)
|
|
|
|
|
|
+ // 上游 check() 会真正建立 configuredWebDav;这里同样落一次配置,
|
|
|
+ // 否则「测试同步」通过后 authorization 仍是空,阅读界面的进度同步入口不会出现。
|
|
|
+ upConfig()
|
|
|
+
|
|
|
appCtx.toastOnUi("WebDAV 服务可用")
|
|
|
true
|
|
|
}.getOrElse {
|