diff --git a/README.md b/README.md
new file mode 100644
index 000000000..d65e3dbdc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# legado
+## 阅读3.0
diff --git a/app/build.gradle b/app/build.gradle
index 966ba8516..c299deee3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,29 +2,60 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
+apply plugin: "de.timfreiheit.resourceplaceholders"
+apply plugin: 'io.fabric'
androidExtensions {
experimental = true
}
+static def releaseTime() {
+ return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8"))
+}
+
+def name = "legado"
+def version = "0." + releaseTime()
+def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim())
+
android {
compileSdkVersion 28
- dataBinding {
- enabled = true
+ signingConfigs {
+ myConfig {
+ storeFile file(RELEASE_STORE_FILE)
+ storePassword RELEASE_STORE_PASSWORD
+ keyAlias RELEASE_KEY_ALIAS
+ keyPassword RELEASE_KEY_PASSWORD
+ v1SigningEnabled true
+ v2SigningEnabled true
+ }
}
defaultConfig {
applicationId "io.legado.app"
minSdkVersion 21
targetSdkVersion 28
- versionCode 1
- versionName "1.0"
+ versionCode gitCommits
+ versionName version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ project.ext.set("archivesBaseName", name + "_" + version)
}
buildTypes {
release {
+ signingConfig signingConfigs.myConfig
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
+ debug {
+ signingConfig signingConfigs.myConfig
+ applicationIdSuffix '.debug'
+ versionNameSuffix 'debug'
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ android.applicationVariants.all { variant ->
+ variant.outputs.all {
+ outputFileName = "${name}_${defaultConfig.versionName}.apk"
+ }
+ }
}
compileOptions {
@@ -32,8 +63,14 @@ android {
targetCompatibility = '1.8'
}
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
}
+resourcePlaceholders {
+ files = ['xml/shortcuts.xml']
+}
kapt {
arguments {
@@ -41,51 +78,83 @@ kapt {
}
}
-kotlin{
- experimental{
- coroutines "enable"
- }
-}
-
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
-
- implementation 'androidx.core:core-ktx:1.0.2'
- implementation 'androidx.appcompat:appcompat:1.0.2'
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ //kotlin
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+
+ //fireBase
+ implementation 'com.google.firebase:firebase-core:17.2.0'
+ implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
+
+ //androidX
+ implementation 'androidx.core:core-ktx:1.2.0-beta01'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.media:media:1.1.0'
+ implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'androidx.recyclerview:recyclerview:1.0.0'
- implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'com.google.android.material:material:1.0.0'
-
- implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- kapt 'androidx.lifecycle:lifecycle-compiler:2.0.0'
-
- implementation 'androidx.room:room-runtime:2.0.0'
- kapt 'androidx.room:room-compiler:2.0.0'
-
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
+ implementation 'androidx.viewpager2:viewpager2:1.0.0-beta05'
+ implementation 'com.google.android.material:material:1.1.0-beta01'
+ implementation 'com.google.android:flexbox:1.1.0'
+
+ //lifecycle
+ def lifecycle_version = '2.1.0'
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
+
+ //room
+ def room_version = '2.2.0'
+ implementation "androidx.room:room-runtime:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
+
+ //paging
implementation 'androidx.paging:paging-runtime:2.1.0'
//anko
def anko_version = '0.10.8'
implementation "org.jetbrains.anko:anko-sdk27:$anko_version"
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
+
+ //liveEventBus
+ implementation 'com.jeremyliao:live-event-bus-x:1.4.5'
+
//协程
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
+ def coroutines_version = '1.2.2'
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+
//规则相关
- implementation 'pub.devrel:easypermissions:3.0.0'
implementation 'com.google.code.gson:gson:2.8.5'
- implementation 'com.jayway.jsonpath:json-path:2.4.0'
implementation 'org.jsoup:jsoup:1.12.1'
+ implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
+ implementation 'com.jayway.jsonpath:json-path:2.4.0'
+
+ //JS
+ implementation 'com.github.gedoor:rhino-android:1.3'
//Retrofit
- implementation 'com.squareup.okhttp3:logging-interceptor:3.14.0'//
- implementation 'com.squareup.retrofit2:retrofit:2.5.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0'
+ implementation 'com.squareup.retrofit2:retrofit:2.6.1'
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test:runner:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+ //Glide
+ implementation 'com.github.bumptech.glide:glide:4.9.0'
+
+ //webServer
+ implementation 'org.nanohttpd:nanohttpd:2.3.1'
+ implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'
+
+ //二维码
+ implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.6'
+
+ //颜色选择
+ implementation 'com.jaredrummler:colorpicker:1.1.0'
+
+ implementation 'org.apache.commons:commons-lang3:3.9'
+ implementation 'org.apache.commons:commons-text:1.8'
}
+
+apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 000000000..b91572ada
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ 阅读.debug
+ 阅读.debug·搜索
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7c5e32f4a..dc9705e30 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,45 +1,126 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:tools="http://schemas.android.com/tools"
+ package="io.legado.app">
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ android:name=".App"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:networkSecurityConfig="@xml/network_security_config"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme.Light"
+ tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
+
+
+
+
+
+
+
+ android:name=".ui.book.read.ReadBookActivity"
+ android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
+ android:launchMode="singleTask" />
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/bg/午后沙滩.jpg b/app/src/main/assets/bg/午后沙滩.jpg
new file mode 100644
index 000000000..afe088dd9
Binary files /dev/null and b/app/src/main/assets/bg/午后沙滩.jpg differ
diff --git a/app/src/main/assets/bg/宁静夜色.jpg b/app/src/main/assets/bg/宁静夜色.jpg
new file mode 100644
index 000000000..501a46748
Binary files /dev/null and b/app/src/main/assets/bg/宁静夜色.jpg differ
diff --git a/app/src/main/assets/bg/小清新.jpg b/app/src/main/assets/bg/小清新.jpg
new file mode 100644
index 000000000..d9f229e1c
Binary files /dev/null and b/app/src/main/assets/bg/小清新.jpg differ
diff --git a/app/src/main/assets/bg/山水墨影.jpg b/app/src/main/assets/bg/山水墨影.jpg
new file mode 100644
index 000000000..588374c68
Binary files /dev/null and b/app/src/main/assets/bg/山水墨影.jpg differ
diff --git a/app/src/main/assets/bg/山水画.jpg b/app/src/main/assets/bg/山水画.jpg
new file mode 100644
index 000000000..4c85a5b5d
Binary files /dev/null and b/app/src/main/assets/bg/山水画.jpg differ
diff --git a/app/src/main/assets/bg/护眼漫绿.jpg b/app/src/main/assets/bg/护眼漫绿.jpg
new file mode 100644
index 000000000..daf109e8d
Binary files /dev/null and b/app/src/main/assets/bg/护眼漫绿.jpg differ
diff --git a/app/src/main/assets/bg/新羊皮纸.jpg b/app/src/main/assets/bg/新羊皮纸.jpg
new file mode 100644
index 000000000..eec310669
Binary files /dev/null and b/app/src/main/assets/bg/新羊皮纸.jpg differ
diff --git a/app/src/main/assets/bg/明媚倾城.jpg b/app/src/main/assets/bg/明媚倾城.jpg
new file mode 100644
index 000000000..a68967cf2
Binary files /dev/null and b/app/src/main/assets/bg/明媚倾城.jpg differ
diff --git a/app/src/main/assets/bg/浅黄飘带.jpg b/app/src/main/assets/bg/浅黄飘带.jpg
new file mode 100644
index 000000000..ed033c057
Binary files /dev/null and b/app/src/main/assets/bg/浅黄飘带.jpg differ
diff --git a/app/src/main/assets/bg/深宫魅影.jpg b/app/src/main/assets/bg/深宫魅影.jpg
new file mode 100644
index 000000000..27896f0a6
Binary files /dev/null and b/app/src/main/assets/bg/深宫魅影.jpg differ
diff --git a/app/src/main/assets/bg/清新.jpg b/app/src/main/assets/bg/清新.jpg
new file mode 100644
index 000000000..cd9593da7
Binary files /dev/null and b/app/src/main/assets/bg/清新.jpg differ
diff --git a/app/src/main/assets/bg/清新时光.jpg b/app/src/main/assets/bg/清新时光.jpg
new file mode 100644
index 000000000..4bdd5aba6
Binary files /dev/null and b/app/src/main/assets/bg/清新时光.jpg differ
diff --git a/app/src/main/assets/bg/羊皮纸1.jpg b/app/src/main/assets/bg/羊皮纸1.jpg
new file mode 100644
index 000000000..40f397735
Binary files /dev/null and b/app/src/main/assets/bg/羊皮纸1.jpg differ
diff --git a/app/src/main/assets/bg/羊皮纸2.jpg b/app/src/main/assets/bg/羊皮纸2.jpg
new file mode 100644
index 000000000..3e46c8e0f
Binary files /dev/null and b/app/src/main/assets/bg/羊皮纸2.jpg differ
diff --git a/app/src/main/assets/bg/羊皮纸3.jpg b/app/src/main/assets/bg/羊皮纸3.jpg
new file mode 100644
index 000000000..31b99a870
Binary files /dev/null and b/app/src/main/assets/bg/羊皮纸3.jpg differ
diff --git a/app/src/main/assets/bg/羊皮纸4.jpg b/app/src/main/assets/bg/羊皮纸4.jpg
new file mode 100644
index 000000000..cee31be58
Binary files /dev/null and b/app/src/main/assets/bg/羊皮纸4.jpg differ
diff --git a/app/src/main/assets/bg/边彩画布.jpg b/app/src/main/assets/bg/边彩画布.jpg
new file mode 100644
index 000000000..6638b9f21
Binary files /dev/null and b/app/src/main/assets/bg/边彩画布.jpg differ
diff --git a/app/src/main/assets/bg/雾花.jpg b/app/src/main/assets/bg/雾花.jpg
new file mode 100644
index 000000000..1326d1aa5
Binary files /dev/null and b/app/src/main/assets/bg/雾花.jpg differ
diff --git a/app/src/main/assets/disclaimer.md b/app/src/main/assets/disclaimer.md
new file mode 100644
index 000000000..0f630a9e2
--- /dev/null
+++ b/app/src/main/assets/disclaimer.md
@@ -0,0 +1,16 @@
+# 免责声明(Disclaimer)
+
+* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。
+* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。
+各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。
+任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。
+第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。
+您应该对使用搜索引擎的结果自行承担风险。
+* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。
+因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。
+阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。
+* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。
+阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。
+阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。
+任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。
+阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。
\ No newline at end of file
diff --git a/app/src/main/assets/number.ttf b/app/src/main/assets/number.ttf
new file mode 100644
index 000000000..f2804dbe9
Binary files /dev/null and b/app/src/main/assets/number.ttf differ
diff --git a/app/src/main/assets/readConfig.json b/app/src/main/assets/readConfig.json
new file mode 100644
index 000000000..21cea9f69
--- /dev/null
+++ b/app/src/main/assets/readConfig.json
@@ -0,0 +1,72 @@
+[
+ {
+ "bgStr": "羊皮纸2.jpg",
+ "bgType": 1,
+ "darkStatusIcon": true,
+ "textColor": "#5E432E",
+ "textSize": 22,
+ "letterSpacing": 0,
+ "lineSpacingExtra": 10,
+ "lineSpacingMultiplier": 1.2,
+ "paddingLeft": 16,
+ "paddingRight": 16,
+ "paddingTop": 0,
+ "paddingBottom": 0
+ },
+ {
+ "bgStr": "新羊皮纸.jpg",
+ "bgType": 1,
+ "darkStatusIcon": true,
+ "textColor": "#5E432E",
+ "textSize": 22,
+ "letterSpacing": 0,
+ "lineSpacingExtra": 10,
+ "lineSpacingMultiplier": 1.2,
+ "paddingLeft": 16,
+ "paddingRight": 16,
+ "paddingTop": 0,
+ "paddingBottom": 0
+ },
+ {
+ "bgStr": "#015A86",
+ "bgType": 0,
+ "darkStatusIcon": false,
+ "textColor": "#FFFFFF",
+ "textSize": 22,
+ "letterSpacing": 0,
+ "lineSpacingExtra": 10,
+ "lineSpacingMultiplier": 1.2,
+ "paddingLeft": 16,
+ "paddingRight": 16,
+ "paddingTop": 0,
+ "paddingBottom": 0
+ },
+ {
+ "bgStr": "宁静夜色",
+ "bgType": 1,
+ "darkStatusIcon": false,
+ "textColor": "#adadad",
+ "textSize": 22,
+ "letterSpacing": 0,
+ "lineSpacingExtra": 10,
+ "lineSpacingMultiplier": 1.2,
+ "paddingLeft": 16,
+ "paddingRight": 16,
+ "paddingTop": 0,
+ "paddingBottom": 0
+ },
+ {
+ "bgStr": "#000000",
+ "bgType": 0,
+ "darkStatusIcon": false,
+ "textColor": "#adadad",
+ "textSize": 22,
+ "letterSpacing": 0,
+ "lineSpacingExtra": 10,
+ "lineSpacingMultiplier": 1.2,
+ "paddingLeft": 16,
+ "paddingRight": 16,
+ "paddingTop": 0,
+ "paddingBottom": 0
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/assets/txtChapterRule.json b/app/src/main/assets/txtChapterRule.json
new file mode 100644
index 000000000..b035fccb5
--- /dev/null
+++ b/app/src/main/assets/txtChapterRule.json
@@ -0,0 +1,38 @@
+[
+ {
+ "enable": true,
+ "name": "默认正则1",
+ "rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$",
+ "serialNumber": 0
+ },
+ {
+ "enable": true,
+ "name": "默认正则2",
+ "rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$",
+ "serialNumber": 1
+ },
+ {
+ "enable": true,
+ "name": "默认正则3",
+ "rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$",
+ "serialNumber": 2
+ },
+ {
+ "enable": true,
+ "name": "默认正则4",
+ "rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$",
+ "serialNumber": 3
+ },
+ {
+ "enable": true,
+ "name": "默认正则5",
+ "rule": "^(\\s{0,4})(正文)(.{0,20})$",
+ "serialNumber": 4
+ },
+ {
+ "enable": true,
+ "name": "默认正则6",
+ "rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$",
+ "serialNumber": 5
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md
new file mode 100644
index 000000000..93e1287fd
--- /dev/null
+++ b/app/src/main/assets/updateLog.md
@@ -0,0 +1,5 @@
+## 本软件为开源软件,没有上架Google Play,请不要在任何地方购买!
+### 关注公众号[开源阅读软件]点击广告可支持作者!
+### 捐赠里点击红包搜索码可开启高级功能!
+## 更新日志
+
diff --git a/app/src/main/assets/web/bookshelf.css b/app/src/main/assets/web/bookshelf.css
new file mode 100644
index 000000000..f6e26caa9
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.css
@@ -0,0 +1,176 @@
+html, body {
+ height: 100%;
+ margin: 0;
+}
+
+.hide {
+ display: none;
+}
+
+.top, .showchapter, .hidebooks {
+ width: 60px;
+ height: 50px;
+ position: absolute;
+ right: 30px;
+ bottom: 30px;
+ color: black;
+ font-size: 28px;
+ background-color: #ddd;
+ opacity: 0.85;
+}
+
+.top {
+ bottom: 150px;
+}
+
+.showchapter {
+ bottom: 90px;
+ bottom: 90px;
+}
+
+.address {
+ width: 270px;
+}
+
+.nav {
+ border-bottom: solid 1px #ccc;
+}
+
+input, button {
+ width: 110px;
+ line-height: 34px;
+ background-color: #eee;
+ color: #555;
+ border: none;
+ margin: 10px 5px;
+ font-weight: 500;
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+}
+
+input {
+ padding: 0 10px;
+ cursor: text;
+}
+
+ input:hover, button:hover {
+ border-color: #aaa;
+ background-color: #efefef;
+ color: #222;
+ outline: solid 1px #ccc;
+ }
+
+.allcontent {
+ height: calc(100% - 60px);
+}
+
+.allscreen {
+ height: 100%
+}
+
+.books > div {
+ display: inline-block;
+ margin: 10px;
+ vertical-align: top;
+ border: solid 1px #ddd;
+}
+
+.read > .books {
+ width: 420px;
+ float: left;
+ height: 100%;
+ overflow: auto;
+ border-right: solid 1px #ccc;
+}
+
+ .read > .books > div {
+ margin-right: 0;
+ border-right: none;
+ }
+
+
+.more {
+ overflow-y: auto;
+ height: 100%;
+ display: none;
+}
+
+.read .more {
+ display: block;
+}
+
+.books > div > img {
+ width: 120px;
+ height: 180px;
+ float: left;
+ margin-right: 10px;
+ cursor: pointer;
+}
+
+.info {
+ padding: 10px 20px 0 20px;
+ width: 600px;
+ margin: 0 auto;
+}
+
+ .info > img {
+ width: 600px;
+ height: 900px;
+ }
+
+ .info p {
+ line-height: 1.5;
+ text-align: justify;
+ margin: 0;
+ }
+
+.books tr:nth-child(n+2) td {
+ border-top: solid 1px #999;
+}
+
+.books td:nth-child(1) {
+ vertical-align: top;
+ width: 50px;
+}
+
+.books td:nth-child(2) {
+ vertical-align: top;
+ width: 200px;
+}
+
+.clear {
+ clear: both;
+}
+
+.chapter {
+ margin: 10px;
+ max-height: 500px;
+ overflow-y: auto;
+ border-top: solid 1px #333;
+ border-bottom: solid 1px #333;
+}
+
+ .chapter button {
+ width: 230px;
+ text-align: left;
+ text-indent: 14px;
+ margin: 10px 4px;
+ }
+
+
+.content {
+ padding: 20px;
+ text-align: justify;
+ min-height: 1000px;
+ padding-bottom: 200px;
+}
+
+ .content h2 {
+ font-family: "Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu;
+ font-weight: 500;
+ text-align: center;
+ line-height: 100px;
+ font-size: 40px;
+ margin: 0;
+ }
diff --git a/app/src/main/assets/web/bookshelf.html b/app/src/main/assets/web/bookshelf.html
new file mode 100644
index 000000000..485622373
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.html
@@ -0,0 +1,32 @@
+
+
+
+
+ 阅读书架
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/web/bookshelf.js b/app/src/main/assets/web/bookshelf.js
new file mode 100644
index 000000000..22c8ff87e
--- /dev/null
+++ b/app/src/main/assets/web/bookshelf.js
@@ -0,0 +1,161 @@
+var $ = document.querySelector.bind(document)
+ , $$ = document.querySelectorAll.bind(document)
+ , $c = document.createElement.bind(document)
+ , randomImg = "http://acg.bakayun.cn/randbg.php?t=dfzh"
+ , randomImg2 = "http://img.xjh.me/random_img.php"
+ , books
+ ;
+
+var formatTime = value => {
+ return new Date(value).toLocaleString('zh-CN', {
+ hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"
+ }).replace(/\//g, "-");
+};
+
+var apiMap = {
+ getBookshelf: "/getBookshelf",
+ getChapterList: "/getChapterList",
+ getBookContent: "/getBookContent",
+ saveBook: "/saveBook"
+};
+
+var apiAddress = (apiName, url) => {
+ let address = $('#address').value || window.location.host;
+ if (!(/^http|^\/\//).test(address)) {
+ address = "//" + address;
+ }
+ if (!(/:\d{4,}/).test(address.split("//")[1].split("/")[0])) {
+ address += ":1122";
+ }
+ localStorage.setItem('address', address);
+ return address + apiMap[apiName] + (url ? "?url=" + encodeURIComponent(url) : "");
+};
+
+var init = () => {
+ $('#allcontent').classList.remove("read");
+ $('#books').innerHTML = "";
+ fetch(apiAddress("getBookshelf"), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(getBookshelf.errorMsg);
+ return;
+ }
+ books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);
+ books.forEach(book => {
+ let bookDiv = $c("div");
+ let img = $c("img");
+ img.src = book.bookInfoBean.coverUrl || randomImg;
+ img.setAttribute("data-series-num", book.serialNumber);
+ bookDiv.appendChild(img);
+ bookDiv.innerHTML += `
+ 书名: | ${book.bookInfoBean.name} |
+ 作者: | ${book.bookInfoBean.author} |
+ 阅读: | ${book.durChapterName} ${formatTime(book.finalDate)} |
+ 更新: | ${book.lastChapterName} ${formatTime(book.finalRefreshData)} |
+ 来源: | ${book.bookInfoBean.origin} |
+
`;
+ $('#books').appendChild(bookDiv);
+ });
+ $$('#books img').forEach(bookImg =>
+ bookImg.addEventListener("click", () => {
+ $('#allcontent').classList.add("read");
+ var book = books[bookImg.getAttribute("data-series-num")];
+ $("#info").innerHTML = `
+ 来源:${book.bookInfoBean.origin}
+ 书名:${book.bookInfoBean.name}
+ 作者:${book.bookInfoBean.author}
+ 阅读章节:${book.durChapterName}
+ 阅读时间:${formatTime(book.finalDate)}
+ 最新章节:${book.lastChapterName}
+ 检查时间:${formatTime(book.finalRefreshData)}
+ 简介:${book.bookInfoBean.introduce.trim().replace(/\n/g, "
")}
`;
+ window.location.hash = "";
+ window.location.hash = "#info";
+ $("#content").innerHTML = "章节列表加载中...";
+ $("#chapter").innerHTML = "";
+ fetch(apiAddress("getChapterList", book.noteUrl), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(data.errorMsg);
+ $("#content").innerHTML = "章节列表加载失败!";
+ return;
+ }
+
+ data.data.forEach(chapter => {
+ let ch = $c("button");
+ ch.setAttribute("data-url", chapter.durChapterUrl);
+ ch.setAttribute("title", chapter.durChapterName);
+ ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + "..." : chapter.durChapterName;
+ $("#chapter").appendChild(ch);
+ });
+ $('#chapter').scrollTop = 0;
+ $("#content").innerHTML = "章节列表加载完成!";
+ });
+
+ }));
+ });
+};
+
+$("#back").addEventListener("click", () => {
+ if (window.location.hash === "#content") {
+ window.location.hash = "#chapter";
+ } else if (window.location.hash === "#chapter") {
+ window.location.hash = "#info";
+ } else {
+ $('#allcontent').classList.remove("read");
+ }
+});
+
+$("#refresh").addEventListener("click", init);
+
+$('#hidebooks').addEventListener("click", () => {
+ $("#books").classList.toggle("hide");
+ $(".nav").classList.toggle("hide");
+ $("#allcontent").classList.toggle("allscreen");
+});
+
+$('#top').addEventListener("click", () => {
+ window.location.hash = "";
+ window.location.hash = "#info";
+});
+
+$('#showchapter').addEventListener("click", () => {
+ window.location.hash = "";
+ window.location.hash = "#chapter";
+});
+
+$('#chapter').addEventListener("click", (e) => {
+ if (e.target.tagName === "BUTTON") {
+ var url = e.target.getAttribute("data-url");
+ var name = e.target.getAttribute("title");
+ if (!url) {
+ alert("未取得章节地址");
+ }
+ $("#content").innerHTML = "" + name + " 加载中...
";
+ fetch(apiAddress("getBookContent", url), { mode: "cors" })
+ .then(res => res.json())
+ .then(data => {
+ if (!data.isSuccess) {
+ alert(data.errorMsg);
+ $("#content").innerHTML = "" + name + " 加载失败!
";
+ return;
+ }
+ var content = data.data.trim().split("\n\n");
+ if (content.length === 2) {
+ $("#content").innerHTML = `${content[0]}
(全文 ${content[1].length} 字)
` + content[1].trim().replace(/\n/g, "
");
+ } else {
+ $("#content").innerHTML = `${name || e.target.innerHTML}
(全文 ${data.data.length} 字)
` + data.data.trim().replace(/\n/g, "
");
+ }
+ window.location.hash = "";
+ window.location.hash = "#content";
+ });
+ }
+});
+
+$('#address').setAttribute("placeholder", "阅读APP地址或IP:" + window.location.host);
+if (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {
+ $('#address').value = localStorage.getItem('address');
+}
+init();
diff --git a/app/src/main/assets/web/favicon.ico b/app/src/main/assets/web/favicon.ico
new file mode 100644
index 000000000..c3d5e6bc0
Binary files /dev/null and b/app/src/main/assets/web/favicon.ico differ
diff --git a/app/src/main/assets/web/index.css b/app/src/main/assets/web/index.css
new file mode 100644
index 000000000..a3e76ba06
--- /dev/null
+++ b/app/src/main/assets/web/index.css
@@ -0,0 +1,148 @@
+body {
+ margin: 0;
+}
+.editor {
+ display: flex;
+ align-items: stretch;
+}
+.setbox,
+.menu,
+.outbox {
+ flex: 1;
+ display: flex;
+ flex-flow: column;
+ max-height: 100vh;
+ overflow-y: auto;
+}
+.menu {
+ justify-content: center;
+ max-width: 90px;
+ margin: 0 5px;
+}
+.menu .button {
+ width: 90px;
+ height: 30px;
+ min-height: 30px;
+ margin: 5px 0px;
+ cursor: pointer;
+}
+@keyframes stroker {
+ 0% {
+ stroke-dashoffset: 0
+ }
+ 100% {
+ stroke-dashoffset: -240
+ }
+}
+.button rect {
+ width: 100%;
+ height: 100%;
+ fill: transparent;
+ stroke: #666;
+ stroke-width: 2px;
+}
+.button rect.busy {
+ stroke: #fD1850;
+ stroke-dasharray: 30 90;
+ animation: stroker 1s linear infinite;
+}
+.button text {
+ text-anchor: middle;
+ dominant-baseline: middle;
+}
+.setbox {
+ min-width: 40em;
+}
+.rules,
+.tabbox {
+ flex: 1;
+ display: flex;
+ flex-flow: column;
+}
+.rules>* {
+ display: flex;
+ margin: 2px 0;
+}
+.rules textarea {
+ flex: 1;
+ margin-left: 5px;
+}
+.rules>*,
+.rules>*>div,
+.rules textarea {
+ min-height: 1em;
+}
+textarea {
+ word-break: break-all;
+}
+.tabtitle {
+ display: flex;
+ z-index: 1;
+ justify-content: flex-end;
+}
+.tabtitle>div {
+ cursor: pointer;
+ padding: 1px 10px 0 10px;
+ border-bottom: 3px solid transparent;
+ font-weight: bold;
+}
+.tabtitle>.this {
+ color: #4f9da6;
+ border-bottom-color: #4EBBE4;
+}
+.tabbody {
+ flex: 1;
+ display: flex;
+ margin-top: -1px;
+ border: 1px solid #A9A9A9;
+ height: 0;
+}
+.tabbody>* {
+ flex: 1;
+ flex-flow: column;
+ display: none;
+}
+.tabbody>.this {
+ display: flex;
+}
+.tabbody>*>.titlebar{
+ display: flex;
+}
+.tabbody>*>.titlebar>*{
+ flex: 1;
+ margin: 1px 1px 1px 1px;
+}
+.tabbody>*>.context {
+ flex: 1;
+ flex-flow: column;
+ border: 0;
+ padding: 5px;
+ overflow-y: auto;
+}
+.tabbody>*>.inputbox{
+ border: 0;
+ border-bottom: #A9A9A9 solid 1px;
+ height: 15px;
+ text-align:center;
+}
+.link>* {
+ display: flex;
+ margin: 5px;
+ border-bottom: 1px solid;
+ text-decoration: none;
+}
+#RuleList>label>* {
+ background: #eee;
+ padding-left: 3px;
+ margin: 2px 0;
+ cursor: pointer;
+}
+#RuleList input[type=radio] {
+ display: none;
+}
+#RuleList input[type="radio"]:checked+* {
+ background: #15cda8;
+}
+.isError {
+ color: #FF0000;
+}
\ No newline at end of file
diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html
new file mode 100644
index 000000000..2c47adb7f
--- /dev/null
+++ b/app/src/main/assets/web/index.html
@@ -0,0 +1,306 @@
+
+
+
+
+
+ 书源编辑器v3.8
+
+
+
+
+
+
+
+
书源基础信息
+
+
+
+
+
书籍发现规则
+
+
+
+
+
+
+
+
+
+
书籍搜索规则
+
+
+
+
+
+
+
+
+
+
+
书籍详情规则
+
+
+
+
+
+
+
+
+
目录列表规则
+
+
+
+
+
正文阅读规则
+
+
+
其它规则
+
+
+
+
+
+
+
+
+
+
+
编辑书源
+
调试书源
+
书源列表
+
帮助信息
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/web/index.js b/app/src/main/assets/web/index.js
new file mode 100644
index 000000000..9463800ce
--- /dev/null
+++ b/app/src/main/assets/web/index.js
@@ -0,0 +1,362 @@
+// 简化js原生选择器
+function $(selector) { return document.querySelector(selector); }
+function $$(selector) { return document.querySelectorAll(selector); }
+// 读写Hash值(val未赋值时为读取)
+function hashParam(key, val) {
+ let hashstr = decodeURIComponent(window.location.hash);
+ let regKey = new RegExp(`${key}=([^&]*)`);
+ let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;
+ if (val == undefined) return getVal;
+ if (hashstr == '' || hashstr == '#') {
+ window.location.hash = `#${key}=${val}`;
+ }
+ else {
+ if (getVal) window.location.hash = hashstr.replace(getVal, val);
+ else {
+ window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;
+ }
+ }
+}
+// 创建书源规则容器对象
+const RuleJSON = (() => {
+ let ruleJson = {};
+ $$('.rules textarea').forEach(item => ruleJson[item.id] = '');
+// for (let item of $$('.rules textarea')) ruleJson[item.id] = '';
+ ruleJson.serialNumber = 0;
+ ruleJson.weight = 0;
+ ruleJson.enable = true;
+ return ruleJson;
+})();
+// 选项卡Tab切换事件处理
+function showTab(tabName) {
+ $$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
+ $$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });
+ $(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';
+ $(`.tabtitle>*[name=${tabName}]`).className += ' this';
+ hashParam('tab', tabName);
+}
+// 书源列表列表标签构造函数
+function newRule(rule) {
+ return ``;
+}
+// 缓存规则列表
+var RuleSources = [];
+if (localStorage.getItem('RuleSources')) {
+ RuleSources = JSON.parse(localStorage.getItem('RuleSources'));
+ RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
+}
+// 页面加载完成事件
+window.onload = () => {
+ $$('.tabtitle>*').forEach(item => {
+ item.addEventListener('click', () => {
+ showTab(item.innerHTML);
+ });
+ });
+ if (hashParam('tab')) showTab(hashParam('tab'));
+}
+// 获取数据
+function HttpGet(url) {
+ return fetch(hashParam('domain') ? hashParam('domain') + url : url)
+ .then(res => res.json()).catch(err => console.error('Error:', err));
+}
+// 提交数据
+function HttpPost(url, data) {
+ return fetch(hashParam('domain') ? hashParam('domain') + url : url, {
+ body: JSON.stringify(data),
+ method: 'POST',
+ mode: "cors",
+ headers: new Headers({
+ 'Content-Type': 'application/json;charset=utf-8'
+ })
+ }).then(res => res.json()).catch(err => console.error('Error:', err));
+}
+// 将书源表单转化为书源对象
+function rule2json() {
+ Object.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value);
+ RuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber);
+ RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
+ RuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
+ return RuleJSON;
+}
+// 将书源对象填充到书源表单
+function json2rule(RuleEditor) {
+ Object.keys(RuleJSON).forEach((key) => $("#" + key).value = RuleEditor[key] ? RuleEditor[key] : '');
+}
+// 记录操作过程
+var course = { "old": [], "now": {}, "new": [] };
+if (localStorage.getItem('course')) {
+ course = JSON.parse(localStorage.getItem('course'));
+ json2rule(course.now);
+}
+else {
+ course.now = rule2json();
+ window.localStorage.setItem('course', JSON.stringify(course));
+}
+function todo() {
+ course.old.push(Object.assign({}, course.now));
+ course.now = rule2json();
+ course.new = [];
+ if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小
+ localStorage.setItem('course', JSON.stringify(course));
+}
+function undo() {
+ course = JSON.parse(localStorage.getItem('course'));
+ if (course.old.length > 0) {
+ course.new.push(course.now);
+ course.now = course.old.pop();
+ localStorage.setItem('course', JSON.stringify(course));
+ json2rule(course.now);
+ }
+}
+function redo() {
+ course = JSON.parse(localStorage.getItem('course'));
+ if (course.new.length > 0) {
+ course.old.push(course.now);
+ course.now = course.new.pop();
+ localStorage.setItem('course', JSON.stringify(course));
+ json2rule(course.now);
+ }
+}
+function setRule(editRule) {
+ let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);
+ if ($(`input[id="${editRule.bookSourceUrl}"]`)) {
+ Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });
+ $(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}
${editRule.bookSourceUrl}`;
+ } else {
+ RuleSources.push(editRule);
+ $('#RuleList').innerHTML += newRule(editRule);
+ }
+}
+$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });
+$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });
+// 处理按钮点击事件
+$('.menu').addEventListener('click', e => {
+ let thisNode = e.target;
+ thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :
+ thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;
+ if (!thisNode) return;
+ if (thisNode.getAttribute('class') == 'busy') return;
+ thisNode.setAttribute('class', 'busy');
+ switch (thisNode.id) {
+ case 'push':
+ $$('#RuleList>label>div').forEach(item => { item.className = ''; });
+ (async () => {
+ await HttpPost(`/saveSources`, RuleSources).then(json => {
+ if (json.isSuccess) {
+ let okData = json.data;
+ if (Array.isArray(okData)) {
+ let failMsg = ``;
+ if (RuleSources.length > okData.length) {
+ RuleSources.forEach(item => {
+ if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }
+ else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }
+ });
+ failMsg = '\n推送失败的书源将用红色字体标注!';
+ }
+ alert(`批量推送书源到「阅读APP」\n共计: ${RuleSources.length} 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}`);
+ }
+ else {
+ alert(`批量推送书源到「阅读APP」成功!\n共计: ${RuleSources.length} 条`);
+ }
+ }
+ else {
+ alert(`批量推送书源失败!\nErrorMsg: ${json.errorMsg}`);
+ }
+ }).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\n${err}`); });
+ thisNode.setAttribute('class', '');
+ })();
+ return;
+ case 'pull':
+ showTab('书源列表');
+ (async () => {
+ await HttpGet(`/getSources`).then(json => {
+ if (json.isSuccess) {
+ $('#RuleList').innerHTML = ''
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));
+ RuleSources.forEach(item => {
+ $('#RuleList').innerHTML += newRule(item);
+ });
+ alert(`成功拉取 ${RuleSources.length} 条书源`);
+ }
+ else {
+ alert(`批量拉取书源失败!\nErrorMsg: ${json.errorMsg}`);
+ }
+ }).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\n${err}`); });
+ thisNode.setAttribute('class', '');
+ })();
+ return;
+ case 'editor':
+ if ($('#RuleJsonString').value == '') break;
+ try {
+ json2rule(JSON.parse($('#RuleJsonString').value));
+ todo();
+ } catch (error) {
+ console.log(error);
+ alert(error);
+ }
+ break;
+ case 'conver':
+ showTab('编辑书源');
+ $('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);
+ break;
+ case 'initial':
+ $$('.rules textarea').forEach(item => { item.value = '' });
+ todo();
+ break;
+ case 'undo':
+ undo()
+ break;
+ case 'redo':
+ redo()
+ break;
+ case 'debug':
+ showTab('调试书源');
+ let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1));
+ let DebugInfos = $('#DebugConsole');
+ function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
+ let saveRule = [rule2json()];
+ HttpPost(`/saveSources`, saveRule).then(sResult => {
+ if (sResult.isSuccess) {
+ let sKey = DebugKey.value ? DebugKey.value : '我的';
+ $('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
+ let ws = new WebSocket(`${wsOrigin}/sourceDebug`);
+ ws.onopen = () => {
+ ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
+ };
+ ws.onmessage = (msg) => {
+ DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data);
+ if (msg.data == 'finish') setRule(saveRule[0]);
+ };
+ ws.onerror = (err) => {
+ throw `${err.data}`;
+ }
+ ws.onclose = () => {
+ thisNode.setAttribute('class', '');
+ DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`);
+ }
+ } else throw `${sResult.errorMsg}`;
+ }).catch(err => {
+ DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
+ thisNode.setAttribute('class', '');
+ });
+ return;
+ case 'accept':
+ (async () => {
+ let saveRule = [rule2json()];
+ await HttpPost(`/saveSources`, saveRule).then(json => {
+ alert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
+ setRule(saveRule[0]);
+ }).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\n${err}`); });
+ thisNode.setAttribute('class', '');
+ })();
+ return;
+ default:
+ }
+ setTimeout(() => { thisNode.setAttribute('class', ''); }, 500);
+});
+$('#DebugKey').addEventListener('keydown', e => {
+ if (e.keyCode == 13) {
+ let clickEvent = document.createEvent('MouseEvents');
+ clickEvent.initEvent("click", true, false);
+ $('#debug').dispatchEvent(clickEvent);
+ }
+});
+
+// 列表规则更改事件
+$('#RuleList').addEventListener('click', e => {
+ let editRule = null;
+ if (e.target && e.target.getAttribute('name') == 'rule') {
+ editRule = rule2json();
+ json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));
+ } else return;
+ if (editRule.bookSourceUrl == '') return;
+ if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
+ setRule(editRule);
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
+});
+// 处理列表按钮事件
+$('.tab3>.titlebar').addEventListener('click', e => {
+ let thisNode = e.target;
+ if (thisNode.nodeName != 'BUTTON') return;
+ switch (thisNode.id) {
+ case 'Import':
+ let fileImport = document.createElement('input');
+ fileImport.type = 'file';
+ fileImport.accept = '.json';
+ fileImport.addEventListener('change', () => {
+ let file = fileImport.files[0];
+ let reader = new FileReader();
+ reader.onloadend = function (evt) {
+ if (evt.target.readyState == FileReader.DONE) {
+ let fileText = evt.target.result;
+ try {
+ let fileJson = JSON.parse(fileText);
+ let newSources = [];
+ newSources.push(...fileJson);
+ if (window.confirm(`如何处理导入的书源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources));
+ $('#RuleList').innerHTML = ''
+ RuleSources.forEach(item => {
+ $('#RuleList').innerHTML += newRule(item);
+ });
+ }
+ else {
+ newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
+ RuleSources.push(...newSources);
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources));
+ newSources.forEach(item => {
+ $('#RuleList').innerHTML += newRule(item);
+ });
+ }
+ alert(`成功导入 ${newSources.length} 条书源`);
+ }
+ catch (err) {
+ alert(`导入书源文件失败!\n${err}`);
+ }
+ }
+ };
+ reader.readAsText(file);
+ }, false);
+ fileImport.click();
+ break;
+ case 'Export':
+ let fileExport = document.createElement('a');
+ fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;
+ let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" });
+ fileExport.href = window.URL.createObjectURL(myBlob);
+ fileExport.click();
+ break;
+ case 'Delete':
+ let selectRule = $('#RuleList input:checked');
+ if (!selectRule) {
+ alert(`没有书源被选中!`);
+ return;
+ }
+ if (confirm(`确定要删除选定书源吗?\n(同时删除APP内书源)`)) {
+ let selectRuleUrl = selectRule.id;
+ let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的书源
+ let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl)); // 提取待留下的书源
+ HttpPost(`/deleteSources`, deleteSources).then(json => {
+ if (json.isSuccess) {
+ let selectNode = document.getElementById(selectRuleUrl).parentNode;
+ selectNode.parentNode.removeChild(selectNode);
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));
+ if ($('#bookSourceUrl').value == selectRuleUrl) {
+ $$('.rules textarea').forEach(item => { item.value = '' });
+ todo();
+ }
+ console.log(deleteSources);
+ console.log(`以上书源已删除!`)
+ }
+ }).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\n${err}`); });
+ }
+ break;
+ case 'ClrAll':
+ if (confirm(`确定要清空当前书源列表吗?\n(不会删除APP内书源)`)) {
+ localStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));
+ $('#RuleList').innerHTML = ''
+ }
+ break;
+ default:
+ }
+});
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
new file mode 100644
index 000000000..7e2060a9c
Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt
index 6ef0e877d..54238a6ad 100644
--- a/app/src/main/java/io/legado/app/App.kt
+++ b/app/src/main/java/io/legado/app/App.kt
@@ -1,8 +1,29 @@
package io.legado.app
+import android.app.Activity
import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatDelegate
+import com.jeremyliao.liveeventbus.LiveEventBus
+import io.legado.app.constant.AppConst.channelIdDownload
+import io.legado.app.constant.AppConst.channelIdReadAloud
+import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.data.AppDatabase
+import io.legado.app.help.ActivityHelp
+import io.legado.app.help.CrashHandler
+import io.legado.app.help.ReadBookConfig
+import io.legado.app.lib.theme.ThemeStore
+import io.legado.app.ui.widget.page.ChapterProvider
+import io.legado.app.utils.getCompatColor
+import io.legado.app.utils.getPrefInt
+import io.legado.app.utils.isNightTheme
+@Suppress("DEPRECATION")
class App : Application() {
companion object {
@@ -15,9 +36,148 @@ class App : Application() {
private set
}
+ var versionCode = 0
+ var versionName = ""
+
override fun onCreate() {
super.onCreate()
INSTANCE = this
+ CrashHandler().init(this)
db = AppDatabase.createDatabase(INSTANCE)
+ packageManager.getPackageInfo(packageName, 0)?.let {
+ versionCode = it.versionCode
+ versionName = it.versionName
+ }
+
+ if (!ThemeStore.isConfigured(this, versionCode)) applyTheme()
+ initNightTheme()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId()
+
+ LiveEventBus.get()
+ .config()
+ .supportBroadcast(this)
+ .lifecycleObserverAlwaysActive(true)
+ .autoClear(false)
+
+ registerActivityLife()
+ }
+
+ /**
+ * 更新主题
+ */
+ fun applyTheme() {
+ if (isNightTheme) {
+ ThemeStore.editTheme(this)
+ .primaryColor(
+ getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600))
+ )
+ .accentColor(
+ getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800))
+ )
+ .backgroundColor(
+ getPrefInt("colorBackgroundNight", getCompatColor(R.color.md_grey_800))
+ )
+ .apply()
+ } else {
+ ThemeStore.editTheme(this)
+ .primaryColor(getPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)))
+ .accentColor(getPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)))
+ .backgroundColor(getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)))
+ .apply()
+ }
+ ChapterProvider.upReadAloudSpan()
+ }
+
+ fun applyDayNight() {
+ ReadBookConfig.upBg()
+ applyTheme()
+ initNightTheme()
+ }
+
+
+ private fun initNightTheme() {
+ val targetMode = if (isNightTheme) {
+ AppCompatDelegate.MODE_NIGHT_YES
+ } else {
+ AppCompatDelegate.MODE_NIGHT_NO
+ }
+ AppCompatDelegate.setDefaultNightMode(targetMode)
+ }
+
+
+ /**
+ * 创建通知ID
+ */
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createChannelId() {
+ val notificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
+ notificationManager?.let {
+ //用唯一的ID创建渠道对象
+ val downloadChannel = NotificationChannel(
+ channelIdDownload,
+ getString(R.string.download_offline),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ //初始化channel
+ downloadChannel.enableLights(false)
+ downloadChannel.enableVibration(false)
+ downloadChannel.setSound(null, null)
+
+ //用唯一的ID创建渠道对象
+ val readAloudChannel = NotificationChannel(
+ channelIdReadAloud,
+ getString(R.string.read_aloud),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ //初始化channel
+ readAloudChannel.enableLights(false)
+ readAloudChannel.enableVibration(false)
+ readAloudChannel.setSound(null, null)
+
+ //用唯一的ID创建渠道对象
+ val webChannel = NotificationChannel(
+ channelIdWeb,
+ getString(R.string.web_service),
+ NotificationManager.IMPORTANCE_LOW
+ )
+ //初始化channel
+ webChannel.enableLights(false)
+ webChannel.enableVibration(false)
+ webChannel.setSound(null, null)
+
+ //向notification manager 提交channel
+ it.createNotificationChannels(listOf(downloadChannel, readAloudChannel, webChannel))
+ }
+ }
+
+ private fun registerActivityLife() {
+ registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
+ override fun onActivityPaused(activity: Activity) {
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ ActivityHelp.remove(activity)
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ }
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ ActivityHelp.add(activity)
+ }
+
+ })
}
}
diff --git a/app/src/main/java/io/legado/app/base/BaseActivity.kt b/app/src/main/java/io/legado/app/base/BaseActivity.kt
index f2f2aab07..c02f4731a 100644
--- a/app/src/main/java/io/legado/app/base/BaseActivity.kt
+++ b/app/src/main/java/io/legado/app/base/BaseActivity.kt
@@ -1,58 +1,98 @@
package io.legado.app.base
import android.os.Bundle
+import android.view.Menu
import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
-import androidx.databinding.DataBindingUtil
-import androidx.databinding.ViewDataBinding
-import androidx.lifecycle.ViewModel
+import io.legado.app.R
+import io.legado.app.lib.theme.ATH
+import io.legado.app.lib.theme.ColorUtils
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.utils.applyTint
+import io.legado.app.utils.disableAutoFill
+import io.legado.app.utils.hideSoftInput
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
-abstract class BaseActivity : AppCompatActivity() {
- protected lateinit var dataBinding: BD
- private set
-
- protected abstract val viewModel: VM
-
- protected abstract val layoutID: Int
+abstract class BaseActivity(private val layoutID: Int, private val fullScreen: Boolean = true) :
+ AppCompatActivity(),
+ CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
+ window.decorView.disableAutoFill()
+ initTheme()
+ setupSystemBar()
super.onCreate(savedInstanceState)
- dataBinding = DataBindingUtil.setContentView(this, layoutID)
- onViewModelCreated(viewModel, savedInstanceState)
+ setContentView(layoutID)
+ onActivityCreated(savedInstanceState)
+ observeLiveBus()
}
- open fun onViewModelCreated(viewModel: VM, savedInstanceState: Bundle?){
+ override fun onDestroy() {
+ super.onDestroy()
+ cancel()
+ }
+ abstract fun onActivityCreated(savedInstanceState: Bundle?)
+
+ final override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ return menu?.let {
+ val bool = onCompatCreateOptionsMenu(it)
+ it.applyTint(this)
+ bool
+ } ?: super.onCreateOptionsMenu(menu)
}
- override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+
+ open fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ final override fun onOptionsItemSelected(item: MenuItem?): Boolean {
item?.let {
if (it.itemId == android.R.id.home) {
supportFinishAfterTransition()
return true
}
}
- return if (item == null) false else onCompatOptionsItemSelected(item)
+ return item != null && onCompatOptionsItemSelected(item)
}
open fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
- return true
+ return super.onOptionsItemSelected(item)
}
- override fun setTitle(title: CharSequence?) {
- supportActionBar?.title = title
+ private fun initTheme() {
+ ATH.applyBackgroundTint(window.decorView)
+ if (ColorUtils.isColorLight(primaryColor)) {
+ setTheme(R.style.AppTheme_Light)
+ } else {
+ setTheme(R.style.AppTheme_Dark)
+ }
}
- override fun setTitle(titleId: Int) {
- supportActionBar?.setTitle(titleId)
+ private fun setupSystemBar() {
+ if (fullScreen) {
+ window.clearFlags(
+ WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+ or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
+ )
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ window.decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ }
+ ATH.setStatusBarColorAuto(this, fullScreen)
}
- fun setSubTitle(subtitle: CharSequence?) {
- supportActionBar?.subtitle = subtitle
+ open fun observeLiveBus() {
}
- fun setSubTitle(subtitleId: Int) {
- supportActionBar?.setSubtitle(subtitleId)
+ override fun finish() {
+ currentFocus?.hideSoftInput()
+ super.finish()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/base/BaseFragment.kt b/app/src/main/java/io/legado/app/base/BaseFragment.kt
new file mode 100644
index 000000000..1f3ff2dcb
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/BaseFragment.kt
@@ -0,0 +1,51 @@
+package io.legado.app.base
+
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import androidx.appcompat.view.SupportMenuInflater
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.Fragment
+import io.legado.app.utils.applyTint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+
+abstract class BaseFragment(layoutID: Int) : Fragment(layoutID),
+ CoroutineScope by MainScope() {
+
+ var supportToolbar: Toolbar? = null
+ private set
+
+ val menuInflater: MenuInflater
+ get() = SupportMenuInflater(requireContext())
+
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cancel()
+ }
+
+ fun setSupportToolbar(toolbar: Toolbar) {
+ supportToolbar = toolbar
+ supportToolbar?.let {
+ it.menu.apply {
+ onCompatCreateOptionsMenu(this)
+ applyTint(requireContext())
+ }
+
+ it.setOnMenuItemClickListener { item ->
+ onCompatOptionsItemSelected(item)
+ true
+ }
+ }
+ }
+
+
+ open fun onCompatCreateOptionsMenu(menu: Menu) {
+ }
+
+ open fun onCompatOptionsItemSelected(item: MenuItem) {
+ }
+
+}
diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt
new file mode 100644
index 000000000..c67acb95c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/BaseService.kt
@@ -0,0 +1,21 @@
+package io.legado.app.base
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+
+abstract class BaseService : Service(), CoroutineScope by MainScope() {
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cancel()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt
index 74bc4bfc2..3610a00a5 100644
--- a/app/src/main/java/io/legado/app/base/BaseViewModel.kt
+++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt
@@ -1,52 +1,65 @@
package io.legado.app.base
import android.app.Application
+import android.content.Context
+import androidx.annotation.CallSuper
import androidx.lifecycle.AndroidViewModel
+import io.legado.app.App
+import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.*
+import org.jetbrains.anko.AnkoLogger
+import org.jetbrains.anko.toast
import kotlin.coroutines.CoroutineContext
-open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
- override val coroutineContext: CoroutineContext
- get() = Dispatchers.Main
+open class BaseViewModel(application: Application) : AndroidViewModel(application),
+ CoroutineScope by MainScope(),
+ AnkoLogger {
+ val context: Context by lazy { this.getApplication() }
- private val launchManager: MutableList = mutableListOf()
+ fun execute(
+ scope: CoroutineScope = this,
+ context: CoroutineContext = Dispatchers.IO,
+ block: suspend CoroutineScope.() -> T
+ ): Coroutine {
+ return Coroutine.async(scope, context) { block() }
+ }
+
+ fun submit(
+ scope: CoroutineScope = this,
+ context: CoroutineContext = Dispatchers.IO,
+ block: suspend CoroutineScope.() -> Deferred
+ ): Coroutine {
+ return Coroutine.async(scope, context) { block().await() }
+ }
- protected fun launchOnUI(
- tryBlock: suspend CoroutineScope.() -> Unit,//成功
- errorBlock: suspend CoroutineScope.(Throwable) -> Unit,//失败
- finallyBlock: suspend CoroutineScope.() -> Unit//结束
- ) {
- launchOnUI {
- tryCatch(tryBlock, errorBlock, finallyBlock)
+ @CallSuper
+ override fun onCleared() {
+ super.onCleared()
+ cancel()
+ }
+
+ open fun toast(message: Int) {
+ launch {
+ context.toast(message)
}
}
- /**
- * add launch task to [launchManager]
- */
- private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
- val job = launch { block() }//主线程
- launchManager.add(job)
- job.invokeOnCompletion { launchManager.remove(job) }
+ open fun toast(message: CharSequence) {
+ launch {
+ context.toast(message)
+ }
}
- private suspend fun tryCatch(
- tryBlock: suspend CoroutineScope.() -> Unit,
- errorBlock: suspend CoroutineScope.(Throwable) -> Unit,
- finallyBlock: suspend CoroutineScope.() -> Unit
- ) {
- try {
- coroutineScope { tryBlock() }
- } catch (e: Throwable) {
- coroutineScope { errorBlock(e) }
- } finally {
- coroutineScope { finallyBlock() }
+ open fun longToast(message: Int) {
+ launch {
+ context.toast(message)
}
}
- override fun onCleared() {
- super.onCleared()
- launchManager.clear()
+ open fun longToast(message: CharSequence) {
+ launch {
+ context.toast(message)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt
new file mode 100644
index 000000000..8267a2c8b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt
@@ -0,0 +1,10 @@
+package io.legado.app.base
+
+import androidx.lifecycle.ViewModel
+
+abstract class VMBaseActivity(layoutID: Int, fullScreen: Boolean = true) :
+ BaseActivity(layoutID, fullScreen) {
+
+ protected abstract val viewModel: VM
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/base/VMBaseFragment.kt b/app/src/main/java/io/legado/app/base/VMBaseFragment.kt
new file mode 100644
index 000000000..cc78d19dd
--- /dev/null
+++ b/app/src/main/java/io/legado/app/base/VMBaseFragment.kt
@@ -0,0 +1,9 @@
+package io.legado.app.base
+
+import androidx.lifecycle.ViewModel
+
+abstract class VMBaseFragment(layoutID: Int) : BaseFragment(layoutID) {
+
+ protected abstract val viewModel: VM
+
+}
diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
index ce7b3d4a6..a87416469 100644
--- a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt
@@ -53,11 +53,11 @@ abstract class CommonRecyclerAdapter- (protected val context: Context) : Rec
}
fun > addItemViewDelegate(viewType: Int, delegate: DELEGATE) {
- itemDelegates.put(viewType, delegate)
+ itemDelegates[viewType] = delegate
}
fun > addItemViewDelegate(delegate: DELEGATE) {
- itemDelegates.put(itemDelegates.size, delegate)
+ itemDelegates[itemDelegates.size] = delegate
}
fun > addItemViewDelegates(vararg delegates: DELEGATE) {
@@ -122,7 +122,7 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
}
}
- fun setItems(items: List
- ?) {
+ fun setItems(items: List
- ?, notify: Boolean = true) {
synchronized(lock) {
if (this.items.isNotEmpty()) {
this.items.clear()
@@ -130,7 +130,9 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
if (items != null) {
this.items.addAll(items)
}
- notifyDataSetChanged()
+ if (notify) {
+ notifyDataSetChanged()
+ }
}
}
@@ -148,26 +150,15 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.add(item)) {
- if (oldSize == 0) {
- notifyDataSetChanged()
- } else {
- notifyItemInserted(oldSize + getHeaderCount())
- }
+ notifyItemInserted(oldSize + getHeaderCount())
}
}
}
fun addItems(position: Int, newItems: List
- ) {
synchronized(lock) {
- val oldSize = getActualItemCount()
- if (position in 0 until oldSize) {
- if (if (oldSize == 0) this.items.addAll(newItems) else this.items.addAll(position, newItems)) {
- if (oldSize == 0) {
- notifyDataSetChanged()
- } else {
- notifyItemRangeChanged(position + getHeaderCount(), newItems.size)
- }
- }
+ if (this.items.addAll(position, newItems)) {
+ notifyItemRangeInserted(position + getHeaderCount(), newItems.size)
}
}
}
@@ -176,33 +167,32 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
synchronized(lock) {
val oldSize = getActualItemCount()
if (this.items.addAll(newItems)) {
- if (oldSize == 0) {
- notifyDataSetChanged()
- } else {
- notifyItemRangeChanged(oldSize + getHeaderCount(), newItems.size)
- }
+ notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size)
}
}
}
fun removeItem(position: Int) {
synchronized(lock) {
- if (this.items.removeAt(position) != null)
+ if (this.items.removeAt(position) != null) {
notifyItemRemoved(position + getHeaderCount())
+ }
}
}
fun removeItem(item: ITEM) {
synchronized(lock) {
- if (this.items.remove(item))
+ if (this.items.remove(item)) {
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
+ }
}
}
fun removeItems(items: List
- ) {
synchronized(lock) {
- if (this.items.removeAll(items))
+ if (this.items.removeAll(items)) {
notifyDataSetChanged()
+ }
}
}
@@ -210,8 +200,21 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
synchronized(lock) {
val size = getActualItemCount()
if (oldPosition in 0 until size && newPosition in 0 until size) {
- Collections.swap(this.items, oldPosition + getHeaderCount(), newPosition + getHeaderCount())
- notifyDataSetChanged()
+ val srcPosition = oldPosition + getHeaderCount()
+ val targetPosition = newPosition + getHeaderCount()
+ Collections.swap(this.items, srcPosition, targetPosition)
+ notifyItemChanged(srcPosition)
+ notifyItemChanged(targetPosition)
+ }
+ }
+ }
+
+ fun updateItem(item: ITEM) {
+ synchronized(lock) {
+ val index = this.items.indexOf(item)
+ if (index >= 0) {
+ this.items[index] = item
+ notifyItemChanged(index)
}
}
}
@@ -234,6 +237,13 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
}
}
+ fun clearItems() {
+ synchronized(lock) {
+ this.items.clear()
+ notifyDataSetChanged()
+ }
+ }
+
fun isEmpty(): Boolean {
return items.isEmpty()
}
@@ -295,7 +305,7 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
}
else -> {
- val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutID, parent, false))
+ val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutId, parent, false))
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
@@ -324,7 +334,7 @@ abstract class CommonRecyclerAdapter
- (protected val context: Context) : Rec
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) {
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
- getItem(holder.layoutPosition)?.let {
+ getItem(holder.layoutPosition - getHeaderCount())?.let {
itemDelegates.getValue(getItemViewType(holder.layoutPosition))
.convert(holder, it, payloads)
}
diff --git a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
index fc7c48bab..a679f3e08 100644
--- a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt
@@ -14,7 +14,7 @@ abstract class InfiniteScrollListener() : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// if (dy < 0 || dataLoading.isDataLoading()) return
- val layoutManager:LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
+ val layoutManager: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
val visibleItemCount = recyclerView.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
index 6db4a7415..cf2f40305 100644
--- a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt
@@ -2,12 +2,7 @@ package io.legado.app.base.adapter
import android.view.animation.Interpolator
import android.view.animation.LinearInterpolator
-import io.legado.app.base.adapter.animations.AlphaInAnimation
-import io.legado.app.base.adapter.animations.BaseAnimation
-import io.legado.app.base.adapter.animations.ScaleInAnimation
-import io.legado.app.base.adapter.animations.SlideInBottomAnimation
-import io.legado.app.base.adapter.animations.SlideInLeftAnimation
-import io.legado.app.base.adapter.animations.SlideInRightAnimation
+import io.legado.app.base.adapter.animations.*
/**
* Created by Invincible on 2017/12/15.
diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
index 789ca250e..2ac1090f2 100644
--- a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt
@@ -7,9 +7,7 @@ import android.content.Context
*
* item代理,
*/
-abstract class ItemViewDelegate(protected val context: Context) {
-
- abstract val layoutID: Int
+abstract class ItemViewDelegate
- (protected val context: Context, val layoutId: Int) {
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList)
diff --git a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
index 176098f50..c65cfabf4 100644
--- a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt
@@ -5,22 +5,18 @@ import android.content.Context
/**
* Created by Invincible on 2017/12/15.
*/
-abstract class SimpleRecyclerAdapter
- (context: Context) : CommonRecyclerAdapter
- (context) {
+abstract class SimpleRecyclerAdapter
- (context: Context, private val layoutId: Int) :
+ CommonRecyclerAdapter
- (context) {
init {
- addItemViewDelegate(object : ItemViewDelegate
- (context) {
- override val layoutID: Int
- get() = this@SimpleRecyclerAdapter.layoutID
+ addItemViewDelegate(object : ItemViewDelegate
- (context, layoutId) {
override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) {
this@SimpleRecyclerAdapter.convert(holder, item, payloads)
}
})
-
}
- abstract val layoutID: Int
-
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList)
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
index b307300a3..e3a5b523a 100644
--- a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt
@@ -8,7 +8,7 @@ import android.view.View
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation {
override fun getAnimators(view: View): Array =
- arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
+ arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
companion object {
diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
index 0fd2bfde6..941562201 100644
--- a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt
@@ -3,11 +3,10 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
-import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInBottomAnimation : BaseAnimation {
override fun getAnimators(view: View): Array =
- arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
+ arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
}
diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
index daa6f17a1..8cfae170b 100644
--- a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt
@@ -3,12 +3,11 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
-import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInLeftAnimation : BaseAnimation {
override fun getAnimators(view: View): Array =
- arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
+ arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
}
diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
index 71c8feac7..e7f1c85e5 100644
--- a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
+++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt
@@ -3,12 +3,11 @@ package io.legado.app.base.adapter.animations
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
-import io.legado.app.base.adapter.animations.BaseAnimation
class SlideInRightAnimation : BaseAnimation {
override fun getAnimators(view: View): Array =
- arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
+ arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
}
diff --git a/app/src/main/java/io/legado/app/constant/Action.kt b/app/src/main/java/io/legado/app/constant/Action.kt
new file mode 100644
index 000000000..edccff54c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/Action.kt
@@ -0,0 +1,14 @@
+package io.legado.app.constant
+
+object Action {
+
+ const val play = "play"
+ const val stop = "stop"
+ const val resume = "resume"
+ const val pause = "pause"
+ const val addTimer = "addTimer"
+ const val setTimer = "setTimer"
+ const val prevParagraph = "prevParagraph"
+ const val nextParagraph = "nextParagraph"
+ const val upTtsSpeechRate = "upTtsSpeechRate"
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt
index 89a1d204f..991da0859 100644
--- a/app/src/main/java/io/legado/app/constant/AppConst.kt
+++ b/app/src/main/java/io/legado/app/constant/AppConst.kt
@@ -1,11 +1,56 @@
package io.legado.app.constant
+import android.annotation.SuppressLint
+import io.legado.app.App
+import io.legado.app.R
+import io.legado.app.data.entities.BookGroup
+import io.legado.app.utils.getPrefBoolean
+import io.legado.app.utils.putPrefBoolean
+import java.text.SimpleDateFormat
+import javax.script.ScriptEngine
+import javax.script.ScriptEngineManager
+
+@SuppressLint("SimpleDateFormat")
object AppConst {
+
+ const val APP_TAG = "Legado"
+
const val channelIdDownload = "channel_download"
const val channelIdReadAloud = "channel_read_aloud"
const val channelIdWeb = "channel_web"
- const val APP_TAG = "Legado"
- const val RC_IMPORT_YUEDU_DATA = 100
+ val userAgent: String by lazy {
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
+ }
+
+ val SCRIPT_ENGINE: ScriptEngine by lazy {
+ ScriptEngineManager().getEngineByName("rhino")
+ }
+
+ val TIME_FORMAT: SimpleDateFormat by lazy {
+ SimpleDateFormat("HH:mm")
+ }
+
+ val keyboardToolChars: List by lazy {
+ arrayListOf(
+ "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".",
+ "href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag"
+ )
+ }
+
+ val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all))
+ val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local))
+ val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio))
+
+ var bookGroupLocalShow: Boolean
+ get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false)
+ set(value) {
+ App.INSTANCE.putPrefBoolean("bookGroupLocal", value)
+ }
+ var bookGroupAudioShow: Boolean
+ get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false)
+ set(value) {
+ App.INSTANCE.putPrefBoolean("bookGroupAudio", value)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/BookType.kt b/app/src/main/java/io/legado/app/constant/BookType.kt
new file mode 100644
index 000000000..686e0623b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/BookType.kt
@@ -0,0 +1,7 @@
+package io.legado.app.constant
+
+object BookType {
+ const val default = 0
+ const val audio = 1
+ const val local = "loc_book"
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/Bus.kt b/app/src/main/java/io/legado/app/constant/Bus.kt
new file mode 100644
index 000000000..d81f8f224
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/Bus.kt
@@ -0,0 +1,16 @@
+package io.legado.app.constant
+
+object Bus {
+ const val RECREATE = "RECREATE"
+ const val UP_BOOK = "sourceDebugLog"
+ const val ALOUD_STATE = "aloud_state"
+ const val TTS_START = "ttsStart"
+ const val TTS_TURN_PAGE = "ttsTurnPage"
+ const val TTS_DS = "ttsDs"
+ const val BATTERY_CHANGED = "batteryChanged"
+ const val TIME_CHANGED = "timeChanged"
+ const val READ_ALOUD_BUTTON = "readAloudButton"
+ const val UP_CONFIG = "upConfig"
+ const val OPEN_CHAPTER = "openChapter"
+ const val REPLACE = "replace"
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/Pattern.kt b/app/src/main/java/io/legado/app/constant/Pattern.kt
new file mode 100644
index 000000000..fd623bf69
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/Pattern.kt
@@ -0,0 +1,8 @@
+package io.legado.app.constant
+
+import java.util.regex.Pattern
+
+object Pattern {
+ val JS_PATTERN: Pattern = Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
+ val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/RSSKeywords.kt b/app/src/main/java/io/legado/app/constant/RSSKeywords.kt
new file mode 100644
index 000000000..bb8d4207a
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/RSSKeywords.kt
@@ -0,0 +1,19 @@
+package io.legado.app.constant
+
+object RSSKeywords {
+
+ const val RSS_ITEM = "item"
+ const val RSS_ITEM_TITLE = "title"
+ const val RSS_ITEM_LINK = "link"
+ const val RSS_ITEM_AUTHOR = "author"
+ const val RSS_ITEM_CATEGORY = "category"
+ const val RSS_ITEM_THUMBNAIL = "media:thumbnail"
+ const val RSS_ITEM_ENCLOSURE = "enclosure"
+ const val RSS_ITEM_DESCRIPTION = "description"
+ const val RSS_ITEM_CONTENT = "content:encoded"
+ const val RSS_ITEM_PUB_DATE = "pubDate"
+ const val RSS_ITEM_TIME = "time"
+ const val RSS_ITEM_URL = "url"
+ const val RSS_ITEM_TYPE = "type"
+ const val RSS_ITEM_GUID = "guid"
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/constant/Status.kt b/app/src/main/java/io/legado/app/constant/Status.kt
new file mode 100644
index 000000000..678c62221
--- /dev/null
+++ b/app/src/main/java/io/legado/app/constant/Status.kt
@@ -0,0 +1,7 @@
+package io.legado.app.constant
+
+object Status {
+ const val STOP = 0
+ const val PLAY = 1
+ const val PAUSE = 3
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt
index f5db6c908..be65f7730 100644
--- a/app/src/main/java/io/legado/app/data/AppDatabase.kt
+++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt
@@ -6,23 +6,22 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
-import io.legado.app.data.dao.BookDao
-import io.legado.app.data.dao.ReplaceRuleDao
-import io.legado.app.data.entities.Book
-import io.legado.app.data.entities.Chapter
-import io.legado.app.data.entities.ReplaceRule
-import javax.xml.transform.Source
+import io.legado.app.data.dao.*
+import io.legado.app.data.entities.*
-@Database(entities = [Book::class, Chapter::class, ReplaceRule::class], version = 1, exportSchema = true)
-// @TypeConverters(Converters::class)
+@Database(
+ entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class,
+ SearchBook::class, SearchKeyword::class, SourceCookie::class, RssSource::class, Bookmark::class,
+ RssArticle::class],
+ version = 1,
+ exportSchema = true
+)
abstract class AppDatabase : RoomDatabase() {
companion object {
private const val DATABASE_NAME = "legado.db"
-
-
private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.run {
@@ -35,23 +34,30 @@ abstract class AppDatabase : RoomDatabase() {
}
fun createDatabase(context: Context): AppDatabase {
- return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
+ return Room.databaseBuilder(
+ context.applicationContext,
+ AppDatabase::class.java,
+ DATABASE_NAME
+ )
// .addMigrations(MIGRATION_1_2)
// .addMigrations(MIGRATION_2_3)
// .addMigrations(MIGRATION_3_4)
// .addMigrations(MIGRATION_4_5)
// .addMigrations(MIGRATION_5_6)
- .addCallback(object : Callback() {
- override fun onCreate(db: SupportSQLiteDatabase) {
- super.onCreate(db)
- }
- })
.build()
}
}
abstract fun bookDao(): BookDao
+ abstract fun bookGroupDao(): BookGroupDao
+ abstract fun bookSourceDao(): BookSourceDao
+ abstract fun bookChapterDao(): BookChapterDao
abstract fun replaceRuleDao(): ReplaceRuleDao
-
+ abstract fun searchBookDao(): SearchBookDao
+ abstract fun searchKeywordDao(): SearchKeywordDao
+ abstract fun sourceCookieDao(): SourceCookieDao
+ abstract fun rssSourceDao(): RssSourceDao
+ abstract fun bookmarkDao(): BookmarkDao
+ abstract fun rssArticleDao(): RssArticleDao
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt b/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
deleted file mode 100644
index 08e06f52f..000000000
--- a/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package io.legado.app.data.api
-
-import kotlinx.coroutines.Deferred
-import retrofit2.http.*
-
-interface CommonHttpApi {
-
- @GET
- fun get(@Url url: String, @QueryMap map: Map): Deferred
-
- @FormUrlEncoded
- @POST
- fun post(@Url url: String, @FieldMap map: Map): Deferred
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt b/app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt
new file mode 100644
index 000000000..e4180fa59
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt
@@ -0,0 +1,42 @@
+package io.legado.app.data.api
+
+import kotlinx.coroutines.Deferred
+import retrofit2.Call
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.HeaderMap
+import retrofit2.http.QueryMap
+import retrofit2.http.Url
+
+/**
+ * Created by GKF on 2018/1/21.
+ * get web content
+ */
+
+interface IHttpGetApi {
+ @GET
+ fun getAsync(
+ @Url url: String,
+ @HeaderMap headers: Map
+ ): Deferred>
+
+ @GET
+ fun getMapAsync(
+ @Url url: String,
+ @QueryMap(encoded = true) queryMap: Map,
+ @HeaderMap headers: Map
+ ): Deferred>
+
+ @GET
+ fun get(
+ @Url url: String,
+ @HeaderMap headers: Map
+ ): Call
+
+ @GET
+ fun getMap(
+ @Url url: String,
+ @QueryMap(encoded = true) queryMap: Map,
+ @HeaderMap headers: Map
+ ): Call
+}
diff --git a/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt b/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt
new file mode 100644
index 000000000..92b5f1cf3
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt
@@ -0,0 +1,53 @@
+package io.legado.app.data.api
+
+import kotlinx.coroutines.Deferred
+import okhttp3.RequestBody
+import retrofit2.Call
+import retrofit2.Response
+import retrofit2.http.*
+
+/**
+ * Created by GKF on 2018/1/29.
+ * post
+ */
+
+interface IHttpPostApi {
+
+ @FormUrlEncoded
+ @POST
+ fun postMapAsync(
+ @Url url: String,
+ @FieldMap(encoded = true) fieldMap: Map,
+ @HeaderMap headers: Map
+ ): Deferred>
+
+ @POST
+ fun postBodyAsync(
+ @Url url: String,
+ @Body body: RequestBody,
+ @HeaderMap headers: Map
+ ): Deferred>
+
+ @FormUrlEncoded
+ @POST
+ fun postMap(
+ @Url url: String,
+ @FieldMap(encoded = true) fieldMap: Map,
+ @HeaderMap headers: Map
+ ): Call
+
+ @POST
+ fun postBody(
+ @Url url: String,
+ @Body body: RequestBody,
+ @HeaderMap headers: Map
+ ): Call
+
+ @FormUrlEncoded
+ @POST
+ fun postMapByte(
+ @Url url: String,
+ @FieldMap(encoded = true) fieldMap: Map,
+ @HeaderMap headers: Map
+ ): Call
+}
diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
new file mode 100644
index 000000000..9a4db0cb1
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt
@@ -0,0 +1,31 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import io.legado.app.data.entities.BookChapter
+
+@Dao
+interface BookChapterDao {
+
+ @Query("select * from chapters where bookUrl = :bookUrl")
+ fun observeByBook(bookUrl: String): LiveData
>
+
+ @Query("select * from chapters where bookUrl = :bookUrl")
+ fun getChapterList(bookUrl: String): List
+
+ @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
+ fun getChapter(bookUrl: String, index: Int): BookChapter?
+
+ @Query("select count(url) from chapters where bookUrl = :bookUrl")
+ fun getChapterCount(bookUrl: String): Int
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg bookChapter: BookChapter)
+
+ @Query("delete from chapters where bookUrl = :bookUrl")
+ fun delByBook(bookUrl: String)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
index 6e71ae680..70091e69d 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
@@ -1,19 +1,56 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
-import androidx.paging.DataSource
-import androidx.room.Dao
-import androidx.room.Query
+import androidx.room.*
+import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
-
@Dao
interface BookDao {
+ @Query("SELECT * FROM books order by durChapterTime desc")
+ fun observeAll(): LiveData>
+
+ @Query("SELECT * FROM books WHERE type = ${BookType.audio} order by durChapterTime desc")
+ fun observeAudio(): LiveData>
+
+ @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc")
+ fun observeLocal(): LiveData>
+
@Query("SELECT * FROM books WHERE `group` = :group")
- fun observeByGroup(group: Int): DataSource.Factory
+ fun observeByGroup(group: Int): LiveData>
- @Query("SELECT descUrl FROM books WHERE `group` = :group")
+ @Query("SELECT bookUrl FROM books WHERE `group` = :group")
fun observeUrlsByGroup(group: Int): LiveData>
+ @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
+ fun liveDataSearch(key: String): LiveData>
+
+ @Query("SELECT * FROM books WHERE `name` in (:names)")
+ fun findByName(vararg names: String): List
+
+ @Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
+ fun getBook(bookUrl: String): Book?
+
+ @get:Query("SELECT * FROM books")
+ val allBooks: List
+
+ @get:Query("SELECT * FROM books ORDER BY durChapterTime DESC limit 1")
+ val lastReadBook: Book?
+
+ @get:Query("SELECT bookUrl FROM books")
+ val allBookUrls: List
+
+ @get:Query("SELECT COUNT(*) FROM books")
+ val allBookCount: Int
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg book: Book)
+
+ @Update
+ fun update(vararg books: Book)
+
+ @Query("delete from books where bookUrl = :bookUrl")
+ fun delete(bookUrl: String)
+
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
new file mode 100644
index 000000000..6162ffc5a
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
@@ -0,0 +1,24 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import io.legado.app.data.entities.BookGroup
+
+@Dao
+interface BookGroupDao {
+
+ @Query("SELECT * FROM book_groups ORDER BY `order`")
+ fun liveDataAll(): LiveData>
+
+ @get:Query("SELECT MAX(groupId) FROM book_groups")
+ val maxId: Int
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg bookGroup: BookGroup)
+
+ @Update
+ fun update(vararg bookGroup: BookGroup)
+
+ @Delete
+ fun delete(vararg bookGroup: BookGroup)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
new file mode 100644
index 000000000..d2b48fa49
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
@@ -0,0 +1,82 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.paging.DataSource
+import androidx.room.*
+import io.legado.app.data.entities.BookSource
+
+@Dao
+interface BookSourceDao {
+
+ @Query("select * from book_sources order by customOrder asc")
+ fun liveDataAll(): LiveData>
+
+ @Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc")
+ fun liveDataSearch(searchKey: String = ""): LiveData>
+
+ @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc")
+ fun liveExplore(): LiveData>
+
+ @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc")
+ fun liveExplore(key: String): LiveData>
+
+ @Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''")
+ fun liveGroup(): LiveData>
+
+ @Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''")
+ fun liveGroupEnabled(): LiveData>
+
+ @Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey")
+ fun searchIsEnable(searchKey: String = ""): List
+
+ @Query("update book_sources set enabled = 1 where bookSourceUrl in (:sourceUrls)")
+ fun enableSection(vararg sourceUrls: String)
+
+ @Query("update book_sources set enabled = 0 where bookSourceUrl in (:sourceUrls)")
+ fun disableSection(vararg sourceUrls: String)
+
+ @Query("delete from book_sources where bookSourceUrl in (:sourceUrls)")
+ fun delSection(vararg sourceUrls: String)
+
+ @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc")
+ fun observeFind(): DataSource.Factory
+
+ @Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'")
+ fun getByGroup(group: String): List
+
+ @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
+ fun getEnabledByGroup(group: String): List
+
+ @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
+ val noGroup: List
+
+ @get:Query("select * from book_sources order by customOrder asc")
+ val all: List
+
+ @get:Query("select * from book_sources where enabled = 1 order by customOrder asc")
+ val allEnabled: List
+
+ @Query("select * from book_sources where bookSourceUrl = :key")
+ fun getBookSource(key: String): BookSource?
+
+ @Query("select count(*) from book_sources")
+ fun allCount(): Int
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg bookSource: BookSource)
+
+ @Update
+ fun update(vararg bookSource: BookSource)
+
+ @Delete
+ fun delete(vararg bookSource: BookSource)
+
+ @Query("delete from book_sources where bookSourceUrl = :key")
+ fun delete(key: String)
+
+ @get:Query("select min(customOrder) from book_sources")
+ val minOrder: Int
+
+ @get:Query("select max(customOrder) from book_sources")
+ val maxOrder: Int
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
new file mode 100644
index 000000000..eb757c547
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt
@@ -0,0 +1,18 @@
+package io.legado.app.data.dao
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Query
+import io.legado.app.data.entities.Bookmark
+
+
+@Dao
+interface BookmarkDao {
+
+ @Query("select * from bookmarks")
+ fun all(): List
+
+ @Query("select * from bookmarks where bookUrl = :bookUrl")
+ fun observeByBook(bookUrl: String): DataSource.Factory
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
index 9b92e4c6a..5940272af 100644
--- a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt
@@ -1,7 +1,6 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
-import androidx.paging.DataSource
import androidx.room.*
import io.legado.app.data.entities.ReplaceRule
@@ -10,10 +9,13 @@ import io.legado.app.data.entities.ReplaceRule
interface ReplaceRuleDao {
@Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
- fun observeAll(): DataSource.Factory
+ fun liveDataAll(): LiveData>
- @Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC")
- fun observeAllIds(): LiveData>
+ @Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
+ fun liveDataSearch(key: String): LiveData>
+
+ @get:Query("SELECT MIN(sortOrder) FROM replace_rules")
+ val minOrder: Int
@get:Query("SELECT MAX(sortOrder) FROM replace_rules")
val maxOrder: Int
@@ -25,25 +27,53 @@ interface ReplaceRuleDao {
val allEnabled: List
@Query("SELECT * FROM replace_rules WHERE id = :id")
- fun findById(id: Int): ReplaceRule?
+ fun findById(id: Long): ReplaceRule?
@Query("SELECT * FROM replace_rules WHERE id in (:ids)")
- fun findByIds(vararg ids: Int): List
+ fun findByIds(vararg ids: Long): List
+
+ @Query("update replace_rules set isEnabled = 1 where id in (:ids)")
+ fun enableSection(vararg ids: Long)
+
+ @Query("update replace_rules set isEnabled = 0 where id in (:ids)")
+ fun disableSection(vararg ids: Long)
- @Query("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'")
+ @Query("delete from replace_rules where id in (:ids)")
+ fun delSection(vararg ids: Long)
+
+ @Query(
+ """SELECT * FROM replace_rules WHERE isEnabled = 1
+ AND (scope LIKE '%' || :scope || '%' or scope = null or scope = '')"""
+ )
fun findEnabledByScope(scope: String): List
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(vararg replaceRules: ReplaceRule)
+ @Query(
+ """SELECT * FROM replace_rules WHERE isEnabled = 1
+ AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope = null or scope = '')"""
+ )
+ fun findEnabledByScope(name: String, origin: String): List
+
+ @Query("select `group` from replace_rules where `group` is not null and `group` <> ''")
+ fun liveGroup(): LiveData>
+
+ @Query("select * from replace_rules where `group` like '%' || :group || '%'")
+ fun getByGroup(group: String): List
+
+ @get:Query("select * from replace_rules where `group` is null or `group` = ''")
+ val noGroup: List
+
+ @get:Query("SELECT COUNT(*) - SUM(isEnabled) FROM replace_rules")
+ val summary: Int
+
+ @Query("UPDATE replace_rules SET isEnabled = :enable")
+ fun enableAll(enable: Boolean)
@Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insert(replaceRule: ReplaceRule): Long
+ fun insert(vararg replaceRule: ReplaceRule): List
@Update
fun update(vararg replaceRules: ReplaceRule)
@Delete
fun delete(vararg replaceRules: ReplaceRule)
-
-
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
new file mode 100644
index 000000000..a38fa15d9
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt
@@ -0,0 +1,24 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import io.legado.app.data.entities.RssArticle
+
+@Dao
+interface RssArticleDao {
+
+ @Query("select * from rssArticles where origin = :origin and title = :title")
+ fun get(origin: String, title: String): RssArticle?
+
+ @Query("select * from rssArticles where origin = :origin order by `order` desc")
+ fun liveByOrigin(origin: String): LiveData>
+
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ fun insert(vararg rssArticle: RssArticle)
+
+ @Update
+ fun update(vararg rssArticle: RssArticle)
+
+ @Query("delete from rssArticles where origin = :origin")
+ fun delete(origin: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
new file mode 100644
index 000000000..6a70e4666
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt
@@ -0,0 +1,60 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import io.legado.app.data.entities.RssSource
+
+@Dao
+interface RssSourceDao {
+
+ @Query("select * from rssSources where sourceUrl = :key")
+ fun getByKey(key: String): RssSource?
+
+ @get:Query("SELECT * FROM rssSources")
+ val all: List
+
+ @Query("SELECT * FROM rssSources order by customOrder")
+ fun liveAll(): LiveData>
+
+ @Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder")
+ fun liveSearch(key: String): LiveData>
+
+ @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
+ fun liveEnabled(): LiveData>
+
+ @Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''")
+ fun liveGroup(): LiveData>
+
+ @Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)")
+ fun enableSection(vararg sourceUrls: String)
+
+ @Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)")
+ fun disableSection(vararg sourceUrls: String)
+
+ @get:Query("select min(customOrder) from rssSources")
+ val minOrder: Int
+
+ @get:Query("select max(customOrder) from rssSources")
+ val maxOrder: Int
+
+ @Query("delete from rssSources where sourceUrl in (:sourceUrls)")
+ fun delSection(vararg sourceUrls: String)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg rssSource: RssSource)
+
+ @Update
+ fun update(vararg rssSource: RssSource)
+
+ @Delete
+ fun delete(vararg rssSource: RssSource)
+
+ @Query("delete from rssSources where sourceUrl = :sourceUrl")
+ fun delete(sourceUrl: String)
+
+ @get:Query("select * from rssSources where sourceGroup is null or sourceGroup = ''")
+ val noGroup: List
+
+ @Query("select * from rssSources where sourceGroup like '%' || :group || '%'")
+ fun getByGroup(group: String): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
new file mode 100644
index 000000000..a8b61a068
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt
@@ -0,0 +1,44 @@
+package io.legado.app.data.dao
+
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import io.legado.app.data.entities.SearchBook
+import io.legado.app.data.entities.SearchShow
+
+@Dao
+interface SearchBookDao {
+
+ @Query(
+ """SELECT name, author, min(time) time, max(kind) kind, max(coverUrl) coverUrl,max(intro) intro, max(wordCount) wordCount,
+ max(latestChapterTitle) latestChapterTitle, count(origin) originCount
+ FROM searchBooks where time >= :time
+ group by name, author
+ order by case when name = :key then 1 when author = :key then 2 when name like '%'||:key||'%' then 3 when author like '%'||:key||'%' then 4 else 5 end, time"""
+ )
+ fun observeShow(key: String, time: Long): DataSource.Factory
+
+ @Query("SELECT * FROM searchBooks")
+ fun observeAll(): DataSource.Factory
+
+ @Query("SELECT * FROM searchBooks where time >= :time")
+ fun observeNew(time: Long): DataSource.Factory
+
+ @Query("select * from searchBooks where bookUrl = :bookUrl")
+ fun getSearchBook(bookUrl: String): SearchBook?
+
+ @Query("select * from searchBooks where name = :name and author = :author order by originOrder limit 1")
+ fun getFirstByNameAuthor(name: String, author: String): SearchBook?
+
+ @Query("select * from searchBooks where name = :name and author = :author order by originOrder")
+ fun getByNameAuthor(name: String, author: String): List
+
+ @Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder")
+ fun getByNameAuthorEnable(name: String, author: String): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg searchBook: SearchBook): List
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt
new file mode 100644
index 000000000..be7ad75c5
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt
@@ -0,0 +1,35 @@
+package io.legado.app.data.dao
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import io.legado.app.data.entities.SearchKeyword
+
+
+@Dao
+interface SearchKeywordDao {
+
+ @Query("SELECT * FROM search_keywords ORDER BY usage DESC")
+ fun liveDataByUsage(): LiveData>
+
+ @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC")
+ fun liveDataByTime(): LiveData>
+
+ @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC")
+ fun liveDataSearch(key: String): LiveData>
+
+ @Query("select * from search_keywords where word = :key")
+ fun get(key: String): SearchKeyword?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(vararg keywords: SearchKeyword)
+
+ @Update
+ fun update(vararg keywords: SearchKeyword)
+
+ @Delete
+ fun delete(vararg keywords: SearchKeyword)
+
+ @Query("DELETE FROM search_keywords")
+ fun deleteAll()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/dao/SourceCookieDao.kt b/app/src/main/java/io/legado/app/data/dao/SourceCookieDao.kt
new file mode 100644
index 000000000..fcf4abf70
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/dao/SourceCookieDao.kt
@@ -0,0 +1,12 @@
+package io.legado.app.data.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+
+@Dao
+interface SourceCookieDao {
+
+ @Query("SELECT cookie FROM cookies Where url = :url")
+ fun getCookieByUrl(url: String): String?
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/entities/BaseBook.kt b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt
new file mode 100644
index 000000000..bd5c28db3
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt
@@ -0,0 +1,26 @@
+package io.legado.app.data.entities
+
+import io.legado.app.utils.splitNotBlank
+
+interface BaseBook {
+ var variableMap: HashMap
+ var kind: String?
+ var wordCount: String?
+
+ var infoHtml: String?
+ var tocHtml: String?
+
+ fun putVariable(key: String, value: String)
+
+ fun getKindList(): List {
+ val kindList = arrayListOf()
+ wordCount?.let {
+ if (it.isNotBlank()) kindList.add(it)
+ }
+ kind?.let {
+ val kinds = it.splitNotBlank(",", "\n")
+ kindList.addAll(kinds)
+ }
+ return kindList
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt
index fc601f9fe..2b26dcf72 100644
--- a/app/src/main/java/io/legado/app/data/entities/Book.kt
+++ b/app/src/main/java/io/legado/app/data/entities/Book.kt
@@ -1,50 +1,93 @@
package io.legado.app.data.entities
import android.os.Parcelable
-import androidx.room.*
-import io.legado.app.utils.strim
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import io.legado.app.constant.BookType
+import io.legado.app.utils.GSON
+import io.legado.app.utils.fromJsonObject
+import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
+import kotlin.math.max
@Parcelize
-@Entity(tableName = "books", indices = [(Index(value = ["descUrl"], unique = true))])
-data class Book(@PrimaryKey
- var descUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
- var tocUrl: String = "", // 目录页Url (toc=table of Contents)
- var sourceId: Int = -1, // 书源规则id(默认-1,表示本地书籍)
- var name: String = "", // 书籍名称(书源获取)
- var customName: String? = null, // 书籍名称(用户修改)
- var author: String? = null, // 作者名称(书源获取)
- var customAuthor: String? = null, // 作者名称(用户修改)
- var tag: String? = null, // 分类信息(书源获取)
- var customTag: String? = null, // 分类信息(用户修改)
- var coverUrl: String? = null, // 封面Url(书源获取)
- var customCoverUrl: String? = null, // 封面Url(用户修改)
- var description: String? = null, // 简介内容(书源获取)
- var customDescription: String? = null, // 简介内容(用户修改)
- var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
- var type: Int = 0, // 0: 文本读物, 1: 有声读物
- var group: Int = 0, // 自定义分组索引号
- var latestChapterTitle: String? = null, // 最新章节标题
- var latestChapterTime: Long = 0, // 最新章节标题更新时间
- var lastCheckTime: Long = 0, // 最近一次更新书籍信息的时间
- var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
- var totalChapterNum: Int = 0, // 书籍目录总数
- var durChapterTitle: String? = null, // 当前章节名称
- var durChapterIndex: Int = 0, // 当前章节索引
- var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
- var durChapterTime: Long = 0, // 最近一次阅读书籍的时间(打开正文的时间)
- var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
- var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
-) : Parcelable {
-
- fun getUnreadChapterNum() = Math.max(totalChapterNum - durChapterIndex - 1, 0)
-
- fun getDisplayName() = customName.strim() ?: name
-
- fun getDisplayAuthor() = customAuthor.strim() ?: author
-
- fun getDisplayCover() = customCoverUrl.strim() ?: coverUrl
-
- fun getDisplayDescription() = customDescription.strim() ?: description
+@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
+data class Book(
+ @PrimaryKey
+ var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
+ var tocUrl: String = "", // 目录页Url (toc=table of Contents)
+ var origin: String = BookType.local, // 书源URL(默认BookType.local)
+ var originName: String = "", //书源名称
+ var name: String = "", // 书籍名称(书源获取)
+ var author: String = "", // 作者名称(书源获取)
+ override var kind: String? = null, // 分类信息(书源获取)
+ var customTag: String? = null, // 分类信息(用户修改)
+ var coverUrl: String? = null, // 封面Url(书源获取)
+ var customCoverUrl: String? = null, // 封面Url(用户修改)
+ var intro: String? = null, // 简介内容(书源获取)
+ var customIntro: String? = null, // 简介内容(用户修改)
+ var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
+ var type: Int = 0, // @BookType
+ var group: Int = 0, // 自定义分组索引号
+ var latestChapterTitle: String? = null, // 最新章节标题
+ var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间
+ var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间
+ var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
+ var totalChapterNum: Int = 0, // 书籍目录总数
+ var durChapterTitle: String? = null, // 当前章节名称
+ var durChapterIndex: Int = 0, // 当前章节索引
+ var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
+ var durChapterTime: Long = System.currentTimeMillis(), // 最近一次阅读书籍的时间(打开正文的时间)
+ override var wordCount: String? = null,
+ var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
+ var order: Int = 0, // 手动排序
+ var originOrder: Int = 0, //书源排序
+ var useReplaceRule: Boolean = true, // 正文使用净化替换规则
+ var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息)
+) : Parcelable, BaseBook {
+ @Ignore
+ @IgnoredOnParcel
+ override var variableMap: HashMap = GSON.fromJsonObject(variable) ?: HashMap()
+ @Ignore
+ @IgnoredOnParcel
+ override var infoHtml: String? = null
+
+ @Ignore
+ @IgnoredOnParcel
+ override var tocHtml: String? = null
+
+ fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
+
+ fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
+
+ fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
+
+ override fun putVariable(key: String, value: String) {
+ variableMap[key] = value
+ variable = GSON.toJson(variableMap)
+ }
+
+ fun toSearchBook(): SearchBook {
+ return SearchBook(
+ name = name,
+ author = author,
+ kind = kind,
+ bookUrl = bookUrl,
+ origin = origin,
+ originName = originName,
+ wordCount = wordCount,
+ latestChapterTitle = latestChapterTitle,
+ coverUrl = coverUrl,
+ intro = intro,
+ tocUrl = tocUrl,
+ originOrder = originOrder,
+ variable = variable
+ ).apply {
+ this.infoHtml = this@Book.infoHtml
+ this.tocHtml = this@Book.tocHtml
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt
new file mode 100644
index 000000000..61006a6ea
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt
@@ -0,0 +1,32 @@
+package io.legado.app.data.entities
+
+import android.os.Parcelable
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import kotlinx.android.parcel.Parcelize
+
+
+@Parcelize
+@Entity(
+ tableName = "chapters",
+ primaryKeys = ["url", "bookUrl"],
+ indices = [(Index(value = ["bookUrl"], unique = false)), (Index(value = ["bookUrl", "index"], unique = true))],
+ foreignKeys = [(ForeignKey(
+ entity = Book::class,
+ parentColumns = ["bookUrl"],
+ childColumns = ["bookUrl"],
+ onDelete = ForeignKey.CASCADE
+ ))]
+) // 删除书籍时自动删除章节
+data class BookChapter(
+ var url: String = "", // 章节地址
+ var title: String = "", // 章节标题
+ var bookUrl: String = "", // 书籍地址
+ var index: Int = 0, // 章节序号
+ var resourceUrl: String? = null, // 音频真实URL
+ var tag: String? = null, //
+ var start: Long? = null, // 章节起始位置
+ var end: Long? = null // 章节终止位置
+) : Parcelable
+
diff --git a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt
new file mode 100644
index 000000000..5b3bb0992
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt
@@ -0,0 +1,15 @@
+package io.legado.app.data.entities
+
+import android.os.Parcelable
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@Entity(tableName = "book_groups")
+data class BookGroup(
+ @PrimaryKey
+ var groupId: Int = 0,
+ var groupName: String,
+ var order: Int = 0
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/data/entities/BookSource.kt b/app/src/main/java/io/legado/app/data/entities/BookSource.kt
new file mode 100644
index 000000000..3236d5796
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/entities/BookSource.kt
@@ -0,0 +1,176 @@
+package io.legado.app.data.entities
+
+import android.os.Parcelable
+import androidx.room.Entity
+import androidx.room.Ignore
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import io.legado.app.App
+import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppConst.userAgent
+import io.legado.app.data.entities.rule.*
+import io.legado.app.help.JsExtensions
+import io.legado.app.utils.ACache
+import io.legado.app.utils.GSON
+import io.legado.app.utils.fromJsonObject
+import io.legado.app.utils.getPrefString
+import kotlinx.android.parcel.IgnoredOnParcel
+import kotlinx.android.parcel.Parcelize
+import java.util.*
+import javax.script.SimpleBindings
+
+@Parcelize
+@Entity(
+ tableName = "book_sources",
+ indices = [(Index(value = ["bookSourceUrl"], unique = false))]
+)
+data class BookSource(
+ var bookSourceName: String = "", // 名称
+ var bookSourceGroup: String? = null, // 分组
+ @PrimaryKey
+ var bookSourceUrl: String = "", // 地址,包括 http/https
+ var bookSourceType: Int = 0, // 类型,0 文本,1 音频
+ var bookUrlPattern: String? = null,
+ var customOrder: Int = 0, // 手动排序编号
+ var enabled: Boolean = true, // 是否启用
+ var enabledExplore: Boolean = true, //启用发现
+ var header: String? = null, // header
+ var loginUrl: String? = null, // 登录地址
+ var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
+ var weight: Int = 0, // 智能排序的权重
+ var exploreUrl: String? = null, //发现url
+ var ruleExplore: String? = null, // 发现规则
+ var searchUrl: String? = null, //搜索url
+ var ruleSearch: String? = null, // 搜索规则
+ var ruleBookInfo: String? = null, // 书籍信息页规则
+ var ruleToc: String? = null, // 目录页规则
+ var ruleContent: String? = null // 正文页规则
+) : Parcelable {
+ @Ignore
+ @IgnoredOnParcel
+ var searchRuleV: SearchRule? = null
+
+ @Ignore
+ @IgnoredOnParcel
+ var exploreRuleV: ExploreRule? = null
+
+ @Ignore
+ @IgnoredOnParcel
+ var bookInfoRuleV: BookInfoRule? = null
+
+ @Ignore
+ @IgnoredOnParcel
+ var tocRuleV: TocRule? = null
+
+ @Ignore
+ @IgnoredOnParcel
+ var contentRuleV: ContentRule? = null
+
+ @Throws(Exception::class)
+ fun getHeaderMap(): Map {
+ val headerMap = HashMap()
+ headerMap["User-Agent"] = App.INSTANCE.getPrefString("user_agent") ?: userAgent
+ header?.let {
+ val header1 = when {
+ it.startsWith("@js:", true) ->
+ evalJS(it.substring(4)).toString()
+ it.startsWith("", true) ->
+ evalJS(it.substring(4, it.lastIndexOf("<"))).toString()
+ else -> it
+ }
+ GSON.fromJsonObject