@ -0,0 +1,4 @@ | 
				
			||||
<resources> | 
				
			||||
    <string name="app_name">阅读.debug</string> | 
				
			||||
    <string name="receiving_shared_label">阅读.debug·搜索</string> | 
				
			||||
</resources> | 
				
			||||
@ -1,45 +1,126 @@ | 
				
			||||
<?xml version="1.0" encoding="utf-8"?> | 
				
			||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | 
				
			||||
          xmlns:tools="http://schemas.android.com/tools" | 
				
			||||
          package="io.legado.app"> | 
				
			||||
 | 
				
			||||
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> | 
				
			||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> | 
				
			||||
    <uses-permission android:name="android.permission.INTERNET"/> | 
				
			||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> | 
				
			||||
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> | 
				
			||||
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/> | 
				
			||||
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> | 
				
			||||
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> | 
				
			||||
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/> | 
				
			||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> | 
				
			||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> | 
				
			||||
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> | 
				
			||||
    xmlns:tools="http://schemas.android.com/tools" | 
				
			||||
    package="io.legado.app"> | 
				
			||||
 | 
				
			||||
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | 
				
			||||
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | 
				
			||||
    <uses-permission android:name="android.permission.INTERNET" /> | 
				
			||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | 
				
			||||
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | 
				
			||||
    <uses-permission android:name="android.permission.READ_PHONE_STATE" /> | 
				
			||||
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> | 
				
			||||
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> | 
				
			||||
    <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | 
				
			||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | 
				
			||||
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | 
				
			||||
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> | 
				
			||||
 | 
				
			||||
    <application | 
				
			||||
            android:name=".App" | 
				
			||||
            android:allowBackup="true" | 
				
			||||
            android:icon="@mipmap/ic_launcher" | 
				
			||||
            android:label="@string/app_name" | 
				
			||||
            android:roundIcon="@mipmap/ic_launcher_round" | 
				
			||||
            android:supportsRtl="true" | 
				
			||||
            android:theme="@style/AppTheme" | 
				
			||||
            tools:ignore="AllowBackup,GoogleAppIndexingWarning"> | 
				
			||||
        <activity android:name=".ui.search.SearchActivity"> | 
				
			||||
        </activity> | 
				
			||||
        <activity android:name=".ui.about.AboutActivity"> | 
				
			||||
        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"> | 
				
			||||
        <activity | 
				
			||||
            android:name=".ui.welcome.WelcomeActivity" | 
				
			||||
            android:label="@string/app_name"> | 
				
			||||
            <intent-filter> | 
				
			||||
                <action android:name="android.intent.action.MAIN" /> | 
				
			||||
                <category android:name="android.intent.category.LAUNCHER" /> | 
				
			||||
            </intent-filter> | 
				
			||||
            <meta-data | 
				
			||||
                android:name="android.app.shortcuts" | 
				
			||||
                android:resource="@xml/shortcuts" | 
				
			||||
                android:launchMode="singleTask" /> | 
				
			||||
        </activity> | 
				
			||||
        <activity android:name=".ui.main.MainActivity" /> | 
				
			||||
        <activity | 
				
			||||
                android:name=".ui.main.MainActivity" | 
				
			||||
                android:label="@string/app_name" | 
				
			||||
                android:theme="@style/AppTheme.NoActionBar"> | 
				
			||||
            android:name=".ui.book.read.ReadBookActivity" | 
				
			||||
            android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" | 
				
			||||
            android:launchMode="singleTask" /> | 
				
			||||
        <activity | 
				
			||||
            android:name=".help.permission.PermissionActivity" | 
				
			||||
            android:theme="@style/Activity.Permission" /> | 
				
			||||
        <activity | 
				
			||||
            android:name=".ui.book.source.edit.BookSourceEditActivity" | 
				
			||||
            android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" | 
				
			||||
            android:launchMode="singleTask" | 
				
			||||
            android:windowSoftInputMode="adjustResize|stateHidden" /> | 
				
			||||
        <activity | 
				
			||||
            android:name=".ui.rss.source.edit.RssSourceEditActivity" | 
				
			||||
            android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout" | 
				
			||||
            android:launchMode="singleTask" | 
				
			||||
            android:windowSoftInputMode="adjustResize|stateHidden" /> | 
				
			||||
        <activity android:name=".ui.config.ConfigActivity" /> | 
				
			||||
        <activity android:name=".ui.replacerule.ReplaceRuleActivity" /> | 
				
			||||
        <activity android:name="io.legado.app.ui.book.search.SearchActivity" /> | 
				
			||||
        <activity android:name=".ui.about.AboutActivity" /> | 
				
			||||
        <activity android:name=".ui.qrcode.QrCodeActivity" /> | 
				
			||||
        <activity android:name=".ui.about.DonateActivity" /> | 
				
			||||
        <activity android:name=".ui.book.info.BookInfoActivity" /> | 
				
			||||
        <activity android:name="io.legado.app.ui.book.info.edit.BookInfoEditActivity" /> | 
				
			||||
        <activity android:name=".ui.book.source.debug.BookSourceDebugActivity" /> | 
				
			||||
        <activity android:name=".ui.book.source.manage.BookSourceActivity"> | 
				
			||||
            <intent-filter> | 
				
			||||
                <action android:name="android.intent.action.MAIN"/> | 
				
			||||
                <action android:name="android.intent.action.VIEW" /> | 
				
			||||
 | 
				
			||||
                <category android:name="android.intent.category.LAUNCHER"/> | 
				
			||||
                <category android:name="android.intent.category.DEFAULT" /> | 
				
			||||
                <category android:name="android.intent.category.BROWSABLE" /> | 
				
			||||
 | 
				
			||||
                <data | 
				
			||||
                    android:host="booksource" | 
				
			||||
                    android:scheme="yuedu" /> | 
				
			||||
            </intent-filter> | 
				
			||||
        </activity> | 
				
			||||
        <activity android:name=".ui.chapterlist.ChapterListActivity" /> | 
				
			||||
        <activity android:name=".ui.rss.read.ReadRssActivity" /> | 
				
			||||
        <activity android:name=".ui.audio.AudioPlayActivity" /> | 
				
			||||
        <activity android:name=".ui.explore.ExploreShowActivity" /> | 
				
			||||
        <activity android:name=".ui.rss.source.manage.RssSourceActivity" /> | 
				
			||||
        <activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" /> | 
				
			||||
        <activity android:name=".ui.rss.article.RssArticlesActivity" /> | 
				
			||||
        <activity | 
				
			||||
            android:name=".receiver.SharedReceiverActivity" | 
				
			||||
            android:label="@string/receiving_shared_label"> | 
				
			||||
            <intent-filter> | 
				
			||||
                <action android:name="android.intent.action.PROCESS_TEXT" /> | 
				
			||||
                <category android:name="android.intent.category.DEFAULT" /> | 
				
			||||
                <data android:mimeType="text/plain" /> | 
				
			||||
            </intent-filter> | 
				
			||||
            <intent-filter> | 
				
			||||
                <action android:name="android.intent.action.SEND" /> | 
				
			||||
                <category android:name="android.intent.category.DEFAULT" /> | 
				
			||||
                <data android:mimeType="text/plain" /> | 
				
			||||
            </intent-filter> | 
				
			||||
        </activity> | 
				
			||||
        <activity android:name=".ui.replacerule.ReplaceRuleActivity"/> | 
				
			||||
 | 
				
			||||
        <service android:name=".service.CheckSourceService" /> | 
				
			||||
        <service android:name=".service.DownloadService" /> | 
				
			||||
        <service android:name=".service.UpdateService" /> | 
				
			||||
        <service android:name=".service.WebService" /> | 
				
			||||
        <service android:name=".service.TTSReadAloudService" /> | 
				
			||||
        <service android:name=".service.HttpReadAloudService" /> | 
				
			||||
 | 
				
			||||
        <receiver android:name=".receiver.MediaButtonReceiver"> | 
				
			||||
            <intent-filter> | 
				
			||||
                <action android:name="android.intent.action.MEDIA_BUTTON" /> | 
				
			||||
            </intent-filter> | 
				
			||||
        </receiver> | 
				
			||||
 | 
				
			||||
        <provider | 
				
			||||
            android:name="androidx.core.content.FileProvider" | 
				
			||||
            android:authorities="${applicationId}.fileProvider" | 
				
			||||
            android:exported="false" | 
				
			||||
            android:grantUriPermissions="true"> | 
				
			||||
            <meta-data | 
				
			||||
                android:name="android.support.FILE_PROVIDER_PATHS" | 
				
			||||
                android:resource="@xml/file_paths" /> | 
				
			||||
        </provider> | 
				
			||||
    </application> | 
				
			||||
 | 
				
			||||
