Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/dictionaries/project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ LightNovelReader 是一个完全免费、开源的项目。
如果你喜欢这个项目或它对你有所帮助,欢迎通过 [爱发电](https://www.ifdian.net/a/lightnovelreader) 支持我们。
所有款项将用于持续开发、新功能的实现、(如果有)服务器维护以及社区建设。

## 翻译

[![Crowdin](https://badges.crowdin.net/lightnovelreader/localized.svg)](https://crowdin.com/project/lightnovelreader)

LightNovelReader 使用 [Crowdin](https://crowdin.com/project/lightnovelreader) 管理翻译工作。如果你希望帮助翻译或改进现有的翻译,欢迎前往 Crowdin 项目页面参与贡献!

> 没有找到你的语言?欢迎在 [Crowdin](https://crowdin.com/project/lightnovelreader) 申请添加新语言!

## License

```
Expand Down
8 changes: 8 additions & 0 deletions README_TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ LightNovelReader 是一個完全免費、開源的專案。
所有贊助款項將用於持續開發、新功能實作、(若有)伺服器維護及社群建設。
你的支持讓我們能不斷前進,打造更好的閱讀體驗。

## 翻譯

[![Crowdin](https://badges.crowdin.net/lightnovelreader/localized.svg)](https://crowdin.com/project/lightnovelreader)

LightNovelReader 使用 [Crowdin](https://crowdin.com/project/lightnovelreader) 管理在地化工作。如果你希望協助翻譯或改進現有的譯文,歡迎前往 Crowdin 專案頁面參與貢獻!

> 找不到你的語言?歡迎在 [Crowdin](https://crowdin.com/project/lightnovelreader) 申請新增語言!

## 授權條款

```
Expand Down
7 changes: 7 additions & 0 deletions README_US.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ If you enjoy using it or find it helpful, consider supporting us through [Aifadi
All contributions go toward continuous development, new features, possible future server maintenance, and community growth.
Your support helps keep the project alive and makes reading even better for everyone.

## Translation

[![Crowdin](https://badges.crowdin.net/lightnovelreader/localized.svg)](https://crowdin.com/project/lightnovelreader)

LightNovelReader uses [Crowdin](https://crowdin.com/project/lightnovelreader) to manage translations. Want to help localize the app into your language? Head over to the Crowdin project to contribute!

> Don't see your language? Request it on [Crowdin](https://crowdin.com/project/lightnovelreader)!

## License

Expand Down
10 changes: 7 additions & 3 deletions api/src/main/kotlin/io/nightfish/lightnovelreader/api/Route.kt
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ object Route {
/** 已安装插件列表界面路由 */
@Serializable
object AppList
/** 插件仓库界面路由 */
@Serializable
object Repository
}
/** 数据源切换界面路由组 */
@Serializable
Expand Down Expand Up @@ -290,4 +287,11 @@ object Route {
/** 下载管理器路由 */
@Serializable
object DownloadManager

/** 插件商店安装底栏
*
* @param pluginId 目标插件id
*/
@Serializable
data class PluginStoreInstall(val pluginId: String)
}
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lightnovelreader"/>
</intent-filter>
</activity>
<activity-alias
android:name=".PluginInstallAlias"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package indi.dmzz_yyhyy.lightnovelreader

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import indi.dmzz_yyhyy.lightnovelreader.data.logging.LogLevel
import indi.dmzz_yyhyy.lightnovelreader.data.logging.LoggerRepository
import indi.dmzz_yyhyy.lightnovelreader.data.plugin.PluginManager
import indi.dmzz_yyhyy.lightnovelreader.data.plugin.PluginUpdateCheckRepository
import indi.dmzz_yyhyy.lightnovelreader.data.userdata.UserDataRepository
import indi.dmzz_yyhyy.lightnovelreader.utils.CxHttpInit
import indi.dmzz_yyhyy.lightnovelreader.utils.analytics.MatomoAnalytics
Expand All @@ -32,6 +31,7 @@ class LightNovelReaderApplication : Application(), Configuration.Provider {
@Inject lateinit var loggerRepository: LoggerRepository
@Inject lateinit var userDataRepository: UserDataRepository
@Inject lateinit var pluginManager: PluginManager
@Inject lateinit var pluginUpdateCheckRepository: PluginUpdateCheckRepository
@Inject lateinit var matomoAnalytics: MatomoAnalytics

override val workManagerConfiguration: Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ class PluginManager @Inject constructor(
val pluginsTempDir: File = appContext.cacheDir.resolve("plugins_tmp")
var appPluginInfos: List<PluginAppInfo> = emptyList()
private set

private val onInitializedCallbacks = mutableListOf<() -> Unit>()
fun addOnInitializedCallback(callback: () -> Unit) {
onInitializedCallbacks += callback
}
fun getPluginDir(name: String): File = pluginsDir.resolve(name)
fun getPluginDataDir(pluginDir: File) = pluginDir.resolve("data")
fun getPluginFile(pluginDir: File): File = pluginDir.resolve("plugin")
Expand Down Expand Up @@ -140,13 +145,6 @@ class PluginManager @Inject constructor(

runCatching {
val apkFile = File(apkPath)
apkFile.inputStream().buffered().use { inputStream ->
val tempDir = pluginsTempDir.also { it.mkdirs() }
val tempFile = File(tempDir, "install_${System.currentTimeMillis()}.apk")
tempFile.outputStream().buffered().use {
inputStream.copyTo(it)
}
}
var error: InstallState.Error? = null
runBlocking(Dispatchers.IO) {
installPlugin(apkFile).collect {
Expand Down Expand Up @@ -175,13 +173,15 @@ class PluginManager @Inject constructor(
}

fun initAllPlugin() {
pluginsTempDir.deleteRecursively()
webBookDataSourceManager.loadWebDataSourceFromClass(
Wenku8Api::class.java,
pluginInjector
)
appPluginInfos = initAllAppPlugin()
val enabledPlugins = enabledPluginsUserData.getOrDefault(emptyList())
val pluginDirs = pluginsDir.listFiles() ?: return
val pluginDirs = pluginsDir.listFiles()
if (pluginDirs != null) {
for (dir in pluginDirs) {
if (getPluginInstallLock(dir).exists()) continue
val metadataFile = getPluginMetadataFile(dir)
Expand Down Expand Up @@ -215,6 +215,8 @@ class PluginManager @Inject constructor(
}
}
}
}
onInitializedCallbacks.forEach { it() }
}

private fun extractAssetFromApk(apk: File, targetDir: File) = runCatching {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package indi.dmzz_yyhyy.lightnovelreader.data.plugin

import android.util.Log
import indi.dmzz_yyhyy.lightnovelreader.BuildConfig
import indi.dmzz_yyhyy.lightnovelreader.utils.update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PluginUpdateCheckRepository @Inject constructor(
private val pluginManager: PluginManager
) {
companion object {
private const val TAG = "PluginUpdateCheck"
}

private val _updates = MutableStateFlow<Map<String, PluginUpdateInfo>>(emptyMap())
val updates: StateFlow<Map<String, PluginUpdateInfo>> = _updates

private val json = Json { ignoreUnknownKeys = true }
private val base = "eNpb85aBtYRBK6OkpKDYSl-_IKc0PTOvWC8vsSgzO18vvyhdP7EgEyZsn5liCwDAsBIV"
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val httpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build()

init {
pluginManager.addOnInitializedCallback {
coroutineScope.launch { checkAll() }
}
}

private suspend fun checkAll() {
Log.d(TAG, "START checking updates")
val plugins = pluginManager.allPluginList.toList()
val result = mutableMapOf<String, PluginUpdateInfo>()
plugins.forEach { meta ->
if (meta.source == PluginSource.InstalledApp) return@forEach
val pluginId = meta.packageName
Log.d(TAG, "Checking updates for plugin $pluginId")
try {
val url = "${update(base)}$pluginId&ref=lnr-app&ver=${BuildConfig.VERSION_NAME}"
val request = Request.Builder().url(url).get().build()
val body = httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("HTTP ${response.code}")
response.body.string()
}
val storePlugin = json.decodeFromJsonElement(
StorePlugin.serializer(),
json.parseToJsonElement(body).jsonObject["plugin"]!!
)
val remoteVersionCode = storePlugin.release.versionCode ?: return@forEach
if (remoteVersionCode > meta.version) {
Log.d(TAG, "Updates available for $pluginId: ${storePlugin.release.versionName} ($remoteVersionCode)")

result[meta.packageName] = PluginUpdateInfo(
pluginId = pluginId,
versionName = storePlugin.release.versionName,
storePlugin = storePlugin
)
} else {
Log.d(TAG, "Up to date for $pluginId: ${storePlugin.release.versionName} ($remoteVersionCode)")
}
} catch (_: Exception) {
}
}
_updates.emit(result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package indi.dmzz_yyhyy.lightnovelreader.data.plugin

data class PluginUpdateInfo(
val pluginId: String,
val versionName: String,
val storePlugin: StorePlugin
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package indi.dmzz_yyhyy.lightnovelreader.data.plugin

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class StorePlugin(
val id: String,
val name: String,
val author: String,
val summary: String,
val description: String = "",
@SerialName("is_nsfw") val isNsfw: Boolean = false,
val compatibility: Compatibility = Compatibility(),
val release: Release,
val assets: Assets = Assets(),
val download: Download,
val changelog: String = ""
) {
@Serializable
data class Compatibility(
@SerialName("target_api") val targetApi: Int? = null
)

@Serializable
data class Release(
@SerialName("version_name") val versionName: String,
@SerialName("version_code") val versionCode: Int? = null
)

@Serializable
data class Assets(
val icon: Icon? = null
) {
@Serializable
data class Icon(val url: String)
}

@Serializable
data class Download(
val type: String,
@SerialName("output_file") val outputFile: String,
@SerialName("size_bytes") val sizeBytes: Long? = null,
val parts: List<Part>
) {
@Serializable
data class Part(
@SerialName("file_name") val fileName: String,
val url: String,
@SerialName("size_bytes") val sizeBytes: Long? = null
)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package indi.dmzz_yyhyy.lightnovelreader.data.update

import android.util.Log
import indi.dmzz_yyhyy.lightnovelreader.BuildConfig
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile

/**
Expand All @@ -18,6 +20,11 @@ object APIParser {
private const val BASE_URL = "https://lnr.nariko.org"
private const val API_PATH = "/api/update"

private val httpClient = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()

class APIRelease(
override val version: Int,
override val versionName: String,
Expand All @@ -33,22 +40,19 @@ object APIParser {
): Release? {
return try {
updatePhase.tryEmit("API步骤: 正在请求 $channel 频道更新信息")
val url = URL("$BASE_URL$API_PATH?channel=$channel")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 5000
connection.readTimeout = 10000
connection.setRequestProperty("Accept", "application/json")

val responseCode = connection.responseCode
if (responseCode != HttpURLConnection.HTTP_OK) {
Log.e(TAG, "API request failed with status code: $responseCode")
return null
val request = Request.Builder()
.url("$BASE_URL$API_PATH?channel=$channel&ref=lnr-app&ver=${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.get()
.build()
val responseBody = httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Log.e(TAG, "API request failed with status code: ${response.code}")
return null
}
response.body.string()
}

val responseBody = connection.inputStream.bufferedReader().use { it.readText() }
connection.disconnect()

updatePhase.tryEmit("API步骤: 解析更新信息")
val json = JSONObject(responseBody)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.navigation.compose.rememberNavController
import android.content.Intent
import indi.dmzz_yyhyy.lightnovelreader.ui.dialog.UpdatesAvailableDialogViewModel
import indi.dmzz_yyhyy.lightnovelreader.ui.dialog.navigateToPluginInstallerDialog
import indi.dmzz_yyhyy.lightnovelreader.ui.dialog.navigateToPluginStoreInstall
import indi.dmzz_yyhyy.lightnovelreader.ui.dialog.navigateUpdatesAvailableDialog
import indi.dmzz_yyhyy.lightnovelreader.ui.navigation.LightNovelReaderNavHost
import io.nightfish.lightnovelreader.api.ui.ReaderStyle
Expand All @@ -34,8 +35,12 @@ fun LightNovelReaderApp(
LaunchedEffect(Unit) {
intentFlow.collect { intent ->
if (intent.action == Intent.ACTION_VIEW) {
intent.data?.toString()?.let { uriString ->
navController.navigateToPluginInstallerDialog(uriString)
val uri = intent.data ?: return@collect
if (uri.scheme == "lightnovelreader" && uri.host == "install_plugin") {
val pluginId = uri.getQueryParameter("id") ?: return@collect
navController.navigateToPluginStoreInstall(pluginId)
} else {
navController.navigateToPluginInstallerDialog(uri.toString())
}
}
}
Expand Down
Loading
Loading