</manifest> | 
				
			||||
| 
		 After Width: | Height: | Size: 46 KiB  | 
| 
		 After Width: | Height: | Size: 38 KiB  | 
| 
		 After Width: | Height: | Size: 173 KiB  | 
| 
		 After Width: | Height: | Size: 39 KiB  | 
| 
		 After Width: | Height: | Size: 20 KiB  | 
| 
		 After Width: | Height: | Size: 112 KiB  | 
| 
		 After Width: | Height: | Size: 42 KiB  | 
| 
		 After Width: | Height: | Size: 65 KiB  | 
| 
		 After Width: | Height: | Size: 34 KiB  | 
| 
		 After Width: | Height: | Size: 49 KiB  | 
| 
		 After Width: | Height: | Size: 58 KiB  | 
| 
		 After Width: | Height: | Size: 42 KiB  | 
| 
		 After Width: | Height: | Size: 246 KiB  | 
| 
		 After Width: | Height: | Size: 312 KiB  | 
| 
		 After Width: | Height: | Size: 124 KiB  | 
| 
		 After Width: | Height: | Size: 132 KiB  | 
| 
		 After Width: | Height: | Size: 287 KiB  | 
| 
		 After Width: | Height: | Size: 39 KiB  | 
@ -0,0 +1,16 @@ | 
				
			||||
# 免责声明(Disclaimer) | 
				
			||||
 | 
				
			||||
* 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 | 
				
			||||
* 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。 | 
				
			||||
各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。 | 
				
			||||
任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。 | 
				
			||||
第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。 | 
				
			||||
您应该对使用搜索引擎的结果自行承担风险。 | 
				
			||||
* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。 | 
				
			||||
因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。 | 
				
			||||
阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 | 
				
			||||
* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。 | 
				
			||||
阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 | 
				
			||||
阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。 | 
				
			||||
任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。 | 
				
			||||
阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 | 
				
			||||
@ -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 | 
				
			||||
  } | 
				
			||||
] | 
				
			||||
@ -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 | 
				
			||||
  } | 
				
			||||
] | 
				
			||||
@ -0,0 +1,5 @@ | 
				
			||||
## 本软件为开源软件,没有上架Google Play,请不要在任何地方购买! | 
				
			||||
### 关注公众号[开源阅读软件]点击广告可支持作者! | 
				
			||||
### 捐赠里点击红包搜索码可开启高级功能! | 
				
			||||
## 更新日志 | 
				
			||||
 | 
				
			||||
| 
		 After Width: | Height: | Size: 4.2 KiB  | 
@ -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; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,306 @@ | 
				
			||||
<!DOCTYPE html> | 
				
			||||
<html> | 
				
			||||
 | 
				
			||||
<head> | 
				
			||||
    <meta charset="UTF-8"> | 
				
			||||
    <title>书源编辑器v3.8</title> | 
				
			||||
    <link rel="stylesheet" type="text/css" href="/index.css"/> | 
				
			||||
</head> | 
				
			||||
 | 
				
			||||
<body> | 
				
			||||
<div class="editor"> | 
				
			||||
    <div class="setbox"> | 
				
			||||
        <div class="rules"> | 
				
			||||
            <div><b>书源基础信息</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>书源名称:</div> | 
				
			||||
                <textarea rows="1" id="bookSourceName" placeholder="书源名称(bookSourceName) | 会显示在书源列表"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书源分组:</div> | 
				
			||||
                <textarea rows="1" id="bookSourceGroup" placeholder="书源分组(bookSourceGroup) | 描述书源的特征信息"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书源域名:</div> | 
				
			||||
                <textarea rows="1" id="bookSourceUrl" | 
				
			||||
                          placeholder="书源URL(bookSourceUrl) | 通常填写网站主页(标头不可省略),例: https://www.qidian.com"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>登录网页:</div> | 
				
			||||
                <textarea rows="1" id="loginUrl" placeholder="登录URL(loginUrl) | 填写网站登录网址,仅在需要登录的书源有用"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>书籍发现规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>发现菜单:</div> | 
				
			||||
                <textarea rows="5" id="ruleFindUrl" | 
				
			||||
                          placeholder="发现分类菜单规则(ruleFindUrl),将显示在发现菜单
每行一条发现分类(网址域名可省略):
名称1::网址(Url)1
名称2::网址(Url)2
..."></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>结果列表:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindList" | 
				
			||||
                          placeholder="发现页列表规则(ruleFindList) | 选择书籍节点 (规则结果为List<Element>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍名称:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindName" | 
				
			||||
                          placeholder="发现页书名规则(ruleFindName) | 选择节点书名 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍作者:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindAuthor" | 
				
			||||
                          placeholder="发现页作者规则(ruleFindAuthor) | 选择节点作者 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍分类:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindKind" | 
				
			||||
                          placeholder="发现页分类规则(ruleFindKind) | 选择节点分类信息 (规则结果为List<String>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>最新章节:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindLastChapter" | 
				
			||||
                          placeholder="发现页最新章节规则(ruleFindLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>简介内容:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindIntroduce" | 
				
			||||
                          placeholder="发现页简介规则(ruleFindIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>封面链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindCoverUrl" | 
				
			||||
                          placeholder="发现页封面规则(ruleFindCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>详情链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleFindNoteUrl" | 
				
			||||
                          placeholder="发现页详情规则(ruleFindNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>书籍搜索规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>搜索网址:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchUrl" | 
				
			||||
                          placeholder="搜索网址(ruleSearchUrl) | [域名可省略]/search.php@kw=searchKey|char=utf-8"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>结果验证:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookUrlPattern" | 
				
			||||
                          placeholder="搜索页URL验证(ruleBookUrlPattern) | 正则验证URL是否为详情页,成功则跳过搜索页解析"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>结果列表:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchList" | 
				
			||||
                          placeholder="搜索页列表规则(ruleSearchList) | 选择书籍节点 (规则结果为List<Element>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍名称:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchName" | 
				
			||||
                          placeholder="搜索页书名规则(ruleSearchName) | 选择节点书名 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍作者:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchAuthor" | 
				
			||||
                          placeholder="搜索页作者规则(ruleSearchAuthor) | 选择节点作者 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍分类:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchKind" | 
				
			||||
                          placeholder="搜索页分类规则(ruleSearchKind) | 选择节点分类信息 (规则结果为List<String>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>最新章节:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchLastChapter" | 
				
			||||
                          placeholder="搜索页最新章节规则(ruleSearchLastChapter) | 选择节点最新章节 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>简介内容:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchIntroduce" | 
				
			||||
                          placeholder="搜索页简介规则(ruleSearchIntroduce) | 选择节点书籍简介 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>封面链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchCoverUrl" | 
				
			||||
                          placeholder="搜索页封面规则(ruleSearchCoverUrl) | 选择节点书籍封面 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>详情链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleSearchNoteUrl" | 
				
			||||
                          placeholder="搜索页详情规则(ruleSearchNoteUrl) | 选择书籍详情页网址 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>书籍详情规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>页面处理:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookInfoInit" | 
				
			||||
                          placeholder="详情页信息预处理(ruleBookInfoInit) | 用于加速详情信息检索"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍名称:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookName" | 
				
			||||
                          placeholder="书名规则(ruleBookName) | 选择详情页书名 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍作者:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookAuthor" | 
				
			||||
                          placeholder="作者规则(ruleBookAuthor) | 选择详情页作者 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>书籍分类:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookKind" | 
				
			||||
                          placeholder="分类规则(ruleBookKind) | 选择详情页分类信息 (规则结果为List<String>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>最新章节:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookLastChapter" | 
				
			||||
                          placeholder="最新章节规则(ruleBookLastChapter) | 选择详情页最新章节 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>简介内容:</div> | 
				
			||||
                <textarea rows="1" id="ruleIntroduce" | 
				
			||||
                          placeholder="简介规则(ruleIntroduce) | 选择详情页书籍简介 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>封面链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleCoverUrl" placeholder="封面规则(ruleCoverUrl) | 选择详情页书籍封面 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>目录链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleChapterUrl" | 
				
			||||
                          placeholder="目录URL规则(ruleChapterUrl) | 选择目录页网址 (规则结果为Url, 与详情页相同时可省略)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>目录列表规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>目录翻页:</div> | 
				
			||||
                <textarea rows="1" id="ruleChapterUrlNext" | 
				
			||||
                          placeholder="目录下一页规则(ruleChapterUrlNext) | 选择目录下一页链接 (规则结果为List<Url>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>目录列表:</div> | 
				
			||||
                <textarea rows="1" id="ruleChapterList" | 
				
			||||
                          placeholder="目录列表规则(ruleChapterList) | 选择目录列表的章节节点 (规则结果为List<Element>)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>章节名称:</div> | 
				
			||||
                <textarea rows="1" id="ruleChapterName" | 
				
			||||
                          placeholder="章节名称规则(ruleChapterName) | 选择章节名称 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>章节链接:</div> | 
				
			||||
                <textarea rows="1" id="ruleContentUrl" | 
				
			||||
                          placeholder="章节URL规则(ruleContentUrl) | 选择章节链接 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>正文阅读规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>章节正文:</div> | 
				
			||||
                <textarea rows="1" id="ruleBookContent" | 
				
			||||
                          placeholder="正文规则(ruleBookContent) | 选择正文内容 (规则结果为String)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>正文翻页:</div> | 
				
			||||
                <textarea rows="1" id="ruleContentUrlNext" | 
				
			||||
                          placeholder="正文翻页URL规则(ruleContentUrlNext) | 选择下一分页(不是下一章)链接 (规则结果为Url)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div><b>其它规则</b></div> | 
				
			||||
            <div> | 
				
			||||
                <div>浏览标识:</div> | 
				
			||||
                <textarea rows="1" id="httpUserAgent" | 
				
			||||
                          placeholder="浏览器UA(HttpUserAgent) | 浏览器标识:User-Agent (可选)"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>排序编号:</div> | 
				
			||||
                <textarea rows="1" id="serialNumber" placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>搜索权重:</div> | 
				
			||||
                <textarea rows="1" id="weight" placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea> | 
				
			||||
            </div> | 
				
			||||
            <div> | 
				
			||||
                <div>是否启用:</div> | 
				
			||||
                <textarea rows="1" id="enable" placeholder="默认启用=true,手动启用=false (可选,默认true)"></textarea> | 
				
			||||
            </div> | 
				
			||||
        </div> | 
				
			||||
    </div> | 
				
			||||
    <div class="menu"> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">⇈推送书源</text> | 
				
			||||
            <rect id="push"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">⇊拉取书源</text> | 
				
			||||
            <rect id="pull"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">⋘编辑书源</text> | 
				
			||||
            <rect id="editor"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">⋙生成书源</text> | 
				
			||||
            <rect id="conver"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">✗清空表单</text> | 
				
			||||
            <rect id="initial"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">↶撤销操作</text> | 
				
			||||
            <rect id="undo"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">↷重做操作</text> | 
				
			||||
            <rect id="redo"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">⇏调试书源</text> | 
				
			||||
            <rect id="debug"></rect> | 
				
			||||
        </svg> | 
				
			||||
        <svg class="button"> | 
				
			||||
            <text x="50%" y="55%">✓保存书源</text> | 
				
			||||
            <rect id="accept"></rect> | 
				
			||||
        </svg> | 
				
			||||
    </div> | 
				
			||||
    <div class="outbox"> | 
				
			||||
        <div class="tabbox"> | 
				
			||||
            <div class="tabtitle"> | 
				
			||||
                <div name="编辑书源" class="tab1 this">编辑书源</div> | 
				
			||||
                <div name="调试书源" class="tab2">调试书源</div> | 
				
			||||
                <div name="书源列表" class="tab3">书源列表</div> | 
				
			||||
                <div name="帮助信息" class="tab4">帮助信息</div> | 
				
			||||
            </div> | 
				
			||||
            <div class="tabbody"> | 
				
			||||
                <div class="tab1 this"> | 
				
			||||
                    <textarea class="context" id="RuleJsonString" placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea> | 
				
			||||
                </div> | 
				
			||||
                <div class="tab2"> | 
				
			||||
                    <input type="text" class="inputbox" id="DebugKey" placeholder="我的"> | 
				
			||||
                    <textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea> | 
				
			||||
                </div> | 
				
			||||
                <div class="tab3"> | 
				
			||||
                    <div class="titlebar"> | 
				
			||||
                        <button id="Import">导入书源文件</button> | 
				
			||||
                        <button id="Export">导出书源文件</button> | 
				
			||||
                        <button id="Delete">删除选中书源</button> | 
				
			||||
                        <button id="ClrAll">清空当前列表</button> | 
				
			||||
                    </div> | 
				
			||||
                    <div class="context" id="RuleList"></div> | 
				
			||||
                </div> | 
				
			||||
                <div class="tab4"> | 
				
			||||
                    <div class="context link"> | 
				
			||||
                        <a target="_blank" href="https://gedoor.github.io/MyBookshelf/sourcerule.html">官方书源教程</a> | 
				
			||||
                        <a target="_blank" href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a> | 
				
			||||
                        <a target="_blank" href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a> | 
				
			||||
                        <a target="_blank" href="https://www.w3cschool.cn/regex_rmjc/?">正则表达式教程</a> | 
				
			||||
                        <a target="_blank" href="https://regexr.com/">正则表达式在线验证工具</a> | 
				
			||||
                        <div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义 | 
				
			||||
                            <br>(?s) 前缀表示跨行解析 | 
				
			||||
                            <br>(?m) 前缀表示逐行匹配 | 
				
			||||
                            <br>(?i) 前缀表示忽略大小写 | 
				
			||||
                        </div> | 
				
			||||
                        <a target="_blank" href="https://www.beta.browxy.com/">代码在线运行工具</a> | 
				
			||||
                    </div> | 
				
			||||
                </div> | 
				
			||||
            </div> | 
				
			||||
        </div> | 
				
			||||
    </div> | 
				
			||||
</div> | 
				
			||||
<script type="text/javascript" src="/index.js"></script> | 
				
			||||
</body> | 
				
			||||
 | 
				
			||||
</html> | 
				
			||||
@ -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 `<label for="${rule.bookSourceUrl}"><input type="radio" name="rule" id="${rule.bookSourceUrl}"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`; | 
				
			||||
} | 
				
			||||
// 缓存规则列表
 | 
				
			||||
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}<br>${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: | 
				
			||||
	} | 
				
			||||
}); | 
				
			||||
| 
		 After Width: | Height: | Size: 200 KiB  | 
@ -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<BD : ViewDataBinding, VM : ViewModel> : 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() | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -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) { | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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() | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -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<App>() } | 
				
			||||
 | 
				
			||||
    private val launchManager: MutableList<Job> = mutableListOf() | 
				
			||||
    fun <T> execute( | 
				
			||||
        scope: CoroutineScope = this, | 
				
			||||
        context: CoroutineContext = Dispatchers.IO, | 
				
			||||
        block: suspend CoroutineScope.() -> T | 
				
			||||
    ): Coroutine<T> { | 
				
			||||
        return Coroutine.async(scope, context) { block() } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun <R> submit( | 
				
			||||
        scope: CoroutineScope = this, | 
				
			||||
        context: CoroutineContext = Dispatchers.IO, | 
				
			||||
        block: suspend CoroutineScope.() -> Deferred<R> | 
				
			||||
    ): Coroutine<R> { | 
				
			||||
        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) | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,10 @@ | 
				
			||||
package io.legado.app.base | 
				
			||||
 | 
				
			||||
import androidx.lifecycle.ViewModel | 
				
			||||
 | 
				
			||||
abstract class VMBaseActivity<VM : ViewModel>(layoutID: Int, fullScreen: Boolean = true) : | 
				
			||||
    BaseActivity(layoutID, fullScreen) { | 
				
			||||
 | 
				
			||||
    protected abstract val viewModel: VM | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,9 @@ | 
				
			||||
package io.legado.app.base | 
				
			||||
 | 
				
			||||
import androidx.lifecycle.ViewModel | 
				
			||||
 | 
				
			||||
abstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) { | 
				
			||||
 | 
				
			||||
    protected abstract val viewModel: VM | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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" | 
				
			||||
} | 
				
			||||
@ -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<String> 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) | 
				
			||||
        } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,7 @@ | 
				
			||||
package io.legado.app.constant | 
				
			||||
 | 
				
			||||
object BookType { | 
				
			||||
    const val default = 0 | 
				
			||||
    const val audio = 1 | 
				
			||||
    const val local = "loc_book" | 
				
			||||
} | 
				
			||||
@ -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" | 
				
			||||
} | 
				
			||||
@ -0,0 +1,8 @@ | 
				
			||||
package io.legado.app.constant | 
				
			||||
 | 
				
			||||
import java.util.regex.Pattern | 
				
			||||
 | 
				
			||||
object Pattern { | 
				
			||||
    val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) | 
				
			||||
    val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") | 
				
			||||
} | 
				
			||||
@ -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" | 
				
			||||
} | 
				
			||||
@ -0,0 +1,7 @@ | 
				
			||||
package io.legado.app.constant | 
				
			||||
 | 
				
			||||
object Status { | 
				
			||||
    const val STOP = 0 | 
				
			||||
    const val PLAY = 1 | 
				
			||||
    const val PAUSE = 3 | 
				
			||||
} | 
				
			||||
@ -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<String, String>): Deferred<String> | 
				
			||||
 | 
				
			||||
    @FormUrlEncoded | 
				
			||||
    @POST | 
				
			||||
    fun post(@Url url: String, @FieldMap map: Map<String, String>): Deferred<String> | 
				
			||||
} | 
				
			||||
@ -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<String, String> | 
				
			||||
    ): Deferred<Response<String>> | 
				
			||||
 | 
				
			||||
    @GET | 
				
			||||
    fun getMapAsync( | 
				
			||||
        @Url url: String, | 
				
			||||
        @QueryMap(encoded = true) queryMap: Map<String, String>, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Deferred<Response<String>> | 
				
			||||
 | 
				
			||||
    @GET | 
				
			||||
    fun get( | 
				
			||||
        @Url url: String, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Call<String> | 
				
			||||
 | 
				
			||||
    @GET | 
				
			||||
    fun getMap( | 
				
			||||
        @Url url: String, | 
				
			||||
        @QueryMap(encoded = true) queryMap: Map<String, String>, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Call<String> | 
				
			||||
} | 
				
			||||
@ -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<String, String>, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Deferred<Response<String>> | 
				
			||||
 | 
				
			||||
    @POST | 
				
			||||
    fun postBodyAsync( | 
				
			||||
        @Url url: String, | 
				
			||||
        @Body body: RequestBody, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Deferred<Response<String>> | 
				
			||||
 | 
				
			||||
    @FormUrlEncoded | 
				
			||||
    @POST | 
				
			||||
    fun postMap( | 
				
			||||
        @Url url: String, | 
				
			||||
        @FieldMap(encoded = true) fieldMap: Map<String, String>, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Call<String> | 
				
			||||
 | 
				
			||||
    @POST | 
				
			||||
    fun postBody( | 
				
			||||
        @Url url: String, | 
				
			||||
        @Body body: RequestBody, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Call<String> | 
				
			||||
 | 
				
			||||
    @FormUrlEncoded | 
				
			||||
    @POST | 
				
			||||
    fun postMapByte( | 
				
			||||
        @Url url: String, | 
				
			||||
        @FieldMap(encoded = true) fieldMap: Map<String, String>, | 
				
			||||
        @HeaderMap headers: Map<String, String> | 
				
			||||
    ): Call<ByteArray> | 
				
			||||
} | 
				
			||||
@ -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<List<BookChapter>> | 
				
			||||
 | 
				
			||||
    @Query("select * from chapters where bookUrl = :bookUrl") | 
				
			||||
    fun getChapterList(bookUrl: String): List<BookChapter> | 
				
			||||
 | 
				
			||||
    @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) | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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<List<Book>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE type = ${BookType.audio} order by durChapterTime desc") | 
				
			||||
    fun observeAudio(): LiveData<List<Book>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") | 
				
			||||
    fun observeLocal(): LiveData<List<Book>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE `group` = :group") | 
				
			||||
    fun observeByGroup(group: Int): DataSource.Factory<Int, Book> | 
				
			||||
    fun observeByGroup(group: Int): LiveData<List<Book>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT descUrl FROM books WHERE `group` = :group") | 
				
			||||
    @Query("SELECT bookUrl FROM books WHERE `group` = :group") | 
				
			||||
    fun observeUrlsByGroup(group: Int): LiveData<List<String>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") | 
				
			||||
    fun liveDataSearch(key: String): LiveData<List<Book>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE `name` in (:names)") | 
				
			||||
    fun findByName(vararg names: String): List<Book> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM books WHERE bookUrl = :bookUrl") | 
				
			||||
    fun getBook(bookUrl: String): Book? | 
				
			||||
 | 
				
			||||
    @get:Query("SELECT * FROM books") | 
				
			||||
    val allBooks: List<Book> | 
				
			||||
 | 
				
			||||
    @get:Query("SELECT * FROM books ORDER BY durChapterTime DESC limit 1") | 
				
			||||
    val lastReadBook: Book? | 
				
			||||
 | 
				
			||||
    @get:Query("SELECT bookUrl FROM books") | 
				
			||||
    val allBookUrls: List<String> | 
				
			||||
 | 
				
			||||
    @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) | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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<List<BookGroup>> | 
				
			||||
 | 
				
			||||
    @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) | 
				
			||||
} | 
				
			||||
@ -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<List<BookSource>> | 
				
			||||
 | 
				
			||||
    @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<List<BookSource>> | 
				
			||||
 | 
				
			||||
    @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc") | 
				
			||||
    fun liveExplore(): LiveData<List<BookSource>> | 
				
			||||
 | 
				
			||||
    @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<List<BookSource>> | 
				
			||||
 | 
				
			||||
    @Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''") | 
				
			||||
    fun liveGroup(): LiveData<List<String>> | 
				
			||||
 | 
				
			||||
    @Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''") | 
				
			||||
    fun liveGroupEnabled(): LiveData<List<String>> | 
				
			||||
 | 
				
			||||
    @Query("select distinct  enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey") | 
				
			||||
    fun searchIsEnable(searchKey: String = ""): List<Boolean> | 
				
			||||
 | 
				
			||||
    @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<Int, BookSource> | 
				
			||||
 | 
				
			||||
    @Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'") | 
				
			||||
    fun getByGroup(group: String): List<BookSource> | 
				
			||||
 | 
				
			||||
    @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'") | 
				
			||||
    fun getEnabledByGroup(group: String): List<BookSource> | 
				
			||||
 | 
				
			||||
    @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''") | 
				
			||||
    val noGroup: List<BookSource> | 
				
			||||
 | 
				
			||||
    @get:Query("select * from book_sources order by customOrder asc") | 
				
			||||
    val all: List<BookSource> | 
				
			||||
 | 
				
			||||
    @get:Query("select * from book_sources where enabled = 1 order by customOrder asc") | 
				
			||||
    val allEnabled: List<BookSource> | 
				
			||||
 | 
				
			||||
    @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 | 
				
			||||
} | 
				
			||||
@ -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<Bookmark> | 
				
			||||
 | 
				
			||||
    @Query("select * from bookmarks where bookUrl = :bookUrl") | 
				
			||||
    fun observeByBook(bookUrl: String): DataSource.Factory<Int, Bookmark> | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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<List<RssArticle>> | 
				
			||||
 | 
				
			||||
    @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) | 
				
			||||
} | 
				
			||||
@ -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<RssSource> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM rssSources order by customOrder") | 
				
			||||
    fun liveAll(): LiveData<List<RssSource>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder") | 
				
			||||
    fun liveSearch(key: String): LiveData<List<RssSource>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") | 
				
			||||
    fun liveEnabled(): LiveData<List<RssSource>> | 
				
			||||
 | 
				
			||||
    @Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''") | 
				
			||||
    fun liveGroup(): LiveData<List<String>> | 
				
			||||
 | 
				
			||||
    @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<RssSource> | 
				
			||||
 | 
				
			||||
    @Query("select * from rssSources where sourceGroup like '%' || :group || '%'") | 
				
			||||
    fun getByGroup(group: String): List<RssSource> | 
				
			||||
} | 
				
			||||
@ -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<Int, SearchShow> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM searchBooks") | 
				
			||||
    fun observeAll(): DataSource.Factory<Int, SearchBook> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM searchBooks where time >= :time") | 
				
			||||
    fun observeNew(time: Long): DataSource.Factory<Int, SearchBook> | 
				
			||||
 | 
				
			||||
    @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<SearchBook> | 
				
			||||
 | 
				
			||||
    @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<SearchBook> | 
				
			||||
 | 
				
			||||
    @Insert(onConflict = OnConflictStrategy.REPLACE) | 
				
			||||
    fun insert(vararg searchBook: SearchBook): List<Long> | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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<List<SearchKeyword>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC") | 
				
			||||
    fun liveDataByTime(): LiveData<List<SearchKeyword>> | 
				
			||||
 | 
				
			||||
    @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC") | 
				
			||||
    fun liveDataSearch(key: String): LiveData<List<SearchKeyword>> | 
				
			||||
 | 
				
			||||
    @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() | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -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? | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,26 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import io.legado.app.utils.splitNotBlank | 
				
			||||
 | 
				
			||||
interface BaseBook { | 
				
			||||
    var variableMap: HashMap<String, String> | 
				
			||||
    var kind: String? | 
				
			||||
    var wordCount: String? | 
				
			||||
 | 
				
			||||
    var infoHtml: String? | 
				
			||||
    var tocHtml: String? | 
				
			||||
 | 
				
			||||
    fun putVariable(key: String, value: String) | 
				
			||||
 | 
				
			||||
    fun getKindList(): List<String> { | 
				
			||||
        val kindList = arrayListOf<String>() | 
				
			||||
        wordCount?.let { | 
				
			||||
            if (it.isNotBlank()) kindList.add(it) | 
				
			||||
        } | 
				
			||||
        kind?.let { | 
				
			||||
            val kinds = it.splitNotBlank(",", "\n") | 
				
			||||
            kindList.addAll(kinds) | 
				
			||||
        } | 
				
			||||
        return kindList | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -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<String, String> = 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 | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -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 | 
				
			||||
 | 
				
			||||
@ -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 | 
				
			||||
@ -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<String, String> { | 
				
			||||
        val headerMap = HashMap<String, String>() | 
				
			||||
        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("<js>", true) -> | 
				
			||||
                    evalJS(it.substring(4, it.lastIndexOf("<"))).toString() | 
				
			||||
                else -> it | 
				
			||||
            } | 
				
			||||
            GSON.fromJsonObject<Map<String, String>>(header1)?.let { map -> | 
				
			||||
                headerMap.putAll(map) | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return headerMap | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getSearchRule(): SearchRule { | 
				
			||||
        searchRuleV ?: let { | 
				
			||||
            searchRuleV = GSON.fromJsonObject<SearchRule>(ruleSearch) | 
				
			||||
            searchRuleV ?: let { searchRuleV = SearchRule() } | 
				
			||||
        } | 
				
			||||
        return searchRuleV!! | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getExploreRule(): ExploreRule { | 
				
			||||
        exploreRuleV ?: let { | 
				
			||||
            exploreRuleV = GSON.fromJsonObject<ExploreRule>(ruleExplore) | 
				
			||||
            exploreRuleV ?: let { exploreRuleV = ExploreRule() } | 
				
			||||
        } | 
				
			||||
        return exploreRuleV!! | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getBookInfoRule(): BookInfoRule { | 
				
			||||
        bookInfoRuleV ?: let { | 
				
			||||
            bookInfoRuleV = GSON.fromJsonObject<BookInfoRule>(ruleBookInfo) | 
				
			||||
            bookInfoRuleV ?: let { bookInfoRuleV = BookInfoRule() } | 
				
			||||
        } | 
				
			||||
        return bookInfoRuleV!! | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getTocRule(): TocRule { | 
				
			||||
        tocRuleV ?: let { | 
				
			||||
            tocRuleV = GSON.fromJsonObject<TocRule>(ruleToc) | 
				
			||||
            tocRuleV ?: let { tocRuleV = TocRule() } | 
				
			||||
        } | 
				
			||||
        return tocRuleV!! | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getContentRule(): ContentRule { | 
				
			||||
        contentRuleV ?: let { | 
				
			||||
            contentRuleV = GSON.fromJsonObject<ContentRule>(ruleContent) | 
				
			||||
            contentRuleV ?: let { contentRuleV = ContentRule() } | 
				
			||||
        } | 
				
			||||
        return contentRuleV!! | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun getExploreKinds(): ArrayList<ExploreKind>? { | 
				
			||||
        val exploreKinds = arrayListOf<ExploreKind>() | 
				
			||||
        exploreUrl?.let { | 
				
			||||
            var a = it | 
				
			||||
            if (a.isNotBlank()) { | 
				
			||||
                try { | 
				
			||||
                    if (it.startsWith("<js>", false)) { | 
				
			||||
                        val aCache = ACache.get(App.INSTANCE, "explore") | 
				
			||||
                        a = aCache.getAsString(bookSourceUrl) ?: "" | 
				
			||||
                        if (a.isBlank()) { | 
				
			||||
                            val bindings = SimpleBindings() | 
				
			||||
                            bindings["baseUrl"] = bookSourceUrl | 
				
			||||
                            bindings["java"] = JsExtensions | 
				
			||||
                            a = AppConst.SCRIPT_ENGINE.eval( | 
				
			||||
                                it.substring(4, it.lastIndexOf("<")), | 
				
			||||
                                bindings | 
				
			||||
                            ).toString() | 
				
			||||
                            aCache.put(bookSourceUrl, a) | 
				
			||||
                        } | 
				
			||||
                    } | 
				
			||||
                    val b = a.split("(&&|\n)+".toRegex()) | 
				
			||||
                    b.map { c -> | 
				
			||||
                        val d = c.split("::") | 
				
			||||
                        if (d.size > 1) | 
				
			||||
                            exploreKinds.add(ExploreKind(d[0], d[1])) | 
				
			||||
                    } | 
				
			||||
                } catch (e: Exception) { | 
				
			||||
                    exploreKinds.add(ExploreKind(e.localizedMessage)) | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return exploreKinds | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 执行JS | 
				
			||||
     */ | 
				
			||||
    @Throws(Exception::class) | 
				
			||||
    private fun evalJS(jsStr: String): Any { | 
				
			||||
        val bindings = SimpleBindings() | 
				
			||||
        bindings["java"] = JsExtensions | 
				
			||||
        return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    data class ExploreKind( | 
				
			||||
        var title: String, | 
				
			||||
        var url: String? = null | 
				
			||||
    ) | 
				
			||||
} | 
				
			||||
@ -0,0 +1,21 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "bookmarks", indices = [(Index(value = ["bookUrl"], unique = true))]) | 
				
			||||
data class Bookmark( | 
				
			||||
    @PrimaryKey | 
				
			||||
    var time: Long = System.currentTimeMillis(), | 
				
			||||
    var bookUrl: String = "", | 
				
			||||
    var bookName: String = "", | 
				
			||||
    var chapterName: String = "", | 
				
			||||
    var chapterIndex: Int = 0, | 
				
			||||
    var pageIndex: Int = 0, | 
				
			||||
    var content: String = "" | 
				
			||||
 | 
				
			||||
) : Parcelable | 
				
			||||
@ -1,28 +0,0 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.ForeignKey | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "chapters", | 
				
			||||
    indices = [(Index(value = ["url"], unique = true)), (Index(value = ["bookUrl", "index"], unique = true))], | 
				
			||||
    foreignKeys = [(ForeignKey(entity = Book::class, | 
				
			||||
        parentColumns = ["descUrl"], | 
				
			||||
        childColumns = ["bookUrl"], | 
				
			||||
        onDelete = ForeignKey.CASCADE))])    // 删除书籍时自动删除章节 | 
				
			||||
data class Chapter(@PrimaryKey | 
				
			||||
                    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 | 
				
			||||
 | 
				
			||||
@ -0,0 +1,3 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
data class EditEntity(var key: String, var value: String?, var hint: Int) | 
				
			||||
@ -0,0 +1,29 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Ignore | 
				
			||||
 | 
				
			||||
 | 
				
			||||
@Entity( | 
				
			||||
    tableName = "rssArticles", | 
				
			||||
    primaryKeys = ["origin", "title"] | 
				
			||||
) | 
				
			||||
data class RssArticle( | 
				
			||||
    var origin: String = "", | 
				
			||||
    var title: String = "", | 
				
			||||
    var order: Long = 0, | 
				
			||||
    var author: String? = null, | 
				
			||||
    var link: String? = null, | 
				
			||||
    var pubDate: String? = null, | 
				
			||||
    var description: String? = null, | 
				
			||||
    var content: String? = null, | 
				
			||||
    var image: String? = null, | 
				
			||||
    var categories: String? = null, | 
				
			||||
    var read: Boolean = false, | 
				
			||||
    var star: Boolean = false | 
				
			||||
) { | 
				
			||||
 | 
				
			||||
    @Ignore | 
				
			||||
    var categoryList: MutableList<String> = mutableListOf() | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,33 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))]) | 
				
			||||
data class RssSource( | 
				
			||||
    @PrimaryKey | 
				
			||||
    var sourceUrl: String = "", | 
				
			||||
    var sourceName: String = "", | 
				
			||||
    var sourceIcon: String = "", | 
				
			||||
    var sourceGroup: String? = null, | 
				
			||||
    var enabled: Boolean = true, | 
				
			||||
    //列表规则 | 
				
			||||
    var ruleArticles: String? = null, | 
				
			||||
    var ruleNextPage: String? = null, | 
				
			||||
    var ruleTitle: String? = null, | 
				
			||||
    var rulePubDate: String? = null, | 
				
			||||
    //类别规则 | 
				
			||||
    var ruleCategories: String? = null, | 
				
			||||
    //webView规则 | 
				
			||||
    var ruleDescription: String? = null, | 
				
			||||
    var ruleImage: String? = null, | 
				
			||||
    var ruleLink: String? = null, | 
				
			||||
    var ruleContent: String? = null, | 
				
			||||
    var enableJs: Boolean = false, | 
				
			||||
    var loadWithBaseUrl: Boolean = false, | 
				
			||||
    var customOrder: Int = 0 | 
				
			||||
) : Parcelable | 
				
			||||
@ -1,5 +0,0 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
class Rule { | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -1,3 +1,87 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
class SearchBook | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Ignore | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import io.legado.app.utils.GSON | 
				
			||||
import io.legado.app.utils.fromJsonObject | 
				
			||||
import kotlinx.android.parcel.IgnoredOnParcel | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "searchBooks", indices = [(Index(value = ["bookUrl"], unique = true))]) | 
				
			||||
data class SearchBook( | 
				
			||||
    @PrimaryKey | 
				
			||||
    var bookUrl: String = "", | 
				
			||||
    var origin: String = "",                     // 书源规则 | 
				
			||||
    var originName: String = "", | 
				
			||||
    var name: String = "", | 
				
			||||
    var author: String = "", | 
				
			||||
    override var kind: String? = null, | 
				
			||||
    var coverUrl: String? = null, | 
				
			||||
    var intro: String? = null, | 
				
			||||
    override var wordCount: String? = null, | 
				
			||||
    var latestChapterTitle: String? = null, | 
				
			||||
    var tocUrl: String = "",                    // 目录页Url (toc=table of Contents) | 
				
			||||
    var time: Long = System.currentTimeMillis(), | 
				
			||||
    var variable: String? = null, | 
				
			||||
    var originOrder: Int = 0 | 
				
			||||
) : Parcelable, BaseBook, Comparable<SearchBook> { | 
				
			||||
 | 
				
			||||
    @Ignore | 
				
			||||
    @IgnoredOnParcel | 
				
			||||
    override var infoHtml: String? = null | 
				
			||||
 | 
				
			||||
    @Ignore | 
				
			||||
    @IgnoredOnParcel | 
				
			||||
    override var tocHtml: String? = null | 
				
			||||
 | 
				
			||||
    override fun equals(other: Any?): Boolean { | 
				
			||||
        if (other is SearchBook) { | 
				
			||||
            if (other.bookUrl == bookUrl) { | 
				
			||||
                return true | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return false | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun hashCode(): Int { | 
				
			||||
        return bookUrl.hashCode() | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun compareTo(other: SearchBook): Int { | 
				
			||||
        return other.originOrder - this.originOrder | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    @IgnoredOnParcel | 
				
			||||
    @Ignore | 
				
			||||
    override var variableMap: HashMap<String, String> = GSON.fromJsonObject(variable) ?: HashMap() | 
				
			||||
 | 
				
			||||
    override fun putVariable(key: String, value: String) { | 
				
			||||
        variableMap[key] = value | 
				
			||||
        variable = GSON.toJson(variableMap) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fun toBook(): Book { | 
				
			||||
        return Book( | 
				
			||||
            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@SearchBook.infoHtml | 
				
			||||
            this.tocUrl = this@SearchBook.tocUrl | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,17 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))]) | 
				
			||||
data class SearchKeyword( | 
				
			||||
    @PrimaryKey | 
				
			||||
    var word: String = "",                      // 搜索关键词 | 
				
			||||
    var usage: Int = 1,                         // 使用次数 | 
				
			||||
    var lastUseTime: Long = System.currentTimeMillis()      // 最后一次使用时间 | 
				
			||||
) : Parcelable | 
				
			||||
@ -0,0 +1,29 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import io.legado.app.utils.splitNotBlank | 
				
			||||
 | 
				
			||||
data class SearchShow( | 
				
			||||
    var name: String = "", | 
				
			||||
    var author: String = "", | 
				
			||||
    var kind: String? = null, | 
				
			||||
    var coverUrl: String? = null, | 
				
			||||
    var intro: String? = null, | 
				
			||||
    var wordCount: String? = null, | 
				
			||||
    var latestChapterTitle: String? = null, | 
				
			||||
    var time: Long = 0L, | 
				
			||||
    var originCount: Int = 0 | 
				
			||||
) { | 
				
			||||
 | 
				
			||||
    fun getKindList(): List<String> { | 
				
			||||
        val kindList = arrayListOf<String>() | 
				
			||||
        wordCount?.let { | 
				
			||||
            if (it.isNotBlank()) kindList.add(it) | 
				
			||||
        } | 
				
			||||
        kind?.let { | 
				
			||||
            val kinds = it.splitNotBlank(",", "\n") | 
				
			||||
            kindList.addAll(kinds) | 
				
			||||
        } | 
				
			||||
        return kindList | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -1,31 +0,0 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import android.os.Parcelable | 
				
			||||
import androidx.room.ColumnInfo | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
import kotlinx.android.parcel.Parcelize | 
				
			||||
 | 
				
			||||
@Parcelize | 
				
			||||
@Entity(tableName = "sources", | 
				
			||||
    indices = [(Index(value = ["sourceId"]))]) | 
				
			||||
data class Source(@PrimaryKey(autoGenerate = true) | 
				
			||||
                  @ColumnInfo(name = "sourceId") | 
				
			||||
                  var id: Int = 0,                          // 编号 | 
				
			||||
                  var name: String = "",                    // 名称 | 
				
			||||
                  var origin: String = "",                  // 地址,包括 http/https | 
				
			||||
                  var type: Int = 0,                        // 类型,0 文本,1 音频 | 
				
			||||
                  var group: String? = null,                // 分组 | 
				
			||||
                  var header: String? = null,               // header | 
				
			||||
                  var loginUrl: String? = null,             // 登录地址 | 
				
			||||
                  var isEnabled: Boolean = true,            // 是否启用 | 
				
			||||
                  var lastUpdateTime: Long = 0,             // 最后更新时间,用于排序 | 
				
			||||
                  var customOrder: Int = 0,                 // 手动排序编号 | 
				
			||||
                  var weight: Int = 0,                      // 智能排序的权重 | 
				
			||||
                  var exploreRule: String? = null,          // 发现规则 | 
				
			||||
                  var searchRule: String? = null,           // 搜索规则 | 
				
			||||
                  var bookInfoRule: String? = null,         // 书籍信息页规则 | 
				
			||||
                  var tocRule: String? = null,              // 目录页规则 | 
				
			||||
                  var contentRule: String? = null           // 正文页规则 | 
				
			||||
                  ) : Parcelable | 
				
			||||
@ -0,0 +1,12 @@ | 
				
			||||
package io.legado.app.data.entities | 
				
			||||
 | 
				
			||||
import androidx.room.Entity | 
				
			||||
import androidx.room.Index | 
				
			||||
import androidx.room.PrimaryKey | 
				
			||||
 | 
				
			||||
@Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))]) | 
				
			||||
data class SourceCookie( | 
				
			||||
    @PrimaryKey | 
				
			||||
    var url: String = "", | 
				
			||||
    var cookie: String = "" | 
				
			||||
) | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
data class BookInfoRule( | 
				
			||||
    var init: String? = null, | 
				
			||||
    var name: String? = null, | 
				
			||||
    var author: String? = null, | 
				
			||||
    var intro: String? = null, | 
				
			||||
    var kind: String? = null, | 
				
			||||
    var lastChapter: String? = null, | 
				
			||||
    var updateTime: String? = null, | 
				
			||||
    var coverUrl: String? = null, | 
				
			||||
    var tocUrl: String? = null, | 
				
			||||
    var wordCount: String? = null | 
				
			||||
) | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
interface BookListRule { | 
				
			||||
    var bookList: String? | 
				
			||||
    var name: String? | 
				
			||||
    var author: String? | 
				
			||||
    var intro: String? | 
				
			||||
    var kind: String? | 
				
			||||
    var lastChapter: String? | 
				
			||||
    var updateTime: String? | 
				
			||||
    var bookUrl: String? | 
				
			||||
    var coverUrl: String? | 
				
			||||
    var wordCount: String? | 
				
			||||
} | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
data class ContentRule( | 
				
			||||
    var content: String? = null, | 
				
			||||
    var nextContentUrl: String? = null | 
				
			||||
) | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
data class ExploreRule( | 
				
			||||
    override var bookList: String? = null, | 
				
			||||
    override var name: String? = null, | 
				
			||||
    override var author: String? = null, | 
				
			||||
    override var intro: String? = null, | 
				
			||||
    override var kind: String? = null, | 
				
			||||
    override var lastChapter: String? = null, | 
				
			||||
    override var updateTime: String? = null, | 
				
			||||
    override var bookUrl: String? = null, | 
				
			||||
    override var coverUrl: String? = null, | 
				
			||||
    override var wordCount: String? = null | 
				
			||||
) : BookListRule | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
data class SearchRule( | 
				
			||||
        override var bookList: String? = null, | 
				
			||||
        override var name: String? = null, | 
				
			||||
        override var author: String? = null, | 
				
			||||
        override var intro: String? = null, | 
				
			||||
        override var kind: String? = null, | 
				
			||||
        override var lastChapter: String? = null, | 
				
			||||
        override var updateTime: String? = null, | 
				
			||||
        override var bookUrl: String? = null, | 
				
			||||
        override var coverUrl: String? = null, | 
				
			||||
        override var wordCount: String? = null | 
				
			||||
) : BookListRule | 
				
			||||
@ -0,0 +1,8 @@ | 
				
			||||
package io.legado.app.data.entities.rule | 
				
			||||
 | 
				
			||||
data class TocRule( | 
				
			||||
    var chapterList: String? = null, | 
				
			||||
    var chapterName: String? = null, | 
				
			||||
    var chapterUrl: String? = null, | 
				
			||||
    var nextTocUrl: String? = null | 
				
			||||
) | 
				
			||||
@ -0,0 +1,85 @@ | 
				
			||||
package io.legado.app.help | 
				
			||||
 | 
				
			||||
import android.app.Activity | 
				
			||||
import java.lang.ref.WeakReference | 
				
			||||
import java.util.* | 
				
			||||
 | 
				
			||||
/** | 
				
			||||
 * Activity管理器,管理项目中Activity的状态 | 
				
			||||
 */ | 
				
			||||
object ActivityHelp { | 
				
			||||
 | 
				
			||||
    private val activities: MutableList<WeakReference<Activity>> = arrayListOf() | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 判断指定Activity是否存在 | 
				
			||||
     */ | 
				
			||||
    fun isExist(activityClass: Class<*>): Boolean { | 
				
			||||
        for (item in activities) { | 
				
			||||
            if (item.get()?.javaClass == activityClass) { | 
				
			||||
                return true | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return false | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 添加Activity | 
				
			||||
     */ | 
				
			||||
    fun add(activity: Activity) { | 
				
			||||
        activities.add(WeakReference(activity)) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 移除Activity | 
				
			||||
     */ | 
				
			||||
    fun remove(activity: Activity) { | 
				
			||||
        for (temp in activities) { | 
				
			||||
            if (null != temp.get() && temp.get() === activity) { | 
				
			||||
                activities.remove(temp) | 
				
			||||
                break | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 移除Activity | 
				
			||||
     */ | 
				
			||||
    fun remove(activityClass: Class<*>) { | 
				
			||||
        val iterator = activities.iterator() | 
				
			||||
        while (iterator.hasNext()) { | 
				
			||||
            val item = iterator.next() | 
				
			||||
            if (item.get()?.javaClass == activityClass) { | 
				
			||||
                iterator.remove() | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 关闭指定 activity | 
				
			||||
     */ | 
				
			||||
    fun finishActivity(vararg activities: Activity) { | 
				
			||||
        for (activity in activities) { | 
				
			||||
            activity.finish() | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * 关闭指定 activity(class) | 
				
			||||
     */ | 
				
			||||
    fun finishActivity(vararg activityClasses: Class<*>) { | 
				
			||||
        val waitFinish = ArrayList<WeakReference<Activity>>() | 
				
			||||
        for (temp in activities) { | 
				
			||||
            for (activityClass in activityClasses) { | 
				
			||||
                if (temp.get()?.javaClass == activityClass) { | 
				
			||||
                    waitFinish.add(temp) | 
				
			||||
                    break | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        for (activityWeakReference in waitFinish) { | 
				
			||||
            activityWeakReference.get()?.finish() | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
} | 
				
			||||
@ -0,0 +1,30 @@ | 
				
			||||
package io.legado.app.help | 
				
			||||
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView | 
				
			||||
 | 
				
			||||
internal class AdapterDataObserverProxy(var adapterDataObserver: RecyclerView.AdapterDataObserver, var headerCount: Int) : RecyclerView.AdapterDataObserver() { | 
				
			||||
    override fun onChanged() { | 
				
			||||
        adapterDataObserver.onChanged() | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | 
				
			||||
        adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { | 
				
			||||
        adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // 当第n个数据被获取,更新第n+1个position | 
				
			||||
    override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | 
				
			||||
        adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { | 
				
			||||
        adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { | 
				
			||||
        super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount) | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,60 @@ | 
				
			||||
package io.legado.app.help | 
				
			||||
 | 
				
			||||
import android.annotation.TargetApi | 
				
			||||
import android.content.Context | 
				
			||||
import android.graphics.Bitmap | 
				
			||||
import android.os.Build | 
				
			||||
import android.renderscript.Allocation | 
				
			||||
import android.renderscript.Element | 
				
			||||
import android.renderscript.RenderScript | 
				
			||||
import android.renderscript.ScriptIntrinsicBlur | 
				
			||||
 | 
				
			||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool | 
				
			||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation | 
				
			||||
 | 
				
			||||
import java.security.MessageDigest | 
				
			||||
 | 
				
			||||
/** | 
				
			||||
 * 模糊 | 
				
			||||
 */ | 
				
			||||
class BlurTransformation(context: Context, private val radius: Int) : BitmapTransformation() { | 
				
			||||
    private val rs: RenderScript = RenderScript.create(context) | 
				
			||||
 | 
				
			||||
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) | 
				
			||||
    override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { | 
				
			||||
        val blurredBitmap = toTransform.copy(Bitmap.Config.ARGB_8888, true) | 
				
			||||
 | 
				
			||||
        // Allocate memory for Renderscript to work with | 
				
			||||
        //分配用于渲染脚本的内存 | 
				
			||||
        val input = Allocation.createFromBitmap( | 
				
			||||
            rs, | 
				
			||||
            blurredBitmap, | 
				
			||||
            Allocation.MipmapControl.MIPMAP_FULL, | 
				
			||||
            Allocation.USAGE_SHARED | 
				
			||||
        ) | 
				
			||||
        val output = Allocation.createTyped(rs, input.type) | 
				
			||||
 | 
				
			||||
        // Load up an instance of the specific script that we want to use. | 
				
			||||
        //加载我们想要使用的特定脚本的实例。 | 
				
			||||
        val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) | 
				
			||||
        script.setInput(input) | 
				
			||||
 | 
				
			||||
        // Set the blur radius | 
				
			||||
        //设置模糊半径 | 
				
			||||
        script.setRadius(radius.toFloat()) | 
				
			||||
 | 
				
			||||
        // Start the ScriptIntrinsicBlur | 
				
			||||
        //启动 ScriptIntrinsicBlur, | 
				
			||||
        script.forEach(output) | 
				
			||||
 | 
				
			||||
        // Copy the output to the blurred bitmap | 
				
			||||
        //将输出复制到模糊的位图 | 
				
			||||
        output.copyTo(blurredBitmap) | 
				
			||||
 | 
				
			||||
        return blurredBitmap | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    override fun updateDiskCacheKey(messageDigest: MessageDigest) { | 
				
			||||
        messageDigest.update("blur transformation".toByteArray()) | 
				
			||||
    } | 
				
			||||
} | 
				
			||||