diff --git a/.github/ISSUE_TEMPLATE/01-bugReport.md b/.github/ISSUE_TEMPLATE/01-bugReport.md new file mode 100644 index 000000000..27219b16f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bugReport.md @@ -0,0 +1,52 @@ +--- +name: "[BUG] 问题提交模板" +about: 请从符号">"后面开始填写内容,填写内容可以参考 https://github.com/gedoor/legado/issues/505                     +title: "[BUG] " +labels: 'BUG' +assignees: '' +--- + + +### 机型(如Redmi K30 Pro) +> + + +### 安卓版本(如Android 7.1.1) +> + + +### 阅读Legdao版本(我的-关于-版本,如3.20.112220) +> + +### 网络环境(移动,联通,电信,移动宽带,联通宽带,电信宽带,等等..) +> + + +### 问题描述(简要描述发生的问题) +> + + +### 使用书源(填写URL或者JSON) +> + + +```json + + + + + + +``` + +### 复现步骤(详细描述导致问题产生的操作步骤,如果能稳定复现) +> + + + + +### 日志提交(问题截图或者日志) +> + + + diff --git a/.github/ISSUE_TEMPLATE/02-featureRequest.md b/.github/ISSUE_TEMPLATE/02-featureRequest.md new file mode 100644 index 000000000..9a09a80b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-featureRequest.md @@ -0,0 +1,19 @@ +--- +name: "[FeatureRequest] 功能请求模板" +about: 提交你希望能够在阅读中增加的功能 +title: "[Feature Request] " +labels: '需求' +assignees: '' +--- + +### 功能描述(请清晰的、详细的描述你想要的功能) +> + +### 期望实现方式(阅读应该如何实现该功能) +> + +### 附加信息(其他的与功能相关的附加信息) +> + +### 效果演示(可以手绘一些草图,或者提供可借鉴的图片) +> diff --git a/.github/scripts/lzy_web.py b/.github/scripts/lzy_web.py new file mode 100644 index 000000000..8cd01a62c --- /dev/null +++ b/.github/scripts/lzy_web.py @@ -0,0 +1,99 @@ +import requests, os, datetime, sys + +# Cookie 中 phpdisk_info 的值 +cookie_phpdisk_info = os.environ.get('phpdisk_info') +# Cookie 中 ylogin 的值 +cookie_ylogin = os.environ.get('ylogin') + +# 请求头 +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Referer': 'https://pc.woozooo.com/account.php?action=login' +} + +# 小饼干 +cookie = { + 'ylogin': cookie_ylogin, + 'phpdisk_info': cookie_phpdisk_info +} + + +# 日志打印 +def log(msg): + utc_time = datetime.datetime.utcnow() + china_time = utc_time + datetime.timedelta(hours=8) + print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}") + + +# 检查是否已登录 +def login_by_cookie(): + url_account = "https://pc.woozooo.com/account.php" + if cookie['phpdisk_info'] is None: + log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!') + return False + if cookie['ylogin'] is None: + log('ERROR: 请指定 Cookie 中 ylogin 的值!') + return False + res = requests.get(url_account, headers=headers, cookies=cookie, verify=True) + if '网盘用户登录' in res.text: + log('ERROR: 登录失败,请更新Cookie') + return False + else: + log('登录成功') + return True + + +# 上传文件 +def upload_file(file_dir, folder_id): + file_name = os.path.basename(file_dir) + url_upload = "https://up.woozooo.com/fileup.php" + headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}' + post_data = { + "task": "1", + "folder_id": folder_id, + "id": "WU_FILE_0", + "name": file_name, + } + files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')} + res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120, + verify=True).json() + log(f"{file_dir} -> {res['info']}") + return res['zt'] == 1 + + +# 上传文件夹内的文件 +def upload_folder(folder_dir, folder_id): + file_list = os.listdir(folder_dir) + for file in file_list: + path = os.path.join(folder_dir, file) + if os.path.isfile(path): + upload_file(path, folder_id) + else: + upload_folder(path, folder_id) + + +# 上传 +def upload(dir, folder_id): + if dir is None: + log('ERROR: 请指定上传的文件路径') + return + if folder_id is None: + log('ERROR: 请指定蓝奏云的文件夹id') + return + if os.path.isfile(dir): + upload_file(dir, str(folder_id)) + else: + upload_folder(dir, str(folder_id)) + + +if __name__ == '__main__': + argv = sys.argv[1:] + if len(argv) != 2: + log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id') + # 需上传的路径 + upload_path = argv[0] + # 蓝奏云文件夹id + lzy_folder_id = argv[1] + if login_by_cookie(): + upload(upload_path, lzy_folder_id) \ No newline at end of file diff --git a/.github/workflows/legado.yml b/.github/workflows/legado.yml index eb3ab638e..3702e0f5c 100644 --- a/.github/workflows/legado.yml +++ b/.github/workflows/legado.yml @@ -1,15 +1,10 @@ name: Android CI -on: +on: release: types: [published] push: - branches: - - master - tags: - - '3.*' - pull_request: - branches: + branches: - master # watch: # types: [started] @@ -18,40 +13,68 @@ on: jobs: build: - runs-on: ubuntu-latest - + env: + # 登录蓝奏云后在控制台运行document.cookie + ylogin: ${{ secrets.LANZOU_ID }} + phpdisk_info: ${{ secrets.LANZOU_PSD }} + # 蓝奏云里的文件夹ID(阅读3测试版:2670621) + LANZOU_FOLDER_ID: '2670621' + # 是否上传到artifact + UPLOAD_ARTIFACT: 'true' steps: - - uses: actions/checkout@v2 - - name: set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: clear 18PlusList.txt - run: | - echo "清空18PlusList.txt" - echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt - - name: release apk sign - run: | - echo "给apk增加签名" - cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks - sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i - sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i - - name: apk live together - run: | - echo "设置apk共存" - sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i - sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i - - name: build with gradle - run: | - echo "开始进行release构建" - chmod +x gradlew - ./gradlew assembleAppRelease - - name : upload apk - uses: actions/upload-artifact@master - if: always() - with: - name: legado apk - path: ${{ github.workspace }}/app/build/outputs/apk/app/release + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}- + + - name: Clear 18PlusList.txt + run: | + echo "清空18PlusList.txt" + echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt + - name: Release Apk Sign + run: | + echo "给apk增加签名" + cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks + sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i + - name: Apk Live Together + run: | + echo "设置apk共存" + sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i + sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i + - name: Build With Gradle + run: | + echo "开始进行release构建" + chmod +x gradlew + ./gradlew assembleAppRelease --build-cache --parallel + - name: Upload App To Artifact + if: ${{ env.UPLOAD_ARTIFACT != 'false' }} + uses: actions/upload-artifact@v2 + with: + name: legado apk + path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk + - name: Upload App To Lanzou + if: ${{ env.ylogin }} + run: | + path="$GITHUB_WORKSPACE/app/build/outputs/apk/app/release" + files=$(ls $path) + for f in $files + do + if [[ $f == *"apk" ]]; then + file=$f + echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file" + break + fi + done + + python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID" + echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b" + diff --git a/README.md b/README.md index e11d95c0c..843400d98 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,55 @@ -# legado +
+ legado +

Legado

+
+

阅读3.0, 阅读是一款可以自定义来源阅读网络内容的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。

+
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) +[![Build Action](https://github.com/gedoor/legado/workflows/Android%20CI/badge.svg)](https://github.com/gedoor/legado/actions) +[![Downloads](https://img.shields.io/github/downloads/gedoor/legado/total.svg)](https://github.com/gedoor/legado/releases/latest) +[![GitHub issues](https://img.shields.io/github/issues/gedoor/legado)](https://github.com/gedoor/legado/issues) +[![GitHub contributors](https://img.shields.io/github/contributors/gedoor/legado)](https://github.com/gedoor/legado/graphs/contributors) ## 阅读3.0 -* 书源规则 https://celeter.github.io -* 更新日志 [updateLog.md](/app/src/main/assets/updateLog.md) +* [书源规则](https://alanskycn.gitee.io/teachme) +* [更新日志](/app/src/main/assets/updateLog.md) +* [帮助文档](/app/src/main/assets/help/appHelp.md) +* [web端](https://github.com/celetor/web-yuedu3) +## 下载 +Google Play or CoolApk or [Releases](https://github.com/gedoor/legado/releases/latest) + +## 阅读API +* 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。 +* 可通过url唤起阅读进行一键导入,url格式: legado://import/{path}?src={url} +* path类型: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig +* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版 + +## 感谢 +``` +org.jsoup:jsoup +cn.wanghaomiao:JsoupXpath +com.jayway.jsonpath:json-path +com.github.gedoor:rhino-android +com.squareup.okhttp3:okhttp +com.ljx.rxhttp:rxhttp +com.github.bumptech.glide:glide +org.nanohttpd:nanohttpd +org.nanohttpd:nanohttpd-websocket +cn.bingoogolapple:bga-qrcode-zxing +com.jaredrummler:colorpicker +org.apache.commons:commons-text +io.noties.markwon:core +io.noties.markwon:image-glide +com.hankcs:hanlp +com.positiondev.epublib:epublib-core +``` + +## 免责声明 +https://gedoor.github.io/MyBookshelf/disclaimer.html + +## 界面 ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B1.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B2.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B3.jpg) @@ -13,8 +57,5 @@ ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B5.jpg) ![image](https://github.com/gedoor/gedoor.github.io/blob/master/images/%E9%98%85%E8%AF%BB%E7%AE%80%E4%BB%8B6.jpg) -### 阅读API -阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。 - -## 免责声明 -https://gedoor.github.io/MyBookshelf/disclaimer.html +## 其它 +其它网友做的IOS版本: https://github.com/kaich/Yuedu diff --git a/api.md b/api.md index 269044458..a8b821ec5 100644 --- a/api.md +++ b/api.md @@ -4,7 +4,7 @@ ## 使用 ### Web 以下说明假设您的操作在本机进行,且开放端口为1234。 -如果您要从远程计算机访问[阅读],请将`127.0.0.1`替换成手机IP。 +如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。 #### 插入单个书源 ``` URL = http://127.0.0.1:1234/saveSource @@ -74,13 +74,18 @@ Method = GET 获取指定图书的章节列表。 #### 获取书籍内容 - ``` URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1 Method = GET ``` +获取指定图书的第`index`章节的文本内容。 + +#### 获取封面 +``` +URL = http://127.0.0.1:1234/cover?path=xxxxx +Method = GET +``` -获取指定图书的第`index`章节的文本内容。 ### Content Provider * 需声明`io.legado.READ_WRITE`权限 @@ -164,6 +169,11 @@ Method = query URL = content://providerHost/book/content/query?url=xxx&index=1 Method = query ``` - 获取指定图书的第`index`章节的文本内容。 用`Cursor.getString(0)`取出返回结果。 + +#### 获取封面 +``` +URL = content://providerHost/book/cover/query?path=xxxx +Method = query +``` diff --git a/app/build.gradle b/app/build.gradle index 43a086553..902af3cd7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'de.timfreiheit.resourceplaceholders' -apply plugin: 'io.fabric' - -androidExtensions { - experimental = true -} static def releaseTime() { return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8")) @@ -18,7 +13,7 @@ def version = "3." + releaseTime() def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim()) android { - compileSdkVersion 29 + compileSdkVersion 30 signingConfigs { if (project.hasProperty("RELEASE_STORE_FILE")) { myConfig { @@ -34,15 +29,16 @@ android { defaultConfig { applicationId "io.legado.app" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode gitCommits versionName version testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" project.ext.set("archivesBaseName", name + "_" + version) multiDexEnabled true + javaCompileOptions { annotationProcessorOptions { - arguments = [ + arguments += [ "room.incremental" : "true", "room.expandProjection": "true", "room.schemaLocation" : "$projectDir/schemas".toString() @@ -50,6 +46,9 @@ android { } } } + buildFeatures { + viewBinding true + } buildTypes { release { if (project.hasProperty("RELEASE_STORE_FILE")) { @@ -86,17 +85,29 @@ android { applicationId "io.legado.play" manifestPlaceholders = [APP_CHANNEL_VALUE: "google"] } + cronet { + dimension "mode" + applicationId "io.legado.cronet" + manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"] + ndk { + abiFilters 'arm64-v8a','armeabi-v7a','x86_64','x86' + } + } } compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true - // Sets Java compatibility to Java 8 - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Sets Java compatibility to Java 11 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" + } + buildToolsVersion '30.0.3' + tasks.withType(JavaCompile) { + //options.compilerArgs << "-Xlint:unchecked" } } @@ -111,102 +122,98 @@ kapt { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' - implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - implementation "com.android.support:multidex:1.0.3" + coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5') + testImplementation('junit:junit:4.13.2') + androidTestImplementation('androidx.test:runner:1.4.0') + androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0') + implementation('androidx.multidex:multidex:2.0.1') //kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - - //fireBase - implementation 'com.google.firebase:firebase-core:17.5.0' - implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") + //协程 + def coroutines_version = '1.5.1' + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version") //androidX - implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.media:media:1.1.0' - implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.viewpager2:viewpager2:1.0.0' - implementation 'com.google.android.material:material:1.2.1' - implementation 'com.google.android:flexbox:1.1.0' - implementation 'com.google.code.gson:gson:2.8.6' + implementation('androidx.appcompat:appcompat:1.3.1') + implementation('androidx.core:core-ktx:1.6.0') + implementation("androidx.activity:activity-ktx:1.3.0") + implementation("androidx.fragment:fragment-ktx:1.3.6") + implementation('androidx.preference:preference-ktx:1.1.1') + implementation('androidx.constraintlayout:constraintlayout:2.0.4') + implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0') + implementation('androidx.viewpager2:viewpager2:1.0.0') + implementation('com.google.android.material:material:1.4.0') + implementation('com.google.android.flexbox:flexbox:3.0.0') + implementation('com.google.code.gson:gson:2.8.7') + implementation('androidx.webkit:webkit:1.4.0') + + //media + def media2_version = "1.1.3" + implementation("androidx.media2:media2-session:$media2_version") + implementation("androidx.media:media:1.4.0") + //implementation "androidx.media2:media2-player:$media2_version" + //implementation 'com.google.android.exoplayer:exoplayer:2.13.0' + + //Splitties + def splitties_version = '2.1.1' + implementation("com.louiscad.splitties:splitties-appctx:$splitties_version") + implementation("com.louiscad.splitties:splitties-systemservices:$splitties_version") + implementation("com.louiscad.splitties:splitties-views:$splitties_version") //lifecycle - def lifecycle_version = '2.2.0' - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + def lifecycle_version = '2.3.1' + implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version") //room - def room_version = '2.2.5' - implementation "androidx.room:room-runtime:$room_version" - kapt "androidx.room:room-compiler:$room_version" - testImplementation "androidx.room:room-testing:2.2.5" - - //paging - implementation 'androidx.paging:paging-runtime-ktx:2.1.2' - - //anko - def anko_version = '0.10.8' - implementation "org.jetbrains.anko:anko-sdk27:$anko_version" - implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" + def room_version = '2.3.0' + implementation("androidx.room:room-runtime:$room_version") + implementation("androidx.room:room-ktx:$room_version") + kapt("androidx.room:room-compiler:$room_version") + testImplementation("androidx.room:room-testing:$room_version") //liveEventBus - implementation 'com.jeremyliao:live-event-bus-x:1.5.7' - - //协程 - def coroutines_version = '1.3.7' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + implementation('io.github.jeremyliao:live-event-bus-x:1.8.0') //规则相关 - implementation 'org.jsoup:jsoup:1.13.1' - implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' - implementation 'com.jayway.jsonpath:json-path:2.4.0' + implementation('org.jsoup:jsoup:1.14.1') + implementation('com.jayway.jsonpath:json-path:2.6.0') + implementation('cn.wanghaomiao:JsoupXpath:2.5.0') + implementation(project(path: ':epublib')) //JS rhino - implementation 'com.github.gedoor:rhino-android:1.4' + implementation('com.github.gedoor:rhino-android:1.6') //网络 - //noinspection GradleDependency - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' + implementation('com.squareup.okhttp3:okhttp:4.9.1') + compileOnly(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar'])) + cronetImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar'])) //Glide - implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation('com.github.bumptech.glide:glide:4.12.0') //webServer - implementation 'org.nanohttpd:nanohttpd:2.3.1' - implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1' + implementation('org.nanohttpd:nanohttpd:2.3.1') + implementation('org.nanohttpd:nanohttpd-websocket:2.3.1') + implementation('org.nanohttpd:nanohttpd-apache-fileupload:2.3.1') //二维码 - implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7' + implementation('com.github.jenly1314:zxing-lite:2.1.0') //颜色选择 - implementation 'com.jaredrummler:colorpicker:1.1.0' + implementation('com.jaredrummler:colorpicker:1.1.0') //apache - implementation 'org.apache.commons:commons-lang3:3.11' - implementation 'org.apache.commons:commons-text:1.8' + implementation('org.apache.commons:commons-text:1.9') //MarkDown - implementation 'ru.noties.markwon:core:3.1.0' + def markwonVersion = "4.6.2" + implementation("io.noties.markwon:core:$markwonVersion") + implementation("io.noties.markwon:image-glide:$markwonVersion") + implementation("io.noties.markwon:ext-tables:$markwonVersion") + implementation("io.noties.markwon:html:$markwonVersion") //转换繁体 - implementation 'com.hankcs:hanlp:portable-1.7.8' - - //epub - implementation('com.positiondev.epublib:epublib-core:3.1') { - exclude group: 'org.slf4j' - exclude group: 'xmlpull' - } + implementation('com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.1') - //E-Ink 有些手机会出现重影 - //implementation 'fadeapp.widgets:scrollless-recyclerView:1.0.2' } - -apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/cronetlib/cronet_api.jar b/app/cronetlib/cronet_api.jar new file mode 100644 index 000000000..d62e993bb Binary files /dev/null and b/app/cronetlib/cronet_api.jar differ diff --git a/app/cronetlib/cronet_impl_common_java.jar b/app/cronetlib/cronet_impl_common_java.jar new file mode 100644 index 000000000..f851e7120 Binary files /dev/null and b/app/cronetlib/cronet_impl_common_java.jar differ diff --git a/app/cronetlib/cronet_impl_native_java.jar b/app/cronetlib/cronet_impl_native_java.jar new file mode 100644 index 000000000..1a29039ad Binary files /dev/null and b/app/cronetlib/cronet_impl_native_java.jar differ diff --git a/app/cronetlib/cronet_impl_platform_java.jar b/app/cronetlib/cronet_impl_platform_java.jar new file mode 100644 index 000000000..6adb23853 Binary files /dev/null and b/app/cronetlib/cronet_impl_platform_java.jar differ diff --git a/app/cronetlib/src/cronet_api-src.jar b/app/cronetlib/src/cronet_api-src.jar new file mode 100644 index 000000000..78bf99a0e Binary files /dev/null and b/app/cronetlib/src/cronet_api-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_common_java-src.jar b/app/cronetlib/src/cronet_impl_common_java-src.jar new file mode 100644 index 000000000..7d7da6f33 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_common_java-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_native_java-src.jar b/app/cronetlib/src/cronet_impl_native_java-src.jar new file mode 100644 index 000000000..83a357a91 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_native_java-src.jar differ diff --git a/app/cronetlib/src/cronet_impl_platform_java-src.jar b/app/cronetlib/src/cronet_impl_platform_java-src.jar new file mode 100644 index 000000000..48c544ef7 Binary files /dev/null and b/app/cronetlib/src/cronet_impl_platform_java-src.jar differ diff --git a/app/google-services.json b/app/google-services.json index 314a5ea7c..fcbc11f0d 100644 --- a/app/google-services.json +++ b/app/google-services.json @@ -8,9 +8,9 @@ "client": [ { "client_info": { - "mobilesdk_app_id": "1:453392274790:android:1d2b1eefbe0e78cff624a7", + "mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7", "android_client_info": { - "package_name": "io.legado.app" + "package_name": "io.legado.app.debug" } }, "oauth_client": [ @@ -37,12 +37,20 @@ }, { "client_info": { - "mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7", + "mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7", "android_client_info": { - "package_name": "io.legado.app.debug" + "package_name": "io.legado.app.release" } }, "oauth_client": [ + { + "client_id": "453392274790-trrgennt5njr1lhil1sgtf0ogcgd38fo.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.app.release", + "certificate_hash": "fd67dba87b7b761631266f19ddde249054aac5c1" + } + }, { "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", "client_type": 3 @@ -66,12 +74,20 @@ }, { "client_info": { - "mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7", + "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", "android_client_info": { - "package_name": "io.legado.app.release" + "package_name": "io.legado.play.release" } }, "oauth_client": [ + { + "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.play.release", + "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" + } + }, { "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", "client_type": 3 @@ -97,10 +113,18 @@ "client_info": { "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", "android_client_info": { - "package_name": "io.legado.play.release" + "package_name": "io.legado.play.debug" } }, "oauth_client": [ + { + "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.play.debug", + "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" + } + }, { "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", "client_type": 3 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 215f31c33..a2949e6bf 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -54,6 +54,8 @@ # Android开发中一些需要保留的公共部分 # ############################################# +# 屏蔽错误Unresolved class name +#noinspection ShrinkerUnresolvedReference # 保留我们使用的四大组件,自定义的Application等等这些类不被混淆 # 因为这些子类都有可能被外部调用 @@ -66,7 +68,6 @@ -keep public class * extends android.preference.Preference -keep public class * extends android.view.View - # 保留androidx下的所有类及其内部类 -keep class androidx.** {*;} @@ -221,3 +222,10 @@ public static **[] values(); public static ** valueOf(java.lang.String); } + + +# Keep all of Cronet API as it's used by the Cronet module. +-keep public class org.chromium.net.* { + !private *; + *; +} \ No newline at end of file diff --git a/app/src/debug/res/values-zh-rHK/strings.xml b/app/src/debug/res/values-zh-rHK/strings.xml deleted file mode 100644 index 07b0a5dbe..000000000 --- a/app/src/debug/res/values-zh-rHK/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 閲讀.D - 閲讀·D·搜索 - \ No newline at end of file diff --git a/app/src/debug/res/values-zh-rTW/strings.xml b/app/src/debug/res/values-zh-rTW/strings.xml deleted file mode 100644 index 07b0a5dbe..000000000 --- a/app/src/debug/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 閲讀.D - 閲讀·D·搜索 - \ No newline at end of file diff --git a/app/src/google/res/values-zh-rTW/strings.xml b/app/src/google/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..35770fc68 --- /dev/null +++ b/app/src/google/res/values-zh-rTW/strings.xml @@ -0,0 +1,6 @@ + + + + 閱讀Pro + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 524fe903a..ffefdee66 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,11 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="io.legado.app"> - - - @@ -18,6 +13,12 @@ + + + + - + @@ -46,6 +49,7 @@ @@ -62,6 +66,7 @@ @@ -78,6 +83,7 @@ @@ -94,6 +100,7 @@ @@ -110,6 +117,7 @@ @@ -126,6 +134,7 @@ @@ -147,7 +156,16 @@ + android:exported="true" + android:launchMode="singleTask"> + + + + + + + + + + + android:launchMode="standard" /> + android:launchMode="singleTask" + android:screenOrientation="behind" /> + android:launchMode="singleTask" + android:screenOrientation="behind" /> + android:launchMode="singleTop" + android:screenOrientation="behind" /> + android:launchMode="singleTop" + android:screenOrientation="behind" /> - + android:name=".ui.replace.ReplaceRuleActivity" + android:launchMode="singleTop" + android:screenOrientation="behind" /> + android:launchMode="singleTop" + android:screenOrientation="behind" /> + android:launchMode="singleTop" + android:screenOrientation="behind" /> + + @@ -248,13 +287,13 @@ - + @@ -262,10 +301,18 @@ android:name=".ui.about.ReadRecordActivity" android:configChanges="orientation|screenSize" android:hardwareAccelerated="true" /> + + + android:exported="true" + android:label="@string/receiving_shared_label" + android:theme="@style/AppTheme.Transparent"> @@ -281,10 +328,11 @@ - + @@ -292,74 +340,86 @@ - + + - + + + - + + + + + + + + + - - - + - - - - - - - - - - + - + - - - + + + + + + + + + + + + - + + + + + + + - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/18PlusList.txt b/app/src/main/assets/18PlusList.txt index 52a5bb3cc..3451937dc 100644 --- a/app/src/main/assets/18PlusList.txt +++ b/app/src/main/assets/18PlusList.txt @@ -80,3 +80,205 @@ cm5neHM= OTl3ZW5rdQ== bGFvc2lqaXhz ZnVzaHV6aGFpMQ== +cG8xOA== +czUyMTc= +c2FuaGFveHM= +NTJrc2h1 +NDhyeA== +ZWNub3ZlbA== +bGllaHVvenc= +eGlhb3FpYW5nd3g= +NTJrc2h1 +NDh3eA== +NTJrc2h1 +MDB1aQ== +MDFieg== +c2h1YmFvMQ== +ZG54aWFvc2h1b2E= +am5zaHViYQ== +MThzaHV3dQ== +bGV4cw== +MzM1eHM= +dXB1 +ZnVndW9kdQ== +ODB0eHQ= +YWFyZWFk +eWlkdWR1MQ== +YmFuemh1d2FuZw== +cWloYW9xaWhhbw== +OHhpYW54cw== +amluamlzaHV3dQ== +d21wOA== +ZXl1c2h1d3U= +NTB4c2Y= +aGF4d3g1 +cG93YW5qdWFu +d2luMTBjaXR5 +eWV5ZXhzdw== +bXlzaHVnZQ== +eGlhbmd0eHN3 +Y3Vpd2VpanV4 +MzY2eHN3 +aHVheXVld2Vua3U= +eW91ZGlhbmxlbg== +c291Nzg= +bGFucm91Mg== +cXFib29r +eW91d3V4cw== +cnVpbGlzYWxl +MzY1bXd3 +ZnV3ZW5o +bGVzYmw= +YXd1Ym9vaw== +bGl5dXhpYW5nMjAyMA== +OTJwb3Bv +ZnVzaHV0dWFu +ODhkYW5tZWk= +ZG14cw== +eXVsaW56aGFueWU= +M2hlYmFv +eGd1YWd1YXhz +ZGl5aWJhbnpodTY= +aXJlYWR4cw== +c2h1YmFvOTY= +ZGl5aWJhbnpodTU1NQ== +c2Fuaml1enc= +N3Fpbmc3 +NjZsZXdlbg== +a3l4czU= +MjIyMjJ4cw== +c2hhb3NodWdl +amlsaW41NQ== +bWt4czY= +amluc2h1bG91 +eGlhbndhbmdz +eWlkdWR1 +cWR0eHQ= +MTZib29rMQ== +am1zaHV3dQ== +MzY2eHN3 +ZHliejk= +c2hvdWRhOA== +ZnlxMTg= +eWlzaHVn +eXV6aGFpd3VsYQ== +MTFiYW56aHU= +MTIzeGlhb3FpYW5n +ZGl5aWJhbnpodTk= +ZGl5aWJhbnpodQ== +MzY2eHN3 +ODdzaHV3dQ== +NnF3eA== +emhlbmh1bnhpYW9zaHVv +bG9uZ3Rlbmc1Mg== +eGlueGluZ3hpYW5nemhpZmE= +ZHliejk= +ZHVvemhla2Fu +MTIzeGlhb3FpYW5n +MzM1eHM= +am1zaHV3dQ== +c2hhb3NodWdl +bGF3ZW54cw== +cnVzaHV3dQ== +MzY2eHN3 +NTB4c2Y= +bGV3ZW41NQ== +aGFpdGFuZzEyMw== +aGViYW81MjA= +bHVvcWl1enc= +c3NzeHN3 +c2h1c2h1d3V4cw== +cm5neHM= +cWR4aWFvc2h1bw== +dHl1ZQ== +Y2hlNDM= +bG9uZ3Rlbmcy +amZ5eHNo +aGV0dTI= +bGFvc2lqaXhz +bG9uZ3Rlbmd4cw== +bGllaHVvenc= +c2h1YmFvYW4= +eHNodW9zaHVv +NTIxZGFubWVp +YmFuemh1MjI= +cWtzaHU= +eWZ4aWFvc2h1b2U= +a3lnc28= +c2h1bG91YmE= +NXRucw== +N3Fpbmc3 +bWlhb2R1NQ== +eXVzaHV3ZW4= +YWFyZWFk +cXRzaHU= +MTdzaHV3dQ== +c2h1YmFvMnM= +YnowMDE= +ZGFtb2dl +MTMxdGI= +aXhpYW9z +bXlzaHVnZQ== +OXhpYW53ZW4= +ZHVvemhla2Fu +MTIwdw== +c2h1c2h1d3U1MjA= +c2h1YmFvMnM= +YWd4c3c= +OTR4c3c= +cG8xOA== +eWFvY2hpeHM= +eGlhb3FpYW5neHM= +Ym9va2Js +c2Fuaml1eHM= +d29kZXNodWJhbw== +em9uZ2NhaXhpYW9zaHVvMg== +OWI4OTEzOTRkZjVi +MThub3ZlbA== +YWFib29r +YjF0eHQ= +eXVjYWl6dw== +Yzl0eHQ= +ZGl5aWJhbnpodTU1NQ== +MzBtYw== +eGlueXVzaHV3dQ== +c2h1YmFvd2FuZzEyMw== +YWd4cw== +YmlxdWdlbmw= +c2hpcWlzaHV3dQ== +c2lsdWtl +ZGl5aWJhbnpodTg= +ZGl5aWJhbnpodTk= +aGV0dW54cw== +OTl3ZW5rdQ== +aGFpdGFuZ3NodXd1 +OTd5ZA== +eXV6aGFpd3UxMQ== +Y3Vpd2VpanV4cw== +Y2JpcXU= +NTIxZGFubWVp +c2h1YmFvMzM= +c2FuaGFvMQ== +dGlhbm1lbmd3ZW5rdQ== +eXVzaHV3dTUyMA== +c2h1YmFvMjIy +c2h1YmFvd2FuZzEyMw== +eXVib29r +Y2JpcXU= +MWxld2Vu +MTV4c3c= +eG5jd3h3 +c2h1YmFvd2FuZzEyMw== +c2FuaGFveHM= +eXV3YW5nc2hl +YmlxdXRz +bGFtZWl4cw== +eGJhbnpodQ== +cWR4aWFvc2h1bw== +bWh0bGE= +OTl3ZW5rdQ== +eGlhb3FpYW5nNTIw +dGlhbm1lbmd3ZW5rdQ== +YWlmdXNodQ== +bWlhb2R1NQ== +bWlmZW5neHM= diff --git a/app/src/main/assets/defaultData/httpTTS.json b/app/src/main/assets/defaultData/httpTTS.json new file mode 100644 index 000000000..08d3386cd --- /dev/null +++ b/app/src/main/assets/defaultData/httpTTS.json @@ -0,0 +1,237 @@ +[ + { + "id": -100, + "name": "0", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}" + }, + { + "id": -99, + "name": "zaixianai.cn", + "url": "\nlet url='https://www.zaixianai.cn/voiceCompose';\n\nlet ua=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\";\n\nlet doc=java.get(url,{\"User-Agent\":ua});\nlet cookie=String(doc.header(\"set-cookie\")).match(/laravel_session=[^\\n]+/)[0];\nlet token=String(doc.body()).match(/token=\"([^\"]+)/)[1];\n\nurl='https://www.zaixianai.cn/Api_getVoice,'+JSON.stringify({\n\"method\": \"POST\",\n\"body\": \"content=\" + java.encodeURI(speakText) + \"&volume=50&speech_rate=0&voice=Aixia&_token=\"+token,\n\"headers\": {\n\"User-Agent\": ua,\n\"cookie\": cookie\n}\n});\n\nlet res=java.ajax(url);\n\n'https://www.zaixianai.cn/voice/'+JSON.parse(res).data.file_name+','+JSON.stringify({\n\"headers\": {\n\"User-Agent\": ua,\n\"accept\": \"*/*\",\n\"referer\": \"https://www.zaixianai.cn/voiceCompose\",\n\"cookie\": cookie,\n\"accept-encoding\": \"identity;q=1, *;q=0\"\n}\n})\n" + }, + { + "id": -98, + "name": "台湾女声", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4007&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -1, + "name": "度丫丫", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -2, + "name": "度博文①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -3, + "name": "度博文②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -4, + "name": "度博文③", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -97, + "name": "度小乔", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=3&_res_tag_=audio\"\n}" + }, + { + "id": -5, + "name": "度小娇", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -6, + "name": "度小宇", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=2&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -7, + "name": "度小童", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -96, + "name": "度小童", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -95, + "name": "度小粤", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=cte&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -8, + "name": "度小美", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -94, + "name": "度小芳", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4125&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -9, + "name": "度小萌", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -93, + "name": "度小贤", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"}" + }, + { + "id": -92, + "name": "度小雯", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -10, + "name": "度小鹿①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -12, + "name": "度小鹿②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4119&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -11, + "name": "度小鹿③", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5118&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -91, + "name": "度灵儿", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -13, + "name": "度米朵①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -14, + "name": "度米朵②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -15, + "name": "度逍遥-基础", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -16, + "name": "度逍遥-精品①", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -17, + "name": "度逍遥-精品②", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -18, + "name": "情感女声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4105&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -19, + "name": "情感男声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4115&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -20, + "name": "标准女声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -90, + "name": "标准女声-基础", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=100&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -21, + "name": "标准男声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160=&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -89, + "name": "温柔女声", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4126&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -88, + "name": "甜美女声①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=1200&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=&rate=32=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -24, + "name": "甜美女声②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -25, + "name": "甜美女声③", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5117&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -22, + "name": "电台女声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5120&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=&rate=32=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -23, + "name": "电台男声", + "url": "http://tsn.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5121&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -26, + "name": "百度主持", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -87, + "name": "百度主持", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4127&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=11&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -86, + "name": "百度解说①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4123&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -85, + "name": "百度解说②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4128&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -84, + "name": "百度解说③", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4129&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=12&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -27, + "name": "百度评书①", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=301&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -28, + "name": "百度评书②", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -83, + "name": "萝莉少女音", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=5201&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}" + }, + { + "id": -29, + "name": "阿里云语音", + "url": "/*播音人Aiting可改其他https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/speaker.json,详见https://ai.aliyun.com/nls/tts*/;eval(''+java.ajax('https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/ttsDemo.js'));ttsDemo(speakText,speakSpeed,'Aiting')" + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/readConfig.json b/app/src/main/assets/defaultData/readConfig.json new file mode 100644 index 000000000..cd62a05be --- /dev/null +++ b/app/src/main/assets/defaultData/readConfig.json @@ -0,0 +1,107 @@ +[ + { + "bgStr": "#ffc0edc6", + "bgStrEInk": "#FFFFFF", + "bgStrNight": "#000000", + "bgType": 0, + "bgTypeEInk": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconEInk": true, + "darkStatusIconNight": false, + "footerMode": 0, + "footerPaddingBottom": 10, + "footerPaddingLeft": 13, + "footerPaddingRight": 17, + "footerPaddingTop": 0, + "headerMode": 0, + "headerPaddingBottom": 0, + "headerPaddingLeft": 19, + "headerPaddingRight": 16, + "headerPaddingTop": 10, + "letterSpacing": 0, + "lineSpacingExtra": 10, + "name": "微信读书", + "paddingBottom": 4, + "paddingLeft": 22, + "paddingRight": 22, + "paddingTop": 5, + "pageAnim": 3, + "pageAnimEInk": 3, + "paragraphIndent": "  ", + "paragraphSpacing": 6, + "showFooterLine": true, + "showHeaderLine": true, + "textBold": 0, + "textColor": "#ff0b0b0b", + "textColorEInk": "#000000", + "textColorNight": "#ADADAD", + "textSize": 24, + "tipColor": -10461088, + "tipFooterLeft": 7, + "tipFooterMiddle": 0, + "tipFooterRight": 6, + "tipHeaderLeft": 1, + "tipHeaderMiddle": 0, + "tipHeaderRight": 2, + "titleBottomSpacing": 0, + "titleMode": 0, + "titleSize": 4, + "titleTopSpacing": 0 + }, + { + "name": "预设1", + "bgStr": "#FFFFFF", + "bgStrNight": "#000000", + "textColor": "#000000", + "textColorNight": "#FFFFFF", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "name": "预设2", + "bgStr": "#DDC090", + "bgStrNight": "#3C3F43", + "textColor": "#3E3422", + "textColorNight": "#DCDFE1", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "name": "预设3", + "bgStr": "#C2D8AA", + "bgStrNight": "#3C3F43", + "textColor": "#596C44", + "textColorNight": "#88C16F", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "name": "预设4", + "bgStr": "#DBB8E2", + "bgStrNight": "#3C3F43", + "textColor": "#68516C", + "textColorNight": "#F6AEAE", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "name": "预设5", + "bgStr": "#ABCEE0", + "bgStrNight": "#3C3F43", + "textColor": "#3D4C54", + "textColorNight": "#90BFF5", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/rssSources.json b/app/src/main/assets/defaultData/rssSources.json new file mode 100644 index 000000000..0537719fa --- /dev/null +++ b/app/src/main/assets/defaultData/rssSources.json @@ -0,0 +1,30 @@ +[ + { + "customOrder": 1, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceIcon": "http:\/\/ku.mumuceo.com\/static\/images\/applogo\/yuedu.png", + "sourceName": "使用说明", + "sourceUrl": "https://www.yuque.com/legado" + }, + { + "customOrder": 2, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceIcon": "http:\/\/mmbiz.qpic.cn\/mmbiz_png\/hpfMV8hEuL2eS6vnCxvTzoOiaCAibV6exBzJWq9xMic9xDg3YXAick87tsfafic0icRwkQ5ibV0bJ84JtSuxhPuEDVquA\/0?wx_fmt=png", + "sourceName": "小说拾遗", + "sourceUrl": "snssdk1128:\/\/user\/profile\/562564899806367" + }, + { + "customOrder": 3, + "enableJs": true, + "enabled": true, + "loadWithBaseUrl": false, + "singleUrl": true, + "sourceIcon": "https://Cloud.miaogongzi.net/images/icon.png", + "sourceName": "Meow云", + "sourceUrl": "https://pan.miaogongzi.net" + } +] \ No newline at end of file diff --git a/app/src/main/assets/themeConfig.json b/app/src/main/assets/defaultData/themeConfig.json similarity index 100% rename from app/src/main/assets/themeConfig.json rename to app/src/main/assets/defaultData/themeConfig.json diff --git a/app/src/main/assets/txtTocRule.json b/app/src/main/assets/defaultData/txtTocRule.json similarity index 60% rename from app/src/main/assets/txtTocRule.json rename to app/src/main/assets/defaultData/txtTocRule.json index 11131b334..ad6f7d591 100644 --- a/app/src/main/assets/txtTocRule.json +++ b/app/src/main/assets/defaultData/txtTocRule.json @@ -2,50 +2,50 @@ { "id": -1, "enable": true, - "name": "目录", - "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", + "name": "目录(去空白)", + "rule": "(?<=[ \\s])(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", "serialNumber": 0 }, { "id": -2, "enable": true, - "name": "目录(去空白)", - "rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", + "name": "目录", + "rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", "serialNumber": 1 }, { "id": -3, "enable": false, "name": "目录(匹配简介)", - "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", "serialNumber": 2 }, { "id": -4, "enable": false, "name": "目录(古典、轻小说备用)", - "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$", "serialNumber": 3 }, { "id": -5, "enable": false, "name": "数字(纯数字标题)", - "rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$", + "rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$", "serialNumber": 4 }, { "id": -6, "enable": true, "name": "数字 分隔符 标题名称", - "rule": "^[  \\t]{0,4}\\d{1,5}[,., 、_—\\-].{1,30}$", + "rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$", "serialNumber": 5 }, { "id": -7, "enable": true, "name": "大写数字 分隔符 标题名称", - "rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$", + "rule": "^[  \\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\-].{1,30}$", "serialNumber": 6 }, { @@ -73,7 +73,7 @@ "id": -11, "enable": true, "name": "特殊符号 序号 标题", - "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", + "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", "serialNumber": 10 }, { @@ -94,7 +94,7 @@ "id": -14, "enable": true, "name": "章/卷 序号 标题", - "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", + "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", "serialNumber": 13 }, { @@ -108,21 +108,21 @@ "id": -16, "enable":false, "name": "双标题(前向)", - "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", + "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", "serialNumber": 15 }, { "id": -17, "enable":false, "name": "双标题(后向)", - "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", + "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", "serialNumber": 16 }, { "id":-18, "enable": true, "name": "标题 特殊符号 序号", - "rule": "^.{1,20}[((][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$", + "rule": "^.{1,20}[((][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \t]{0,4}$", "serialNumber": 17 } -] +] \ No newline at end of file diff --git a/app/src/main/assets/epub/chapter.html b/app/src/main/assets/epub/chapter.html new file mode 100644 index 000000000..6067f6649 --- /dev/null +++ b/app/src/main/assets/epub/chapter.html @@ -0,0 +1,16 @@ + + + + + Chapter + + + + + +

{title}

+{content} + + diff --git a/app/src/main/assets/epub/cover.html b/app/src/main/assets/epub/cover.html new file mode 100644 index 000000000..cb6df6353 --- /dev/null +++ b/app/src/main/assets/epub/cover.html @@ -0,0 +1,21 @@ + + + + + Cover + + + +
+

{name}

+
{author} / 著
+ + \ No newline at end of file diff --git a/app/src/main/assets/epub/fonts.css b/app/src/main/assets/epub/fonts.css new file mode 100644 index 000000000..3457a6511 --- /dev/null +++ b/app/src/main/assets/epub/fonts.css @@ -0,0 +1,267 @@ +@charset "utf-8"; +/*---常用---*/ + +@font-face { + font-family: "zw"; + src: + local("宋体"),local("明体"),local("明朝"), + local("Songti"),local("Songti SC"),local("Songti TC"), /*iOS6+iBooks3*/ + local("Song S"),local("Song T"),local("STBShusong"),local("TBMincho"),local("HYMyeongJo"), /*Kindle Paperwihite*/ + local("DK-SONGTI"), + url(../Fonts/zw.ttf), + url(res:///opt/sony/ebook/FONT/zw.ttf), + url(res:///Data/FONT/zw.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/zw.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/zw.ttf), + url(res:///ebook/fonts/zw.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/zw.ttf), + url(res:///../../media/mmcblk0p1/fonts/zw.ttf), + url(file:///mnt/us/DK_System/system/fonts/zw.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/zw.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/zw.ttf), + url(res:///system/fonts/zw.ttf), + url(res:///system/media/sdcard/fonts/zw.ttf), + url(res:///media/fonts/zw.ttf), + url(res:///sdcard/fonts/zw.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/zw.ttf), + url(res:///media/flash/fonts/zw.ttf), + url(res:///media/sd/fonts/zw.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/zw.ttf), + url(res:///../fonts/zw.ttf), + url(../../../../../zw.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/zw.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/zw.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/zw.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "fs"; + src: + local("amasis30"),local("仿宋"),local("仿宋_GB2312"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), /*iOS6+iBooks3*/ + local("DK-FANGSONG"), + url(../Fonts/fs.ttf), + url(res:///opt/sony/ebook/FONT/fs.ttf), + url(res:///Data/FONT/fs.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/fs.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/fs.ttf), + url(res:///ebook/fonts/fs.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/fs.ttf), + url(res:///../../media/mmcblk0p1/fonts/fs.ttf), + url(file:///mnt/us/DK_System/system/fonts/fs.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/fs.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/fs.ttf), + url(res:///system/fonts/fs.ttf), + url(res:///system/media/sdcard/fonts/fs.ttf), + url(res:///media/fonts/fs.ttf), + url(res:///sdcard/fonts/fs.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/fs.ttf), + url(res:///media/flash/fonts/fs.ttf), + url(res:///media/sd/fonts/fs.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/fs.ttf), + url(res:///../fonts/fs.ttf), + url(../../../../../fs.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/fs.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/fs.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/fs.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "kt"; + src: + local("Caecilia"),local("楷体"),local("楷体_GB2312"), + local("Kaiti"),local("Kaiti SC"),local("Kaiti TC"), /*iOS6+iBooks3*/ + local("MKai PRC"),local("MKaiGB18030C-Medium"),local("MKaiGB18030C-Bold"), /*Kindle Paperwihite*/ + local("DK-KAITI"), + url(../Fonts/kt.ttf), + url(res:///opt/sony/ebook/FONT/kt.ttf), + url(res:///Data/FONT/kt.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/kt.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/kt.ttf), + url(res:///ebook/fonts/kt.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/kt.ttf), + url(res:///../../media/mmcblk0p1/fonts/kt.ttf), + url(file:///mnt/us/DK_System/system/fonts/kt.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/kt.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/kt.ttf), + url(res:///system/fonts/kt.ttf), + url(res:///system/media/sdcard/fonts/kt.ttf), + url(res:///media/fonts/kt.ttf), + url(res:///sdcard/fonts/kt.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/kt.ttf), + url(res:///media/flash/fonts/kt.ttf), + url(res:///media/sd/fonts/kt.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/kt.ttf), + url(res:///../fonts/kt.ttf), + url(../../../../../kt.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/kt.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/kt.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/kt.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "ht"; + src: + local("黑体"),local("微软雅黑"), + local("Heiti"),local("Heiti SC"),local("Heiti TC"), /*iOS6+iBooks3*/ + local("MYing Hei S"),local("MYing Hei T"),local("TBGothic"), /*Kindle Paperwihite*/ + local("DK-HEITI"), + url(../Fonts/ht.ttf), + url(res:///opt/sony/ebook/FONT/ht.ttf), + url(res:///Data/FONT/ht.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/ht.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/ht.ttf), + url(res:///ebook/fonts/ht.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/ht.ttf), + url(res:///../../media/mmcblk0p1/fonts/ht.ttf), + url(file:///mnt/us/DK_System/system/fonts/ht.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/ht.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/ht.ttf), + url(res:///system/fonts/ht.ttf), + url(res:///system/media/sdcard/fonts/ht.ttf), + url(res:///media/fonts/ht.ttf), + url(res:///sdcard/fonts/ht.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/ht.ttf), + url(res:///media/flash/fonts/ht.ttf), + url(res:///media/sd/fonts/ht.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/ht.ttf), + url(res:///../fonts/ht.ttf), + url(../../../../../ht.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/ht.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/ht.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/ht.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} +@font-face { + font-family:"h1"; + src: + local("方正兰亭特黑长_GBK"),local("方正兰亭特黑长简体"),local("方正兰亭特黑长繁体"), + local("LantingTeheichang"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), + local("DK-HEITI"), + url(../Fonts/h1.ttf), + url(res:///opt/sony/ebook/FONT/h1.ttf), + url(res:///Data/FONT/h1.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h1.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h1.ttf), + url(res:///ebook/fonts/h1.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h1.ttf), + url(res:///../../media/mmcblk0p1/fonts/h1.ttf), + url(file:///mnt/us/DK_System/system/fonts/h1.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h1.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h1.ttf), + url(res:///system/fonts/h1.ttf), + url(res:///system/media/sdcard/fonts/h1.ttf), + url(res:///media/fonts/h1.ttf), + url(res:///sdcard/fonts/h1.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h1.ttf), + url(res:///media/flash/fonts/h1.ttf), + url(res:///media/sd/fonts/h1.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h1.ttf), + url(res:///../fonts/h1.ttf), + url(../../../../../h1.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h1.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h1.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h1.ttf); /*ADE1,8, 2.0 Windows Path*/ +} +@font-face { + font-family:"h2"; + src: + local("方正大标宋_GBK"),local("方正大标宋简体"),local("方正大标宋繁体"), + local("Dabiaosong"), + local("Heiti"),local("Heiti SC"),local("Heiti TC"), + local("DK-XIAOBIAOSONG"), + url(../Fonts/h2.ttf), + url(res:///opt/sony/ebook/FONT/h2.ttf), + url(res:///Data/FONT/h2.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h2.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h2.ttf), + url(res:///ebook/fonts/h2.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h2.ttf), + url(res:///../../media/mmcblk0p1/fonts/h2.ttf), + url(file:///mnt/us/DK_System/system/fonts/h2.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h2.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h2.ttf), + url(res:///system/fonts/h2.ttf), + url(res:///system/media/sdcard/fonts/h2.ttf), + url(res:///media/fonts/h2.ttf), + url(res:///sdcard/fonts/h2.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h2.ttf), + url(res:///media/flash/fonts/h2.ttf), + url(res:///media/sd/fonts/h2.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h2.ttf), + url(res:///../fonts/h2.ttf), + url(../../../../../h2.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h2.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h2.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h2.ttf); /*ADE1,8, 2.0 Windows Path*/ +} + +@font-face { + font-family:"h3"; + src: + local("方正华隶_GBK"),local("方正行黑简体"),local("方正行黑繁体"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), + local("DK-FANGSONG"), + url(../Fonts/h3.ttf), + url(res:///opt/sony/ebook/FONT/h3.ttf), + url(res:///Data/FONT/h3.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h3.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h3.ttf), + url(res:///ebook/fonts/h3.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h3.ttf), + url(res:///../../media/mmcblk0p1/fonts/h3.ttf), + url(file:///mnt/us/DK_System/system/fonts/h3.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h3.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h3.ttf), + url(res:///system/fonts/h3.ttf), + url(res:///system/media/sdcard/fonts/h3.ttf), + url(res:///media/fonts/h3.ttf), + url(res:///sdcard/fonts/h3.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h3.ttf), + url(res:///media/flash/fonts/h3.ttf), + url(res:///media/sd/fonts/h3.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h3.ttf), + url(res:///../fonts/h3.ttf), + url(../../../../../h3.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h3.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h3.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h3.ttf); /*ADE1,8, 2.0 Windows Path*/ +} + +@font-face { + font-family:"luohua"; + src:local("汉仪落花体"), + url("../Fonts/hylh.ttf"); +} \ No newline at end of file diff --git a/app/src/main/assets/epub/intro.html b/app/src/main/assets/epub/intro.html new file mode 100644 index 000000000..5c64026f8 --- /dev/null +++ b/app/src/main/assets/epub/intro.html @@ -0,0 +1,11 @@ + + + + + Intro + + + + +

内容简介

{intro} + diff --git a/app/src/main/assets/epub/logo.png b/app/src/main/assets/epub/logo.png new file mode 100644 index 000000000..104d1777c Binary files /dev/null and b/app/src/main/assets/epub/logo.png differ diff --git a/app/src/main/assets/epub/main.css b/app/src/main/assets/epub/main.css new file mode 100644 index 000000000..fcb287442 --- /dev/null +++ b/app/src/main/assets/epub/main.css @@ -0,0 +1,551 @@ +@charset "utf-8"; +@import url("../Styles/fonts.css"); +body { + padding: 0%; + margin-top: 0%; + margin-bottom: 0%; + margin-left: 0.5%; + margin-right: 0.5%; + line-height: 130%; + text-align: justify; + font-family: "DK-SONGTI","st","宋体","zw",sans-serif; +} + +p { + text-align: justify; + text-indent: 2em; + line-height: 130%; + margin-right: 0.5%; + margin-left: 0.5%; + font-family: "DK-SONGTI","st","宋体","zw",sans-serif; +} +p.kaiti { + font-family: "DK-KAITI","kt","楷体","zw",serif; +} + +p.fangsong { + font-family: "DK-FANGSONG","fs","仿宋","zw",serif; +} + +span.xinli { + font-family: "DK-KAITI","kt","楷体","zw",serif; + color: #4e753f; +} +/** 英文斜体字 **/ +span.english{ + font-style: italic; +} +div { + margin: 0px; + padding: 0px; + line-height: 120%; + text-align: justify; + font-family: "zw"; +} +div.foot { + text-indent: 2em; + margin: 30% 5% 0 5%; + padding: 8px 0; +} +p.foot { + font-family: "DK-KAITI","kt","楷体","zw",serif; +} + +/*扉页*/ +.booksubtitle { + padding: 10px 0 0px 0; + text-indent: 0em; + font-size: 75%; + font-family: "ht"; +} + +.booktitle { + padding: 9% 0 0 0; + font-size: 1.3em; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-weight: normal; + text-indent: 0em; + color: #000; + text-align: center; + line-height: 1.6; +} + +.booktitle0 { + font-size: 1.2em; + font-family: "fs"; + text-indent: 0em; + text-align: center; + line-height: 1.8; +} + +.booktitle1 { + padding: 0 0 0 0; + font-size: 0.85em; + font-family: "fs"; + text-indent: 0em; + text-align: center; + line-height: 1.6; +} + +.bookauthor { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + padding: 5% 5px 0px 5px; + text-indent: 0em; + text-align: center; + color: #000; + font-size: 90%; + line-height: 1.3; +} + +.booktranslator { + padding: 1% 5px 0px 5px; + text-indent: 0em; + text-align: center; + font-size: 85%; + line-height: 1.3; +} + +.bookpub { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + padding: 30% 5px 5px 5px; + text-indent: 0em; + color: #000; + text-align: center; + font-size: 80%; +} + +/*标题页*/ +body.head { + background-repeat:no-repeat no-repeat; + background-size:160px 229px; + background-position:bottom right; + background-attachment:fixed; +} + +body.xhead { + background-color: #FDF5E6; +} + +h1.head { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.2em; + font-weight: bold; + color: #311a02; + text-indent: 0em; + font-weight: normal; + duokan-text-indent: 0em; + padding: auto; + text-align: center; + margin-top: -8em; +} + +div.head { + border: solid 2px #ffffff; + padding: 2px; + margin: 2em auto 0.7em auto; + text-align: center; + width: 1em; +} + +h1.head b { + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-weight: bold; + font-size: 1.2em; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + color: #311a02; + margin: 0.5em auto; + line-height: 140%; +} + +div.back { + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + margin: 4em auto; +} + +img.back { + width: 70%; +} +img.back2 { + width: 40%; + margin: 2em 0 0 0; +} +/*正文*/ +/**楷体引文**/ +.titou { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; +} +.yinwen { + font-family: "DK-KAITI","kt","楷体","zw",serif; + margin-left: 2em; + text-indent: 0em; +} +.nicename { + font-family: "DK-HEITI",黑体,sans-serif; + font-weight: bold; + font-size: 0.9em; +} +body.head3 { + background-color: #a7bdcc; + color: #354f66; +} + +body.head4 { + background-color: #bfd19b; + color: #4e753f; +} + +h2.head { + font-family: "小标宋"; + text-align: left; + font-weight: bold; + font-size: 1.1em; + margin: -3em 2em 2em 0; + color: #3f83e8; + line-height: 140%; +} + +h2.head span { + font-family: "仿宋"; + font-size: 0.7em; + background-color: #3f83e8; + border-radius: 9px; + padding: 4px; + color: #fff; +} + + +div.logo { + margin: -2em 0% 0 0; + text-align: right; +} + +img.logo { + width: 40%; +} +.imgl { + /*图片居右*/ + margin: -8.8em 1em 4em 0em; + width: 80%; + text-align: right; +} + +h1.head { + line-height:130%; + font-size:1.4em; + text-align: center; + color: #BA2213; + font-weight: bold; + margin-top: 2em; + margin-bottom: 1em; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + +} +h3 { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.1em; + margin: 1em 0; + border-left: 1.2em solid #00a1e9; + line-height: 120%; + padding-left: 3px; + color: #00a1e9; +} +h4 { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.1em; + text-align: center; + margin: 1em 0; + line-height: 120%; + color: #000; +} +h1.post { + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + text-align: center; + font-size: 1.3em; + color: #026fca; + margin: 3em auto 2em auto; +} +.banquan { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + text-align: left; + color: #000; + font-size:1.1em; + margin-bottom:1em; + text-indent: 1em; + duokan-text-indent: 1em; +} +p.post { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; +} +p.zy { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + margin: 1em 0 0 1em; + padding: 5px 0px 5px 10px; + text-indent: 0em; + border-left: 5px solid #a9b5c1; +} +.sign { + font-family: "DK-KAITI","kt","楷体","zw",serif; + margin: 1em 2px 0 auto; + text-align: right; + font-size: 0.8em; + text-indent: 0em; + duokan-text-indent: 0em; +} + +.mark { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 0.9em; + color: #fff; + text-indent: 0em; + duokan-text-indent: 0em; + background-color: maroon; + text-align: center; + padding: 0px; + margin: 2em 30%; +} + +/*出版社*/ +.chubanshe img{ + width:106px; + height:28px; +} +.chubanshe { + margin-top:20px; +} +.cr { + font-size:0.9em; +} + +/*多看画廊*/ +div.duokan-image-single { + text-align: center; + margin: 0.5em auto; /*插图盒子上下外边距为0.5em,左右设置auto是为了水平居中这个盒子*/ +} +img.picture-80 { + margin: 0; /*清除img元素的外边距*/ + width: 80%; /*预览窗口的宽度*/ + box-shadow: 3px 3px 10px #bfbfbf; /*给图片添加阴影效果*/ +} +p.duokan-image-maintitle { + margin: 1em 0 0; /*图片说明的段间距*/ + font-family: "楷体"; /*图片说明使用的字体*/ + font-size: 0.9em; /*字体大小*/ + text-indent: 0; /*首行缩进为零,当你使用单标签p来指定首行缩进为2em时,记得在需要居中的文本中清除缩进,因为样式是叠加的*/ + text-align: center; /*图片说明水平居中*/ + color: #a52a2a; /*字体颜色*/ + line-height: 1.25em; /*行高,防止有很长的图片说明*/ +} + + +/*制作说明页*/ +body.description { + background-image: url(../Images/001.png); + background-position: bottom center; + background-repeat: no-repeat; + background-size: cover; + padding: 25% 10% 0; + font-size: 0.9em; +} + +div.description-body { + width: 55%; + padding: 2em 1.3em; + border-radius: 0.5em; + font-size: 0.9em; + border-style: solid; + border-color: #393939; + border-width: 0.3em; + border-radius: 5em; + background-color: #5a5a5a; + box-shadow: 2px 2px 3px #828281; +} + +h1.description-title { + text-align: center; + font-family: "黑体"; + font-size: 1.2em; + margin: 0 0 1em 0; + color: #FF9; + text-shadow: 1px 1px 0 black; +} + +p.description-text { + color: #f9ddd2; + font-family: "准圆"; + margin: 0; + text-align: justify; + text-indent: 0; + duokan-text-indent: 0; +} + +hr.description-hr { + margin: 0.5em -1em; + border-style: dotted; + border-color: #9C9; + border-width: 0.05em 0 0 0; +} + +p.tips { + text-align: justify; + text-indent: 0; + duokan-text-indent: 0; + font-family: "楷体"; + font-size: 0.7em; + color: #FFC; + margin: 0; +} + +/*版本说明页*/ +.ver { + font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; + font-weight: bold; + font-size: 100%; + color: #000; + margin: 1em 0 1em 0; + text-align: center; +} + +.vertitle { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + font-size: 100%; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; +} + +.vertxt { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + line-height: 100%; + font-size: 85%; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; +} +.verchar { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + text-align: left; + text-indent: 1em; + duokan-text-indent: 1em; + margin-bottom: 1em; + margin-top: 1em; +} +.vernote { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + font-size: 75%; + color: #686d70; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; + padding-bottom: 15px; +} + +.line { + border: dotted #A2906A; + border-width: 1px 0 0 0; +} + +.entry { + margin-left: 18px; + font-size: 83%; + color: #8fe0a3; + text-indent: 0em; + duokan-text-indent: 0em; +} +/*版权信息*/ +.vol { + text-indent: 0em; + text-align: center; + padding: 0.8em; + margin: 0 auto 3px auto; + color: #000; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-size: 130%; + text-shadow: none; +} + +.cp { + font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; + color: #412938; + font-size: 70%; + text-align: left; + text-indent: 0em; + duokan-text-indent: 0em; +} + +.xchar { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + text-indent: 0em; + duokan-text-indent: 0em; +} +/*多看弹注*/ +sup img { + line-height: 100%; + width: auto; + height: 1.0em; + margin: 0em; + padding: 0em; + vertical-align: text-top; +} + +ol { + margin-bottom:0; + padding:0 auto; + list-style-type: decimal; +} +.hr { + width:50%; + margin:2em 0 0 0.5em; + padding:0; + height:2px; + background-color: #F3221D; +} + +.duokan-footnote-content{ + padding:0 auto; + text-align: left; +} + +.duokan-footnote-item { + font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; + text-align: left; + font-size: 80%; + line-height: 100%; + clear: both; + color:#000; + list-style-type:decimal; +} + +li.duokan-footnote-item a { + font-family:"DK-HEITI"; + text-align: left; +} +a{ + text-decoration: none; + color: #222; +} + +a:hover {background: #81caf9} +a:active {background: yellow} +.duokan-image-maintitle { + font-family:"DK-HEITI",黑体,"hei",sans-serif; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + font-size:90%; + color: #1F4150; + margin-top: 1em; +} + +.duokan-image-subtitle { + font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + font-size:70%; + color: #3A3348; + margin-top: 1em; +} \ No newline at end of file diff --git a/app/src/main/assets/number.ttf b/app/src/main/assets/font/number.ttf similarity index 100% rename from app/src/main/assets/number.ttf rename to app/src/main/assets/font/number.ttf diff --git a/app/src/main/assets/help.md b/app/src/main/assets/help.md deleted file mode 100644 index a6e6ecf0b..000000000 --- a/app/src/main/assets/help.md +++ /dev/null @@ -1,129 +0,0 @@ -## 常见问题 - -1.为什么第一次安装好之后什么东西都没有? -* 因为阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号[开源阅读]()、QQ群、酷安评论里获取由书友制作分享的书源。 - -2.如何导入本地书源文件? -* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入); -* 打开阅读软件; -* 我的 - 点击“书源管理”; -* 点击右上角选择“本地导入”; -* 左下角选择书源文件所在的路径; -* 点击书源文件导入; -* 导入后返回书源管理界面; -* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/ - -3.如何新建大佬发的单独书源? -* 复制书源代码; -* 打开阅读软件; -* 我的 - 点击“书源管理”; -* 右上角选择“新建书源”; -* 进入新建书源后点击右上角“粘贴源”; -* 粘贴书源完成后点击上方保存; -* 本次新建单独书源操作完成。 -* 注:如果书源有错误或者复制不全会显示格式错误,请重新复制。 - -4.为什么导入2.0书源后看不了书? -* 2.0部分书源并不适用3.0,建议导入后进行筛选。 - -5.阅读2.0数据如何导入阅读3.0? -* 先对阅读2.0的数据进行备份,然后进入阅读3.0,点击“我的”,选择“备份与恢复”,再点击“导入旧版本数据”。 - -6.如何给朋友分享我的书源? -* 打开阅读软件; -* 点击备份; -* 打开手机自带的文件管理; -* 手机自带内存根目录找到YueDu3.0文件夹; -* 找到myBookSource.json长按选择分享; -* 选择微信分享或者QQ分享; -* 选择你要分享的好友点击发送; -* 好友接收后在手机自带内存根目录找到myBookSource.json文件(QQ在tencent--QQfile_recv微信在Tencent--MicroMsg--Download); -* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹); -* 打开阅读软件点击恢复。 -* 注:备份路径如已修改过请在修改后的路径下查找书源文件。 - -7.目前阅读支持哪些格式的本地书籍? -* 目前支持TXT、EPUB格式(只支持显示EPUB里的文本内容,还不支持显示图片)。 - -8.如何刷新书架? -* 在书架界面下拉即可刷新。 - -9.书架界面书籍右上角的红色或者灰色背景小数字代表什么? -* 红色代表书籍有更新,灰色代表无更新,数字代表未读章节。 - -10.如何查看书籍详情? -* 长按书籍。 - -11.如何对书架上的书进行删除、切换书架的操作? -* 书籍详情页操作即可。 - -12.如何禁止或允许某本书更新? -* 书籍详情页,点击右上角 - “允许更新”。 - -13.如何更换小说封面、名字、作者或简介? -* 书籍详情页,点击右上角修改按钮。 - -14.怎么使用自定义字体? -* 阅读界面 - 字体-点击右上角选择字体文件路径。 - -15.目前支持哪些格式的字体文件? -* 目前支持ttf、otf格式。 - -16.书籍经常“正在加载中”怎么办? -* 在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。 - -17.书籍内容只有标题,正文内容是路径怎么办? -* 通常是缓存路径引起的,更换缓存路径即可。 - -18.效验书源显示失效就说明书源不能用了吗? -* 效验书源只是测试书源,可以做个参考,失效了不代表书源不能用了。 - -19.发现和正版书源能不能使用? -* 发现和正版书源只能用来找书,看排行榜,不能用来看书,如需看书请切换书源。 - -20.替换净化是什么? -* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 - -21.如何自己填写净化替换规则? -* 第一行:替换规则名称 - 根据自己需求对替换净化规则进行命名; -* 第二行:分组 - 净化规则的分组组别; -* 第三行:替换规则 - 填写需要被替换的内容; -* 第四行:替换为 - 填写想替换成的内容(如不填则默认表示删除第二行里填写的内容); -* 第五行:替换范围,选填书名或者源名 - 填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。 -* 注:如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第二行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行百度学习)。 - -22.如何听书? -* 可以使用手机自带的朗读引擎,也可使用第三方如谷歌、小米等朗读引擎。 -* 【具体操作:安装-系统设置-其他高级设置-辅助功能-TTS输出-选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。】 - -23.如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页、自定义翻页案件? -* 阅读界面,设置(可上划,下面还有其他设置)。 - -24.搜索的时候感觉手机卡顿,如何解决? -* 我的 - 其他设置 - “更新和搜索线程数”调低。 - -25.更新前有什么注意事项? -* 要做好备份。 - -26.看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办? -* 一般是书源问题,切换书源即可。 - -27.为什么书源这么多,发现里却只有一点点? -* 书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。 - -28.云备份在哪? -* 我的 - 备份与恢复 - WebDav设置。 - -29.如何操作进行云备份? -* 侧栏设置,WebDav设置; -* 正确填写WebDAV 服务器地址、WebDAV 账号、WebDAV 密码;(要获得这三项的信息,需要注册一个坚果云账号,如果直接在手机上注册,坚果云会让你下载app,过程比较麻烦,为了一步到位,最好是在电脑上打开这个注册链接:https://www.jianguoyun.com/d/signup;注册后,进入坚果云;点击右上角账户名处选择 “账户信息”,然后选择“安全选项”;在“安全选项” 中找到“第三方应用管理”,并选择“添加应用”,输入名称如“阅读”后,会生成密码,选择完成;其中https://dav.jianguoyun.com/dav/就是填入“WebDAV 服务器地址”的内容,“使用情况”后面的邮箱地址就是你的“WebDAV 账号”,点击显示密码后得到的密码就是你的“WebDAV 密码”。) -* 无需操作,APP默认每天自动云备份一次。 - -30.关于云备份的相关说明 -* 在正确设置好云备份的情况下,APP默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。 - -31.本地备份和云备份都能备份哪些东西? -* 书架、看书进度、搜索记录、书源、替换、APP设置等都会备份,基本涵盖所有内容。 - -32.出现某些未知bug怎么办? -* 清除软件数据试试看,不行再进行反馈。 \ No newline at end of file diff --git a/app/src/main/assets/help/SourceMBookHelp.md b/app/src/main/assets/help/SourceMBookHelp.md new file mode 100644 index 000000000..e14590232 --- /dev/null +++ b/app/src/main/assets/help/SourceMBookHelp.md @@ -0,0 +1,25 @@ +# 书源管理界面帮助 + +* 书源右上角标志 + * 绿点表示书源有发现,且启用了发现 + * 红点表示书源有发现,但是未启用 + * 没有标志表示此书源没有发现 +* 右上角有分组菜单,可以按分组筛选书源 +* 右上角更多菜单里包含 + * 新建书源 + * 本地导入 + * 网络导入 + * 二维码导入 + * 分享选中源 +* 选择源的更多操作在右下角的菜单里面,操作都是针对选择的书源 + * 启用所选 + * 禁用所选 + * 添加分组 + * 移除分组 + * 启用发现 + * 禁用发现 + * 置顶所选 + * 置底所选 + * 导出所选 + * 校验所选 +* 校验失败的书源分组会加上"失效",选择"失效"分组即可批量操作 \ No newline at end of file diff --git a/app/src/main/assets/help/SourceMRssHelp.md b/app/src/main/assets/help/SourceMRssHelp.md new file mode 100644 index 000000000..5f29a5128 --- /dev/null +++ b/app/src/main/assets/help/SourceMRssHelp.md @@ -0,0 +1,21 @@ +# 订阅源管理界面帮助 + +* 订阅源可以通过规则订阅一些网络内容 +* 书源右上角标志 + * 绿点表示书源有发现,且启用了发现 + * 红点表示书源有发现,但是未启用 + * 没有标志表示此书源没有发现 +* 右上角有分组菜单,可以按分组筛选书源 +* 右上角更多菜单里包含 + * 新建订阅源 + * 本地导入 + * 网络导入 + * 二维码导入 + * 分享选中源 +* 选择源的更多操作在右下角的菜单里面,操作都是针对选择的书源 + * 启用所选 + * 禁用所选 + * 置顶所选 + * 置底所选 + * 导出所选 +* 校验失败的书源分组会加上"失效",选择"失效"分组即可批量操作 \ No newline at end of file diff --git a/app/src/main/assets/help/appHelp.md b/app/src/main/assets/help/appHelp.md new file mode 100644 index 000000000..d1e2963f7 --- /dev/null +++ b/app/src/main/assets/help/appHelp.md @@ -0,0 +1,190 @@ +# 帮助文档 + +## **新人必读** + +【温馨提醒】 *本帮助可以在我的-右上角帮助按钮再次打开,更新前一定要做好备份,以免数据丢失!* + +1. 为什么第一次安装好之后什么东西都没有? +* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 + +2. 正文出现缺字漏字、内容缺失、排版错乱等情况,如何处理? +* 有可能是净化规则出现问题,先关闭替换净化并刷新,再观察是否正常。如果正常说明净化规则存在误杀,如果关闭后仍然出现相关问题,请点击源链接查看原文与正文是否相同,如果不同,再进行反馈。 + +3. 漫画源看书显示乱码,如何解决? +* 异次元和阅读是两个不同的软件,**两个软件的源并不通用**,请导入阅读的支持的漫画源! + +## 书源相关 + +1. 如何导入本地书源文件? +* 下载群文件里的书源文件(书源格式后缀有txt、json,其中json文件某些情况下无法导入,需要修改后缀为txt格式才可导入); +* 打开阅读软件; +* 我的 - 点击“书源管理”; +* 点击右上角选择“本地导入”; +* 左下角选择书源文件所在的路径; +* 点击书源文件导入; +* 导入后返回书源管理界面; +* 新版qq下载路径:Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/ + +![QQ导入书源](https://cdn.jsdelivr.net/gh/gedoor/gedoor.github.io@master/images/importSource.jpg) + +2. 如何新建大佬发的单独书源? +* 复制书源代码; +* 打开阅读软件; +* 我的 - 点击“书源管理”; +* 右上角选择“新建书源”; +* 进入新建书源后点击右上角“粘贴源”; +* 粘贴书源完成后点击上方保存; +* 本次新建单独书源操作完成。 +* 注:如果书源有错误或者复制不全会显示格式错误,请重新复制。 + +3. 为什么导入2.0书源后无法阅读? +* 部分2.0书源并不适用于3.0版本的阅读,建议导入后进行筛选。 + +4. 阅读2.0数据如何导入阅读3.0? +* 先对阅读2.0的数据进行备份,然后进入阅读3.0,点击“我的”,选择“备份与恢复”,再点击“导入旧版本数据”。 + +5. 如何给朋友分享我的书源? +* 打开阅读软件; +* 点击备份; +* 打开手机自带的文件管理; +* 手机自带内存根目录找到YueDu3.0文件夹; +* 找到myBookSource.json长按选择分享; +* 选择微信分享或者QQ分享; +* 选择你要分享的好友点击发送; +* 好友接收后在手机自带内存根目录找到myBookSource.json文件(最新版QQ 安卓10及以下版本在Android/data/com.tencent.mobileqq/Tencent/QQfile_recv/,安卓11版本用户由于系统限制无法访问data目录,微信在Tencent/MicroMsg/Download); +* 复制该文件到手机自带内存根目录找到YueDu3.0文件夹(如已有该文件请先删除该文件或者备份到其他地方再复制到文件夹); +* 打开阅读软件点击恢复。 +* 注:备份路径如已修改过请在修改后的路径下查找书源文件。 + +6. 效验书源显示失效就说明书源不能用了吗? +* 效验书源只是测试书源,可以做个参考,失效了不代表书源不能用了。 + +7. 发现和正版书源能不能使用? +* 发现和正版书源只能用来找书,看排行榜,不能用来看书,如需看书请切换书源。 + +8. 为什么书源这么多,发现里却只有一点点? +* 书源想要在发现界面里显示需要在书源里添加发现规则,并不是所有书源都有发现规则。 + +## 本地书籍相关 +1. 目前阅读支持哪些格式的本地书籍? +* 目前支持TXT、EPUB格式 + +2. 如何导入本地书籍? +* 在书架页面点击右上角,选择“添加本地”,授予相关权限后即可导入本地书籍。也可在文件管理器中使用 **阅读** 打开相关书籍。 + +3. 导入TXT文件提示“LoadTocError”或“List is empty”是怎么回事? +* 请先去应用详情中确认是否授予了阅读“读写手机存储”的权限。 +* 自动识别目录失败,可能是相关目录规则未开启,请点击右上角的换源按钮手动更换目录规则。 +* 如果尝试所有规则均无法识别,请在github上提交issue并附件相关txt文件,也可以发送邮件至i@qnmlgb.trade(标题:legado本地文件章节无法识别,内容对其具体情况进行简要说明,附件上传相关txt文件)。 + +4. 如何下载书籍到本地? +* 把在线书籍加入到书架后,在书架页面点击右上角,选择“离线缓存”即可。 + +5. 如何自定义导出的txt/epub文件名称? +* 点击 **离线缓存** - **导出文件名**. +* 使用方法: + - 导出文件名支持js语法 + - 可用变量: name - 书名 author-作者 + - 示例: + > {name+"作者:"+author} + - 导出文件名: + > Legado是最好的在线阅读软件 作者: kunfei. + +**注意:** name、author等变量与字符串的拼接都需要在js环境中进行,即必须使用{ } 将变量与字符串包裹起来. + +6. 为什么我打开本地的TXT文件,显示内容却是乱码? +* 部分编码在阅读上会识别错误,建议先用文本编辑器转换为常用的UTF-8格式。 + +7. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决? +* 点击右上角更换目录规则即可。 + +## 书籍界面相关 + +1. 如何刷新书架? +* 在书架界面下拉即可刷新。 + +2. 书架界面书籍右上角的红色或者灰色背景小数字代表什么? +* 红色代表书籍有更新,灰色代表无更新,数字代表未读章节。 + +3. 如何查看书籍详情? +* 长按书籍。 + +4. 如何对书架上的书进行删除、切换书架的操作? +* 书籍详情页操作即可。 + +5. 如何禁止或允许某本书更新? +* 书籍详情页,点击右上角 - “允许更新”。 + +6. 如何更换小说封面、名字、作者或简介? +* 书籍详情页,点击右上角修改按钮。 + +7. 怎么使用自定义字体? +* 阅读界面 - 字体-点击右上角选择字体文件路径。 + +8. 目前支持哪些格式的字体文件? +* 目前支持ttf、otf格式。 + +9. 书籍经常“正在加载中”怎么办? +* 在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。 + +10. 书籍内容只有标题,正文内容是路径怎么办? +* 通常是缓存路径引起的,更换缓存路径即可。 + +11. 看书时如遇到“目录为空”、“加载失败”和长串英文等情况怎么办? +* 在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。 + +12. 为什么每一章的最后一页,阅读的文字和横线背景总是对不齐? +* 请在 设置-文字底部对齐 选项中关闭底部对齐,再调整排版。 + +13. 漫画源或图片章节只能看到第一页,如何解决? +* 请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的 **⁝** 按钮,在弹出的菜单中,选择 **翻页动画(本书)**,将翻页动画更改为 **滚动**。 + +14. 阅读图片章节、漫画或epub插图时,图片被缩放到一页中,以至无法看清,如何处理? +* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考Q13中的方案将翻页动画更改为**滚动** +* 3.0旧版可以点击书籍界面的章节标题进入 **编辑书源** 界面,在 正文-图片样式 中填入 *full*,保存更改,刷新当前章节即可。 +* 3.0新版可以直接在书籍阅读界面点击右上角的 **⁝** 按钮,选择 图片样式- *full*. + + +## 替换净化相关 +1. 替换净化是什么? +* 替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 + +2. 如何自己填写净化替换规则? +* 第一行:替换规则名称 - 根据自己需求对替换净化规则进行命名; +* 第二行:分组 - 净化规则的分组组别; +* 第三行:替换规则 - 填写需要被替换的内容; +* 第四行:替换为 - 填写想替换成的内容(如不填则默认表示删除第二行里填写的内容); +* 第五行:替换范围,选填书名或者源名 - 填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。 +* 注:如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第二行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行百度学习)。 + + +## 备份相关 + +1. 云备份在哪? +* 我的 - 备份与恢复 - WebDav设置。 + +2. 如何操作进行云备份? +* 侧栏设置,WebDav设置; +* 正确填写WebDAV 服务器地址、WebDAV 账号、WebDAV 密码;(要获得这三项的信息,需要注册一个坚果云账号,如果直接在手机上注册,坚果云会让你下载app,过程比较麻烦,为了一步到位,最好是在电脑上打开这个注册链接:https://www.jianguoyun.com/d/signup ;注册后,进入坚果云;点击右上角账户名处选择 “账户信息”,然后选择“安全选项”;在“安全选项” 中找到“第三方应用管理”,并选择“添加应用”,输入名称如“阅读”后,会生成密码,选择完成;其中 https://dav.jianguoyun.com/dav/ 就是填入“WebDAV 服务器地址”的内容,“使用情况”后面的邮箱地址就是你的“WebDAV 账号”,点击显示密码后得到的密码就是你的“WebDAV 密码”。) +* 无需操作,APP默认每天自动云备份一次。 + +3. 关于云备份的相关说明 +* 在正确设置好云备份的情况下,APP默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。 + +4. 本地备份和云备份都能备份哪些东西? +* 书架、看书进度、搜索记录、书源、替换、APP设置等都会备份,基本涵盖所有内容。 + +5. 出现某些未知bug怎么办? +* 清除软件数据试试看,不行再进行反馈。 + + +## 其他 +1. 如何听书? +* 可以使用手机自带的朗读引擎,也可使用第三方如谷歌、小米等朗读引擎。 +* 【具体操作:安装-系统设置-其他高级设置-辅助功能-TTS输出-选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。】 + +2. 如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页、自定义翻页案件? +* 阅读界面,设置(可上划,下面还有其他设置)。 + +3. 搜索的时候感觉手机卡顿,如何解决? +* 我的 - 其他设置 - “更新和搜索线程数”调低。 diff --git a/app/src/main/assets/help/debugHelp.md b/app/src/main/assets/help/debugHelp.md new file mode 100644 index 000000000..dcfd3677e --- /dev/null +++ b/app/src/main/assets/help/debugHelp.md @@ -0,0 +1,22 @@ +# 书源调试 + +* 调试搜索>>输入关键字,如: +``` +系统 +``` +* 调试发现>>输入发现URL,如: +``` +月票榜::https://www.qidian.com/rank/yuepiao?page={{page}} +``` +* 调试详情页>>输入详情页URL,如: +``` +https://m.qidian.com/book/1015609210 +``` +* 调试目录页>>输入目录页URL,如: +``` +++https://www.zhaishuyuan.com/read/30394 +``` +* 调试正文页>>输入正文页URL,如: +``` +--https://www.zhaishuyuan.com/chapter/30394/20940996 +``` diff --git a/app/src/main/assets/help/httpTTSHelp.md b/app/src/main/assets/help/httpTTSHelp.md new file mode 100644 index 000000000..0f261888a --- /dev/null +++ b/app/src/main/assets/help/httpTTSHelp.md @@ -0,0 +1,15 @@ +# 在线朗读规则说明 + +* 在线朗读规则为url规则,同书源url +* js参数 +``` +speakText //朗读文本 +speakSpeed //朗读速度,0-45 +``` +* 例: +``` +http://tts.baidu.com/text2audio,{ + "method": "POST", + "body": "tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio" +} +``` \ No newline at end of file diff --git a/app/src/main/assets/help/readMenuHelp.md b/app/src/main/assets/help/readMenuHelp.md new file mode 100644 index 000000000..85a976046 --- /dev/null +++ b/app/src/main/assets/help/readMenuHelp.md @@ -0,0 +1,58 @@ +# 阅读界面帮助文档 + +## 阅读界面主菜单 +* 顶部操作 + * 章节名称:点击可编辑书源 + * 章节url:点击可打开浏览器浏览 + * 菜单:**不同类型的书籍显示的菜单不同**。详情请查看菜单文字,长按菜单图标可显示文字 +* 中间左侧-亮度调节 + * 亮度调节的顶端有跟随系统亮度的开关,打开后亮度跟随系统,关闭后才可以调节亮度条 +* 底部操作 + * 4个圆形按钮依次为 全文搜索✧自动翻页✧替换净化✧切换夜间模式 + * 上一章✧下一章中间的进度条为页数进度,要快速跳转章节点击目录按钮进入目录快速跳转 + * 目录->目录和书签界面 + * 朗读->单击开始朗读,长按进入朗读设置界面 + * 界面->所有排版设置都在里面 + * 设置->其它一些设置,找不到的设置去这里看看,可滚动 + +## 朗读设置界面 +* 后台->进入后台朗读,可以做一些其它事 +* 设置->朗读引擎设置,可以切换本地TTS和在线朗读,在线朗读可自定义 + +## 排版设置界面 +* 白天模式和夜间模式背景不同布局相同 +* 共用布局->启用共用布局时所有背景使用同一布局,关闭共用布局则每个背景单独布局 +* 长按背景可进入文字颜色和背景设置界面 + +## 其它设置界面 +* 屏幕方向 +* 屏幕超时 +* 隐藏状态栏 +* 扩展到刘海 +* 隐藏导航栏 +* 文字两端对齐 +* 文字底部对齐 +* 音量键翻页 +* 点击翻页 +* 朗读时音量键翻页 +* 自动换源->书源被删除时自动切换到其它书源 +* 长按选择文本 +* 显示亮度调节控件 +* 点击区域设置 +* 自定义翻页按键 + +## Txt目录正则说明 + +### 菜单区 + +- 新增目录规则,当Legado自带的规则不能够满足需求时,用户可根据自己的情况自定义目录规则 +- 导入默认规则 在旧版本中,Legado自带的规则不会随着软件的更新而更新。用户需要使用最新规则或对自带规则修改后需要重置时,可点击 导入默认规则。**注意:导入默认规则不会重置用户自定义的规则,但如果您对自带的规则进行了修改,则修改的规则会被重置为默认规则。** +- 导入在线规则 为了方便异步调试以及用户导入他人的目录规则,Legado增加目录规则的网络导入功能。点击 网络导入 的输入框,可以通过内置的链接导入在线规则。(在线规则优先比内置的规则更加激进,但适配了更多类型的标题格式,用户需根据自己的情况选择是否导入) + +### 操作区 +![Functions][example] + - 按钮① 当前书籍规则 如果Legado的自动识别的目录情况不太理想,用户可以手动点击各个规则前面的按钮,临时对本书启用该规则,该按钮仅**针对当前书籍生效**。 + - 按钮组② 左边的开关被点亮时,表示该规则针对**所有TXT书籍生效**。开启后会对所有的TXT格式的书籍启用当前规则扫描符合条件的标题格式;中间的按钮表示编辑当前规则,当识别出的目录与你所期望的不一致时,可以修改当前规则以适应你所导入的书籍;右边的按钮表示删除当前规则,当用户不需要当前规则时可直接删除。(默认的规则删除后可点击 导入默认规则 按钮恢复) + - 按钮组③ 在当前界面进行操作后,需要点击确认按钮使得选择生效。 + +[example]:data:image/jpg;base64,/9j/4RZnRXhpZgAATU0AKgAAAAgADAEAAAMAAAABBDgAAAEBAAMAAAABCWAAAAECAAMAAAADAAAAngEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEVAAMAAAABAAMAAAEaAAUAAAABAAAApAEbAAUAAAABAAAArAEoAAMAAAABAAIAAAExAAIAAAAiAAAAtAEyAAIAAAAUAAAA1odpAAQAAAABAAAA7AAAASQACAAIAAgACvyAAAAnEAAK/IAAACcQQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpADIwMjA6MTE6MjQgMTY6NDQ6MjIAAAAABJAAAAcAAAAEMDIyMaABAAMAAAAB//8AAKACAAQAAAABAAABLKADAAQAAAABAAACWAAAAAAAAAAGAQMAAwAAAAEABgAAARoABQAAAAEAAAFyARsABQAAAAEAAAF6ASgAAwAAAAEAAgAAAgEABAAAAAEAAAGCAgIABAAAAAEAABTdAAAAAAAAAEgAAAABAAAASAAAAAH/2P/tAAxBZG9iZV9DTQAB/+4ADkFkb2JlAGSAAAAAAf/bAIQADAgICAkIDAkJDBELCgsRFQ8MDA8VGBMTFRMTGBEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAENCwsNDg0QDg4QFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgAoABQAwEiAAIRAQMRAf/dAAQABf/EAT8AAAEFAQEBAQEBAAAAAAAAAAMAAQIEBQYHCAkKCwEAAQUBAQEBAQEAAAAAAAAAAQACAwQFBgcICQoLEAABBAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEFQVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHxY3M1FqKygyZEk1RkRcKjdDYX0lXiZfKzhMPTdePzRieUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9jdHV2d3h5ent8fX5/cRAAICAQIEBAMEBQYHBwYFNQEAAhEDITESBEFRYXEiEwUygZEUobFCI8FS0fAzJGLhcoKSQ1MVY3M08SUGFqKygwcmNcLSRJNUoxdkRVU2dGXi8rOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vYnN0dXZ3eHl6e3x//aAAwDAQACEQMRAD8AfHw7cisvqc0vD21Np929znhzqwz2+l7vSs+lb/3xFb0u91T7fUrAYz1IDt0j0vtf0mbm/wA0mdhZWO91TsjHpsYSHs+10tcHDcxzXt9b6bdz2Jw3MDS0Z2PtIII+2UwQQWO/w37rluy5rFZrmMP1nD+LzkPh/M0L5LmSe8cWXhl/zWTukZDHUB9jGfaZ2F27SNn0trHe39J9Nns9iiemX/ZnZQsrNbN8gF0n0ztdt9qkHdQa4OHUKQ5o2tP22mQPAfplEsy3NLTm45a4HcPtlEEHV/8Ahvzvz0BzOPS+Yw+PrguPw/mNa5LmdtP1WX/vWnqlKvY3Q+o5e/7IKcn049T0r6X7Znbv22+3dtR/+avX/wDuL/4LV/6UUw5jAdsuP/Hi1TyfNRNSwZYnsccwf+i5UqdFNmRcymuN1hDRJgamO60v+avX/wDuL/4LV/6USH1W+sAIIxYIMgi2uQR/1xA8xhrTLj/xoqHK57F4MhHX0S/71y3Nc1xa7QjnUH8WqRYWGskg72h+naSRB/zVpf8ANXr/AP3FH/blX/pROfqz9YBDn40hg0m2sw0e6B+k+ikeYw1/O4/8aKRyue/5nJvp6JP/0Oqqu62y9rcJpON9qy93tJbP2nI2y4Mcz32+zI9W2r0sf9LR+lV+/K+szbLxRiVWVi2tuOS4CaiX+vZZN/5rfSd9Fik/p+HXcaxn34773usbQ28Ml1j3WP8ATrI3e+170T9jO/7m5v8A29/5gmjiAArYVu2sxw5JGXEI2Sfk9XqPF6kV+V9Y2m70MStways0EkEusJ/WGbfXr9rWO/RbvT/mrP8ASVpOPXMg203VVV172uqfOhDDZZts/SWWena+nG3/AKP+Zyrf9Eifsj/u9mf9vf8AmCX7I/7vZn/b3/mCVy7fixiGIEEZNjfyFp9Fwbsbq+T6p2FmMwtra8vBF+TnZP6R7wHPsq/m2v8A+uf4Rbiq4mBXi2WWi226y1rGOfc7edtZe5jW6N/OusVpKAoV5q5nJ7mQyBvSIuq2j6v+e431kyPrHRi2v6LS2wtrB3BofYHbv0hrqe73vZX/ADTPSt3oPR8n6w3fV1t3Um2V55uaA4Vt9Y45trbZc/G2bPWbSb/8B9Bnqekt2yyuqt1tr2111gufY8hrWgfSc5zvotaoNy8V+P8Aa231uxiNwvD2msidu71Z9P6Scg5P1XB7cd/5yvX/AHeJBiPzndN3vB+2bbNgtaASQX/Z/VY30G+5vpb/AOj/APF0IPSbOpv6dc7qYIul+zc0Mds2B3uaza3+dNjG+yv2f6T+es0K3stY19Tg9jxLXtILSD3a5vtcomyuzHfZU5r2FrwHNIcJAc13ub+64JMT/9H0PIwX2vt22NbVkbBc0s3P9nt/RWbhs9v7zH+lZ+mq96qZP1aw8l97n33t+02susDCwasNjmt3bHO2/pf66uX5rqnWltQfVj7Te8vDXDcN36KrafV2sO76dX+jp9SxVL/rL0rHsurtc8Ox7a6Hw0GX2F7GBnu93uqckp1TqZSWVkfWXpmO6wWiwNr9OX7WwfVLNu39Ju/Rtursu9v6KtSq+sOBbbZUxtpfTYyl4DQfdZvj6D3/AEW1u9v85/omWWJKdNAyc7BxNv2vJqx98lvqvayY52+o5viiU2suqZaydrxInnw7bmrA+smPZkZrK6sd+RYcSwM9IgOYTbT+m2ufX6uz6bK/9Iz07fTostTZkgWGblsccmQRmSI0SSKG395h9YMb6t9bqey3q1FFpYGNcLqnNBY71GF1bnfvfTa17N6j0nH6H0voo6ZX1nFss9duSbnWVlvqMsrva1tHrfzX6Bjf53/hFtVX/s/posyWOa1rjFctc5jHvd6FRdu2OdVWWMdssf8AyPU+mj5OWKcYXtb6m91TK2aNJNz2U17i76Hut96XqrcfZ/6EvlmjwnFU5YwduOHTr/NObTm9BZgOwrep4trbBaLX+tWJ9YvfZ7X2Wf6X891n8tQ6Q/ouDi2YeL1DGyLb3OeBW+sOc41tpaxldb3ud7alp157X4LswsIbWLC5ggn9EXss2bvT+l6X6Pf6f9hZlvU2dU6bXe2l1OzOxqy15YdfVotDmuqc/wDMsYkeIC7H2f2rIDFOQiIzHEavjifw9t//0vSn42PZa26ypr7WRseQCRB3N/zXe5insZJO1snUmBJKpZTcs3v9MW7iGfZXMdFTT/hftTJ93u/nPUa/9B/RP1hVrx9Z/Wv+znH9M3V/Z98CKZf6+/2uc5+z0v8AydaSnVNdZkljTPPtH9ycNYDIa0EAAEAcDj8qycwfWl7b2YTsat7mVjHfaAQ14c12Q9+3duY9j7K6m+n+j9Fn+l/R2Mk9aN2I7HbU2rUZjHETMs2urPu/RbfV/wCESU3xAEAQBoAFUyun/aMhmSzIuxrmMdVupLNWOc2yHC+q/wDOr/NVK7/nQWA1egHGh0zE+sWv9Pc125v6N3pb9lnpKeI36yfaa35bqPs7q6/UrYBLbB6X2k7vpbbP1j0tn/BoEA7roTlA3Hfbvv4SS/svImf2llk+foEfccVNb0iy6s13dQyba3fSY8Y7mnv7mPxC1Wsv7UaCMT25EiC6NsT7pLw5n0VHOGYcKMfccjdVu9Eta4t31/avROR+jb+h9Xbu/wCrQ4R4/aV/vz7Q/wDC8f8A3rXr6TbUwV1dQyq62/RYwY7Wj4MbibVAdFZXWB9qvdVS4XCiKWVl9Z9Wve3Hx6XfzjG/nqzjjPPTS2yWZhbaGGwtc4GX/ZvUczfU/az0v+/oPS29UbgX/tRznXkvLN5rLgzY3l2M1lf876qXCPH7Sr359OEeUMYP28L/AP/T9JsyceuxtVjw17o0M6bjsr3ujbX6r/ZV6n86/wDm1M2Vjl7RBDTLhyfot/rIF2H6jrALCyrIgZFYAO7aNntefdV6lY9K3+R/Nejb+lVR31a6S+3Iuexzn5VzMi33R763Gxv0Wtds93vZu+gkp0jZWOXtEan3Didvj+8n3skje2QYI3DQ/urKyfqv0jJ3+ox82hrbCHD3BhqNQdLdv6P7OxTP1c6SbTd6bhYbWXbg4iHVt9JrWx7Wsc3fu2/6R6SnRZZW+dj2vjna4GP834qSo9N6Nh9Nn7Nv1Lz7yD/Oel6n0Ws/7j1K8kpi97GDc9wY3iXEAT8XJrrqcet1t9jaq2xue8hrRJ2t1P7znbWqGTjMyaXUWk+k4gkN0Jg7gN3uSyMVl2OMcOdUGmtzHMglpqcy2r+cD2u91XuSUzZfTZSL2WNdSQXCwEbYH0ju/kodeVi5WNZbi3V5FYD2l9Tg9sgGW7mF3uTVYVVeG7Dc51tbxYHufG53ql77Zgf8K5Cw+nVdPw7qanvs9Tc9z7ILidjahwP3KmJKf//U9DyM2yp9pa1hrxgw2tc6LHep9H0G/R/kU7v6Tkfq/wCi/nFUs+smOy3JqGLkWHFuZQ4sDHbnWONbXVtFnqbdzfz2rUdVW97bHMa59c7HloLmzzsd9JilJ8UlOVmfWHGw33NfRc/7O9tb3NAj3B79zTu+j+icms+sVFdVlr8XI21Xeho0Eklr7W2c+2t3p/8AgtS15PiluPikpx2fWbEfaK/RtAOT9l3nZtB949VxDvbX7P8AjH71c6Z1FnUsUZLKrKBuLTXcAHAgNd+aXfvbf+MVzc7xTSTykpxvrF9Y2dDpc847r3BjXNM7GS53pNDnw76P+EUOlfWX9o9CHVfs4qeLm4zq3v21lzrK8f1WX7Hfof03+i+n+hW25rXsdW8B7HgtcxwkEHQtc0/SUW0U11ChlbGUtG0VBoDAP3fTjZtSZDLH7fDwfrL/AJzi/R7cDXoz/W6c7OFc7W2n02O3Sai9hDHuFf0/S/wjGf8ACIPSuqHqnT78g1CksL69rX+oD+jbbua4spd9G3/R/wDgfvWg0BoDWgNDRAA0AA8IUXta2h7WgNaGO0Gg1B8EmN//1e/yqMp97nMa9zjs+z3Ns2tpj+c9SncPU93v9rLftH9Gt9Jijk43XLH2mjLZUw2tdSyBpWAWWUPs9F387/P+p/g/5n/hFatzMaqz03uIcNu4hri1u4wz1bGjZVu/lp7M3CqLhZk1MLHBrw6xrdrnasY/c72vfHtSU0aMX6w+ow5GcwsGS572ta0zjx7KP5ir9Jv9v0voe/1N7ErMb6xm1jq8yoMFxc5hHNMscyufSd72s3s/9Wfo7tmfgVBxtyqawx21++xrYdqdjtzvp+x/tSfn4FZeLMqlhqE2h1jRtE+nNnu9n6T9H/XSU0a8T6xNZQHZ9dhZY43ktAL65r2NBbX9Juy783/Df8Glg431jrsxnZuXVaxhf9qYIl4cGirY8Y9X808Pfs21/wDGrV0Oo1B4ISSU431kwvrBlY1g6Pkik+mAKw703ucHbrNlu32vsr9jH+pX6aD0fA6/R9XG43UbH2Z3rNeGi6LG0C2t9mMcxrvdZ6Tb/wDC/wCE9H1VrZ3U+ndPrdbnZDKGsbvO4+7bO321tl7/AHfR2qGP1jpuVgDqOPf6uKXbA9rXl28uFLavR2+v6rrHsb6fp70mYzyezw8A4OL5+H9L93jVi05remejY8syi2wNeXbywuL/AEP0j/V3ekx1f0/X/wCMvQek43UMfp11fULDba4vczc82FrCxvt9R0v/AJ31X/T/AO2/5mu7XlY9mOckPDaQHFz3ywN2SLfVFux1XpbXep6n82h0ZuJm4ttuJaLWNDmOIkEODd21zXhr2+1zXt/4P9J9BJhf/9b0a7Crte8myxrLY9appAY/b7ffLXP9zP0dnpvr9StCs6RgWPte5tm66xtth9V/02Eurc33ezY538239Eo5WfZTe9jXVN9PZtoeD6l2/wD0BDh/xTPZb+m/nPTrQ7es20Pu39PyX1Uuc0PpYXuO07QTXtr9trd11fpW3s9L+e9G2z0klJLuhdJufZY+gh9zxbY5j3sl43+/2OH+lsSs6H0i31N+M0m2sVPdLt20EPbtfO5r2vYx/qfT3qFfWXWOhvT8uHt31PLBsI2h7dz93s3v/RpmdatLXA9OyfVYCSA32OIn21XPFbrPa3d/Nf8AF70lOk0Na0NaIa0AADsBoE6zX9YvLHHH6bl2WAw1r2itrj+d7/0m3b/KYp09TtutbUMDJql7q3PtaGtDmbN7twLt1Wx1jqrv5u70vTYkpbq3Qum9XqfXmMdue0M9Wtxa4bTvY5v5jnVu+jvYodP+r+B07pg6biusbX6gvNpINhta5lzbXHZ6ftdTV7fS9PYgfWTruX0fFttx8I5G2sPFp3em0l2z9J6bT7Kfp2/pK0Lo/wBY8rP+ro6rfVXjWes2kvduFJaba6HZTZdv9Gv1Xf4X6dP86kzmOb2OIyPs8QFcX6X912KMOijE+yNl1MOadx1IeXOs1Zs2/wA476H0EPFwMfp+HbRj7trg55LjJLizZ4N/NY381Ni5tl/TTmNYLHhthYxsgP8ATL2M2x6231vT/N9f/rqD0jqV/UunXZF1TKnNL2N9NxLXDYLNw3tbt2+p6X/W/wDraTA//9f06DzHHfwSkqhlYVtt7ntYxxfs9LIc4h9G36XpMA/677P57+ZyP0KhdgdWL7rMbqPpOe5xY19fqM2k7mB1b3ba3VM/QM9DZvr/AEl2++z9GlOlJSk+KzGYHWpm3qctcw7621NHv2hvsuaGvZVuH5jPU+n/ANZgel9XNrXnqTtrMl9xYNwBqe5jm43OzbU1rtm+u1JTrSUtVVwsXJxwRdkOyPaAC6eQ6x+73fyHsZ/rUrSSlapCdAO2gAQMum++g1Uv9F5IItBIgA9vTLX+5Qz8e+/CNFZD7N1RO9xrDwx9dlrHvqa9zPWYx7foJKbRmdeVG2fTsnnY7n4FVsbFyGdNOLY8MuLbGh9bnEM3l5q9Owiqz9C17Nn0EDpWDlYHT7qcq31XuL3gh9lgaCxrdrX3/pPptfZ/bSU//9D05JJJJSkkkklKWc+/qV+fkY2LbTRXjNqP6Sl1rnGwOd9JuRj7du391aKyzdbh9Vy7XYt91d7aTW+lm8HYHteD7vbymy6dr1ryZsAv3KETIQ9AkIy9XuY/0Z+n5ONN6PXP+5mN/wCwr/8A3tWfm5/XsawNrfRewW10WWDHLQLLfT9KprTml253rV+/+aUxl5oM7c4/pfUj0D9Cd3o63FNmDp+be2/I6bnGxogFrHMkTu2v9Oxu7hMlVaE35ybGIT4v1mOJj4Y8P/oDbpHWb6a7683G2Wsa9s4jwYcN7f8Atb5qF13VMa+irIuouryvVrIZQ6tzdtVt24Pdk3/6P9xBdmZfqWOprzaqnNDaqRj+2uAG+yLWqWRkW5uViObi5FTcc3WWvtrDGgGi2rkv/wBI9qOmlE3Y6y7rRGZMuOEBDhyfoYYn5JcHqjH99//Z/+0eiFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAHHAIAAAIAAAA4QklNBCUAAAAAABDo8VzzL8EYoaJ7Z63FZNW6OEJJTQQ6AAAAAADXAAAAEAAAAAEAAAAAAAtwcmludE91dHB1dAAAAAUAAAAAUHN0U2Jvb2wBAAAAAEludGVlbnVtAAAAAEludGUAAAAAQ2xybQAAAA9wcmludFNpeHRlZW5CaXRib29sAAAAAAtwcmludGVyTmFtZVRFWFQAAAABAAAAAAAPcHJpbnRQcm9vZlNldHVwT2JqYwAAAAVoIWg3i75/bgAAAAAACnByb29mU2V0dXAAAAABAAAAAEJsdG5lbnVtAAAADGJ1aWx0aW5Qcm9vZgAAAAlwcm9vZkNNWUsAOEJJTQQ7AAAAAAItAAAAEAAAAAEAAAAAABJwcmludE91dHB1dE9wdGlvbnMAAAAXAAAAAENwdG5ib29sAAAAAABDbGJyYm9vbAAAAAAAUmdzTWJvb2wAAAAAAENybkNib29sAAAAAABDbnRDYm9vbAAAAAAATGJsc2Jvb2wAAAAAAE5ndHZib29sAAAAAABFbWxEYm9vbAAAAAAASW50cmJvb2wAAAAAAEJja2dPYmpjAAAAAQAAAAAAAFJHQkMAAAADAAAAAFJkICBkb3ViQG/gAAAAAAAAAAAAR3JuIGRvdWJAb+AAAAAAAAAAAABCbCAgZG91YkBv4AAAAAAAAAAAAEJyZFRVbnRGI1JsdAAAAAAAAAAAAAAAAEJsZCBVbnRGI1JsdAAAAAAAAAAAAAAAAFJzbHRVbnRGI1B4bEBSAAAAAAAAAAAACnZlY3RvckRhdGFib29sAQAAAABQZ1BzZW51bQAAAABQZ1BzAAAAAFBnUEMAAAAATGVmdFVudEYjUmx0AAAAAAAAAAAAAAAAVG9wIFVudEYjUmx0AAAAAAAAAAAAAAAAU2NsIFVudEYjUHJjQFkAAAAAAAAAAAAQY3JvcFdoZW5QcmludGluZ2Jvb2wAAAAADmNyb3BSZWN0Qm90dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQAEgAAAABAAEASAAAAAEAAThCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAHjhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAThCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAADhCSU0EAgAAAAAADgAAAAAAAAAAAAAAAAAAOEJJTQQwAAAAAAAHAQEBAQEBAQA4QklNBC0AAAAAAAYAAQAAAAI4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQQeAAAAAAAEAAAAADhCSU0EGgAAAAADlwAAAAYAAAAAAAAAAAAAAlgAAAEsAAAAMQBTAGMAcgBlAGUAbgBzAGgAbwB0AF8AMgAwADIAMAAtADEAMQAtADIANAAtADEANgAtADMANAAtADQANQAtADUAMQA0AF8AaQBvAC4AbABlAGcAYQBkAG8ALgBhAHAAcAAuAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEsAAACWAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAABAAAAABAAAAAAAAbnVsbAAAAAIAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAABUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAACWAAAAABSZ2h0bG9uZwAAASwAAAAGc2xpY2VzVmxMcwAAAAFPYmpjAAAAAQAAAAAABXNsaWNlAAAAEgAAAAdzbGljZUlEbG9uZwAAAAAAAAAHZ3JvdXBJRGxvbmcAAAAAAAAABm9yaWdpbmVudW0AAAAMRVNsaWNlT3JpZ2luAAAADWF1dG9HZW5lcmF0ZWQAAAAAVHlwZWVudW0AAAAKRVNsaWNlVHlwZQAAAABJbWcgAAAABmJvdW5kc09iamMAAAABAAAAAAAAUmN0MQAAAAQAAAAAVG9wIGxvbmcAAAAAAAAAAExlZnRsb25nAAAAAAAAAABCdG9tbG9uZwAAAlgAAAAAUmdodGxvbmcAAAEsAAAAA3VybFRFWFQAAAABAAAAAAAAbnVsbFRFWFQAAAABAAAAAAAATXNnZVRFWFQAAAABAAAAAAAGYWx0VGFnVEVYVAAAAAEAAAAAAA5jZWxsVGV4dElzSFRNTGJvb2wBAAAACGNlbGxUZXh0VEVYVAAAAAEAAAAAAAlob3J6QWxpZ25lbnVtAAAAD0VTbGljZUhvcnpBbGlnbgAAAAdkZWZhdWx0AAAACXZlcnRBbGlnbmVudW0AAAAPRVNsaWNlVmVydEFsaWduAAAAB2RlZmF1bHQAAAALYmdDb2xvclR5cGVlbnVtAAAAEUVTbGljZUJHQ29sb3JUeXBlAAAAAE5vbmUAAAAJdG9wT3V0c2V0bG9uZwAAAAAAAAAKbGVmdE91dHNldGxvbmcAAAAAAAAADGJvdHRvbU91dHNldGxvbmcAAAAAAAAAC3JpZ2h0T3V0c2V0bG9uZwAAAAAAOEJJTQQoAAAAAAAMAAAAAj/wAAAAAAAAOEJJTQQRAAAAAAABAQA4QklNBBQAAAAAAAQAAAAKOEJJTQQMAAAAABT5AAAAAQAAAFAAAACgAAAA8AAAlgAAABTdABgAAf/Y/+0ADEFkb2JlX0NNAAH/7gAOQWRvYmUAZIAAAAAB/9sAhAAMCAgICQgMCQkMEQsKCxEVDwwMDxUYExMVExMYEQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQ0LCw0ODRAODhAUDg4OFBQODg4OFBEMDAwMDBERDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCACgAFADASIAAhEBAxEB/90ABAAF/8QBPwAAAQUBAQEBAQEAAAAAAAAAAwABAgQFBgcICQoLAQABBQEBAQEBAQAAAAAAAAABAAIDBAUGBwgJCgsQAAEEAQMCBAIFBwYIBQMMMwEAAhEDBCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0NhfSVeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAgIBAgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSITBTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJQYWorKDByY1wtJEk1SjF2RFVTZ0ZeLys4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9ic3R1dnd4eXp7fH/9oADAMBAAIRAxEAPwB8fDtyKy+pzS8PbU2n3b3OeHOrDPb6Xu9Kz6Vv/fEVvS73VPt9SsBjPUgO3SPS+1/SZub/ADSZ2FlY73VOyMemxhIez7XS1wcNzHNe31vpt3PYnDcwNLRnY+0ggj7ZTBBBY7/DfuuW7LmsVmuYw/WcP4vOQ+H8zQvkuZJ7xxZeGX/NZO6RkMdQH2MZ9pnYXbtI2fS2sd7f0n02ez2KJ6Zf9mdlCys1s3yAXSfTO1232qQd1Brg4dQpDmja0/baZA8B+mUSzLc0tObjlrgdw+2UQQdX/wCG/O/PQHM49L5jD4+uC4/D+Y1rkuZ20/VZf+9aeqUq9jdD6jl7/sgpyfTj1PSvpftmdu/bb7d21H/5q9f/AO4v/gtX/pRTDmMB2y4/8eLVPJ81E1LBliexxzB/6LlSp0U2ZFzKa43WENEmBqY7rS/5q9f/AO4v/gtX/pRIfVb6wAgjFggyCLa5BH/XEDzGGtMuP/GiocrnsXgyEdfRL/vXLc1zXFrtCOdQfxapFhYaySDvaH6dpJEH/NWl/wA1ev8A/cUf9uVf+lE5+rP1gEOfjSGDSbazDR7oH6T6KR5jDX87j/xopHK57/mcm+nok//Q6qq7rbL2twmk432rL3e0ls/acjbLgxzPfb7Mj1bavSx/0tH6VX78r6zNsvFGJVZWLa245LgJqJf69lk3/mt9J30WKT+n4ddxrGffjvve6xtDbwyXWPdY/wBOsjd77XvRP2M7/ubm/wDb3/mCaOIACthW7azHDkkZcQjZJ+T1eo8XqRX5X1jabvQxK3BrKzQSQS6wn9YZt9ev2tY79Fu9P+as/wBJWk49cyDbTdVVXXva6p86EMNlm2z9JZZ6dr6cbf8Ao/5nKt/0SJ+yP+72Z/29/wCYJfsj/u9mf9vf+YJXLt+LGIYgQRk2N/IWn0XBuxur5PqnYWYzC2try8EX5Odk/pHvAc+yr+ba/wD65/hFuKriYFeLZZaLbbrLWsY59zt521l7mNbo3866xWkoChXmrmcnuZDIG9Ii6raPq/57jfWTI+sdGLa/otLbC2sHcGh9gdu/SGup7ve9lf8ANM9K3eg9HyfrDd9XW3dSbZXnm5oDhW31jjm2ttlz8bZs9ZtJv/wH0Gep6S3bLK6q3W2vbXXWC59jyGtaB9JznO+i1qg3LxX4/wBrbfW7GI3C8PaayJ27vVn0/pJyDk/VcHtx3/nK9f8Ad4kGI/Od03e8H7Zts2C1oBJBf9n9VjfQb7m+lv8A6P8A8XQg9Js6m/p1zupgi6X7NzQx2zYHe5rNrf502Mb7K/Z/pP56zQrey1jX1OD2PEte0gtIPdrm+1yibK7Md9lTmvYWvAc0hwkBzXe5v7rgkxP/0fQ8jBfa+3bY1tWRsFzSzc/2e39FZuGz2/vMf6Vn6ar3qpk/VrDyX3uffe37Tay6wMLBqw2Oa3dsc7b+l/rq5fmuqdaW1B9WPtN7y8NcNw3foqtp9Xaw7vp1f6On1LFUv+svSsey6u1zw7HtrofDQZfYXsYGe73e6pySnVOplJZWR9ZemY7rBaLA2v05ftbB9Us27f0m79G26uy72/oq1Kr6w4FttlTG2l9NjKXgNB91m+PoPf8ARbW72/zn+iZZYkp00DJzsHE2/a8mrH3yW+q9rJjnb6jm+KJTay6plrJ2vEiefDtuasD6yY9mRmsrqx35FhxLAz0iA5hNtP6ba59fq7Ppsr/0jPTt9Oiy1NmSBYZuWxxyZBGZIjRJIobf3mH1gxvq31up7LerUUWlgY1wuqc0FjvUYXVud+99NrXs3qPScfofS+ijplfWcWyz125JudZWW+oyyu9rW0et/NfoGN/nf+EW1Vf+z+mizJY5rWuMVy1zmMe93oVF27Y51VZYx2yx/wDI9T6aPk5Ypxhe1vqb3VMrZo0k3PZTXuLvoe633peqtx9n/oS+WaPCcVTljB244dOv805tOb0FmA7Ct6ni2tsFotf61Yn1i99ntfZZ/pfz3Wfy1DpD+i4OLZh4vUMbItvc54Fb6w5zjW2lrGV1ve53tqWnXntfguzCwhtYsLmCCf0ReyzZu9P6Xpfo9/p/2FmW9TZ1Tptd7aXU7M7GrLXlh19Wi0Oa6pz/AMyxiR4gLsfZ/asgMU5CIjMcRq+OJ/D23//S9KfjY9lrbrKmvtZGx5AJEHc3/Nd7mKexkk7WydSYEkqllNyze/0xbuIZ9lcx0VNP+F+1Mn3e7+c9Rr/0H9E/WFWvH1n9a/7Ocf0zdX9n3wIpl/r7/a5zn7PS/wDJ1pKdU11mSWNM8+0f3Jw1gMhrQQAAQBwOPyrJzB9aXtvZhOxq3uZWMd9oBDXhzXZD37d25j2Psrqb6f6P0Wf6X9HYyT1o3YjsdtTatRmMcRMyza6s+79Ft9X/AIRJTfEAQBAGgAVTK6f9oyGZLMi7GuYx1W6ks1Y5zbIcL6r/AM6v81Urv+dBYDV6AcaHTMT6xa/09zXbm/o3elv2Wekp4jfrJ9prfluo+zurr9StgEtsHpfaTu+lts/WPS2f8GgQDuuhOUDcd9u+/hJL+y8iZ/aWWT5+gR9xxU1vSLLqzXd1DJtrd9Jjxjuae/uY/ELVay/tRoIxPbkSILo2xPukvDmfRUc4Zhwox9xyN1W70S1ri3fX9q9E5H6Nv6H1du7/AKtDhHj9pX+/PtD/AMLx/wDetevpNtTBXV1DKrrb9FjBjtaPgxuJtUB0VldYH2q91VLhcKIpZWX1n1a97cfHpd/OMb+erOOM89NLbJZmFtoYbC1zgZf9m9RzN9T9rPS/7+g9Lb1RuBf+1HOdeS8s3msuDNjeXYzWV/zvqpcI8ftKvfn04R5Qxg/bwv8A/9P0mzJx67G1WPDXujQzpuOyve6Ntfqv9lXqfzr/AObUzZWOXtEENMuHJ+i3+sgXYfqOsAsLKsiBkVgA7to2e1591XqVj0rf5H816Nv6VVHfVrpL7ci57HOflXMyLfdHvrcbG/Ra12z3e9m76CSnSNlY5e0RqfcOJ2+P7yfeySN7ZBgjcND+6srJ+q/SMnf6jHzaGtsIcPcGGo1B0t2/o/s7FM/VzpJtN3puFhtZduDiIdW30mtbHtaxzd+7b/pHpKdFllb52Pa+OdrgY/zfipKj03o2H02fs2/UvPvIP856XqfRaz/uPUrySmL3sYNz3BjeJcQBPxcmuupx63W32NqrbG57yGtEna3U/vOdtaoZOMzJpdRaT6TiCQ3QmDuA3e5LIxWXY4xw51Qaa3McyCWmpzLav5wPa73Ve5JTNl9NlIvZY11JBcLARtgfSO7+Sh15WLlY1luLdXkVgPaX1OD2yAZbuYXe5NVhVV4bsNznW1vFge58bneqXvtmB/wrkLD6dV0/Dupqe+z1Nz3PsguJ2NqHA/cqYkp//9T0PIzbKn2lrWGvGDDa1zosd6n0fQb9H+RTu/pOR+r/AKL+cVSz6yY7LcmoYuRYcW5lDiwMdudY41tdW0Wept3N/PatR1Vb3tscxrn1zseWgubPOx30mKUnxSU5WZ9YcbDfc19Fz/s721vc0CPcHv3NO76P6Jyaz6xUV1WWvxcjbVd6GjQSSWvtbZz7a3en/wCC1LXk+KW4+KSnHZ9ZsR9or9G0A5P2Xedm0H3j1XEO9tfs/wCMfvVzpnUWdSxRksqsoG4tNdwAcCA135pd+9t/4xXNzvFNJPKSnG+sX1jZ0OlzzjuvcGNc0zsZLnek0OfDvo/4RQ6V9Zf2j0IdV+zip4ubjOre/bWXOsrx/VZfsd+h/Tf6L6f6Fbbmtex1bwHseC1zHCQQdC1zT9JRbRTXUKGVsZS0bRUGgMA/d9ONm1JkMsft8PB+sv8AnOL9HtwNejP9bpzs4VztbafTY7dJqL2EMe4V/T9L/CMZ/wAIg9K6oeqdPvyDUKSwvr2tf6gP6Ntu5riyl30bf9H/AOB+9aDQGgNaA0NEADQADwhRe1raHtaA1oY7QaDUHwSY3//V7/Koyn3ucxr3OOz7Pc2za2mP5z1Kdw9T3e/2st+0f0a30mKOTjdcsfaaMtlTDa11LIGlYBZZQ+z0Xfzv8/6n+D/mf+EVq3MxqrPTe4hw27iGuLW7jDPVsaNlW7+WnszcKouFmTUwscGvDrGt2udqxj9zva98e1JTRoxfrD6jDkZzCwZLnva1rTOPHso/mKv0m/2/S+h7/U3sSsxvrGbWOrzKgwXFzmEc0yxzK59J3vazez/1Z+ju2Z+BUHG3KprDHbX77Gth2p2O3O+n7H+1J+fgVl4syqWGoTaHWNG0T6c2e72fpP0f9dJTRrxPrE1lAdn12FljjeS0AvrmvY0Ftf0m7Lvzf8N/waWDjfWOuzGdm5dVrGF/2pgiXhwaKtjxj1fzTw9+zbX/AMatXQ6jUHghJJTjfWTC+sGVjWDo+SKT6YArDvTe5wdus2W7fa+yv2Mf6lfpoPR8Dr9H1cbjdRsfZnes14aLosbQLa32YxzGu91npNv/AML/AIT0fVWtndT6d0+t1udkMoaxu87j7ts7fbW2Xv8Ad9HaoY/WOm5WAOo49/q4pdsD2teXby4Utq9Hb6/qusexvp+nvSZjPJ7PDwDg4vn4f0v3eNWLTmt6Z6NjyzKLbA15dvLC4v8AQ/SP9Xd6THV/T9f/AIy9B6TjdQx+nXV9QsNtri9zNzzYWsLG+31HS/8AnfVf9P8A7b/ma7teVj2Y5yQ8NpAcXPfLA3ZIt9UW7HVeltd6nqfzaHRm4mbi224lotY0OY4iQQ4N3bXNeGvb7XNe3/g/0n0EmF//1vRrsKu17ybLGstj1qmkBj9vt98tc/3M/R2em+v1K0KzpGBY+17m2brrG22H1X/TYS6tzfd7Njnfzbf0SjlZ9lN72NdU309m2h4PqXb/APQEOH/FM9lv6b+c9OtDt6zbQ+7f0/JfVS5zQ+lhe47TtBNe2v22t3XV+lbez0v570bbPSSUku6F0m59lj6CH3PFtjmPeyXjf7/Y4f6WxKzofSLfU34zSbaxU90u3bQQ9u187mva9jH+p9PeoV9ZdY6G9Py4e3fU8sGwjaHt3P3eze/9GmZ1q0tcD07J9VgJIDfY4ifbVc8Vus9rd381/wAXvSU6TQ1rQ1ohrQAAOwGgTrNf1i8sccfpuXZYDDWvaK2uP53v/Sbdv8pinT1O261tQwMmqXurc+1oa0OZs3u3Au3VbHWOqu/m7vS9NiSlurdC6b1ep9eYx257Qz1a3FrhtO9jm/mOdW76O9ih0/6v4HTumDpuK6xtfqC82kg2G1rmXNtcdnp+11NXt9L09iB9ZOu5fR8W23Hwjkbaw8Wnd6bSXbP0nptPsp+nb+krQuj/AFjys/6ujqt9VeNZ6zaS924UlptrodlNl2/0a/Vd/hfp0/zqTOY5vY4jI+zxAVxfpf3XYow6KMT7I2XUw5p3HUh5c6zVmzb/ADjvofQQ8XAx+n4dtGPu2uDnkuMkuLNng381jfzU2Lm2X9NOY1gseG2FjGyA/wBMvYzbHrbfW9P831/+uoPSOpX9S6ddkXVMqc0vY303EtcNgs3De1u3b6npf9b/AOtpMD//1/ToPMcd/BKSqGVhW23ue1jHF+z0shziH0bfpekwD/rvs/nv5nI/QqF2B1Yvusxuo+k57nFjX1+ozaTuYHVvdtrdUz9Az0Nm+v8ASXb77P0aU6UlKT4rMZgdambepy1zDvrbU0e/aG+y5oa9lW4fmM9T6f8A1mB6X1c2teepO2syX3Fg3AGp7mObjc7NtTWu2b67UlOtJS1VXCxcnHBF2Q7I9oALp5DrH7vd/Iexn+tStJKVqkJ0A7aABAy6b76DVS/0Xkgi0EiAD29Mtf7lDPx778I0VkPs3VE73GsPDH12Wse+pr3M9ZjHt+gkptGZ15UbZ9OyedjufgVWxsXIZ004tjwy4tsaH1ucQzeXmr07CKrP0LXs2fQQOlYOVgdPupyrfVe4veCH2WBoLGt2tff+k+m19n9tJT//0PTkkkklKSSSSUpZz7+pX5+RjYttNFeM2o/pKXWucbA530m5GPt27f3VorLN1uH1XLtdi33V3tpNb6Wbwdge14Pu9vKbLp2vWvJmwC/coRMhD0CQjL1e5j/Rn6fk403o9c/7mY3/ALCv/wDe1Z+bn9exrA2t9F7BbXRZYMctAst9P0qmtOaXbnetX7/5pTGXmgztzj+l9SPQP0J3ejrcU2YOn5t7b8jpucbGiAWscyRO7a/07G7uEyVVoTfnJsYhPi/WY4mPhjw/+gNukdZvprvrzcbZaxr2ziPBhw3t/wC1vmoXXdUxr6Ksi6i6vK9WshlDq3N21W3bg92Tf/o/3EF2Zl+pY6mvNqqc0NqpGP7a4Ab7ItapZGRbm5WI5uLkVNxzdZa+2sMaAaLauS//AEj2o6aUTdjrLutEZky44QEOHJ+hhifklweqMf33/9kAOEJJTQQhAAAAAABdAAAAAQEAAAAPAEEAZABvAGIAZQAgAFAAaABvAHQAbwBzAGgAbwBwAAAAFwBBAGQAbwBiAGUAIABQAGgAbwB0AG8AcwBoAG8AcAAgAEMAQwAgADIAMAAxADkAAAABADhCSU0EBgAAAAAABwABAAAAAQEA/+EOxWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjM2YTcwM2M3LWNiOWMtN2Q0Zi05MDZmLWZkYmEwNjEzYTc5ZSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpiMzIyZjg5NS1mNmJjLTZkNDAtYTY2MS1hYzNlZTY0Nzg5OWMiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iODU2NzdDMTcyNUEwMDVDMUJCODcwRDIxQTdGMTk2MTciIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IiIHhtcDpDcmVhdGVEYXRlPSIyMDIwLTExLTI0VDE2OjM4OjAyKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjZkNWJiOTY2LTU5MTYtZTU0Mi1hMGM5LWM3Zjg1YzcxZTY2NCIgc3RFdnQ6d2hlbj0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMzIyZjg5NS1mNmJjLTZkNDAtYTY2MS1hYzNlZTY0Nzg5OWMiIHN0RXZ0OndoZW49IjIwMjAtMTEtMjRUMTY6NDQ6MjIrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSLikaAiIHBob3Rvc2hvcDpMYXllclRleHQ9IuKRoCIvPiA8cmRmOmxpIHBob3Rvc2hvcDpMYXllck5hbWU9IuKRoSIgcGhvdG9zaG9wOkxheWVyVGV4dD0i4pGhIi8+IDxyZGY6bGkgcGhvdG9zaG9wOkxheWVyTmFtZT0i4pGiIiBwaG90b3Nob3A6TGF5ZXJUZXh0PSLikaIiLz4gPC9yZGY6QmFnPiA8L3Bob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+/+4ADkFkb2JlAGSAAAAAAf/bAIQADAgICAkIDAkJDBELCgsRFQ8MDA8VGBMTFRMTGBEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAENCwsNDg0QDg4QFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgCWAEsAwEiAAIRAQMRAf/dAAQAE//EAT8AAAEFAQEBAQEBAAAAAAAAAAMAAQIEBQYHCAkKCwEAAQUBAQEBAQEAAAAAAAAAAQACAwQFBgcICQoLEAABBAEDAgQCBQcGCAUDDDMBAAIRAwQhEjEFQVFhEyJxgTIGFJGhsUIjJBVSwWIzNHKC0UMHJZJT8OHxY3M1FqKygyZEk1RkRcKjdDYX0lXiZfKzhMPTdePzRieUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9jdHV2d3h5ent8fX5/cRAAICAQIEBAMEBQYHBwYFNQEAAhEDITESBEFRYXEiEwUygZEUobFCI8FS0fAzJGLhcoKSQ1MVY3M08SUGFqKygwcmNcLSRJNUoxdkRVU2dGXi8rOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vYnN0dXZ3eHl6e3x//aAAwDAQACEQMRAD8ADJ8UpPimSXTvGryfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFKT4pkklLyfFF3H7Lz+f/wB9QUX/ALTf2/8AvqZLeH97/uJL4fLk/u/93B//0AK5gM6S5tn7RtuqII9P0Wh0j87duCppHhdNIWKsx8Y7vHQlwyB4RLwl8ru9T6T0XBtfiMvybM0sBpZtbtc5/wDNtLg1ZDcLMdkHFbQ85LfpU7TvECdWrT+tgJ60QASTVUAByTHZa2X9pPS7KWOY7rzMdn23aD6px5cfTDvzr9uz1v8A1Wqkc0oY8ZJ4zlA+f9A/ven/ACTfny8MmXLER9sYSa4B/ODX0er/ACriU9BzLulvz212mwPa2uoMkPYRJuaf3WovSum4t/TLcy3FvzLWXioVY5IIaWh+8gNcj9Pt6jd9WclmK+6yyq+trG1ucXNr2jc1oafbUm6VZTX9XbnXWZFLPtjQHYn85Owact/RpTyZOGYMtRkjEcN8XCf7qseLFxYyI6SwymeOuHiHX1JcTpGDkZVVD+k59DLHQ617iGt0+k72LFx66P2j6Nrd9XqPZsdYKpjcG7r3fQW/0nIwXdTxm15XU3vL/ay6PTJh3877vorO6O+5vVrmMxnZVVj3i5jK22OA3O2vHrA1s2u/eQhOY9yyTUAQJGcf3v8AOJyY4H2aAF5CCYxhLpH9GCc9P6WAT9mq0/8ANjX/AORVHruFj4Oc2jGBax1NdhaXb4L53RZ+c1dGzGazHza78jBdex7K6LBRWRWXktYMtjW7Get9D+Qsj6zX5IfXh3NcTSAH3vpbUHubIb9ncwbvQa0/R3JuDLI5QASRrdyn2jLi9a7mcMY4CSADoRUcenqlHgPto20fVsmkOq6iwZBAre70w10nZuYfzmtcl1HG+rmHfkYgGY7IplrXTXsL49vg/YpZX9C+r39r/wA+sU/rD1Qfb87E+xY07iz7R6Z9Xhvv9Td/OJ8eMziAZkHjv1/5vJ7fEsmIRxyJjAEe3R4P87i9zhR9L6bhX4FNtuO/Iyb7rK2tbb6TQ2tnrE/Rd+amz+k4YY/IxS+qpuFTmCtx3km1+zY5/wDJarf1bsbZTTSw+/FfkXWl3tY1tlX2enda72fpLHIObm0VUX4lxNOQzp+PiGt4IJtrfusY395u33Ns+g9Azye8QDLQjTf0yn+7/cXCGI8uDKMdQaNAeqOP9/8Av8TR6/i0YnWMnGx2enTWW7GSTEsY4/Sn85yz1q/Wn/l/L+LP/PbFlKzgJOLGSbJhH/otLmQBnygCgJzAA/vKSSSUjEpJJJJSkkkklKSSSSUpJJJJSkkkklKV3pOPRk5FrLhuazHusaJI9zG7mH2qrVYarWWtDXFhDg143NMfvsP0mrd6P1e+7Jua6jFbtxr3gsoY0y1m7aSPzP32KHPKYhLhHTfi4aZ+Vhjlkjxnr8vDxCTz4MgJK7kdVuyaDS+nGY10e6qlrHCNfa9qpKSJkR6hw/XiYpiIPplxDvXCpJJJOWtzpo6Xuu/aO+Nn6H05+l57f/OFTWr0B3UGuyvsVFd5NUWeoYgfm7f3t3+j/PWUFHE+uYvbh63/AM39BlmP1WM1vxa8PD1/f/TUi/8Aab+3/wB9QkTYPS3yZ3Rtn2/R52/vIy3h/e/7iS2Hy5P7v/dwf//RAkkkuneNSOych1ovfa91zY22lxLht+hDz7vanbk5LLzkMusbeSSbg4h5J+l+knd7kJJDhHYdvonilvZ3vfr3S05OTQXGi59Rfo8scWyP5W0+5WMTrHUMHFdjYlnote8WOe0e+QNu3cfbs/sqkkhKEZaSiD5jsmOScTcZGNWND+9u6lP1l65Va2w5TrNhnY+C0+T9u13/AElQblZLHWursdV68+rsJbuBO7adv5qEkgMcBdRiL3odkyzZZVxTkaurPduWdUyX4A6eG1V4/t3+mwNc8t+i65/56H+0M4101Ove6vHeLKWuO4NcPolu79391V0kRjgP0RvxbfpHqo5ch/SO3Dv+iOjq/wDOnr+v62df5Ff/AKTSP1o68WlpyyQRB9lfB/62spJM+74f81D/ABYr/vXMf57J/jybBz8k4A6eCG4wcXua0QXk/wCld/hNn5iPV1vqDLGWucy91dYpZ6zA+GA7xz+fu/PVBJOOOB0MRqSdusvmWDNkFETOgA36R+VLlZN2XkWZOQ7fbaZe6I8uEJJJOAAAA0AWEkkkmydSSpJJJFSkkkklKSSSSUpJJJJSkkkklKSSSSUpTrttqcXVPLHOaWktMS12j2/2lBJAi91AkGxopJJJFSkkkklJKr7qdxpsdWXja7aSJB/NMIaSSFKs1XZSL/2m/t/99QlP1Gen6X5+7dEaRt/eTZbw/vf9xJfD5cn93/u4P//S6P8A5iu/7nD/ALb/APM0v+Yrv+5w/wC2/wDzNC+vWMzL6j0PFsJFeRc+p5boQHuxmO2zu93uRv8AxuOh/wCnyv8APr/9IKU/E+c4pRiRLhrX0R3HF+4vj8A+FjBhy5sksRziUowjDJlr25yxfN78P3Vv+Yrv+5w/7b/8zS/5iu/7nD/tv/zNP/43HQ/9Plf59f8A6QS/8bjof+nyv8+v/wBIJf6R57w+2H/qtH+hPgn+fn/4Tk/+CFv+Yrv+5w/7b/8AM0v+Yrv+5w/7b/8AM0//AI3HQ/8AT5X+fX/6QS/8bjof+nyv8+v/ANIJf6R57w+2H/qtX+hPgn+fn/4Tk/8Aghb/AJiu/wC5w/7b/wDM0v8AmK7/ALnD/tv/AMzT/wDjcdD/ANPlf59f/pBL/wAbjof+nyv8+v8A9IJf6R57w+2H/qtX+hPgn+fn/wCE5P8A4IW/5iu/7nD/ALb/APM0v+Yrv+5w/wC2/wDzNP8A+Nx0P/T5X+fX/wCkEv8AxuOh/wCnyv8APr/9IJf6R57w+2H/AKrV/oT4J/n5/wDhOT/4IW/5iu/7nD/tv/zNL/mK7/ucP+2//M0//jcdD/0+V/n1/wDpBL/xuOh/6fK/z6//AEgl/pHnvD7Yf+q1f6E+Cf5+f/hOT/4IW/5iu/7nD/tv/wAzS/5iu/7nD/tv/wAzT/8AjcdD/wBPlf59f/pBL/xuOh/6fK/z6/8A0gl/pHnvD7Yf+q1f6E+Cf5+f/hOT/wCCFv8AmK7/ALnD/tv/AMzTH6jwQDntBPE18/8Agiy8v6mYD+r19K6dbcXMZ62bda5rm1sOjGMayuvddZ/XVXN6F9W6ut4nR8W7IvstsDMmzeyGT+Y3bT7rP3v3E0/FecG9DXh/Q3/8LZh/xb+FyIEMs5XA5T+qyDgxx/Sn+v8ATxO//wAxXf8Ac4f9t/8AmaX/ADFd/wBzh/23/wCZrE630PA+rHVulZVVtr8d1wstFkOc0Uvqe4t9Ntf5r/3V6K1zXNDmmWuEgjggp8PiXNSMgZcJjXSEt/8AAavNfAuRxQw5Md5ceYSMZEZMX83LgkP5ybyv/MV3/c4f9t/+Zpf8xXf9zh/23/5murSUn3/mf3/+bD/vWp/ozlP83/zp/wDfPKf8xXf9zh/23/5ml/zFd/3OH/bf/ma6tJL7/wAz+/8A82H/AHqv9Gcp/m/+dP8A755T/mK7/ucP+2//ADNL/mK7/ucP+2//ADNdWkl9/wCZ/f8A+bD/AL1X+jOU/wA3/wA6f/fPKf8AMV3/AHOH/bf/AJml/wAxXf8Ac4f9t/8Ama6tJL7/AMz+/wD82H/eq/0Zyn+b/wCdP/vnlP8AmK7/ALnD/tv/AMzS/wCYrv8AucP+2/8AzNdWkl9/5n9//mw/71X+jOU/zf8Azp/988p/zFd/3OH/AG3/AOZpf8xXf9zh/wBt/wDma6tJL7/zP7//ADYf96r/AEZyn+b/AOdP/vnlP+Yrv+5w/wC2/wDzNL/mK7/ucP8Atv8A8zXVpJff+Z/f/wCbD/vVf6M5T/N/86f/AHzyn/MV3/c4f9t/+Zpf8xXf9zh/23/5murSS+/8z+//AM2H/eq/0Zyn+b/50/8AvnlP+Yrv+5w/7b/8zS/5iu/7nD/tv/zNdWkl9/5n9/8A5sP+9V/ozlP83/zp/wDfPKf8xXf9zh/23/5mg/8ANX9e/Zn2sbvT+0b9nb+b2+nv/wCmuxWdtb/zk3QN32ON3eN3CB57mDXr21+WH/epHw3lBdY9xR9U/wC9+8//0+k+t/8Ay59Xf/DR/wDPmKtbqvW3YmRV0/BxzndUvbvZjg7WsYNPtGTb/gqv+rWT9b/+XPq5/wCGj/58xUK6zLrH1tyMSf2nW9jGFur20Ctuw1/nfzfrPaosf85l84/9B1Z4xPleSvWseTQ6RufNSxjj/qep0Tl/XKnda/H6fmtr1sxcax7bgPBj7fY5/wDWWn0vqeL1XDbmYpdsJLHseNr2Pb/OU2s/NsYvJegX5lfWsN/T3OOU+5ohpJL2k/pW2fv17N3qb16T0gNZ9Z+vMo/mD9ne8DgXuY71f7bmbfUUxDFzfKjGCPTxCPGDEcGnFHGYyhcv3/RJ18jLxMUNOVfXQHaNNr2smP3fULdyjj5+DkvLMbJpveBJbVY15A8drHOXN/XBjn9Z6QGtc8+hl6MxRmnnG/7Tv+h/x39hVuh1PZ9a8Pex7JxMqN+A3B/OxvouZ/SP6n+DQc96qzq3SqrHVW5uPXYw7XsdcwOaR+a5rnbmozMnHfR9pZax+PBd6zXAs2t+k71Adm1q4LrHVX4edZ9tFdWU7KbTZjNuc1wa/wB32+qo4Vr7MFtbPVst/wCM/wAIrFAyq/qJm12Fow7cgNqyGbwbMfJyWHKu/Ssp3UOqyHsqvaxnr1fpfTSU9vTYy+pl1LhZVa0Pre3UOa4bmPb/ACXNUce+jJpF+PY26l0htjDLTtJY73D917dqxPrZb9lrryLMS23Cw2l7raMx2GWuJFbMcU0OrsybLPYzHZ/pP0dSx/qlg5uBZiYtmBcMrH/SXh3USRVVkPtfXbd01z3Vu9j/AKO3+erf/hUlPaX3047BZkPbUxzmsa55gFzyK6mSfzrHu2MRII7Lg87puHh9Ryr+sdJstw7r3ek6usZLrLciwDHttyrbaW1/pHbMfDpxtlH+FyLEWnAy+nfVv6wXuxjhGzEcK7GzS55qZYPUfhB97cXI9/uvx8r0sj/R46Sns2X0WXW49djX3UbTdWDLmbxur9Rv5u9o9qZ+RRXdXjvsa264ONVZMOeGQbdg/O9Pd7lxGX0OqvpuRaz6r3VP9BzzcM5s7gw7bHbcrc/YtXKc49C+rWVvLsluR041vP0nG1raMn3H3fpca271ElNropaOv9ca/S82UOPnX6cVf9+WZ1npuF0/r/QRiVCs3ZN1lruXOcTS6XOPu/OWt1jpuc3MZ1jpEHNqb6duO4w2+qd3pl35lrf8G9ZXUOo4+dndPy8rHzcO/pr3vOOcZ1m8u2e1lrHbf8F9NQz0HCRrxWD4cfE6eAmWQZYEmEsRx5IR+aOSGCWCHFj/AHJS+STU/wAZvHTf+v8A/ohdZ0NtrejYDbf5wY9QdPM7G8rjvrFlX9Y6v0ZmVg2YmFZfsrF8NssD30Nu31A/ovbsXfAACBoBwEMWuXJIf1R/zU86TDkOSwyGo92ZNiX+Vnp6f+cpJJJTuWpJJJJSkklX6i61uBkOpn1BW7bHPnH9lJIFkDuaR3dY6ZTYa33gvbo4NBdB8Jasnrf136Z0yoCgOy8p+rKYLAB/pLXuH0FjiIEcdlyXWXOd1TI3dnAN/qgDanAB0uX5HFKdSJIAsi/mejp/xl9WFu6/EofTOrGFzXR/JscX/wDSau36P1fD6xgszcMnY47Xsdo5jx9KuwfvLxldt/ixfb9o6jXr6OytxHbfLm/9QkQKZee5PDHCckI8EoVttIE8L3qwH/XCn7Tk0YnSupdQbiXPx7cjGqY6r1a9Lq2Osuqf+jd7PoLoG/SHxXPfU7+a6x/6eM7/AM+BNcdNhfWmrJ6hj9Pv6b1Dp1uXvGO/LqY2t7q2+s+vfVbdtf6bXP8Actpc31G/J6n1mjpOU63pnTX3OZWBubfnWUt+0vZVbX/RensYzc+3ey3L/mq10hMmUlKVDqXX+idKcWdRzqMWwM9UVWPAeWa+5lf03/R/MV9c/wBdrn60fVt4Alzs2ncQPzsc2t5/lUpKa+D/AIx/qxl3spssfgixpfXdlGtlZjXa59dtvo2bfzL/AE10eLl4ubjsysO5mRj2TsurIcx0Ha7a9v8AKC4GrrmbmdFabrvU9T6t5uZkghvvva5uMy58N+n/ADq7bolQo6L0+kCBXi0tj4VsSU3Vn/8ArRf+gn/floLP/wDWi/8AQT/vySn/1Ok+t/8Ay59Xf/DR/wDPmKtPqfSMx2czq/R7mY/UWM9O1loJpvrGrar9vuY9n+Duasz63/8ALn1d/wDDR/8APmKuqUWP+cy+cf8AoOjzEzDlORI/zeYEHUSH3jJoXmqqPrILHnD6R0/pmRbpZn722QD9J7Kqq2ve7+utjpHSqelYpoY911tjzbk5Nn07bXfTtf8A98arqSlac80pCqER14b9Vd5T4pNDqPRMDqWTj5OX6u/FbYyoVWvp0t2epvdjursd/NN/PUMX6vdNw8+vqGP6wvqrfU31L7Lm7bCxz/bkvt2u/RN/m1pJJMTks+q/SKbcd+Ox+PXjW/afQrdDbbvcWZGZY5rsnJsr3u9P1MhGZ0DpNeJmYVNHpY3UA77RSxzgyXjY91NRJrx3a7v0DGfpP0i0EklOdndBweoVYdeY++x3TyH0WttdW/1A30/XsdSWb79v+EQ6fq10ynOo6hW7J+148tZa/ItsJrd/OY9rb32NfQ936TZ/pfetVJJTkZn1Yws1zjk5Wc9rrBaKvtVgY17XerW6usH9H6Vg31fuIv7Bxjg5eDbkZd9OdWarfXvda5rSCx3oG3d6X0lpJJKcd/1S6I+l1JGVtcw1n9cyToRs/Ov2/RVsdIxR+zhLyzpQjGrJG0uFf2ZltzY99tVO/wBP/jFdSSUpJJJJTyv1v/5c+rv/AIaP/nzFXVLlfrf/AMufV3/w0f8Az5irqlFj/nMvnH/oN/m/9x8j/czf+7GRSSSSlaCkkkklKSSSSU5tv1f6bY8vAfXu1LWOhv8AZaQdqwPrH9RGZLftPTrHNyGiHNsMtcB29o9v9ZdikjZZoczmhISjM6d9Xyer6l/WCy0VmllYmC8vBA+TfcvQvq30GnoeB9nYd9th33WHQud/31rfzFrJJEkr8/O5s4EZkCO/DEVfmoaGVztf1c69h5GY7pXWKsbGzMm3LNN2ILnNfcd9rfW9evczf9D2LokkGs4GP9X+sv6rhdQ6t1WvNb042PopqxhRL7WHHLrLPWu9jWP+jtW+kkkpSz+s9Jd1JuNZRkHDzcG318TJDRYGuLXU2Mtpft9Wm2qx7Ht31/8AGLQSSU83d9V+rZdDsPK6hi14drDRc3EwhTaaHHdbi1XvvubRXb+f+iXRta1jWsYNrGANa3wAENCdJJSlmRV/znnT1fsXjrt3furTWdA/5xzGv2Tnv9JJT//V7nrv1cwuueh9rfaz7Pv2ek5onfs3bt7LP9Gsr/xueif6bK/z6/8A0guqSUcsOORuUQSW3h+I85hgMeLNKEI3wxGw4vUXlf8Axueif6bK/wA+v/0gl/43PRP9Nlf59f8A6QXVJIfd8X7gZP8AS/P/APiibyv/AI3PRP8ATZX+fX/6QS/8bnon+myv8+v/ANILqkkvu+L9wK/0vz//AIom8r/43PRP9Nlf59f/AKQS/wDG56J/psr/AD6//SC6pJL7vi/cCv8AS/P/APiibyv/AI3PRP8ATZX+fX/6QS/8bnon+myv8+v/ANILqkkvu+L9wK/0vz//AIom8r/43PRP9Nlf59f/AKQS/wDG56J/psr/AD6//SC6pJL7vi/cCv8AS/P/APiibyv/AI3PRP8ATZX+fX/6QS/8bnon+myv8+v/ANILqkkvu+L9wK/0vz//AIom8r/43PRP9Nlf59f/AKQS/wDG56J/psr/AD6//SC6pJL7vi/cCv8AS/P/APiibzWJ9Qej4mVTlV25Jsx7G2sDn1kEsIe3dFLfb7V0vySST4QjD5RVtbmOaz8wQc2Q5DEVHi6K+SXySSTmFXyS+SSSSlfJL5JJJKV8kvkkkkpXyS+SSSSlfJL5JJJKV8kvkkkkpXyS+SSSSlfJL5JJJKV8ln/+tF/6Cf8AfloLP/8AWi/9BP8AvySn/9b05U+o9VxOnNZ62591x20Y9Td9tjvCusf9Wrix+ltbkdc6rm2+67HsZiUT+ZUGNtOz931Xv9ySlO69mUt9XM6RlUYw1da0stLR+9ZTU71FqY+RRlUMyMexttNo3V2N1BCIDGoWP0ituL1jquBSNuMDVksYOGPuB9ZjR+buc31ElOwATwnII5C5X632sb1XpVV1tVdD6spzm5GVbh1Fzfs/pn1cX32Wt3P2Vqt0G2kfWjGpxbsd1b8TJdazFzr8xpLXY/puvZlw2nbuf6T2/wAtJT2cHwKbhcF1DJxrMu7KdTdWyzMGK9pbuc3IsIa2i3Z1enZ6jvz/AEaqdi3/AKl33u6Vbi3tf6mHk31Osc5r2n9I97aq3svy/wCj1uZVYx1z/Sf+i9R6SneSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJAz734+FffX9OthLfj4/2UkgWQB10SPvorO2y1jHeDnAH7iVQ6t9YukdJxxfk3h5dpXVUQ97z/Ja0/8ATcuXPucXPO9ztXOdqSfNcn1qx1nU7p4rIrYPAAJwi6GDkIznUpGgLlT2lP8AjM6c60Nuwr6qif5wOa8jzNY2rrMTLxs3GrysSwXUWiWWN4P/AJk391eJLuP8WWVcXZ+ESTS0MuaOwcSa3x/X2tSIZOd5DFjxHJjuPBVi+LiB9L3axc365/VbAyrMPL6jVXkUnbbXD3bXfuudWxzdy22/SHxXP/UwkU9Yj/y4zv8Az4muU2em/Wz6t9Vyhh9Pz678lwLm1AOaSG6u2eoxm7a1ay5vqXULer9Zp6b0s102YNrhf1a5jXGqwNm/B6VVaP1nOfj/ANKs/mcTG/4RdIefBJSknENY6x5DWNBLnOMAAauc5xSXKfWnpPT876z9B+147bxkty6LWvkteK6Tk47bGtI3em9tiSnZ6d9Y+h9TyPs2BmMuvLS9rAHN3NH0n0+o1jbmt/4JaREaFeZiro2Z0mu49LxK32dCyupufW14Nd1JbTWMbda70atznexd39W8WrD+r/TsapuxrMaokeL3tbZa/X9+xznpKdFZ20f845/7qf8AflorP/8AWi/9BP8AvySn/9f05ZGbi5+F1B/Vem1/aW3ta3OwpDXP2fzeRjud7PXY32uY7+cWukkpyHdeybG+nh9KzH5J0DLmCqtp/wCFuc7bs/qKx0fp1uHVbblWC7OzH+rlWN0bujayqr/gqWexivyfFJJTmdU6PkZ2fiZuPmuwn4ld1Z2VssLxd6X/AHIbZWzb6P7iHi9DzKurUdSyeouzBj020tqfTVXHrGp29r8ZlX+g+jYtdJJTzeR9SsbJzaMnIv8Atm2z1cuzLYLr7Q3eK8Wt014uLifpffXXib1rdJ6U3pVBxKb7LcRkDFot2n0GAfzFdoa22yr9z1/UsZ++rySSlJJJJKUkkkkpSSSSSlJJJJKUkkkkpSSSSSlJJJJKUmc1rmlrhua4EOB4IKdJJTiW/VhheTRkbK+zXt3EeW4ELmPrN9Ss5jvtuG4ZJdpZU0bXafnM3H3L0JIwRB1CNlsY+czY5CQlddCPm83xerpHVrbPSrw7i/iC0tA+Lne1ekfU76vO6LgvdeQ7LySHWkcAD6Fbf6q3hVUDIYAfgpJE2yczz+TPHgIEI7kR/SUDBBXMYeJ9aej3dQqwsLEzcfLzb8yu6zJdS6L3ep6TqvQs91f9ddOkg03nGYn1j6j1vpeb1LExsKjpj7rSacg3OebanYzawz0adv09+/cujSSSUpZXXem5uTZgZ/TjWc3pl5urqvJbXax9b8a+k2sa91L/AE7P0Vnp2e9aqSSnjH/V3ql2G7p+J0jF6WLMR/ThluzX5HpYtr/VyGMxvRZ61u7d6e6xn/GemuxqqZTUylk7K2tY2eYaNoUkklKWf/60X/oJ/wB+Wgs//wBaL/0E/wC/JKf/0PTpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJSpSlJJJTz/1o671Ppd+Bj9OqqutznPrDbQfpA1Nqa0iylrdzrfz1j5v1r+tuA7Zl42DS/nY6xu7/MGZvVj69svs6j0OvHf6V77ntqs/deXYwY/+y5Qd03Op6oOkdEYMd1bBbmdWyK/UsscdTtsta7d9P8xVZ8ZnOpSABiBw+MXd5YctDleXMsOGcpwyTySyie2PLOPFLJGX9z/JZJsML60fW/PJbh4uDe4CSxtjdwH9T7ZvVv8AaP8AjC/8q8X/ADh/72Kp0/Ev6pm53S+oOYOp9OIfjdToaK3gz7fU9MMa9uv0F0f1e6lfn4LhlANzMWx2PlAcepX+e3j6bTvRhEnfJPW+sf0d/wBFZzObHjsw5TlpCPDxDhyH05RxY8kZe7Hjxz/uQcf9o/4wv/KvF/zh/wC9iX7R/wAYX/lXi/5w/wDexdWkpPaP+cn9sf8AvWp/pCH/AIj5X/Ey/wDq55T9o/4wv/KvF/zh/wC9iX7R/wAYX/lXi/5w/wDexdWkl7R/zk/tj/3qv9IQ/wDEfK/4mX/1c8p+0f8AGF/5V4v+cP8A3sS/aP8AjC/8q8X/ADh/72Lq0kvaP+cn9sf+9V/pCH/iPlf8TL/6ueU/aP8AjC/8q8X/ADh/72JftH/GF/5V4v8AnD/3sXVpJe0f85P7Y/8Aeq/0hD/xHyv+Jl/9XPKftH/GF/5V4v8AnD/3sS/aP+ML/wAq8X/OH/vYurSS9o/5yf2x/wC9V/pCH/iPlf8AEy/+rnlP2j/jC/8AKvF/zh/72JftH/GF/wCVeL/nD/3sXVpJe0f85P7Y/wDeq/0hD/xHyv8AiZf/AFc8efrH9a8TqOBi9Uw8bHrzrm1AtlxILmMt27Mi3a5vq/nroP8A1ov/AEE/78sX63/8ufV3/wANH/z5ira/9aL/ANBP+/JsASckDOWhjUv0tuJk5nJjjDk+Yjy+IHJDIZ4qn7EuHJPFH08fH/44/wD/0fTkHLzMXCoORl2topboXvMCT+aP3nIyxcalnUuu5mVlD1K+mPbjYdTtWteWiy/J2/6V27YxJSRn1p6I54a+2ylrjDbbqrK6yT/wtjdi1gQQCDIOoI1BBTWsZdW6q5osreIcx4lpB7FpWR0NpwszP6M1xdj4hZbiTqWVXAu9CT+bVY32JKdhJYP1iz+p0dS6fiYL8lrMirIsubh102WE1GkV7vtv6NlX6V30EDpfUOsft/Gwsp+ace/HvsLc6nHrl9TqAz0H4Xu9vqv9RtiSnpUlxuZ9Yn/a7HUdUezGstLaZcKxM7fRY2zpVzt+72+n61tiNjfWPOs+qeZebHP6tRYcYOewtLXX3fZsCwzVjV27arar/ZSzekp6xJc99YMnqeFZh4eHnOx214WXfdkOZXa+w4raPT9U3tc33+o59uxVOhZ3WsjMH7R6uWVVswnml1FLPUfl1utdil4r3s942V7P0iSnrElxFf1k6ofrI7Ffk5DemG92PW4/ZZD2g7tzfS9Z1Ff09m71/S2K7hdY6hfg9Xtxuo/a6Meuh2J1DLqFDGmxrjkuGynGbbsbssq9uz1P8J6aSnqklwr+p/WMCrEp6hbZk1S4NNWP6rnBvswsqr7Q3fcyj1XZv/dr0n0foFtZP1jdi9Jw3485mbm0MtquyG+jQxtkBuV1Kyr9Fj1MdYxnoUb777P0VP8ApElPQJLAwM7P6VnDo/U7X9SDnM9PqFbNz2Ou3bKuo0Vbvs9VlrLfseS39B6X6G70/S9Sy3hZGRV17qPTrrDbWa6c3F3aljLd+PfQD/o2X4/qVf8AHJKdRJJJJSkkkklPJ/XFwb1v6uucYaMokk8AepirW+sXXqej4m4D1My72YtA1LnH84t/0bFjfXrGry+pdCxbZ9O+99b40O178Zjo/wA5Az/qx1V14dmYw641jRXTcMj7PaGCdrbRZ+ic7X6TVXMpCeXhHWOv+C7EMOGXL8ic0wIiOWXt+mHH+vn+nklDH/e9bt/VbpB6di2XZTxb1HNd62W+QSCdfT9v7m7/AD031ZeL7OqZtY/QZGY70XdnNY1tXqN/kvc1YuB9U+o+q51NA6LTcw1ZBFxyL3VuLXOrrc39DXv2fTXYYeJj4WNXiYzdlNLdrG+SfjB9OnCI/i1ubnAHIRk93JmMboR4ccIerh/VyyY/0YcHBP5Eyfa7wKb46juFzmY19GVbS179rXe33Hg+4d1K0Ho0lmdHzLbS/HtcX7RuY48xwWlaaSlIb8jHrdsstrY4ctc9oOvkSit5HxXH/V3oPROpu6xk9SwKMzI/a2az1bmB79rXhtbN7vdsY36DUlPWV30WktrtZY4CSGuDjH9kqa5LKwOi4XW8XH+rXTqB1zGJfa6oenRRVa30nv6m+n3P31/0XE/nfU/SrrTE6cJKUkkuY+s9GTf13pOGzOy8XF6jXlVZFONYKw401/aKy0lj9j7P0jLElPSMupsc5tdjHuZo8NcHEf1g0+1TXmGJ03o+PiUdQ6XZnYWSej5PUqbGX1/o207G/ZbNmMz7Qx73fSs/0a9A+r7bx0PAOTdZkX2UV223WmXl9jRa+SA32tc/bX/ISU4n1v8A+XPq7/4aP/nzFW1/60X/AKCf9+WL9b/+XPq7/wCGj/58xVtf+tF/6Cf9+UWP+cy+cf8AoN/m/wDcfI/3M3/uxkf/0vTli5Lrui9Tv6h6T7umZ212Uamlz6bmjZ65rb7n0Ws+ns+gtpIGOElOTZ9auhBk05P2qw/Qooa59rj+6Kw32/21PouJltOT1DPaK8zPe1zqQZ9Ktg2UUbv32t91i0msY1xc1jWuPLgAD94TpKef+svQsvquf0++inFvqxa722NyzYGg2+j6bq24/v3fo3oHRvq1m4HXqM9+PhY9DMe6p5wzbuLrHUur3tyd3s/RP+gunSSU8hnfVTq2bn0OyLwabsluXmHFe7GprdWIY6jHb6mTfnP9j/tbr62epXv9JXh9Xc/9gZ3R7L6rC8izDy2tc219rS3IZf1Le631Mj7VWz1bqnfpWf4Or6C6FJJTg9c6P1Dqz8DI9DHs9Oi2vNw77rK2E3ehbsF2NXZ61VduP+krf+jvrUW9I6nd1ejqWZ0/p/q1uZvtrycgkNr3tZYMb0K8a6+lltnoOt+h/pF0CSSnkT9VurOftLqxjuzrck/pnEhljr/0n2b0fS9XZcz/ALUKw/o/1iyOiZOBkmmuMCvCxseq1z2PsZ/OZdlllVPo+pDGMr/SLpkklPLZP1Xz6MqzqWK9uZaM5+dVhyaQTY11Wx+RZZZS30/U9R+3F3q5f0XOs6H0voc1+hWMdvUrpMhmP6dprxmR73ZN1Xpeo7+aqW6kkpoY+HfX13Pz3R6OVRjV1wfduqN5t3N/68xRwsLJHV+odRyg0esKsbEaDJFFIc/e7919+Rfa70/3GVrRSSUpJJJJSkkkklPK/W//AJc+rv8A4aP/AJ8xV1S5r63dN6vl5XS8rpdAyLMGx9pDnNaAQaX1bvUfVua70vzEL9o/4w//ACrxf85v/vYoBLgyZLjI2Y1wxMv0XVlgHMcpygjmwQljjljOOXLDFOPFmnKPpk9S4bmkDQngqNdgfpxYPpMPIP8A5Fcx+0f8Yf8A5V4v+c3/AN7FCzM+v1kb+k4jo4O4T9/2xO94fuT/AMSTD/o2f/ijlf8A2oxfxerfYysS8/Bo5J/daFjW9M6lfa+57WB1ji4jeNPBv9lqzWZX1+rMs6TiB3724E/ecxT/AGj/AIw//KvF/wA5v/vYl7w/cn/iSV/o2f8A4o5X/wBqMX8Xe6d0/wCyBz3uDrX6GOAB+aFcXK/tH/GH/wCVeL/nN/8AexL9o/4w/wDyrxf85v8A72Je8P3J/wCJJX+jZ/8Aijlf/ajF/F6saEFcj0zqGZ0K/qmJkdH6jlG/qOTlVXYlLbKnV3u9SrbY62v3bfpqf7R/xh/+VeL/AJzf/exL9o/4w/8Aysxf85v/AL2Je8P3J/4klf6Nn/4o5X/2oxfxY419+f8AWHp1mB0jO6Xj1X5GV1KzJqbTXa6yl1DHP9O2z17vUNa6xcr+0f8AGH/5WYv+c3/3sS/aP+MP/wAq8X/Ob/72Je8P3J/4klf6Nn/4o5X/ANqMX8Xqli/WLHyxldK6ti478s9Lvssux6o9V1V1NmLY6hryxtj697X+lu96z/2j/jD/APKvF/zm/wDvYl+0f8Yf/lXi/wCc3/3sS94fuT/xJK/0bP8A8Ucr/wC1GL+Ljt6c4YB6f0zp/VX5Lum3dIodl0Mppay9/rPyci4u9npfyf8AMXfY1IoxqaAZFNba58drQz+C5n9o/wCMP/ysxf8AOb/72JftH/GH/wCVeL/nN/8AexL3h+5P/Ekr/Rs//FHK/wDtRi/ir63/APLn1d/8NH/z5ira/wDWi/8AQT/vy5q/D+uHVOq9MyOo4FVNWDe2wuqez6JfU61zg7Itc7a2n8xdL/60X/oJ/wB+TYS1yT4ZUTGhw+r5eH5WXmcIMOT5cZsJnCGUTkMsPZhxZZ5Y8WX5Y+l//9P05JJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpZ/wD60X/oJ/35aCz/AP1ov/QT/vySn//U9OSJABcSABqSdAAksO2gdc6rkY+SSemdNLa3Y4JAuvcPUcb9v06aWn+bSU6lXUenX2+jTl0228bGWNLvulWFQyOg9FyKfQswqWsH0TWwMc3+VXZXtexyD0S7Jrsy+lZdhvtwHN9K930rKLBuodZ/wjP5t6SnVSWD9ZOv5HScrBxqXY9Qy23PdblC1zR6Xp/o2MxQX7n+r+cgdF+s+TndZr6dbZiZDLaLbi/FbexzDUamjf8Aa2ta5tnrfmJKelSXO5XXeqszLasYU2UtcfTe2iyyWfveozKYx/8AmJ6PrRZZ9V8jrDmVuy6HWUihkwbRacPFbYwufZV61vp72uekp6FJUL87Nw/Qpfg5HULTWPWvxRU2sPHts9uRfS5m53vb/I/PWb9XvrJ1HqWDjW5HS8nde97X5FYqFLQ22yoPLTkG79Gxn6X9Gkp6FJcf1z665uH1C3Ew6McNxjk12OvuDXOfTjty2PFW39GzdZ/6Ef8ABrZ+rfXX9ZxrHW1MquxzW2w1WC1jjZVXkbmPaG7f5z31f4NJTrpJcrlMX61dUvwczMubhYNVFxFV2TaSz05hlLm0/pbMm1jfWp9P+e3/AEElPVpLG6N18ZfRL+rZ5pqbjG43CkuJZXVJm+mz9NRe9jfU+zWfpEmfWGtvUMyvK/RYdLcH0Xljg4OzTYz9Z/0bPUbUz/gklOyksXrPXj07IyGCylzMbEdfdSPdkseXCvGuZRuYzIxrXO9N/vq2P/wvvQ+n9b6u/quLgdVwTg/acYlhOxwfk1APzNj67rHVY1bHN9DfX+m/60kp3klx/UPrtl4XULRfVTh4W2MVmRvdfY5p/S2vrwW5Lsbf7Ps2NkNq9Sv9N6v+Cr0rvrFlU/VXI61ZTQ3Mx6y92My4XMYdwawXW0/yD6lja0lO8kuFv+vmfS7LYMnpT/sdIvDh9oiwkWH0a/5f6L/wRdNZ1PJrzOkl7WjE6ox1bh+dXkGv7XR7/wA6uyuu+nZ+/wCmkp1EkjwkkpUpJc8rCyM3qNGRZSb3H03QDA1HLeySndSVDpnUH5QdVdHqsEhw03Djj95X0lKTwfBIakLj+ldJf127quXm9S6hW+rqWVjV14+VZTW2qlwrpYymv2N2tSU9gQRyEy5TIw6vq/1LAfi9Q6jnZd7nMb0m292Schjhs3n13ivEqxHfrD8t3/Frqzz4pKUkkue+sOb1yrq/TundOyqcWnqVeQDbZQbrK7KGetvr/TVNd6jHe1rm+z0/8Ikp6GCOUl55hu6rhVM6jh/WKzLc/p9/UGU5VFtld1NQbufc23Ld9nv9RzNnortuiX5mT0fCys5zHZWRSy601tLGA2D1QxrC6z+ba7Z9L3pKbqz/AP1o/wD0E/78tBZ//rRf+gn/AH5JT//V9OWGMivo3WskZZ9PC6q9ttGS76Dbw0V2Y9zv8Hv276luKNtVV1bqrmNtreIcx4DmkebXJKRZGdhYtByMjIrqpaJLy4RHl+8qHQ23ZN+Z1m2s0tzixuLW8Q/0Khtrssb+a65zt6LT9Xeg0Wi6rApbY3Vp2zB/ktdua1aPKSnnfrLh9Rv6t0y7Cqy7G1U5LbX4VldLml5x/TbZblfotj9n0PpoHS8HqrPrJiZGVTntorxshhszbqb2hz3Y+1tf2T+ae/Y7+c+mupSSU+f5/wBWupZWZj0jBrwq8nNbktrrpZknHZXvNjsnP2041ePY/wBO39mfrPq/zXrrUq6H1Fn1b6jhOxGVdRFjMhl1ZrNeS+lzMqn7OyllLsen9D6FeNdV+g/0l388usSSU879ZKb+ou6O9uDk5eK+yy3JxK3Gh4DqXGn7Q/1cdtXpW/mPu/nFT6Z0k4f1g6bZhdGyelYwGQMlz7hbUQWfomOZVkZLK/0vu97K11ySSnh7fqtn9U6rm2Omql1uS77VkYtbd7rq24fo0V+s7Itprobupy7fSZ6n+lWn9VOn9S6fX1BllJpyHNqNdFlbKqDYyr0K3VZeJZe26u1tVf2h3osuq/0K6VJJTXwMm7Kwqsi7Hfh3vafUxrYLmPBLHN3M9tle4fo7G/zlS4un6u53Tn9Ud9jcLbLajgu6XWWltjaPbay3Nve2vF9R/pWekz1PW/4P9Gu8SSU8z0zo93Uej/Y88ZePdZbj2dTfmNrc7I9IMfbjV21O/orrK/T3u/wf6NSxMN3W8nr1mZh34eH1KmjFrGS0MsJqbe226usOftax11bqbF0iSSnl+v8ATeom3Ge7HPVsZmA/CzQPbbc6x+PsFLans9C266lttuS9/oYlPqWKv0rpXU+nddxm5VGTn2imoDPtvstx6t1bm9Xex1tntvuvZS2pjqfez+2uwSSU+fv6R1PHzskYODfRU7qb3WHHqfWx+Ft+h+hy8Vl9O938yyln/Gq7T0fLd9Ucvp9eE6jK6jmuq0q9NzKTkD08q/c61/pY+K3dX6luR+YuzSSU8Z1vpHVabesWsyepZbcvEqx6PRZS82WOGU307Q2hmyirez1LP0Pp+t/OLYuxMq3K6FhOqIqwB9ryrR9Br6qTiUY7bPzrH25D3/8AFUrbSSUs4EtO3nskx4e3c3jv5H90p0N9LXO3tLq3nlzTE/1klMyWtaXOMNGpPgufyKc3IyLL/s9gFjpaC3Xbw1boobuDnudYRqNx0H9ke1ESU53ScG3H3XXDa942tZ3A5JctFJJJS45C4vof1m6D0W3rGF1XMZh5P7VzLRVaHB2yx4fVZo13ssZ7mLs08/6wkp4vD6x0TI+tmEfq/mfbbeo23v6s7WxwpZS441XqWs3Y2JTkbPTqrd/OLs08pklKXP8A1nczD6h0TrF4IwsDIuGXaAXCtl9FmO220M3ObV6pZvs/MXQJAkcJKfMWu6bj9L+y43VMfqOYOjZHSMfFxQ99tl+RZvrdUwN/m/T2scvSMKg4+Hj47tDTVXWR5ta1n8EaY4gfAQkkpSz/AP1ov/QT/vy0Fm7nf85duw7fsc75ETu+jt+kkp//1vTkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSkkoCUBJSln/8ArRf+gn/floQFn/8ArRf+gn/fklP/1/TkkljZdmZ1TqVvTMW52Lh4gb9uyK9LHveNzcWh/wDg/Z7rbElO1tPgmWOfqtgMbuxLsnEyB9HIbc9zgfF7LXOZaj9GzsrIGRiZwaM/BeK73MENsa4bqcmtv5vqs/6aSnRSWN1v60YPRcmrHyWuc64sjaf3y5u1rRuc63azfXX/AIVNg/Wrp+X1D9nuDqbnubXj6PeLLPSblZNQtqrdjtdh7/Tu/TJKdpJYTvrZXYyu3p/Ts3Opfb6b7a6HhoYC6u3IqdH6b0ns/m/8Ijn6y4AwM/M2XVnpg/T491bqrNzmiyitrH/9yNzGVpKdZJZuZ1puE7Dpyaw3IyW+pkMDxtoY1o9W11kfpP1h9OHR/wByb7lWwfrTTmdTt6cMHMqdUamix9DwJuDn/pwW/q7WbfzvppKdtJYB+uXTR1R3TXNIdXu32bhta1rzW+5x+h6Hps9Tfv8Ap/oNinh/XDpWR0y7Pt9Sl2LXXbl4/p2F9Yu/ozPfVV6r7vzPTSU7iSxf+ctn2o1npPUfs/p7hf8AZ3z6k7XUejG76H6T1Urvrb0it3SoslnWHRQ5wc0taWuc17mbHfStayj0/wDhElO0kqFHUeoWZDKrOk5NDHO2uve+gsaP33Cu99m3+oxZtn1wqbm34gxjFL8isW2WtrYXYobZc619jfToq2v9tnqJKehSWJ0n6x2dXwcnKxMP9JQzdXjuuYbHP9+ym6uvdZiut2fovVb72P3rT6fm0dQwcfOxzNOTW21k8gOE7Xfy2fQekpsJJJJKUkkkkpSSSUJKUklHy81j29YzKbX1Prr3VktPP96SnYSVXAz25jXe3Zaz6TeRB/OarSSlJJASQFy2E/60dav6jfjdYr6dj4udfh1Y4xGXe2h3piw3WWMfus+k5JT1KS5u23rnQsjHy+sdapzOm2PNN1RxRTaXvEYwwm4xvtyb3XfTq/0PqWLpCIMJKUkksTrnXepdP6jidNwcCvKs6hXa7Hvtv9JjX0jfa21npWO2MY5j/p/pElO2kuIxOt/XPDH2zMswep4v2azKdQy+utzq6xvfk4D6KnOfRX9H9Nv/ADF1nSM23qHS8XPuo+yvyq23CgP9Ta1431/pNte5zq9v5iSm2s//ANaL/wBBP+/LQWf/AOtF/wCgn/fklP8A/9D05Y3T7GYfXuoYN52vznty8Rx0Fg2tquraf9JU5n0Fsqtn9Owuo0inMr9RrTuY4Etex379VjffW9JTZOgJdoBqSdAB5rH6LY3N6j1LqtX9FuNePjv7WCgEWXN/keo7YxOfqziWAMysrMyqB/2nuuJrI/dft2usb/WWrXXXWxtdbQytgDWMaIAA4a1oSU8b9drLG9VxvTY3NfXUba8NlduRc21m70LPQx76XYeK9z/Uv6hX+m/Q+j+kS+r78Svr9Tmbv2ZZ6ren5AbY6qzqFra/2o1uTc59z2/oHvouyP5+77Zsus9JdoNONJ5KUlJTwX1kws12Xj4eQOk/tDqVtVLRTVaMj0XOLbLa3Osd6bK62u9S+v8Amv5D1qZnSuoYX1TysN1WL6eEKbcajDbYJrxrGZVtdrsqy2y57mVO2e5dSkkp5f6wNw8zrHSsxjWZDWs9assxLsix7bDtpsZkY7PSrqqba+6uvIs2et+sf4NZWPi2dK+sNeBmZguOPZ0/7HSzHLbMgMquwmPa91z2Nrxm2Psy7P8Ag/8ABLvRoIGg8BoE8mInRJT5zk2Yh61m/tHZkYLrH0ZefW3IdW3Gh9j6b+pY979+bbe5mM3pj6X0VV1+lX/o1t/Vk9Qdg5+O9jKuul1T3VZtTgz7OGso6fY9lb/0m/Ho/S+nZ+hzfVru9NdUNBA0Hh2SkpKfPG9Gs6j184FFfSX/ALPZ9pvFFdzaxkC3ZXXmOrt9V9jNr7GYzrPT/PsqWx1/D6jZl9EvzLofS64eh09u3dd6ORdZ6Tcj1dzb6avsnpWf6e9dWlp93CSng+jdPx3dWN2Pg4978N+CzNqo3FlFrxf9p+y/pHNZbg2/ZXZf0/UqrVTJysZvVc67IFlePYLzW8srDr3et6W7Df6DGX+vXUxmR+sY/o7PVs9Rejp9zvFJTyv1Y6nj1/tLLzMmuy2ipluTbTZ6tXpMdk37m2O22epX6r6X1vZ/g6/R/nPZp/VTFuxfq7gU3tLLTWbHMPLfVc/IFZ/4ttuxazvcIdqPA6hJJSkkkklKSSSSUs+dpI1I1hOCHAOGoOoKSGantJNL9gOpY4S2fL91JSRc1mXsvy7bmGWOd7T4ge3d/aXQela/S54LO7GCAf6zvpIsDwH3BJTldExrGufkPBaxzdrJ0nWS7+qtVJJJS7fpD4rnPqhdSyvrAfY1p/bGdo5wB/nB+8uiWXl/Vb6t5uQ/Ky+mY12RaZstfWC5x/ecf3klOXfWzG+t3Tcm+5nUbuo25FOMXRGHSyl2Rsw62Ocz7Rc9n61mP/S2Vfo/0a6hZ2B9XOgdOyPtOB0/HxrwC0W11gOAP0od/KWikpSwPrCa6uv/AFbybnBlIycihz3GGzfjWsqZuP51ljPYt9CysTFzcd+NmUsycez6dVrQ9hjUSx37qSngLeldU6T9Xzf1KttDMP6vZWA5xsYYybrG+hSNrjudZU1m3/MXedOpNHTsShw2mqiphB5BaxrYVLH+qn1Zxb2ZFHS8Zl1ZDq3+mCWuH0Xt3bve395aqSlLOkf849s+77HMd43crRWb6bP+c3qbff8AY9u7y3TCSn//0fTkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSkkoShJSln/APrRf+gn/floQs3eP+cvp+6fsczGn0v3klP/0vTkkllZ+fnW537K6VsbkMaLMvKtG5lDHfzbW1/4XIt/NYkp1UlkO6X12oGzG6w+64a+nk1sNLv5J9INsr/sq10nqX7QoebKzj5eO805WOTOywfuu/Orsb763JKbqSpZ/WOn9PsZVlWFjrNu32mIcSzfv+j7HD9J+emxut9NysqzEquAvrFZNb4Y4+swZFQZW8tt3ek5u9uxJTeSWRlfWz6uYrq22dQocbLhjkVva7Y525u66Hfo6mOZsss/wav4fUMDPY5+Dk1ZTGHa91Lw8NdG7a7Z9F21JTYSVe/OxqMrGw7HH7RmF4orAJJFbfUtsdH0Kq/9I7/CWV1/nodHV8C/pZ6s2wjDYx77HOaQ5gqLm3ssq+m22p7HsfWkpuJLJ/50dGFYsL7mtLnM91FrSHMO2ytwexvvYiZf1h6Vh4NOfk2Oqx8iwVVl7HNcSZ9/pOHqekxrfUfZ/ov0iSnSSWP/AM7vq99qbi/av0j2Me32Pg7yWMZ9Df6nt3ubs/m/0i0Oo5+N03DtzcsltNAl5Gp1O32j5pKbCSzcr6xdHxL3499zxZXG4NoueNRuG2yql9b/AOy5PnfWDpeBktxsqxzbHNY/RjnANtf6FLnuA9u+32JKdFJUMTreDl5b8Klt/r1HbcH0Wsaw7fUa222xja2epX76/wB9DzvrL0Xp+e3p+XktryCz1HCCQwH+bbc5v83Zd/ga/pvSU6aSz7Ov9Gr6ceqHKa/BDix19bX2AOB2uDm0se9u38/2of8Azm6G280X5bMV4pryCMk+gQy0vFYczI9Oxlv6Pe6tzPYxJTqJJmPY9jXscHscA5rmmQQdWua4fmuTpKUkkkSAJJgDUk8QkpSSyLfrLiMeW1VPuaPzwQ0H+ruWF9YPr9ZjN+y9Op2ZTgC+22HhjTxtY36Vjv5aNFnx8rmnIRjHfv8Ate0SXk1P1z+s9Vwt+3Ot1k12Na5h8tga3/or0P6tfWCnr2Ab2t9LIqOzJpmQ10S1zD/o7PzEiKX8xyOXBHilUo94/o+brJJASYXOU9b+svUL809KwcN2Lh5VuGHZORYyxzqDsss2V0WM2Pd9D3oNV6NJYdPVPrFjZVI61hYmPgW7hbl0ZDnCkhu6t2R9oqoZ6dz/ANCz3/zi3ElKSSWR1j6yY/SsurCdiZeVlZNbrMavGq3izZ/OVtsn6df+E/0e9JTrpLkcX65dcre6zq/QMnHw2sc+22mux5pLfdFotbW26vbu3XUrpem59PUsCjPoZYynJbvrba3Y/afoPLNfbY331/yElNlZvqs/5zelPv8Ase7b5boWks//ANaL/wBBP+/JKf/T9OWP0givrPWcazS99zMho7upcxrGOb/JY9uxbCo9S6TTnuqvbY/FzcefQy6o3tB+lW4H220u/wBG9JTeWR0pzbuu9XyaTNANNBcOHW1tPqx/U3bEndM6/eDTk9WDaDo449ArtcPD1XOf6f8AYatHCwsbBxmYuKwV01iGt5Mn6TnO/Oe785ySnj/r4zFb1DFvzqR9nqZ61VxsrpDradxGKx9lN32nKt3/AKHHv/V/S9ZLoOFhVfWwYljmWX4QuzGW2embXX5LKW3Yf6Fvpb+nN3/o6P5mi/GXXP6fgPyjmWY9b8ks9L1ntDnbNf0Y3Ttb7lGnpXTKK6aqMSmuvGebcdrWACux27fbX+5Y/wBR+5JTyHVR1jBqrbh9Py+nNvuZi49dWdV6LH3PP6RtDN26x73us/SP2ep/Oro/q9j5FFd32rDtx73Fgsyci6u+3I2t2Ntudi+xr62fo/oq7d0zp2Rm059+NXbl4w20XvEuYP8Ag5+j9JWUlPKZ2Jm0/WxjendRjM6jW+zLddUy92Ni1D9BXTLq/s+JbkuZWyrb+nu9a/1LPTVfFx7R0r6z5lmQbbGuyca1tbW10XPqZWftrsdu9teZ7vs99lVn6b0/0tfqrqXdK6a9uUx+LU5ueZzAWg+qQNrfW/f2t+ipM6fgV4R6fVj114RaWHHY0Nr2u+m3YyPp/nJKeCvutqtFrabS3H3UY9r6rXX3MybrcjGc/GdWdn2nKr9Kux9db/8AC/za0LK7T0To2JXj111/andMyMoEl1VJv+zW0UNu/Tb8/wBJrcj9yj1a1178LEsc976WOdaGNsJGpFZc6j/tlz3emoVdN6fTi0YlWOxmNiua+ioD2sew+oyxv8vf79ySnlms63+1ukn1asG3IzMoXVOwywl9VD63W7vtVnr120Us9C2p/wDN+l/ovSV//GDjWZX1Yyaa2Mc0uY6yx4D/AE2tcHb6q3/Tuf8AzLf+MW0OmdPHUD1P7PX9vLPT+0kS/Zxta4/R/souTjUZVD8fJrFtNgh9btQRM6pKeHyr+oV2s9DJy66Q7Mr+z4tgqqt+xVMdZ+zq302uxKK7xfTRVvv9T0v5zYp9avxMn6w9OppcMp19WIWj3m6ykG3MruewPq9VzLKPVu2eh+jt/SLsf2fg/bz1L0GHOLPS+0ke/Zz6bXfmtTZPTOm5YeMrEpvFrmvs9RjXS5o9Ot/uH02M9jUlPJ/VvHDOsY17KgwO9c15YYWsyvZ+surt2+pZ+s/pWfbG1epV+kxfUqVOjH6qeoZNWI7G6Jj473UZ2KzOeMjJfYK8r1r8y6m3a/8AS/02mv7b/gPX9L012mP0To2LeMnGwqab2/RtYwBwkbfpJWdF6NbdZkXYOPbde7dbbZUx7nEAMBc6xrvzGpKeW6tecv6h5eLg4oxBjW2YYGJc401spcfXyjewUPvpfXv/AEdjP099n6RCymZdb/s7cYepkHNrspLrn/an1YjG3OffkP8AtVtb7A+nCf8A4JdjX0vp1WJZg1Y1deJcXGzHa0Bji/Wz2N/fUv2fg/bv2h6DPtor9EZBHvFfPptd+a1JTPEdS/EofQw10uqYaq3DaWsLRsY5h+hsb7dqKkkkpSr9Qqsuwciqr+cfWQ0Duf3f7SsJJJBog9tXhR/qPBcl1hr29TyN35zg5vwI0Xr93T8C95stx63vPLiNT8YWP136ndM6nUDSwY17Po2V8/1XNd7XtThJ0eX5/HCdyiQCKPXhfLF23+LGu31+oXa+jtrZPYvBc/8A6hBq/wAW+abQLsoCqdSxnuj+07au36T0rF6ThsxMVu1jdSeSSfpOc785yRIpl57nsU8Rx4zxmdWaIEQPV+k3W/SHxXP/AFNBNPWI/wDLjO/8+Bb6wrvqdgPyb8ijN6hhfarHX204mU+qo2P1ttFTPz7Xe56a5DW6lRbf9ZMCnrgFuFkX2M6Xg1mag6mp2T9v6lvj7RkP2enj4+30Mb+XYumWLg/VXCw8+nPfl52bfjbvs4zMl97GOe30n2Vsf/hPTdsW0kpSwOusn6y/Vtx0Drcykn/jMV7v+qqW+qnU+lYfVMdtGUHj03i2m2p5rtqsbo26i6v312e5JTwOOM2vodb8iq2oYv1XzWONjXNAtNjamMdvH85sq3bf3F33Sa/R6Tg1f6PGpZ91bAs131Rw7YZmZ/Uc7HkF+LkZLnUvjUNurY2v1Gbv8H9BbqSlLP8A/Wi/9BP+/LQWbP8A2TRt/wC0f05E/S+jt+kkp//U9OSS1S1SUpJLVLVJSkktUtUlKSS1S1SUpJLVLVJSkktUtUlKSS1S1SUpJLVLVJSkktUtUlKSS1S1SUpJLVLVJSkktUtUlKSS1S1SUpJLVLVJSkktUtUlKSS1S1SUpJLVLVJSkktUtUlKWdtb/wA5N0Dd9kjdGsbvFaOqz/8A1ov/AEE/78kp/9X05JJZ3Ueqvx768HCo+2dRubvbTO1jGcevk2/4Ov8Ad/fSU6KSyH2fWuhvrOqw8to1dj0l9b48KrbPY9/9ZXundQx+o4rcrHkNJLXseIex7dH1Wt/NexJTZSSQ8e+jJpryMd7bqbQHV2MMtc08OaUlJElTd1no7HFr8/Fa5pILTdWCCNHA+9FpzsHIZ6lGTVdWHBhfXY1zd7tG17mu273bvoJKTpKuzqGDYKzXkVvFzg2ra4HcXB72NaP5TKrHf9bVmDBMccpKWSQ8e+jKx68rGsbdj2tDq7WGWuafzmuU3Oaxpe8hrGguc4mAANXOcUlLpIVOXi5BLaLmWkMZaQxwd+jsk026f4O3a703odnUum1WPqty6K7Ko9RjrWNc2fo+o1ztzN38pJTZSQMfOwcp5rxcmm94G4sqsa8gcbi1jne1Qb1Xpj6G5Lcul1D3NY2wPbtLnO9FjOfz7v0aSm0kma9j27mOD28S0gjTzCAeo9Pa1znZNTGtsdS4ue1oFjP5yr3lvvYkpsJIH2/A9A5P2mn7O07XXeo3YHfuGzds3aoT+s9HYAX5+K0OG5pddWAWzt3CX/R3NSU3EkK/LxcfGdl33MqxmN3uvc4BgaeHb/o+5FBBEgg/ApKUkkkkpSSpXda6ZTYa33S5ujtjS4A+G5qyOufXjpvTag3Ga7Ky36sqILGgf6S15/N/qo0WWGDLMgRgSTto9IkvOaf8ZXWG3brsbHsqnWtu5jo/k2bn/wDSau46P1jD6zgtzcQnaTtsrd9Jjx9KuxIghfn5TNhAlOPpP6QPEG8kksF31ua7IyacLpPUM9mJc/GsyMdlZrNtelzGerfW/wDRu9v0EGu7ySxcP6zjIz8fAyel53T7MveKLMljAxzq2+s+vdTbdsd6bXfTW0kpSSSzup/WHofSX+n1LNqxrdnqip7oe5mvurr+k/6P5qSnRSXLYP8AjJ+rWVe2q578FtjS6u7J9MMMfmWOqst9Cz+RcujxMzFzsZmVh2syMeyfTtrMtdB2u2u/rBJSZZu8f85dms/Y5mDH0v3lpLM3n/nT6cHb9h3T2nfH/UpKf//W9OWR0eD1jrbrP6QL6268ikVt9GP5H01rrN6h0vIfls6l021uPnsb6bxYJqurBkVZDW+72/4O1qSnSWR0wNH1g6y2qPSPoOsjj1iw+p/a2bNyY3fWu4ek3GxMNx0OSbTaG/yq6NrNzv66u9M6bT03G9CtzrXvcbL736vssd9O2xJTS+sRuxsW7qP7Uvwaaa9voUspeLLD7ams9em6x19z3MqZWxZ/1LxM9nT8ai7qV4v6X+rZvTCyjax7R7WOeyn1/ScxzLqbfW/S/wCkW5f0nCyOo09RyGutuxmxjse4mqt2v6xXj/zf2n3bfX+mlb0nBs6lV1TY5mbS0s9Wtzmb6z/gMprDsyamu99bLv5t/wDNpKeJss6Xi5+Z9jyMZuC57fsza78EhrWt/TusHUqr8ht1mU657/crr34NX1Z6nkOqtx3tymV51zdjnE1ip1V7K+mtrxrGsqspbVsZT6j/AOk2Lr8zFGVjvoFlmMXx+mxyK7Gwd3ss2u+kqVf1fw6umv6dVbkMrusNuRcLP01rnmbvXvcHbvW+hZs2fo/oJKeFPT7Mbp1TbLb7RVZZiNY71ab29TyamtwbXvD9v6Ftn2f9Ba+j9LvXRde6X00ZONVYMizJox68dj34mRnVbGlzt/6At/W7H/ztj7n2en+YtBv1M+rjG+kMX9WFbq2Yhe91LDZpbkVVOcfSynt9v2hn6RaN2D6nThgDJyKg1rWDJZZ+seyPc7IeH77Hx+ke76aSnjfqmzp2RmYucMZ2OPVcen/ZsK6gFsWUvdnXNN2D6Fvueymt/wCi/wAJb6v6NdB9cKcy3pNj67Kx0/Ha+/qWO9zq3ZFVY9T7IMpjbfs9Nm39P+i9S/8AmPUqr9RauBhY/TsKnBxQW4+O0MYCS4x+89zvpPc73OQuo9KxOpeg3M32U0PFv2fcRVY4Q6v7VUP59lT2+pXU/wDR+okp551nWbur9Gudj43TcvJof6LqnvtaaK215FvT87HdTj+z9J+gspt/Vb/+DsesvqltLepdTqBBycm+2moV7BYw78Ox1tzrSzbRdTX6LLN/5llda7Kjo2LV1J/U3WX35Lt4r9e11jKW2FptZiUu9lDbNjEx6F08vfZtdvtym5r3SJNjNvpsJj+Ybt/mklPMdCysO09TrLbW034kt+wubY4Bh9O6vGdS++9uVb9pqr/sfyFmtx8W3phsb0rG9S7Ev6gzHFnosY1rX4oqbR9nf9od01tdbv6R/TL/ALX+h9Vdz07oeD03J+0Y28FtRorrc4FjGOsdk2emxrW+625+97n70O36vYdnR6+jstvoxa9zZqeG2OY/d6tNlu0/orPU9+1JSvq8SzoGK+zGZiTSLHVUHcCC31DaPTrq/TXfzn0P5xcXg2ZOPgZ/rU1+qcrOyqas9js7d6DHX2Nf6ttX2B9LPToyL/032i7IrXo1VVdNTKamhldTQxjRwGtG1jf81ZeT9Wem5FGRQXW1fa7LH321PDbC253q5OL6u3c3EvcP0lSSnDyAbfq9ZV+h9SrOpmnGpbhhlzfTyGt2Pue2/wBf9F6VjbKfVrsrWPNX2Dp9wvdUHtutwHiysbbrftVt11uz1Ws2vuw8P9bf6FXr+n6nqrus3oHR86o05OMx9TnMdawaCw1sNFPrR7rPRrd+j/63+4qY+qHT2tc2vKzWNfWKSBkE/omgtZj7rGve6ljXfzb0lOR9bX32X42GMTKzsw1Ndg4fp1fYDexptufki1zPtbvTbs9P3/Z2f0b08j9Ii/VfFxcXrDn5eFZidWysdwr20Nx8VlNZrN9GIxlttllnq2VvuyMn9Pd/wVf6Jb2Z0PBzRhi91w/Z8+ia7XVuO5n2d3qW07LXfo/3XqOL9X+n4mczPpdkOurrfS0W32XN22Fjn+3IdZtd+ib9BJTpKv1E2twMg0z6grdtjnzj+yrCSSQaIPYvCiIEcdlyXWC49UyN3ZwDf6oA2r1a3oHTbbC/a+vdqWsdDZ8m67VgfWL6iVZTftHTnuZkNEOFh3NcPA7Rub/WTgQ6nLc7hjP1WOIVZHyvni7X/Fi+37R1Fgn0dlbj4b5cP+oWPV9SfrBZaK3V11idXl8gf2W+5eg/VzoNPQ8D7Ow77bDvusPLnf8AkW/mNRJFMnP83hOE44SE5Tr5fVw0eK3Wb9IfFc99Tv5rrH/p4zv/AD4F0A0MrnGfVrrmJkZb+k9bbiY2Zk2ZZosw2Xltlx33fpnXV7mb/oexMcZbqV2R1LrNHSsx1nTumWXOrprbLbs6ylv2qz9NV/ROn1tZ+/62UukJkysDG+r3V3dVwuo9W6u3PHTzY/HprxW4/vtYcdzrLG227mtre72LfSUpYHXGn/nR9W3jQudnU7vDfjGwf9Klb6z+sdJPUmY76ch2Hm4Vvr4mU1ofseWuqe2yl/ttqtqsex7PYkp4ynrufl9FZ62Q6zf9Ws3KyAY99zXNxmWv/l/zjV2/RavR6L0+mI9PFoZHwrYFkXfVfqmVQ7Ey+qUDDtYab2YuEyix1Ljutxq7zdd6Ndv5/wCjXRNa1jWsYNrGgNa0dgBDQkpdZ8n/AJxR2+yfjuWgs/8A9aL/ANBP+/JKf//X9OSS+SXySUpJL5JfJJSkkvkl8klKSS+SXySUpJL5JfJJSkkvkl8klKSS+SXySUpJL5JfJJSkkvkl8klKSS+SXySUpJL5JfJJSkkvkl8klKSS+SXySUpJL5JfJJSkkvkl8klKSS+SXySUpJL5JfJJSkkvkl8klKWf/wCtF/6Cf9+Wh8ln/wDrRf8AoJ/35JT/AP/Q9OSSVPqPVcXpwrFofbfedtGNUN9thH7jP3W/nPckpuJLId1zPob62X0fJqxxq+xjmWuaP3n01nf/AJi08bJoyqGZONYLaLRuZY3ghJSRJIAngJ4PgkpZJJJJSkkoTwR2SUskn2nwKaDMd0lKSSSSUpJPtd4FMkpSSSSSlJJJJKUkkkkpSSSSSlJIdmRj1O222sY791zgD9yz+rfWXpHSccXZF4sc7Sumoh73Efutn2/13pLo45yIEYmRO1B1ElxlP+M3BdaG3YN1dRP8417XkDx9P2/9Uutw83FzsavLxLBdRaJY9v5P5Lm/nNRor8vL5cVHJAxvr0/5qZJJYmZ9dPqtg5VuHldQazIodstYGWP2uHLC+qt7NzfzvcgxO2ksjpv1t+rnVMtuFgZzbsl4LmVFljC4NG5+z1q62v2t/NWukpSSSZzmsY6x7gxjAXPc4gAAauc4n81qSl0lmdO+svQuqZAxcHMbde5pexm17N7R9J9LrmVtua3/AIJafCSlLP8A/Wi/9BP+/LQWf/60X/oJ/wB+SU//0fTlj9Ka2/rfVsy3W6ixmJTP5lTWNthn7vqvfucthZObiZ+J1B3VemMGR6zWszcInabNn83fQ8+312N9vv8A5xJTrAxqFj9IY3F6z1bBpG3GBqyWMH0WPuB9ZrR+bv2+ond1zNsbsxOkZbsk6AXhtVbT42Xbnez+orHSOnW4VVtmTYLs7Lf62Xa3RpdG1tdQ/wBFSz2MSU1es14l3V+l4t7bic03VB9WTdQGCqt2VJqx31tudZt9P3rK+pprtNVltXUrchlmU37Zdba/FcK7bqWN/SXuY/8ARNbWz9B9Ni2OqdEys/qGNn09Rsw3YQccetlVT2h72upuscbmlz/Uqdt2IfSeg5/SxVTX1a23Dqe97sZ9NI3Gxz7rAbmM9Vv6a1z/AGJKcfq7MrH6g+rrfW8mvFxmt6j0y5lWO1zn07mZGM0/Z/02TX6lfpY//aijI/wq6D6v09ZZ06qzrOU/Jzb2NssrcytgpJEmhnoV17ts/pN+/wDSKrZ9Vq857reuZl3UrGknGa39Xqx9S6u3EpxzublV/wDcu2227/R+mr/TMLNwq7KsnPs6gzcPs77mMbaxgG307baQxuS7d/hfSY9JTylmbkWfWfrtRe7Jwy3GoDXOOJUHNc4en+06Gvuqsott9Gr/ALm2erV/gla6Vf8AaOl9TfRivw3PxLGknNuy7WOm7Hq3Yzg51O59d1jbanf4NbGV0Bt+dfmttDbH/Z7MdhaSxl2N6/pW3sY+v7Qz1Mr1fS3V/pK0Cj6qUU9MOJ9oecmzFsw7ssCA9l1jsm79XDtrffZc2j3/AKD1UlPI14xswLGW2PopNzd1oZY01HHrqoyG0us+xUstycr2Xfzn6L/hV0GVZkf82MHpdI+y5fVcsYX6DezYwXWPzb6hZZbdW37HjXP/AJ32eoiN+pTqyWVZzX0eq670snHbcXvdo12Y/wBWn7a6j/tP67P0a0sHoFeO7p9uRe7Iu6Wy5mMQNlYF+1v8251z91FLfs9H6b+aSU879Z8twz+qVW5b201jHqrwjdZVW+qyqx+UytmP/OXu/wCE/nP5tF+p/VK7H5F1WTZmVUUTh4bLrrLrK2EMfb9hzTtp/wAHXj/p/U/0i6BvS8ug9QuxMtteb1G4W+vZVvbWxjGUVUei2yr1PTrr+nv+m9E6X0fE6bRjMrHqX4uMzEGS76bq2HfH7rN1nv8Aakp8/wAamqzEc1rW5Li61hyrm2ttLw97Hmxn7Ur/AElT/Z9D/B+xegdDc89Ixa7G3NfRU2lzshobZYa2iv7RtbZd7L49Rn6VUesfVr7e5wxH4mFXZW5l27Crusc53NzLnPr2P2n85li18PFqw8SjDpn0saplNc6nbW0Vt3f5qSkqSSSSlJJJJKUkkkkpSBn3Px8K+6v6dbCW/Hx/so6ZzWuaWuEtcIcDwQeySQQCCddXhj7iXOO5x1c46knxJXJ9asdZ1O7dxWQxg8AAvS7fqxWXk05BYw8Mc3dHlukLmfrN9Sc1rvtuE4ZLiItqA2kx+cyT9JOBDrctzWETFyqxWulPGruP8WWVbvz8MmaQGXNHYPJNb4/rtauTq6L1i2z0mYV2/j3N2gfFzvavSPqf9XndFwXm4h2XkkOuI4AH0K2/yWIyIpl+I58XsGHEJSnXCAeLr8z0DfpD4rn/AKmkinrEaf5Yzv8Az4FvgwZXM4eF9aukXZ9eDi4WXjZebfmV225D6ngXuFnpPqbRZ/N/10xw2XUc+3q/WKem9NdXjuwrnNu6rcxpeyxrd1+D0iu4fpsx9H9Lv/mcXG/feukPPgudZhfWXqHW+l53U8fExKOlvus/QXPue821Oxm17X007Pp7925dEkpS5X60dK6fm/WjoBysavIGQMuixtglrgyk5OO2xv53pvY/auqWV13publPwM7pzqxndMvN1Vd5Irsa9j8a+l9lYe+lzqrP0duyz3/4NJTxAZ0fM6Sy13S8NjrehZXU3vZWQa7qi2mv7NL3ejXuc72Lu/q5iVYf1f6bjVMDGsxqiWj95zG2Wv8A6z7HOeufs+rnVbsN/T8bpWF0ttuK7p/2v7XbkGrFtf6uRXXj+jV61jne6vfaz3rr6q2VVMqZOytrWNnmGjaElMln/wDrRf8AoJ/35aCzf0n/ADl/N9P7H57p3f5u1JT/AP/S9OSSlKUlKkpJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJJSlKSlJfFKUpSUxFdYMhgB8YUkpSlJSkkpSlJSkkpSlJSkkpSlJSkkpSlJSlmzZ/wA5Y2j0/sf0p1ndxtWlKz//AFov/QT/AL8kp//T9OSSQcvNxMGg5GZa2ilpje88k/mtH0nu/qpKTJLIb9aujFwFj7aGOMNtupsrrP8A117drVrgggOBBBEgjUEHuElKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJPB8FXzc7D6fjuyc21uPS3QveY1/db+c938lqSQCTQFk9AnSXP0/Xz6s23Cr7Q+uTAssrc1n+f+b/aW+1zXtD2ODmOALXNMgg8Oa4JUuniyY644She3EOFdJJBszMOp5rtyKq3jlj3taRP8lzkliZJCqysW5xZTfXa4CS1j2uMeMNJRUlKSSSSUpJDqyca5zmU3V2ur+m1j2uLf64aTtRElKWf/AOtF/wCgn/floLN9Nn/OX1I9/wBj27vLckp//9T05YuJSzqPXs3LyR6jOmPbjYdTtWscWi2/I2/6V+7buW0sXJN/RupX9QbU+/pudtOWKhufTawbPtHpj3WU2s/nNn0ElOzY1trHV2tFlbxDmOEtIPZzSsjobPsWbn9Ha4uxsU13YgOuyu4F3oSfza7G+xSf9auhhn6C85dp0Zj0Mc6xx/d2bfZ/bU+i4mWw5PUM9oZmZ7w99QMiqtg2UY+785zW/wA5/LSU1+oZnVszqz+jdIvZg/ZqGZGZnPrFzm+q57MfHx6HuZXvf6Nj7bbFSzeode6Tn9Gx8q13VH3vy2PrxKm1OvDa2OxvVrsf6VTqnl/qW+rXQxaXUujZd2ezqvSssYOe2v0LfUr9am6oE2V15FO6p++mx36G+qz1P5ytRo6L1F2b0/P6j1BuXkYDslx2UippbkMbU2mtrXv2Mx9n07PVstSUs36yttwRlY3T8m60XPxsjHmmt1FtR2215F2RfVjf8T6dtnrITfrhiZAx29OwsvPtysd2UyqptbS1jHnHuZeb7amstqta+v8A4R/82gZP1QssuGRXkUWvGbk5goy6PWxyMoVtLX0+rXuyMb0v1fI3fn2ez9IqeD9W+t9K6ni0dMya2txun21OzLscupc63Kdk+i2iq6r0LKW2epW1tmxJTeyPrXa/K6IemYluVh9VdaLHAMa8emyzdRtuuqdVkY11X6zvbs9Ouz099isD62YJyxUKLjhuyfsQ6iPT9E5G70fT9P1ftfo+v+r/AGr7P9n9b/txMz6suxsbpTMHK2ZHSrrLvWuZ6guOQLW5vq1sfVsdc7Issr2P/RKtjfU4Yme2zHdh/Y25JymmzDZZmDc/134rc579vo+q79Hd6P2qpns9T/CJKbbPrRQ/puf1MYl9WH0+u53r3bGttfQ6yqyqlrLH2O/SVbfU2LIzuqfWDCq6dT1LqJ6VXZiMtyeqOxW3sdmPP6TCvgehhVUtc3Y97avX/wBN+jWo/wCrTndCq6KcgOp+1/aMp5af0lRyX9RfjNG7273OZTvVvqWJ127I9XpvUasal9fp2Y2Rj+uyZP6xU9lmPb6m07fSs9SlJSDJ+sDcI42GK3dVz7MduTb9j9NjPSkV/amnJuZVtyLP6NQy62yxRd9asS1mMel42R1SzKoGWKscNaWUElnqXOyX01se6xllddG/1bLKrFRf9R6aWYX2J+PbZh4ow3DqOOMqt7Gudcy5jA+l2PdXZbd9B/pem/0v8GrY+r3UMN9OR0nOqoym4zMTK9XGaaLW1lz6bq8XGfjNxraX3XbGV/off+kSUtd1/MH1gZ0z7HdXhWYDsp98Vh7DLP0vuu3NbQ1/o2V+l6v2n/gv0iej6yVDFw6sHFzer3Pw6suzaKvVbTY39FbmPssoo+15G1+zHo99myxWcnouRd1HH6g3KAezEswstrq5FrLNtnq1bXt+z2+szf8A4Vip0fVvqXTmY7ukdQrovbh0YOWb6DbXYMZvp4+XVW26p9ORW19ns9Symz+wkpK763YdjqK+nYmT1K3Jx/tdbMdrBFYeabBc7Isp9K2uxjmem7/Cfo02Z105eL0V3SLC13WsmvY4gB7cdgdlZ8seHta9tNP2f/jLUTo/1ap6Tm05NN7rGU4ZxCx49znuudm3ZTrAdv6W2x/6PYh9F+rL+mZGBZZkjIq6ZiWY2O3ZtPqXWC3IyfpO27qq6qWt/rpKcJ/1n6tVjX9UHVsWy2vMtoq6C6pnq2NbkOxK8eqyuz7X9ofV+krf6L/5f6NdI/6w1VdVZ07JxL8Zt1px8fKsNRY+yHuZ+hruflV13Nqf6F1tGyxS6L0DD6UxzvTquynX33HK9JrbIvsff6fqe6z9G2z0vprMr+pr2dSqyvtFBrozjntsOPOXYXGwux8nO9X311+rtq/RfzbK60lMc/65ZLvq6/rXTOnZHpl9TaLbxUGua+xtVlvpev6np/4Kt3+ksrs/mf0i0czqOZZ1bpHT8ZrsY5Aty86t2xz2Y9LdnoWbTdX+ly76Gb6rP8H/ADiH/wA2f+xNn1c+0w6utjGZQZ+fW9uRXZ6Jd9H1GN31+orWF0vIr6rf1XNuZfkW41GKz02FjWNr325Ba177P6Rk2ept3exjGJKdJBy8j7Ni25EbvSaXAeJ7IyjbUy6p9VgllgLXDyKSRVi9r1eMtysq95sute5559xA/stb9Fcl17LuyOoPre9zq8f2MaSSAY979fznLvrfq3nNeRS5ltf5rnHaY/lNhcl9Z/qz1TCudnGr1KbNXmo7y0j85zQPop4IdjlMuH3AOKIsVHpq86u+/wAWvUrrKcrplji6vH220T+aHktsrH8neN64FoL3BjAXvOga0En/ADQvSvqH0G/pmJdl5bdmRlx7Dy1jfotd/LdO5KWzL8SlAcuYyrikRwDrd/N/ivVN5HxXIfVzofRepHrGT1HAx8y/9r5rPVvqbY/a14DGb3gu2M/MauvGhBXI9Mzs/oV3U8W/o3UMv1+o5OXVfiMrfU6u93qVQ999Tt+36fsTHBVlYfRsXreNjfVnpuMOt4hL7rKm+lRj1Wt9J7upPx9jrX217vsuF/Ob/wBP9BdaYnThcpj25Of9YOm24fR8zpWPRfkZXUbMhldTLXW0Ox2vd6Ntvr3us9P6bV1aSlLmPrPj35HXukYgzcvHxeoV5VWRTj2+mHGmr7TVHtdte/8ASMsXTrF+sONmfauldVxMd2W7pd9ll2NWWi11V1VmLYaPUcxj7K/U3+lv/SJKeKxendGpw6M/ppzcLJPRsnqVNteQ39E2nYz7K7Zj1/aK3vd7n2/6Nd/9X23N6FgfaLrMi9+PXZbdcdz3PsaLXy791rn+z+QuOHTH/YD0/pfTeqnJf023pFL8yuqmltd9nrWZGRdv9vpfyG/9bXe49Iox6qAZFNbawfHa0M/gkpIszdZ/zn27P0X2Kd8/nb/ox/VWms//ANaL/wBBP+/JKf/V9OS44SSSUsGtaS5rQ0nkgAFOkkkpSSSSSlJJJJKUkkkkpSSSSSlJJJJKUkkkkpSSSSSlJJJJKUkkkkpSSSSSlJnMa8bXCQnSSU1mdOwmP3sqaHeIABVkAAQNAkkkpSSSSSlJJJJKUkkkkpSSSSSlLP8A/Wi/9BP+/LQWf/60X/oJ/wB+SU//1vTkkkzi1rS5xDWtElxMADzJSUukqtPVel32+jRmUW28bG2NJ+WqtJKUkkkkpSSSSSlJJJJKUkkkkpSSSSSlJJJJKUkkkkpSSSSSlJJJJKUkkkkpSSUpJKUkkkkpSSSeD4JKWSTkEchMkpSSSSSlJJEEchJJSln/APrRf+gn/floLP8A/Wi/9BP+/JKf/9f05YdlA651bJoyiXdM6a5tf2aSG3Xub6j33x9OqlrvZWtxYf2hnRes5Jyz6eB1R7bask/QZeG+nZRc7/B+pt31vSU38jonSMmj0LcOn0/zdjAxzf5Vb2bXscq/RLcmq3L6TlWG+zAc30b3fSfRYN1Pqf8ACV/zau5HUcDFoORkZNVdLRJeXA/5sfS/sqj0Nl2RfmdYurNIzyxuNU8Q8UVDbU+xv5rrnH1ElK6l1fOZ1BvSekYrMrPNQvuffYaqKKnFzKn3WVstsfZfYxzaqKmf8IqWV9Zc/pmR05nW6KcCvJfksyHVvdeHeixj8Z+Jsay39Ysf/R3UeurPUMHq2L1Z3Wej11ZTr6GY+bg3WejvFRe/Gvx8jZa1l1frPZZXY307K1BuF1rN6p0nqWfTRjHCdlGymqw2Fjbq2VY49RzK/Uu3b/V9P2JKbD/rR0FmHRnHKLsfKc5lJrqtseXMn1WHHqqfkVvq2/pG2VexNk/Wv6u4tVN12a3Zk1evR6bX2l9fHqsZQyx+1v8AhP8AR/4RZVvROv49j7MebcezqOVlXYlGS7FfYy4V/ZLHZVTWvb6G231sbf8ApN/5/prP6Tidc6F1bDxaMSrOzaumXerV65raG2Zz7mOZlWVWert3/pfUYx//AFCSnoM/63dJwszpmOX+tX1WXV31h72hm1zqrW+lXY231bG+lt3fo/536Ctf84uiftH9mfa2/bN/o7Nrtnqxv+zfadv2X7Tt/wC0/ressrH6B1TpuJ0M44qy8jpmRfdlVB5pYRli8X/ZXObZ7MV+V+ire39LUxVMT6rdQxs1mLZQcrBZmHLGU7OtZVtNxzW7+l1j+mVP+h/gLX/pbElOtnfWvpzen593TbBl5OG0NFYa/Z61j/slFT7XNbX/AEl36Wtr/UV/IyOpYlOO2vDf1O0jbkPpdVSA5oG5+zJtq9tjt2xlayH9E6gz6n3dOAa7Prtty6WtMhzxlP6lj17tPda3ZWo/Wvp1nVcfEuxulvyMnJaGWZEsbdiUuHqWbKbraa3Zfu2U/wChu/SWfuJKTfVjrnWOq4eNdl9NextxtFmY19IrGx9jG/oRa7I/M9P+b+mqmX9ZOvY+ZmA41LcetwZTS71DewkH0PWNDLaXfaodl+12yimr7Lb+tIPS+hMxOr4s/V442A0NFNzn1vtovZuf61xoyLHXU3/6R7P0WR/xqa76vdXy8ix1Vba6xl9RcfWcGAi92OcW1jXVZG9rvTt9+xJTpdC+sNuRiZd/VrsSluBtbfZWbKwCR/PWjLZV6dWQ39JS3/rah/zj6g1x6vbiOZ9Wz7G2bXfagOf2rZjfzn2B/wDN+hs+1V1frdlX+jD9XMfIqx8+/qGFdYyqjEqFVtYNl9mHVttfRRad1m+/+j2WbN71JvS+pN6V0ht1ROWeqNz8xjDu9IXWX5NrXO/7r+s2qx6SnSzurGl/S8vFsryOnZt7ca17IfPrgtxMim5h27G5DfTs/wCN/kLUWR1jDffZ0vAxqdmO3LZk3vY0CuuvG/WWs9sbX5GR6TK/+urXOuqSlJJJJKUkkkkpR4STOBLTHPZJjw9u5vHceB/dKSl+dCsLJzOoY+RZSchx2OgHTUct7Lcc5rWlzjDRqSsDIozsjIsv+zWAWOloI1DeG/8ARSU6PS89+Tuquj1WDcHDTcOOP3lfWf0rAtx911w2veNrWdwOfctBJS41IXH9I6QOuXdVy87OzxZV1LKxq20ZVtNbaqXCumtlNTmsbtauwHIXFdF+s3QuiXdYwerZQw8o9Vy7hVYyyTXY8Pps9jHt2WM9zElNnJw8foHU8B2DmdQzM65zmt6U/Iff69bhsNln2h/p4lGK/wDT/anf8WurPPiuMwus9FyvrXhf838o5dufbfZ1d+17neiyl/2as231tdRi037PSprfs3rs0lKXPfWHL63X1jp3T+n5lWJT1KvIa6x1HrPrfQz1/Ur/AEtTXeox30f8H6f+EXQrA+s7m4mf0XrFzXfY+n5Fwy7GtL/Trvosx222NrDn+l6pZvf+Ykp5rDHUcKlnUcH6w3ZD7OnX9RZVk0PsZbTUG7nZLbcx7ar/AFHN2eku36JdmZHR8LJzntsysill1rq27Gg2D1Qxte5/821/p/S968+a7plHS/smJ1OrqWYOjZHSMfFxmWOssuyLN9b6xt/m/T2tfuXpGHQcfDx8c801MrMeLGtZ/BJSZZ//AK0X/oJ/35aCz/8A1ov/AEE/78kp/9D05RsrrtrdXaxtlbxDmOALSPNrlJJJTn0fV/oePaLqcChljdWu2TB/k7vorQ5SSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpDfS1zt4JY/u5hgn+t+8iJJKRihu4Oe51hHG8yB/Z+iiJJJKUkkkkpScOcNJTJJKXLnHQlMkkkpSQMcJJJKX3O8UySSSlLM9Vn/Of0Z9/2LdHlv2rTWf8A+tF/6Cf9+SU//9H06AlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSUqAlASSSU0f250P8A8sMX/t6v/wAml+3Oh/8Alhi/9vV/+TXI/Ur6u9H6p0q3Iz8f1rW3ura7e9vtDKnbYqexv0nuXQf8yfqx/wBwv/Bbf/SqghPNOIkBCj3Mv4OpzPLfDuXzTwzycwZYzwyMYYuH6frG9+3Oh/8Alhi/9vV/+TS/bnQ//LDF/wC3q/8Ayao/8yfqx/3C/wDBbf8A0ql/zJ+rH/cL/wAFt/8ASqdebtD7Zf8AesXD8M/f5r/wvD/6tb37c6H/AOWGL/29X/5NL9udD/8ALDF/7er/APJqj/zJ+rH/AHC/8Ft/9Kpf8yfqx/3C/wDBbf8A0qlebtD7Zf8Aeq4fhn7/ADX/AIXh/wDVre/bnQ//ACwxf+3q/wDyaX7c6H/5YYv/AG9X/wCTVD/mV9V/+4f/AILb/wClVX6j9Wfqd03Efl5eLsqZ4W2kuJ+ixjfV9z3JE5hqRD7Zf96mMPhsiIxlzRkTQAx4f/Vrr/tzof8A5YYv/b1f/k0v250P/wAsMX/t6v8A8muGzOndNq22X4mH0muwbqqsq3JtyC0mGPfTjW/omu/lI2Jg9Aqay3qWDRdgvf6Y6jhX3uqY4/Rbk02W+rQme7kuqh9sv+9bJ+H8lw8QlzBvpGGE34Rl7vBOX9XHN7P9udD/APLDF/7er/8AJpftzof/AJYYv/b1f/k1QH1K+q5AIw5B1BFtv/pVP/zJ+rH/AHC/8Ft/9Kp95u0Ptl/3rV4fhn7/ADX/AIXh/wDVre/bnQ//ACwxf+3q/wDyaX7c6H/5YYv/AG9X/wCTVH/mT9WP+4X/AILb/wClUv8AmT9WP+4X/gtv/pVK83aH2y/71XD8M/f5r/wvD/6tb37c6H/5YYv/AG9X/wCTS/bnQ/8Aywxf+3q//Jqj/wAyfqx/3C/8Ft/9Kpf8yfqx/wBwv/Bbf/SqV5u0Ptl/3quH4Z+/zX/heH/1a62NlYmWw2Yt1eRWDtL6nB4BGu3cwu93uVSW/wDOPbI3fZJ2zrG7wWN/i4/5Dv8A/DT/APz3Qtna3/nJu2jd9jjdGsbuJQ90+z7la1dLvuEf9I/c+M8Pue3x/pP/0vTkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklPK/4uP+Q7/wDw0/8A890Lqlyv+Lj/AJDv/wDDT/8Az3QuqUXL/wA1Dyb/AMX/AN38x/fUkme9lbS+xwYxurnOMAD+U4obcvEeCWX1OA5h7TH/AElK0EqSg66lu3dYxu8FzJcNQ0bnub+9sb9JMMjHLi0WsLm7ZEj/AAn81/25+YkpIsHPazN+teDhW+6rDx35oYfomwvGPU4/8V9Nq3lhdfbfg52L16lhtrxmupza2iXeg/3eq3/iH+9Mnt4AgnybHK/zhANSlCcYf7SUKj/jfIjzundO6TkZfXcii7qN2QWtFYY2w1iI/RtO39Hta1u5Z/1Jxac6nq9rmMbhZ1uwYYM7B753N/M9tn/QU/8AnB1nEz78mqmzrHSMoh2LZjwfT5Pp/omF25rv0b2XKt0zLyOkv6h1TLxxRm9VePsPTG/zjnS4hz6/pMbus973t/0iiuPGD0HFenf9Li/S4nQGPN93yQJEskxiGOQn+5KP6n2f8lLD/lJ/oO79UbLHdGbRY4vOJZZjBx7trcW1/wCbXtatpZ3QcB/TOkVUZDgbgHW5DyRG95Ntnu/k7tquOy8RoYXXVgWO9Nh3CC+N+wa/S2qaAIjEHs5vMyjLPklHWJnIgjrrulSUXW1MDS57WiwhrCSIc4/Ra395zkhZWbDUHD1GtDnM7hpJDXf2tqcwskkkklPK/wCLj/kO/wD8NP8A/PdC2v8A1ov/AEE/78sX/Fx/yHf/AOGn/wDnuhbX/rRf+gn/AH5Vf/Av+C7v/l//AOrf9y//0/TkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklKSSSSUpJJJJSkkkklPK/4uP+Q7/wDw0/8A890Lqlxn1B6l07E6PdXlZVOPYclzgy2xrCQWUjdte5vt9q6X9u9D/wDLHF/7er/8mocEojFDUbd3T+K4Msue5gjHMgz3EZJepbf2bl742+hZM8fRd4rnmXYtbLszIYy5mI7HsuY70rHem6p2P/gWiv8AnHsW0/rXQLGFlmdiPY76TXW1kH4tLlW+0/VX1/X+2YkyCK/Wr9MODTW1/oh2zfsc5Sccf3h9rR+75v8ANT/xJMnUHB6fhVOexr6arC2osD/0orffvpeP5r0ff/wVlSzsS1jbcJ7LA59luO0s9Sp7QI9P9HQyljq9tdj2+x/6NX/tX1X9KuoZ2OK6q7Kqm/aGHay3Sxrdz/3fZX/o2Kb+ofV2w4+7qVG3F2musZLAwln82+xu/wB7mJccf3h9qvu+b/NT/wASTrJc6FUf270P/wAscX/t6v8A8ml+3eh/+WOL/wBvV/8Ak0uOP7w+1X3fN/mp/wCLJq3fVTotlzr66nY1j/pnHsfUD/Yrc1iP0/6v9J6bYbsWgC93NzyX2a/8JaXub/ZU/wBu9D/8scX/ALer/wDJpft3of8A5Y4v/b1f/k0P1YN+n8GU/fZR4T7xjVUfc2bGa+yvDvfUwW2NreWVmIcQDDXbvauVvAporZuYzZjmoBkNYZfQ2yxjH13fp7a7v0r10LuudCc0td1DFLXAgj1q+Dofz1Vbk/VRjXMZl4jGPa1m1t7AA1h3s2w/2+/3vd/hEeOP7w+1i+75v81P/Ekj9cN6Tim59ZwmZJotZYA5pqY9zKNtv6Pa6n02em/Z71Ypyw/r9rv8BbU3Grf431h2VZV/21d/nssSZ1D6ttpbSc/Gsax5ta6y+tzvUJL/AFtxd/O7ne1R+2fVf7IzD+24voVkPZ+sM3B7XeoLvV9T1PW9T3usS44/vD7Vfd83+an/AIknXSVH9vdEP/eji/8Ab1f/AJNL9u9D/wDLHF/7er/8mlxx/eH2q+75v81P/Ek4f+Lj/kO//wANP/8APdC2v/Wi/wDQT/vyxf8AFx/yHf8A+Gn/APnuhbG8f85dkOn7HO6Pb9L9795V/wDwL/gux/5f/wDq3/cv/9T05JfLiSSn6jSXy4kkp+o0l8uJJKfqNJfLiSSn6jSXy4kkp+o0l8uJJKfqNJfLiSSn6jSXy4kkp+o0l8uJJKfqNJfLiSSn6H/5k/Vj/uF/4Lb/AOlUv+ZP1Y/7hf8Agtv/AKVXzwkoP6N/q/8AmOp/w1/5W/8Atw/Q/wDzJ+rH/cL/AMFt/wDSqX/Mn6sf9wv/AAW3/wBKr54SS/o3+r/5iv8Ahr/yt/8Abh+h/wDmT9WP+4X/AILb/wClUv8AmT9WP+4X/gtv/pVfPCSX9G/1f/MV/wANf+Vv/tw/Q/8AzJ+rH/cL/wAFt/8ASqX/ADJ+rH/cL/wW3/0qvnhJL+jf6v8A5iv+Gv8Ayt/9uH6H/wCZP1Y/7hf+C2/+lUv+ZP1Y/wC4X/gtv/pVfPCSX9G/1f8AzFf8Nf8Alb/7cP0P/wAyfqx/3C/8Ft/9Kpf8yfqx/wBwv/Bbf/Sq+eEkv6N/q/8AmK/4a/8AK3/24fof/mT9WP8AuF/4Lb/6VS/5k/Vj/uF/4Lb/AOlV88JJf0b/AFf/ADFf8Nf+Vv8A7cP0P/zJ+rH/AHC/8Ft/9Kpf8yfqx/3C/wDBbf8A0qvnhJL+jf6v/mK/4a/8rf8A24fpnpvS8HpdDsfAq9GpzjY5u5zvcQ1u6bXPd9FjVW3n/nNsgbfsn0twndunZs+l9D3r5vSUno4P0eD/AJlNL+lfeP8AK/eeL+v944//AErxv//Z \ No newline at end of file diff --git a/app/src/main/assets/help/regexHelp.md b/app/src/main/assets/help/regexHelp.md new file mode 100644 index 000000000..8598c718a --- /dev/null +++ b/app/src/main/assets/help/regexHelp.md @@ -0,0 +1,226 @@ +# 正则表达式学习 + +- [基本匹配] +- [元字符] + - [英文句号] + - [字符集] + - [否定字符集] + - [重复] + - [星号] + - [加号] + - [问号] + - [花括号] + - [字符组] + - [分支结构] + - [转义特殊字符] + - [定位符] + - [插入符号] + - [美元符号] +- [简写字符集] +- [断言] + - [正向先行断言] + - [负向先行断言] + - [正向后行断言] + - [负向后行断言] +- [标记] + - [不区分大小写] + - [全局搜索] + - [多行匹配] +- [常用正则表达式] + +## 1. 基本匹配 + +正则表达式只是我们用于在文本中检索字母和数字的模式。例如正则表达式 `cat`,表示: 字母 `c` 后面跟着一个字母 `a`,再后面跟着一个字母 `t`。
"cat" => The cat sat on the mat
+ +正则表达式 `123` 会匹配字符串 "123"。通过将正则表达式中的每个字符逐个与要匹配的字符串中的每个字符进行比较,来完成正则匹配。 +正则表达式通常区分大小写,因此正则表达式 `Cat` 与字符串 "cat" 不匹配。
"Cat" => The cat sat on the Cat
+ +## 2. 元字符 + +元字符是正则表达式的基本组成元素。元字符在这里跟它通常表达的意思不一样,而是以某种特殊的含义去解释。有些元字符写在方括号内的时候有特殊含义。 +元字符如下: + +|元字符|描述| +|:----:|----| +|.|匹配除换行符以外的任意字符。| +|[ ]|字符类,匹配方括号中包含的任意字符。| +|[^ ]|否定字符类。匹配方括号中不包含的任意字符| +|*|匹配前面的子表达式零次或多次| +|+|匹配前面的子表达式一次或多次| +|?|匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。| +|{n,m}|花括号,匹配前面字符至少 n 次,但是不超过 m 次。| +|(xyz)|字符组,按照确切的顺序匹配字符xyz。| +|||分支结构,匹配符号之前的字符或后面的字符。| +|\|转义符,它可以还原元字符原来的含义,允许你匹配保留字符 [ ] ( ) { } . * + ? ^ $ \ || +|^|匹配行的开始| +|$|匹配行的结束| + +## 2.1 英文句号 + +英文句号 `.` 是元字符的最简单的例子。元字符 `.` 可以匹配任意单个字符。它不会匹配换行符和新行的字符。例如正则表达式 `.ar`,表示: 任意字符后面跟着一个字母 `a`, +再后面跟着一个字母 `r`。
".ar" => The car parked in the garage.
+ +## 2.2 字符集 + +字符集也称为字符类。方括号被用于指定字符集。使用字符集内的连字符来指定字符范围。方括号内的字符范围的顺序并不重要。 +例如正则表达式 `[Tt]he`,表示: 大写 `T` 或小写 `t` ,后跟字母 `h`,再后跟字母 `e`。
"[Tt]he" => The car parked in the garage.
+ +然而,字符集中的英文句号表示它字面的含义。正则表达式 `ar[.]`,表示小写字母 `a`,后面跟着一个字母 `r`,再后面跟着一个英文句号 `.` 字符。
"ar[.]" => A garage is a good place to park a car.
+ +### 2.2.1 否定字符集 + +一般来说插入字符 `^` 表示一个字符串的开始,但是当它在方括号内出现时,它会取消字符集。例如正则表达式 `[^c]ar`,表示: 除了字母 `c` 以外的任意字符,后面跟着字符 `a`, +再后面跟着一个字母 `r`。
"[^c]ar" => The car parked in the garage.
+ +## 2.3 重复 + +以下元字符 `+`,`*` 或 `?` 用于指定子模式可以出现多少次。这些元字符在不同情况下的作用不同。 + +### 2.3.1 星号 + +该符号 `*` 表示匹配上一个匹配规则的零次或多次。正则表达式 `a*` 表示小写字母 `a` 可以重复零次或者多次。但是它如果出现在字符集或者字符类之后,它表示整个字符集的重复。 +例如正则表达式 `[a-z]*`,表示: 一行中可以包含任意数量的小写字母。
"[a-z]*" => The car parked in the garage #21.
+ +该 `*` 符号可以与元符号 `.` 用在一起,用来匹配任意字符串 `.*`。该 `*` 符号可以与空格符 `\s` 一起使用,用来匹配一串空格字符。 +例如正则表达式 `\s*cat\s*`,表示: 零个或多个空格,后面跟小写字母 `c`,再后面跟小写字母 `a`,再再后面跟小写字母 `t`,后面再跟零个或多个空格。
"\s*cat\s*" => The fat cat sat on the cat.
+ +### 2.3.2 加号 + +该符号 `+` 匹配上一个字符的一次或多次。例如正则表达式 `c.+t`,表示: 一个小写字母 `c`,后跟任意数量的字符,后跟小写字母 `t`。
"c.+t" => The fat cat sat on the mat.
+ +### 2.3.3 问号 + +在正则表达式中,元字符 `?` 用来表示前一个字符是可选的。该符号匹配前一个字符的零次或一次。 +例如正则表达式 `[T]?he`,表示: 可选的大写字母 `T`,后面跟小写字母 `h`,后跟小写字母 `e`。
"[T]he" => The car is parked in the garage.
"[T]?he" => The car is parked in the garage.
+ +## 2.4 花括号 + +在正则表达式中花括号(也被称为量词 ?)用于指定字符或一组字符可以重复的次数。例如正则表达式 `[0-9]{2,3}`,表示: 匹配至少2位数字但不超过3位(0到9范围内的字符)。
"[0-9]{2,3}" => The number was 9.9997 but we rounded it off to 10.0.
+ +我们可以省略第二个数字。例如正则表达式 `[0-9]{2,}`,表示: 匹配2个或更多个数字。如果我们也删除逗号,则正则表达式 `[0-9]{2}`,表示: 匹配正好为2位数的数字。
"[0-9]{2,}" => The number was 9.9997 but we rounded it off to 10.0.
"[0-9]{2}" => The number was 9.9997 but we rounded it off to 10.0.
+ +## 2.5 字符组 + +字符组是一组写在圆括号内的子模式 `(...)`。正如我们在正则表达式中讨论的那样,如果我们把一个量词放在一个字符之后,它会重复前一个字符。 +但是,如果我们把量词放在一个字符组之后,它会重复整个字符组。 +例如正则表达式 `(ab)*` 表示匹配零个或多个的字符串 "ab"。我们还可以在字符组中使用元字符 `|`。例如正则表达式 `(c|g|p)ar`,表示: 小写字母 `c`、`g` 或 `p` 后面跟字母 `a`,后跟字母 `r`。
"(c|g|p)ar" => The car is parked in the garage.
+ +## 2.6 分支结构 + +在正则表达式中垂直条 `|` 用来定义分支结构,分支结构就像多个表达式之间的条件。现在你可能认为这个字符集和分支机构的工作方式一样。 +但是字符集和分支结构巨大的区别是字符集只在字符级别上有作用,然而分支结构在表达式级别上依然可以使用。 +例如正则表达式 `(T|t)he|car`,表示: 大写字母 `T` 或小写字母 `t`,后面跟小写字母 `h`,后跟小写字母 `e` 或小写字母 `c`,后跟小写字母 `a`,后跟小写字母 `r`。
"(T|t)he|car" => The car is parked in the garage.
+ +## 2.7 转义特殊字符 + +正则表达式中使用反斜杠 `\` 来转义下一个字符。这将允许你使用保留字符来作为匹配字符 `{ } [ ] / \ + * . $ ^ | ?`。在特殊字符前面加 `\`,就可以使用它来做匹配字符。 +例如正则表达式 `.` 是用来匹配除了换行符以外的任意字符。现在要在输入字符串中匹配 `.` 字符,正则表达式 `(f|c|m)at\.?`,表示: 小写字母 `f`、`c` 或者 `m` 后跟小写字母 `a`,后跟小写字母 `t`,后跟可选的 `.` 字符。
"(f|c|m)at\.?" => The fat cat sat on the mat.
+ +## 2.8 定位符 + +在正则表达式中,为了检查匹配符号是否是起始符号或结尾符号,我们使用定位符。 +定位符有两种类型: 第一种类型是 `^` 检查匹配字符是否是起始字符,第二种类型是 `$`,它检查匹配字符是否是输入字符串的最后一个字符。 + +### 2.8.1 插入符号 + +插入符号 `^` 符号用于检查匹配字符是否是输入字符串的第一个字符。如果我们使用正则表达式 `^a` (如果a是起始符号)匹配字符串 `abc`,它会匹配到 `a`。 +但是如果我们使用正则表达式 `^b`,它是匹配不到任何东西的,因为在字符串 `abc` 中 "b" 不是起始字符。 +让我们来看看另一个正则表达式 `^(T|t)he`,这表示: 大写字母 `T` 或小写字母 `t` 是输入字符串的起始符号,后面跟着小写字母 `h`,后跟小写字母 `e`。
"(T|t)he" => The car is parked in the garage.
"^(T|t)he" => The car is parked in the garage.
+ +### 2.8.2 美元符号 + +美元 `$` 符号用于检查匹配字符是否是输入字符串的最后一个字符。例如正则表达式 `(at\.)$`,表示: 小写字母 `a`,后跟小写字母 `t`,后跟一个 `.` 字符,且这个匹配器必须是字符串的结尾。
"(at\.)" => The fat cat. sat. on the mat.
"(at\.)$" => The fat cat sat on the mat.
+ +## 3. 简写字符集 + +正则表达式为常用的字符集和常用的正则表达式提供了简写。简写字符集如下: + +|简写|描述| +|:----:|----| +|.|匹配除换行符以外的任意字符| +|\w|匹配所有字母和数字的字符: `[a-zA-Z0-9_]`| +|\W|匹配非字母和数字的字符: `[^\w]`| +|\d|匹配数字: `[0-9]`| +|\D|匹配非数字: `[^\d]`| +|\s|匹配空格符: `[\t\n\f\r\p{Z}]`| +|\S|匹配非空格符: `[^\s]`| + +## 4. 断言 + +后行断言和先行断言有时候被称为断言,它们是特殊类型的 ***非捕获组*** (用于匹配模式,但不包括在匹配列表中)。当我们在一种特定模式之前或者之后有这种模式时,会优先使用断言。 +例如我们想获取输入字符串 `$4.44 and $10.88` 中带有前缀 `$` 的所有数字。我们可以使用这个正则表达式 `(?<=\$)[0-9\.]*`,表示: 获取包含 `.` 字符且前缀为 `$` 的所有数字。 +以下是正则表达式中使用的断言: + +|符号|描述| +|:----:|----| +|?=|正向先行断言| +|?!|负向先行断言| +|?<=|正向后行断言| +|?"(T|t)he(?=\sfat)" => The fat cat sat on the mat. + +### 4.2 负向先行断言 + +当我们需要从输入字符串中获取不匹配表达式的内容时,使用负向先行断言。负向先行断言的定义跟我们定义的正向先行断言一样, +唯一的区别是不是等号 `=`,我们使用否定符号 `!`,例如 `(?!...)`。 +我们来看看下面的正则表达式 `(T|t)he(?!\sfat)`,表示: 从输入字符串中获取全部 `The` 或者 `the` 且不匹配 `fat` 前面加上一个空格字符。
"(T|t)he(?!\sfat)" => The fat cat sat on the mat.
+ +### 4.3 正向后行断言 + +正向后行断言是用于获取在特定模式之前的所有匹配内容。正向后行断言表示为 `(?<=...)`。例如正则表达式 `(?<=(T|t)he\s)(fat|mat)`,表示: 从输入字符串中获取在单词 `The` 或 `the` 之后的所有 `fat` 和 `mat` 单词。
"(?<=(T|t)he\s)(fat|mat)" => The fat cat sat on the mat.
+ +### 4.4 负向后行断言 + +负向后行断言是用于获取不在特定模式之前的所有匹配的内容。负向后行断言表示为 `(?"(?<!(T|t)he\s)(cat)" => The cat sat on cat. + +## 5. 标记 + +标记也称为修饰符,因为它会修改正则表达式的输出。这些标志可以以任意顺序或组合使用,并且是正则表达式的一部分。 + +|标记|描述| +|:----:|----| +|i|不区分大小写: 将匹配设置为不区分大小写。| +|g|全局搜索: 搜索整个输入字符串中的所有匹配。| +|m|多行匹配: 会匹配输入字符串每一行。| + +### 5.1 不区分大小写 + +`i` 修饰符用于执行不区分大小写匹配。例如正则表达式 `/The/gi`,表示: 大写字母 `T`,后跟小写字母 `h`,后跟字母 `e`。 +但是在正则匹配结束时 `i` 标记会告诉正则表达式引擎忽略这种情况。正如你所看到的,我们还使用了 `g` 标记,因为我们要在整个输入字符串中搜索匹配。
"The" => The fat cat sat on the mat.
"/The/gi" => The fat cat sat on the mat.
+ +### 5.2 全局搜索 + +`g` 修饰符用于执行全局匹配 (会查找所有匹配,不会在查找到第一个匹配时就停止)。 +例如正则表达式 `/.(at)/g`,表示: 除换行符之外的任意字符,后跟小写字母 `a`,后跟小写字母 `t`。 +因为我们在正则表达式的末尾使用了 `g` 标记,它会从整个输入字符串中找到每个匹配项。
".(at)" => The fat cat sat on the mat.
"/.(at)/g" => The fat cat sat on the mat.
+ +### 5.3 多行匹配 + +`m` 修饰符被用来执行多行的匹配。正如我们前面讨论过的 `(^, $)`,使用定位符来检查匹配字符是输入字符串开始或者结束。但是我们希望每一行都使用定位符,所以我们就使用 `m` 修饰符。 +例如正则表达式 `/at(.)?$/gm`,表示: 小写字母 `a`,后跟小写字母 `t`,匹配除了换行符以外任意字符零次或一次。而且因为 `m` 标记,现在正则表达式引擎匹配字符串中每一行的末尾。
"/.at(.)?$/" => The fat
+                cat sat
+                on the mat.
"/.at(.)?$/gm" => The fat
+                  cat sat
+                  on the mat.
+ +## 常用正则表达式 + +* **数字**: `\d+$` +* **用户名**: `^[\w\d_.]{4,16}$` +* **字母数字字符**: `^[a-zA-Z0-9]*$` +* **带空格的字母数字字符**: `^[a-zA-Z0-9 ]*$` +* **小写字母**: `[a-z]+$` +* **大写字母**: `[A-Z]+$` +* **网址**: `^(((http|https|ftp):\/\/)?([[a-zA-Z0-9]\-\.])+(\.)([[a-zA-Z0-9]]){2,4}([[a-zA-Z0-9]\/+=%&_\.~?\-]*))*$` +* **日期 (MM/DD/YYYY)**: `^(0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)?[0-9]{2}$` +* **日期 (YYYY/MM/DD)**: `^(19|20)?[0-9]{2}[- /.](0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])$` +* **求更求转发致谢**: `[\((【].*?[求更谢乐发推].*?[】)\)]` +* **查找最新章节**: `您可以.*?查找最新章节` +* **ps/PS**: `(?i)ps\b.*` +* **Html标签**: `<[^>]+?>` diff --git a/app/src/main/assets/help/replaceRuleHelp.md b/app/src/main/assets/help/replaceRuleHelp.md new file mode 100644 index 000000000..063c8d3a3 --- /dev/null +++ b/app/src/main/assets/help/replaceRuleHelp.md @@ -0,0 +1,6 @@ +# 替换管理界面帮助 + +* 替换规则是用来替换正文内容的一种规则 + * 菜单可以新建和导入规则 + * 可以拖动排序 + * 可以选择操作 \ No newline at end of file diff --git a/app/src/main/assets/help/ruleHelp.md b/app/src/main/assets/help/ruleHelp.md new file mode 100644 index 000000000..043677f4b --- /dev/null +++ b/app/src/main/assets/help/ruleHelp.md @@ -0,0 +1,177 @@ +# 源规则帮助 + +* [书源帮助文档](https://alanskycn.gitee.io/teachme/Rule/source.html) +* [订阅源帮助文档](https://alanskycn.gitee.io/teachme/Rule/rss.html) +* [js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt) +* 辅助键盘❓中可插入URL参数模板,打开帮助,选择文件 +* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行 +``` +@@ 默认规则,直接写时可以省略@@ +@XPath: xpath规则,直接写时以//开头可省略@XPath +@Json: json规则,直接写时以$.开头可省略@Json +: regex规则,不可省略,只可以用在书籍列表和目录列表 +``` + +* 发现url格式 +```json +[ + { + "title": "xxx", + "url": "", + "style": { + "layout_flexGrow": 0, + "layout_flexShrink": 1, + "layout_alignSelf": "auto", + "layout_flexBasisPercent": -1, + "layout_wrapBefore": false + } + } +] +``` + +* 获取登录后的cookie +``` +java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd +java.getCookie("http://baidu.com", "userid") => 1234 +``` + +* 请求头,支持http代理,socks4 socks5代理设置 +``` +socks5代理 +{ + "proxy":"socks5://127.0.0.1:1080" +} +http代理 +{ + "proxy":"http://127.0.0.1:1080" +} +支持代理服务器验证 +{ + "proxy":"socks5://127.0.0.1:1080@用户名@密码" +} +注意:这些请求头是无意义的,会被忽略掉 +``` + +* js 变量和函数 +``` +java 变量-当前类 +baseUrl 变量-当前url,String +result 变量-上一步的结果 +book 变量-书籍类,方法见 io.legado.app.data.entities.Book +cookie 变量-cookie操作类,方法见 io.legado.app.help.http.CookieStore +cache 变量-缓存操作类,方法见 io.legado.app.help.CacheManager +chapter 变量-当前目录类,方法见 io.legado.app.data.entities.BookChapter +title 变量-当前标题,String +src 内容,源码 +``` + +* url添加js参数,解析url时执行,可在访问url时处理url,例 +``` +https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"} +https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"} +``` + +* 增加js方法,用于重定向拦截 + * `java.get(urlStr: String, headers: Map)` + * `java.post(urlStr: String, body: String, headers: Map)` +* 对于搜索重定向的源,可以使用此方法获得重定向后的url +``` +(()=>{ + if(page==1){ + let url='https://www.yooread.net/e/search/index.php,'+JSON.stringify({ + "method":"POST", + "body":"show=title&tempid=1&keyboard="+key + }); + return java.put('surl',String(java.connect(url).raw().request().url())); + } else { + return java.get('surl')+'&page='+(page-1) + } +})() +或者 +(()=>{ + let base='https://www.yooread.net/e/search/'; + if(page==1){ + let url=base+'index.php'; + let body='show=title&tempid=1&keyboard='+key; + return base+java.put('surl',java.post(url,body,{}).header("Location")); + } else { + return base+java.get('surl')+'&page='+(page-1); + } +})() +``` + +* 正文图片链接支持修改headers +``` +let options = { +"headers": {"User-Agent": "xxxx","Referrer":baseUrl,"Cookie":"aaa=vbbb;"} +}; +'' +``` + + ## 部分js对象属性说明 +上述js变量与函数中,一些js的对象属性用的频率较高,在此列举。方便写源的时候快速翻阅。 + +### book对象的可用属性 +> 使用方法: 在js中或{{}}中使用book.属性的方式即可获取.如在正文内容后加上 ##{{book.name+"正文卷"+title}} 可以净化 书名+正文卷+章节名称(如 我是大明星正文卷第二章我爸是豪门总裁) 这一类的字符. +``` +bookUrl // 详情页Url(本地书源存储完整文件路径) +tocUrl // 目录页Url (toc=table of Contents) +origin // 书源URL(默认BookType.local) +originName //书源名称 or 本地书籍文件名 +name // 书籍名称(书源获取) +author // 作者名称(书源获取) +kind // 分类信息(书源获取) +customTag // 分类信息(用户修改) +coverUrl // 封面Url(书源获取) +customCoverUrl // 封面Url(用户修改) +intro // 简介内容(书源获取) +customIntro // 简介内容(用户修改) +charset // 自定义字符集名称(仅适用于本地书籍) +type // 0:text 1:audio +group // 自定义分组索引号 +latestChapterTitle // 最新章节标题 +latestChapterTime // 最新章节标题更新时间 +lastCheckTime // 最近一次更新书籍信息的时间 +lastCheckCount // 最近一次发现新章节的数量 +totalChapterNum // 书籍目录总数 +durChapterTitle // 当前章节名称 +durChapterIndex // 当前章节索引 +durChapterPos // 当前阅读的进度(首行字符的索引位置) +durChapterTime // 最近一次阅读书籍的时间(打开正文的时间) +canUpdate // 刷新书架时更新书籍信息 +order // 手动排序 +originOrder //书源排序 +variable // 自定义书籍变量信息(用于书源规则检索书籍信息) + ``` + +### chapter对象的可用属性 +> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符. + ``` + url // 章节地址 + title // 章节标题 + baseUrl //用来拼接相对url + bookUrl // 书籍地址 + index // 章节序号 + resourceUrl // 音频真实URL + tag // + start // 章节起始位置 + end // 章节终止位置 + variable //变量 + ``` + +### 字体解析使用 +> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码 +``` + +(function(){ + var b64=String(src).match(/ttf;base64,([^\)]+)/); + if(b64){ + var f1 = java.queryBase64TTF(b64[1]); + var f2 = java.queryTTF("https://alanskycn.gitee.io/teachme/assets/font/Source Han Sans CN Regular.ttf"); + return java.replaceFont(result, f1, f2); + } + return result; +})() + +``` + diff --git a/app/src/main/assets/help/webDavHelp.md b/app/src/main/assets/help/webDavHelp.md new file mode 100644 index 000000000..dee978b42 --- /dev/null +++ b/app/src/main/assets/help/webDavHelp.md @@ -0,0 +1,19 @@ +# WebDav备份教程 + +### 阅读支持云备份,采用WebDav协议,所有支持WebDav的云盘都可以,建议采用坚果云,每月免费1G流量,用来备份阅读足够了,下面就采用坚果云来讲解配置步骤. + +1. 打开坚果云网站 https://www.jianguoyun.com/d/home#/ +2. 如果没有注册过坚果云先注册一下 +3. 登录坚果云 +4. 右上角用户名点开点账户信息 +5. 点击安全选项 +6. 在第三方管理里添加应用 +7. 将应用示例里的服务器地址,用户名,和密码填到阅读的WebDav设置里 +8. 阅读的WebDav配置在我的-备份与恢复,创建子文件夹选项保持默认即可 +9. 设置完成后手动执行一下备份,看看是否成功 +10. 恢复时选择想要恢复的备份文件 + +### 自动备份说明 + +* 设置好备份之后每次退出App会自动进行备份 +* WebDav同一天的备份会覆盖,不同日期的备份不会覆盖 \ No newline at end of file diff --git a/app/src/main/assets/httpTTS.json b/app/src/main/assets/httpTTS.json deleted file mode 100644 index 29fda4c6f..000000000 --- a/app/src/main/assets/httpTTS.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "id": 1598233029304, - "name": "度小美", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029305, - "name": "度小宇", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029306, - "name": "度逍遥", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029307, - "name": "度丫丫", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029308, - "name": "度小娇", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029309, - "name": "度米朵", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029310, - "name": "度博文", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029311, - "name": "度小童", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029312, - "name": "度小萌", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029313, - "name": "百度骚男", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029314, - "name": "百度评书", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - }, - { - "id": 1598233029315, - "name": "百度主持", - "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}" - } -] \ No newline at end of file diff --git a/app/src/main/assets/readConfig.json b/app/src/main/assets/readConfig.json deleted file mode 100644 index 968141a0c..000000000 --- a/app/src/main/assets/readConfig.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "bgStr": "#FFFFFF", - "bgStrNight": "#000000", - "textColor": "#000000", - "textColorNight": "#FFFFFF", - "bgType": 0, - "bgTypeNight": 0, - "darkStatusIcon": true, - "darkStatusIconNight": false - }, - { - "bgStr": "#DDC090", - "bgStrNight": "#3C3F43", - "textColor": "#3E3422", - "textColorNight": "#DCDFE1", - "bgType": 0, - "bgTypeNight": 0, - "darkStatusIcon": true, - "darkStatusIconNight": false - }, - { - "bgStr": "#C2D8AA", - "bgStrNight": "#3C3F43", - "textColor": "#596C44", - "textColorNight": "#88C16F", - "bgType": 0, - "bgTypeNight": 0, - "darkStatusIcon": false, - "darkStatusIconNight": false - }, - { - "bgStr": "#DBB8E2", - "bgStrNight": "#3C3F43", - "textColor": "#68516C", - "textColorNight": "#F6AEAE", - "bgType": 0, - "bgTypeNight": 0, - "darkStatusIcon": false, - "darkStatusIconNight": false - }, - { - "bgStr": "#ABCEE0", - "bgStrNight": "#3C3F43", - "textColor": "#3D4C54", - "textColorNight": "#90BFF5", - "bgType": 0, - "bgTypeNight": 0, - "darkStatusIcon": false, - "darkStatusIconNight": false - } -] \ No newline at end of file diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 6563ab994..25069b073 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -1,674 +1,486 @@ # 更新日志 -* 关注公众号 **[开源阅读]()** 菜单•软件下载 提前享受新版本。 -* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 -- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 - -**2020/09/09** -* 修复主题导入导出bug -* 优化分屏模式状态栏 -* 书源基本属性增加“书源注释” - -**2020/09/08** -* 页眉页脚跟随背景 -* 主题导入导出 - -**2020/09/07** -* 订阅源和替换规则添加滑动选择 -* 修复排版配置导入导出 -* 订阅界面添加下载文件功能 - -**2020/09/06** -* 优化翻页 -* EInk模式独立背景 -* 阅读排版配置导入导出,包括背景和字体,支持网络导入 - -**2020/09/03** -* 修复替换中的回车消失的bug -* 所有内容恢复htmlFormat, 在想其它办法解决丢失一些内容的问题 -* 图片(漫画)支持导出 - -**2020/09/02** -* 搜索url支持put,get,js里使用java.put,java.get -* 对于搜索重定向的源,可以使用此方法获得重定向后的url + +* 关注公众号 **[开源阅读]** 菜单•软件下载 提前享受新版本。 +* 关注合作公众号 **[小说拾遗]** 获取好看的小说。 + +## **必读** + +【温馨提醒】 *更新前一定要做好备份,以免数据丢失!* + +* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 +* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题, 出现简体变化问题检查一下简繁转换是否关闭。 +* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! + +**2021/08/02** + +* 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法 + +1. 换源界面功能添加:置顶,置底,删除 by h11128 +2. Cronet:优化 by ag2s20150909 +3. 优化自动翻页 by jiuZhouWorlds +4. 封面设置移到主题里面,白天和夜间可分别设置 + +**2021/08/01** + +1. 为webService添加快捷操作 +2. 规则内替换使用正则错误时自动切换为不使用正则 +3. 优化Cronet +4. 阅读界面菜单显示的时候停止按键翻页和自动阅读 +5. 切换后台停止自动阅读 + +**2021/07/29** + +1. 修复每次更新都重新导入text规则的bug +2. RSS阅读页添加刷新按钮以应对页面内容过期失效的BUG by JiuZhouWorlds +3. 规则内替换使用正则报错时自动使用非正则替换 + +**2021/07/27** + +1. 修复bug +2. web使用api获取封面,不会再出现没有封面的情况 +3. 阅读亮度手动调节分别记住白天和夜间模式 +4. legado://import/auto?src={url}, 自动识别导入类型 +5. 一些优化并更新了一下web首页,感谢沚水, 传书暂时还不好用 + +**2021/07/22** + +1. 非关键规则添加try防止报错中断解析 +2. 添加获取封面的api +3. 获取正文api使用替换规则 +4. 添加一个ronet版本,网络访问使用Chromium内核 +5. web书架增加【最近一次更新书籍信息的时间】 +6. 采用Flow替换LiveData,优化资源使用 +7. 统一网络一键导入路径legado://import/{path}?src={url} + +* path: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig +* 添加了txt小说规则,在线朗读引擎,主题,排版 的一键导入支持,老url依然可用 + +8. 替换规则管理添加置顶所选和置底所选 + +**2021/07/16** + +1. js扩展函数添加删除本地文件方法 +2. js扩展函数对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件,/android/data/{package}/cache/... + +**2021/07/15** + +1. 添加js函数来修复开启js沙箱后某些书源失效。by ag2s20150909 + +```kotlin +/** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ +fun getZipStringContent(url: String, path: String): String + +/** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ +fun getZipByteArrayContent(url: String, path: String): ByteArray? ``` - -var url='https://www.yooread.net/e/search/index.php,'+JSON.stringify({ -"method":"POST", -"body":"show=title&tempid=1&keyboard="+key -}); -String(java.connect(url).raw().request().url()) - + +* web服务添加一个导航页 + +**2021/07/11** + +1. 开启JS沙箱限制 + +* 禁止在js里exec运行命令 +* 禁止在js里通过geClass反射 +* 禁止在js里创建File对象 +* 禁止在js里获取Packages scope + +2. 优化并修复bug + +**2021/07/10** + +1. 阅读界面长按菜单改回原来样式 +2. 解决导入书源时重命名分组和保留名称冲突的问题 + +**2021/07/09** + +1. 发现url添加json格式, 支持设置标签样式 + +* 样式属性可以搜索 [FleboxLayout子元素支持的属性介绍](https://www.jianshu.com/p/3c471953e36d) +* 样式属性可省略,有默认值 + +```json +[ + { + "title": "xxx", + "url": "", + "style": { + "layout_flexGrow": 0, + "layout_flexShrink": 1, + "layout_alignSelf": "auto", + "layout_flexBasisPercent": -1, + "layout_wrapBefore": false + } + } +] ``` -* 正文合并后替换规则支持所有规则写法,包括js - -**2020/09/01** -* 导入书源列表添加全不选 -* 详情页菜单添加清理缓存,清理当前书籍缓存 -* 修复滑动选择,选择数量不更新的bug -* 字体跟随背景,每个背景对应一个字体 -* 优化图片下载 - -**2020/08/29** -* 修复一个null引起的崩溃bug -* 修复我的界面滚动时图标消失的bug -* 修复从详情页目录打开章节内容不对的bug -* 书源选择增加滑动选择, 选择框区域滑动时进行选择 by [Mupceet](https://github.com/Mupceet) -* 请求头,支持http代理,socks4 socks5代理设置 by [10bits](https://github.com/10bits) + +**2021/07/07** + +1. 默认规则新增类似`jsonPath`的索引写法 by bushixuanqi + +* 格式形如 `[index,index, ...]` 或 `[!index,index, ...]` 其中`[!`开头表示筛选方式为排除,`index`可以是单个索引,也可以是区间。 +* 区间格式为 `start:end` 或 `start:end:step`,其中`start`为`0`可省略,`end`为`-1`可省略。 +* 索引、区间两端、区间间隔都支持负数 +* 例如 `tag.div[-1, 3:-2:-10, 2]` +* 特殊用法 `tag.div[-1:0]` 可在任意地方让列表反向 + +2. 允许索引作为@分段后每个部分的首规则,此时相当于前面是`children` + +* `head@.1@text` 与 `head@[1]@text` 与 `head@children[1]@text` 等价 + +3. 添加Umd格式支持 by ag2s20150909 +4. 修复web页面按键重复监听的bug +5. 亮度条往中间移了一点,防止误触 +6. 添加内置字典 + +**2021/06/29** + +* 修复html格式化bug +* 订阅界面webView支持css prefers-color-scheme: dark 查询,需webView v76或更高版本 +* 如webView低于v76可以用js调用activity.isNightTheme()来获取当前是否暗模式 +* 修复一些书籍导出epub失败 by ag2s20150909 + +**2021/06/22** + +* 修复隐藏未读设置不生效的bug +* 修复系统字体大小选择大时导入界面按钮显示不全的bug +* 修复听书从后台打开时不对的bug + +**2021/06/20** + +* viewPager2 改回 viewPager +* 添加配置导入文件规则功能 by bushixuanqi +* 文件夹分组样式优化(未完成) +* epub支持外部模板 +* 修复一些bug + +**2021/06/06** + +* 添加自定义导出文件名 +* 添加书架文件夹分组样式,未完成 +* viewPager2 3层嵌套有问题,书架换回viewPager + +**2021/05/29** + +* 谷歌版可使用外部epub模板 +* Asset文件夹下二级以内目录全文件读取,Asset->文件夹->文件 +* epub元数据修改,使修改字体只对正文生效 +* 修复epub模板文件的排序问题 +* epub可自定义模板,模板路径为书籍导出目录的Asset文件夹,[模板范例](https://wwa.lanzoux.com/ibjBspkn05i) + ``` -socks5代理 -{ - "proxy":"socks5://127.0.0.1:1080" -} -http代理 -{ - "proxy":"http://127.0.0.1:1080" -} -支持代理服务器验证 -{ - "proxy":"socks5://127.0.0.1:1080@用户名@密码" -} -注意:这些请求头是无意义的,会被忽略掉 +Asset中里面必须有Text文件夹,Text文件夹里必须有chapter.html,否则导出正文会为空 +chapter.html的关键字有{title}、{content} +其他html文件的关键字有{name}、{author}、{intro}、{kind}、{wordCount} ``` -**2020/08/28** -* 修复一些bug -* 换源不再改变书名和作者,防止换到一些错误的书源后不能再换源 - -**2020/08/27** -* 修复主题bug -* 修复封面bug -* 优化书籍更新,搜索,换源 -* e-ink模式不再固定背景 - -**2020/08/26** -* js添加java.encodeURI(speakText),用于解决js编码时有~的语句朗读不出来 -* 修复书名太长删除阅读记录按钮不显示的bug -* 完成本地书籍编码选择 - -**2020/08/25** -* 阅读记录可以删除了 -* 修复翻页模式选择颜色问题 -* 修复toolbar在一些情况下文字颜色不对的bug -* 多设备阅读记录叠加 -* 封面链接支持修改headers - -**2020/08/24** -* 应用被杀死时停止朗读 -* 默认封面添加删除操作 -* 备份阅读记录 -* 书源添加移除分组支持多选,多个分组以逗号(中英均可)隔开 -* 可以自定义在线朗读了 - -**2020/08/22** -* 添加阅读时间记录 - -**2020/08/21** -* 图片(漫画源)支持离线下载了 - -**2020/08/20** -* 正文图片(漫画源)链接支持修改headers - -**2020/08/19** -* 选择文本替换时带入书名和书源 - -**2020/08/16** -* 添加亮度调节控件显示开关 -* 添加应用内语言切换 -* 底栏颜色限制去除,自动适配 - -**2020/08/12** -* 增加了Content Provider 接口支持 by [w568w](https://github.com/w568w) -* 修复阅读界面加入书架后,书籍详情页还是显示加入书架按钮的bug -* 修复低版本手机自动阅读速度拉动最左边崩溃的bug -* 给亮度调节加个半透明背景,很多人找不到 -* 修复替换分组选择无效的bug -* 备份添加书签 -* 修复web端进度更新后手机端进入阅读界面进度不变的bug -* 增加了txt目录规则备份 -* 优化了导入功能,导入之前对比已有书源,可选择性导入 -* 其它一些bug修复 - -**2020/08/08** -* 继续适配主题,现在应该所有地方都按照主题变色了 -* 朗读定时增加到3个小时,朗读暂停恢复后继续定时 -* 优化了主题颜色选择,会影响体验的颜色禁止选,会有提示 -* 订阅规则下一页支持页数,下一页规则填page - -**2020/08/07** -* 修复其它一些主题色没有适配到的地方 -* 添加默认启用替换配置 - -**2020/08/06** -* 菜单背景根随主题色 -* 修复其它一些主题色没有适配到的地方 -* 取消图片颜色为FULL时的自动滚动 -* 其它一些优化,升级库文件之类 -* 显示订阅加入恢复忽略列表 - -**2020/08/03** -* 修复一些主题色没有适配到的地方 -* 尝试修复书架最新章节更新不及时的bug - -**2020/08/02** -* 阅读菜单底部几个按钮的背景动态设置为底部操作栏颜色 -* 优化书签功能,解决一些bug - -**2020/07/29** -* 正文图片样式为FULL的自动为滚动模式 - -**2020/07/28** -* 长图正文规则添加图片样式FULL,可以滚动浏览了 - -**2020/07/26** -* 优化翻页,加快翻页速度 - -**2020/07/25** -* 正文规则添加多页合并后的替换规则,格式同样是##regex##replaceTo -* 正文图片添加长按缩放 -* 正文规则添加图片样式规则,可以设置为FULL -* 其它一些bug修复 - -**2020/07/21** -* 优化文字选择,不再缓存 -* 添加忽略恢复列表,方便不同手机配置不同 -* 其它一些bug修复 - -**2020/07/19** -* 添加自定义默认封面 -* 修复封面选择本地图片时书架不显示的bug - -**2020/07/14** -* 添加英文语言 by [52fisher](https://github.com/52fisher) - -**2020/07/13** -* 在线阅读图片支持测试成功,最好把替换净化关了,防止图片url不对 -* 书源保留img标签就行,@html自动保留标签 - -**2020/07/12** -* epub显示图片,未完善 -* 在线阅读也支持图片,还未测试 - -**2020/07/11** -* epub可以正确识别书名和作者了 -* epub封面正确显示 - -**2020/07/10** -* 修复一些窗口再墨水屏上背景透明的问题 -* 添加epub支持 -* web阅读时记住进度 -* 导入书源时系统文件选择器可以选择json文件 - -**2020/07/06** -* 优化下载 - -**2020/07/05** -* 修复夜间模式底栏颜色调整无效的bug -* 【web看书】加了翻页、排序等 by [Celeter](https://github.com/Celeter) -* 两部xx' is recognized as a title by [52fisher](https://github.com/52fisher) -* 添加下载错误日志,从下载菜单浏览 -* 修复vip标识引发的bug - -**2020/07/04** -* 修复滚动bug -* 其它一些优化 +**2021/05/24** -**2020/07/03** -* 修复关闭两端对齐是朗读高亮不准确的bug -* 添加文字底部对齐开关 +* 反转目录后刷新内容 +* 修复上下滑动会导致左右切换问题 +* 精确搜索增加包含关键词的,比如搜索五行 五行天也显示出来, 五天行不显示 -**2020/06/25** -* E-Ink模式合并到主题模式里, E-Ink模式不能修改阅读界面背景和文字颜色 -* 添加判断,防止背景透明引起重影,花屏问题 +**2021/05/21** -**2020/06/22** -* 修复xpath获取正文多了许多逗号的bug -* 修复检验有效书源移除失效分组失败的bug +* 添加反转目录功能 +* 修复分享bug +* 详情页添加登录菜单 +* 添加发现界面隐藏配置 -**2020/06/21** -* 双击书架图标返回顶部 +**2021/05/16** -**2020/06/20** -* 适配NavigationBar +* 添加总是使用默认封面配置 +* 添加一种语言 ptbr translation by mezysinc +* epublib 修bug by ag2s20150909 -**2020/06/19** -* 修复eInk bug -* 修复分组下载bug -* 导入本地添加滚动条 +**2021/05/08** -**2020/06/18** -* fadeapp.widgets:scrollless-recyclerView导致有些手机重影,暂时去除 -* 下载界面添加分组 -* 修复eInk bug +* 预下载章节可调整数目 +* 修复低版本Android使用TTS闪退。 by ag2s20150909 +* 修复WebDav报错 +* 优化翻页动画点击翻页 -**2020/06/17** -* 修复更新书架时更新禁止更新的问题 -* 修复导入旧版本数据问题 +**2021/05/06** -**2020/06/16** -* 刷新时只刷新当前书架 -* 修复恢复备份需要退出重进的问题 -* 保存打开 E-Ink 模式前的主题、翻页动画,关闭后恢复之前的配置, 现在可以切着玩了 -* 修复因繁体语言导致的崩溃bug +* 修复bug +* url参数添加重置次数,retry +* 修改默认tts, 手动导入 +* 升级android studio -**2020/06/15** -* 添加 E-Ink 模式 by [Modificator](https://github.com/Modificator) -* 修复发现打开书时可能的错误 +**2021/04/30** -**2020/06/14** -* 修复txt文件目录识别 -* 书源分组添加已启用已禁用 +* epub插图,epublib优化,图片解码优化,epub读取导出优化。by ag2s20150909 +* 添加高刷设置 +* 其它一些优化 +* pro版本被play商店下架了,先把pro设置图片背景的功能开放到所有版本,使用pro版本的可以使用备份恢复功能切换最新版本 -**2020/06/13** -* 优化搜索 +**2021/04/16** -**2020/06/12** -* 修复分组变化的bug +* 去掉google统计,解决华为手机使用崩溃的bug +* 添加规则订阅时判断重复提醒 +* 添加恢复预设布局的功能, 添加一个微信读书布局作为预设布局 -**2020/06/10** -* 正文字体的粗细选择增加可以选择细体(Android O生效) by [hingbong](https://github.com/hingbong) +**2021/04/08** + +* 缓存时重新检查并缓存图片 +* 订阅源调试添加源码查看 +* web调试不输出源码 * 修复bug +* 换源优化 --- by ag2s20150909 +* 修复localBook获取书名作者名的逻辑 +* 修复导出的epub的标题文字过大的bug +* 优化图片排版 + +**2021/04/02** -**2020/06/09** -* 修复从发现界面打开已在书架的书时,显示不对的问题 +* 修复bug +* 书源调试添加源码查看 +* 添加导出epub by ag2s20150909 +* 换源添加是否校验作者选项 -**2020/06/07** -* 优化书源检测,自定义搜索关键词 -* 失效书源如果校验为有效会去掉失效标志 +**2021/03/31** -**2020/06/06** -* 修复一些bug,包括从阅读界面退出后还是显示红色更新的bug +* 优化epubLib by ag2s20150909 +* 升级库,修改弃用方法 +* tts引擎添加导入导出功能 -**2020/06/03** -* zh-TW translation by david082321 -* 修复音频播放时播放速度调节会再下一章失效的bug +**2021/03/23** -**2020/05/31** -* 更新到android studio 4.0 -* 书源排序添加按url -* 去除朗读通知的进度条 -* 修复恢复问题,暂时去除混淆 +* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 +* js添加一个并发访问的方法 java.ajaxAll(urlList: Array) 返回 Array +* 优化目录并发访问 +* 添加自定义epublib,支持epub v3解析目录。by ag2s20150909 -**2020/05/24** -* 添加自动翻页速度调节 +**2021/03/19** -**2020/05/23** -* 添加文字两端对齐配置 +* 修复图片地址参数缺少的bug +* 修复更改替换规则时多次重新加载正文导致朗读多次停顿的bug +* 修复是否使用替换默认值修改后不及时生效的bug +* 修复繁简转换“勐”“十”问题。使用了剥离HanLP简繁代码的民间库。APK减少6M左右 by hoodie13 +* 百度tsn改为tts -**2020/05/20** -* Rss列表增加一种显示样式 +**2021/03/15** -**2020/05/18** -* 修复http://alanskycn.gitee.io/书源导入失败问题,被屏蔽UA了 -* Rss列表添加样式切换 +* 优化图片TEXT样式显示 +* 图片url在解析正文时就拼接成绝对url +* 修复一些bug -**2020/05/17** -* 自动翻页功能完成 -* 替换规则输入时弹出辅助输入条 +**2021/03/08** -**2020/05/10** -* 添加识别rss分组中的频道信息,在菜单中可以切换频道 from [yangyxd](https://github.com/yangyxd) -* 源管理添加置底,批量置顶,批量置地 -* 封面选择本地图片完成 +* 阅读页面停留10分钟之后自动备份进度 +* 添加了针对中文的断行排版处理-by hoodie13, 需要再阅读界面设置里手动开启 +* 添加朗读快捷方式 +* 优化Epub解析 by hoodie13 +* epub书籍增加cache by hoodie13 +* 修复切换书籍或者章节时的断言崩溃问题。看漫画容易复现。 by hoodie13 +* 修正增加书签alert的正文内容较多时,确定键溢出屏幕问题 by hoodie13 +* 图片样式添加TEXT, 阅读界面菜单里可以选择图片样式 -**2020/05/04** -* 优化txt文件目录解析 +**2021/02/26** -**2020/05/03** -* 优化一些界面显示问题 -* 订阅源添加style -* 修复一些重复目录的bug +* 添加反转内容功能 +* 更新章节时若无目录url将自动加载详情页 +* 添加变量nextChapterUrl +* 订阅跳转外部应用时提示 +* 修复恢复bug +* 详情页拼接url改为重定向后的地址 +* 不重复解析详情页 -**2020/05/02** -* 修复不停换源的bug -* 修复本地书籍自动换源 -* 修复书源校验的一些问题 +**2021/02/21** -**2020/05/01** -* 尝试修复朗读时可能错位的bug -* 添加自动换源配置 -* 换源添加禁用菜单 +* 下一页规则改为在内容规则之后执行 +* 书籍导出增加编码设置和导出文件夹设置,使用替换设置 +* 导入源添加等待框 +* 修复一些崩溃bug -**2020/04/29** -* 修复bug -* 订阅界面添加长按菜单 +**2021/02/16** -**2020/04/26** -* 添加导入旧的书源转换 -* 修复不自动朗读下一章的bug +* 修复分享内容不对的bug +* 优化主题颜色,添加透明度 +* rss分类url支持js +* 打开阅读时同步阅读进度 -**2020/04/25** -* 修复翻页按键设置为空时崩溃的bug -* 翻页按键优先自定义按键,可覆盖音量按键 -* 写书源时的辅助键盘添加※ -* 更改了书源格式,不再需要转义符 +**2021/02/09** -**2020/04/24** -* 坚果云最近调整了策略,必须使用应用密码才能备份,用户信息,安全,第三方应用 -* text目录规则添加id字段,负值为系统自带规则 -* 其它一些优化 +* 修复分组内书籍数目少于搜索线程数目,会导致搜索线程数目变低 +* 修复保存书源时不更新书源时间的bug +* 订阅添加夜间模式,需启用js,还不是很完善 +* 优化源导入界面 -**2020/04/20** -* 优化阅读界面信息显示 +**2021/02/03** -**2020/04/19** -* 添加阅读界面各种信息设置 +* 排版导出文件名修改为配置名称 +* 取消在线朗读下载文件检测,会导致朗读中断 +* 修复其它一些bug -**2020/04/18** -* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, [hingbong](https://github.com/hingbong) -* 修复更新时间不对的bug +**2021/01/30** -**2020/04/13** -* 去除rss朗读时的引号 +* 优化阅读记录界面 +* 自定义分组可以隐藏,删除按钮移到编辑对话框 +* 修复其它一些bug -**2020/04/13** -* 修复调用webView返回结果多了引号的bug +**2021/01/23** -**2020/04/12** -* 解决无法取消加粗的bug -* 修复换源自动加入书架的bug +* 优化书源校验,从搜索到正文全部校验 +* play版可以设置背景图片 +* 添加几个js方法,见io.legado.app.help.JsExtensions -**2020/04/09** -* 修复书架刷新闪烁 +**2021/01/18** -**2020/04/08** -* 可以隐藏书架未分组 +* 增加三星 S Pen 支持 by [dacer](https://github.com/dacer) +* 订阅添加阅读下载,可以从多个渠道下载 +* 修复一些BUG -**2020/04/07** -* 书架添加未分组,有未分组书籍时自动显示 +**2021/01/12** + +* 修复bug +* 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc) +* 换源刷新之前删除原搜索记录 +* 优化web调试 + +**2021/01/03** + +* 导出书单只保留书名与作者,导入时自动查找可用源 +* 添加预加载设置 +* 选择分组时只搜索分组 + +**2020/12/30** + +* 解决文件下载异常,在线语音可正常播放 by [Celeter](https://github.com/Celeter) +* 更新默认在线朗读库, 默认id小于0方便下次更新时删除旧数据, 有重复的自己删除 +* 导入导出书单 * 其它一些优化 -**2020/04/04** -* 优化备份逻辑 -* 修复订阅分类太多显示不全的bug -* 修复一些分类要手动刷新的问题 +**2020/12/27** -**2020/04/02** -* 书架书名和作者作为唯一值 -* 添加订阅分类,分类规则和发现一样,分类一::url1 && 分类2::url2 +* 订阅添加搜索和分组 +* 修复部分手机状态栏bug +* 单url订阅支持内容规则和样式 -**2020/03/29** -* 添加退出软件后是否响应耳机按键的开关 -* 优化书源校验 +**2020/12/19** + +* 书签转移到文本菜单里,会记录选择的文本和位置 +* 订阅源添加单url选项,直接打开url +* 订阅源可以put,get数据 + +**2020/12/15** + +* 修复一些引起崩溃的bug +* 修复搜书和换源可能什么分组都没有的bug +* 添加同步进度开关,默认开启,在备份与恢复里面 + +**2020/12/13** -**2020/03/26** -* 修复txt目录bug -* 最近工作比较忙,只有晚上有时间写软件,bug之类的不要催,白天不回消息 - -**2020/03/25** -* 修复7.1.1的网络问题,是retrofit2库最新版本的bug,暂时退回上版本 -* 去除下载路径的配置,减少错误 -* 添加隐藏状态栏是否扩展到刘海 - -**2020/03/24** -* txt文件第一章之前的文字不再放到简介里 -* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别 -* 修复文件关联 by [wqfantexi](https://github.com/wqfantexi) - -**2020/03/22** -* 添加文件关联 by [wqfantexi](https://github.com/wqfantexi) -* 手动排序可以了,在书架整理里面拖动排序 -* 删除分组时同时删除书籍里的分组信息,下次添加新分组时不会自动出现在分组内 -* 修复换源丢失分组信息的bug -* 修复部分朗读引擎不自动朗读下一章的bug - -**2020/03/21** -* 详情页点击书名搜索 - -**2020/03/20** -* 自动备份文件和手动备份文件分开 -* 修复一些rss收藏取消不了的bug -* 修复rss请求头无效bug - -**2020/03/19** -* 美化界面我的 by [yangyxd](https://github.com/yangyxd) -* 优化搜索 - -**2020/03/18** -* 尝试修复搜索时崩溃 -* 解决看过书籍的移到顶部需要向上滚动才能看到的bug -* 只有再书源被删除找不到书源时才会自动换源 -* 美化界面 by [yangyxd](https://github.com/yangyxd) -* 订阅后台播放 - -**2020/03/16** -* 修复滚动模式切换章节位置不归0的bug -* 修复文字选择更多菜单在部分手机上报错的bug -* 修复文字选择菜单问题 - -**2020/03/15** -* 加载正文无书源时自动换源 - -**2020/03/14** -* 修改导航栏图标 - -**2020/03/13** -* 更改书架控件,ViewPager2替换回2.0使用的ViewPager,解决下拉不流畅问题 -* 修复点击作者搜索后,打开的详情页还是原来的书籍的bug -* 修改朗读菜单 -* 优化rss朗读 - -**2020/03/12** -* 导入本地添加需要权限模式 - -**2020/03/11** -* 修复调节上边距时下边距一起动的bug -* 适配沚水的web阅读 by [六月](https://github.com/Celeter) -* 分组管理页面调整 by [yangyxd](https://github.com/yangyxd) - -**2020/03/10** -* 优化文字选择菜单弹出位置 -* 添加屏幕方向控制 -* 添加点击作者搜索 - -**2020/03/09** -* 底部文字对齐 -* 主题添加阴影调节 by [yangyxd](https://github.com/yangyxd) - -**2020/03/08** -* 订阅长按保存图片 -* 订阅全屏播放 -* 书架全部分组可以隐藏了 -* 内置web书架基本能用了 by [六月](https://github.com/Celeter) -* 书架整理加入未分组 -* 显示总进度 -* 隐藏状态栏时,标题显示在上方 - -**2020/03/07** -* 添加标题上下间距调整 -* 添加标题大小调整 -* 书籍整理添加批量启用禁用更新 -* 换源禁用书源不显示 -* 修复搜索界面简介最下面显示半行文字 -* 搜索历史改为多行 - -**2020/03/06** -* 添加隐藏标题 -* 行距段距改成倍距,根据字体大小变化 -* 修复翻页时右下角页数闪烁 -* 修复朗读错行 -* 添加底部分隔线,开关在边距设置里 - -**2020/03/05** -* 修复翻页动画 -* 修复主题模式跟随 -* 修复滚动翻页切换章节时跳动 -* 适配阅读3.0的web做源 -* 本地目录规则网络导入 - -**2020/03/04** -* 修复仿真翻页动画 -* 添加阅读记录同步,正常退出进入软件时同步阅读记录 - -**2020/03/03** * 修复bug -* 优化排版,确保段距为0时每行在相同的位置 -* 修复底部遮挡 - -**2020/03/02** -* 添加书源登录 -* 替换规则实时生效 -* 页面最后一行计算是否能放下时不计算行距 -* 优化翻页动画 -* 优化书源校验 -* 按键翻页有动画了 - -**2020/03/01** -* 修复书源解析的一个bug -* 添加底部操作栏颜色配置 -* 修复滚动点击翻页,修复滚动最后一页显示加载中 -* 去除备份恢复默认路径 -* 尝试修复部分手机一键导入书源报错 -* 翻页还有些bug不用反馈了,我已经知道,会修复的 - -**2020/02/29** -* 添加书源一键导入 -* 修复主题模式跟随系统 -* 修复书源校验 -* 添加书架排序 -* 添加点击翻页开关 -* 修复共用布局没有记住配置的bug - -**2020/02/28** -* 解决阅读界面部分字体超出范围的问题 -* 修复背景切换有时空白的bug -* 修复滚动翻页问题 - -**2020/02/27** -* 修复bug,边距调节,换源等一些bug,记不清了 -* 修复默认字体问题 -* 改了下包名,好上架应用市场 - -**2020/02/26** -* 修复仿真翻页 -* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by [hingbong](https://github.com/hingbong) - -**2020/02/25** -* 优化文本选择和滚动,感觉很完美了 - -**2020/02/24** -* 滚动暂时可以滚了,先这样吧,头大 -* 紧急修复朗读报错的bug - -**2020/02/23** -* 修复BUG -* 本地目录正则自定义完成 -* 选择文本修复框选不全的问题,增加操作按钮 - -**2020/02/22** -* 长按选择完成 - -**2020/02/21** -* 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整 -* 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题 - -**2020/02/19** -* 导出功能完成 -* 其它一些优化,仿真翻页有点问题,还没找到问题所在 - -**2020/02/15** +* 网络访问框架修改为RxHttp, 有bug及时反馈 +* 优化进度同步 +* 换源界面添加分组选择 +* 沉浸模式时阅读界面导航栏透明 + +**2020/12/09** + * 修复bug -* 添加一个图标 -* 阅读界面文本选择开关 -* 书源管理发现开启关闭标志 +* 优化中文排序 +* 优化编码识别 +* 选择文字时优先选词 +* 优化进度同步,进入书籍时同步,每次同步单本书,减少同步文件大小 -**2020/02/14** -* 书籍分组支持一本书籍在多个分组,既可以在追更,又可以在玄幻 -* 搜索界面限制刷新频率,每秒刷新一次 -* 添加一种图标,2.0的老图标 +**2020/12/04** -**2020/02/13** -* 修复BUG -* 优化已下载检测,解决目录卡顿 -* 添加切换图标 +* 阅读进度从页数改为字数,排版变化时定位更准确 +* 修改viewBinding +* 修复中文排序 +* 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看 -**2020/02/12** -* 修复bug -* 优化,网页编码优先使用书源配置的编码 -* 其它一些优化 -* 添加简繁转换 +**2020/11/18** + +* 优化导航栏 +* js添加java.log(msg: String)用于调试时输出消息 +* js添加cookie变量,方法见io.legado.app.help.http.api.CookieManager +* js添加cache变量,可以用来存储token之类的临时值,可以设置保存时间,方法见io.legado.app.help.CacheManager +* 需要token的网站可以用js来写了,比如阿里tts -**2020/02/10** -* 多页目录并行获取解析 -* 优化详情页 -* 优化换源页面,添加换源是否加载目录配置 -* 换源顺序按书源顺序排列 +**2020/11/15** -**2020/02/09** -* 优化书源管理,备份恢复 -* 主题色修改,底部操作栏更明显 +* 正文规则添加字体规则,返回ByteArray +* js添加方法: -**2020/02/08** -* 书架分组调整顺序后,书架及时变动 +``` +base64DecodeToByteArray(str: String?): ByteArray? +base64DecodeToByteArray(str: String?, flags: Int): ByteArray? +``` -**2020/02/07** -* 优化 -* 书源校验 -* 书架整理 +**2020/11/07** -**2020/02/05** -* 修复bug -* Rss收藏功能完成 -* Rss已读标记不会再丢失 +* 详情页菜单添加拷贝URL +* 解决一些书名太长缓存报错的bug +* 添加备份搜索记录 +* 替换编辑界面添加正则学习教程 +* 去除解析目录时拼接相对url,提升解析速度 +* 自动分段优化 by [tumuyan](https://github.com/tumuyan) +* web支持图片显示 by [六月](https://github.com/Celeter) -**2020/02/04** -* 主界面切换时自动隐藏键盘 -* 添加本地书籍完成,解析txt文件完成,本地txt可以看了 -* 封面换源,书籍信息界面点击封面弹出封面换源界面 -* 默认封面绘制书名和作者 -* 修复在线朗读遇到单独标点,停止朗读的问题 +**2020/10/24** -**2020/02/02** -* merged commit e584606, rss修复BaseURL模式下部分图片无法加载, 修复可能出现的乱码 -* 菜单添加网址功能完成 +* 修复选择错误的bug +* 修复长图最后一张不能滚动的bug +* js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie + by [AndyBernie](https://github.com/AndyBernie) -**2020/01/31** -* 修复搜索闪退,因为默认线程为0了 +``` +java.getCookie("http://baidu.com", null) => userid=1234;pwd=adbcd +java.getCookie("http://baidu.com", "userid") => 1234 +``` -**2020/01/30** -* 优化缓存文件夹选择,不再需要存储权限 -* 修复替换净化导入报错的bug +* 修复简繁转换没有处理标题 +* 每本书可以单独设置翻页动画,在菜单里 +* 添加重新分段功能,针对每本书,在菜单里,分段代码来自[tumuyan](https://github.com/tumuyan) -**2020/01/27** -* 添加根据系统主题切换夜间模式 -* 合并Modificator提交的代码 +**2020/10/18** -**2020/01/26** -* 修复bug -* 未加入书架可查看目录 +* 优化分组管理,默认分组可以重命名了 +* 修复书架空白的bug,是constraintlayout库新版本的bug +* 修复分组和崩溃bug -**2020/01/24** -* 添加线程数配置 -* 记住退出时的书架 -* 添加屏幕超时配置 +**2020/10/11** -**2020/01/11** -* RSS阅读界面添加朗读功能 -* 其它一些优化 -* 合并KKL369提交的代码,重写LinearLayoutManager,修复书籍目录模糊搜索后scrollToPosition在可见范围不置顶 - -**2020/01/10** -* 合并KKL369提交的代码 - -**2020/01/08** -* 导入本地源不再需要存储权限 - -**2020/01/07** -* 修复备份问题 -* 设置背景不再需要存储权限 - -**2020/01/06** -* 适配Android 10 权限 -* 备份恢复不再需要存储权限 - -**2020/01/03** -* 适配Android 10 权限 -* 导入旧版本配置不在需要存储权限 -* 选择字体不在需要存储权限 -* 修改书源调试 - - 调试搜索>>输入关键字,如:`系统` - - 调试发现>>输入发现URL,如:`月票榜::https://www.qidian.com/rank/yuepiao?page={{page}}` - - 调试详情页>>输入详情页URL,如:`https://m.qidian.com/book/1015609210` - - 调试目录页>>输入目录页URL,如:`++https://www.zhaishuyuan.com/read/30394` - - 调试正文页>>输入正文页URL,如:`--https://www.zhaishuyuan.com/chapter/30394/20940996` -* 修改订阅中自动添加style的情景 - 订阅源的内容规则中存在` + + +
+ + + + \ No newline at end of file diff --git a/app/src/main/assets/web/bookshelf/js/about.9f8f9ac0.js b/app/src/main/assets/web/bookshelf/js/about.9f8f9ac0.js new file mode 100644 index 000000000..5b062a0d6 --- /dev/null +++ b/app/src/main/assets/web/bookshelf/js/about.9f8f9ac0.js @@ -0,0 +1 @@ +(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["about"],{"04d1":function(t,e,n){var r=n("342f"),a=r.match(/firefox\/(\d+)/i);t.exports=!!a&&+a[1]},"0cb2":function(t,e,n){var r=n("7b0b"),a=Math.floor,i="".replace,s=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,c=/\$([$&'`]|\d{1,2})/g;t.exports=function(t,e,n,o,l,u){var d=n+t.length,f=o.length,h=c;return void 0!==l&&(l=r(l),h=s),i.call(u,h,(function(r,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,n);case"'":return e.slice(d);case"<":s=l[i.slice(1,-1)];break;default:var c=+i;if(0===c)return r;if(c>f){var u=a(c/10);return 0===u?r:u<=f?void 0===o[u-1]?i.charAt(1):o[u-1]+i.charAt(1):r}s=o[c-1]}return void 0===s?"":s}))}},"4d63":function(t,e,n){var r=n("83ab"),a=n("da84"),i=n("94ca"),s=n("7156"),c=n("9112"),o=n("9bf2").f,l=n("241c").f,u=n("44e7"),d=n("ad6d"),f=n("9f7f"),h=n("6eeb"),g=n("d039"),v=n("5135"),p=n("69f3").enforce,m=n("2626"),b=n("b622"),w=n("fce3"),x=n("107c"),C=b("match"),A=a.RegExp,I=A.prototype,R=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,y=/a/g,k=/a/g,E=new A(y)!==y,M=f.UNSUPPORTED_Y,S=r&&(!E||M||w||x||g((function(){return k[C]=!1,A(y)!=y||A(k)==k||"/a/i"!=A(y,"i")}))),B=function(t){for(var e,n=t.length,r=0,a="",i=!1;r<=n;r++)e=t.charAt(r),"\\"!==e?i||"."!==e?("["===e?i=!0:"]"===e&&(i=!1),a+=e):a+="[\\s\\S]":a+=e+t.charAt(++r);return a},T=function(t){for(var e,n=t.length,r=0,a="",i=[],s={},c=!1,o=!1,l=0,u="";r<=n;r++){if(e=t.charAt(r),"\\"===e)e+=t.charAt(++r);else if("]"===e)c=!1;else if(!c)switch(!0){case"["===e:c=!0;break;case"("===e:R.test(t.slice(r+1))&&(r+=2,o=!0),a+=e,l++;continue;case">"===e&&o:if(""===u||v(s,u))throw new SyntaxError("Invalid capture group name");s[u]=!0,i.push([u,l]),o=!1,u="";continue}o?u+=e:a+=e}return[a,i]};if(i("RegExp",S)){for(var $=function(t,e){var n,r,a,i,o,l,f=this instanceof $,h=u(t),g=void 0===e,v=[],m=t;if(!f&&h&&g&&t.constructor===$)return t;if((h||t instanceof $)&&(t=t.source,g&&(e="flags"in m?m.flags:d.call(m))),t=void 0===t?"":String(t),e=void 0===e?"":String(e),m=t,w&&"dotAll"in y&&(r=!!e&&e.indexOf("s")>-1,r&&(e=e.replace(/s/g,""))),n=e,M&&"sticky"in y&&(a=!!e&&e.indexOf("y")>-1,a&&(e=e.replace(/y/g,""))),x&&(i=T(t),t=i[0],v=i[1]),o=s(A(t,e),f?this:I,$),(r||a||v.length)&&(l=p(o),r&&(l.dotAll=!0,l.raw=$(B(t),n)),a&&(l.sticky=!0),v.length&&(l.groups=v)),t!==m)try{c(o,"source",""===m?"(?:)":m)}catch(b){}return o},_=function(t){t in $||o($,t,{configurable:!0,get:function(){return A[t]},set:function(e){A[t]=e}})},D=l(A),z=0;D.length>z;)_(D[z++]);I.constructor=$,$.prototype=I,h(a,"RegExp",$)}m("RegExp")},"4e82":function(t,e,n){"use strict";var r=n("23e7"),a=n("1c0b"),i=n("7b0b"),s=n("50c4"),c=n("d039"),o=n("addb"),l=n("a640"),u=n("04d1"),d=n("d998"),f=n("2d00"),h=n("512c"),g=[],v=g.sort,p=c((function(){g.sort(void 0)})),m=c((function(){g.sort(null)})),b=l("sort"),w=!c((function(){if(f)return f<70;if(!(u&&u>3)){if(d)return!0;if(h)return h<603;var t,e,n,r,a="";for(t=65;t<76;t++){switch(e=String.fromCharCode(t),t){case 66:case 69:case 70:case 72:n=3;break;case 68:case 71:n=4;break;default:n=2}for(r=0;r<47;r++)g.push({k:e+r,v:n})}for(g.sort((function(t,e){return e.v-t.v})),r=0;rString(n)?1:-1}};r({target:"Array",proto:!0,forced:x},{sort:function(t){void 0!==t&&a(t);var e=i(this);if(w)return void 0===t?v.call(e):v.call(e,t);var n,r,c=[],l=s(e.length);for(r=0;r")}));r("replace",(function(t,e,n){var r=b?"$":"$0";return[function(t,n){var r=o(this),a=void 0==t?void 0:t[h];return void 0!==a?a.call(t,r,n):e.call(String(r),t,n)},function(t,a){if("string"===typeof a&&-1===a.indexOf(r)&&-1===a.indexOf("$<")){var o=n(e,this,t,a);if(o.done)return o.value}var f=i(this),h=String(t),m="function"===typeof a;m||(a=String(a));var b=f.global;if(b){var w=f.unicode;f.lastIndex=0}var x=[];while(1){var C=d(f,h);if(null===C)break;if(x.push(C),!b)break;var A=String(C[0]);""===A&&(f.lastIndex=l(h,s(f.lastIndex),w))}for(var I="",R=0,y=0;y=R&&(I+=h.slice(R,E)+$,R=E+k.length)}return I+h.slice(R)}]}),!w||!m||b)},"6e9d":function(t,e,n){},"6fb2":function(t,e,n){"use strict";n("6e9d")},7156:function(t,e,n){var r=n("861d"),a=n("d2bb");t.exports=function(t,e,n){var i,s;return a&&"function"==typeof(i=e.constructor)&&i!==n&&r(s=i.prototype)&&s!==n.prototype&&a(t,s),t}},"7b5b":function(t,e,n){},a640:function(t,e,n){"use strict";var r=n("d039");t.exports=function(t,e){var n=[][t];return!!n&&r((function(){n.call(null,e||function(){throw 1},1)}))}},addb:function(t,e){var n=Math.floor,r=function(t,e){var s=t.length,c=n(s/2);return s<8?a(t,e):i(r(t.slice(0,c),e),r(t.slice(c),e),e)},a=function(t,e){var n,r,a=t.length,i=1;while(i0)t[r]=t[--r];r!==i++&&(t[r]=n)}return t},i=function(t,e,n){var r=t.length,a=e.length,i=0,s=0,c=[];while(ib)","string".charAt(5));return"b"!==e.exec("b").groups.a||"bc"!=="b".replace(e,"$c")}))},"14c3":function(e,t,n){var r=n("c6b6"),o=n("9263");e.exports=function(e,t){var n=e.exec;if("function"===typeof n){var i=n.call(e,t);if("object"!==typeof i)throw TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==r(e))throw TypeError("RegExp#exec called on incompatible receiver");return o.call(e,t)}},"1d2b":function(e,t,n){"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};r.forEach(["delete","get","head"],(function(e){c.headers[e]={}})),r.forEach(["post","put","patch"],(function(e){c.headers[e]=r.merge(i)})),e.exports=c}).call(this,n("4362"))},"2d83":function(e,t,n){"use strict";var r=n("387f");e.exports=function(e,t,n,o,i){var s=new Error(e);return r(s,t,n,o,i)}},"2e67":function(e,t,n){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},"30b5":function(e,t,n){"use strict";var r=n("c532");function o(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}e.exports=function(e,t,n){if(!t)return e;var i;if(n)i=n(t);else if(r.isURLSearchParams(t))i=t.toString();else{var s=[];r.forEach(t,(function(e,t){null!==e&&"undefined"!==typeof e&&(r.isArray(e)?t+="[]":e=[e],r.forEach(e,(function(e){r.isDate(e)?e=e.toISOString():r.isObject(e)&&(e=JSON.stringify(e)),s.push(o(t)+"="+o(e))})))})),i=s.join("&")}if(i){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+i}return e}},"387f":function(e,t,n){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},3934:function(e,t,n){"use strict";var r=n("c532");e.exports=r.isStandardBrowserEnv()?function(){var e,t=/(msie|trident)/i.test(navigator.userAgent),n=document.createElement("a");function o(e){var r=e;return t&&(n.setAttribute("href",r),r=n.href),n.setAttribute("href",r),{href:n.href,protocol:n.protocol?n.protocol.replace(/:$/,""):"",host:n.host,search:n.search?n.search.replace(/^\?/,""):"",hash:n.hash?n.hash.replace(/^#/,""):"",hostname:n.hostname,port:n.port,pathname:"/"===n.pathname.charAt(0)?n.pathname:"/"+n.pathname}}return e=o(window.location.href),function(t){var n=r.isString(t)?o(t):t;return n.protocol===e.protocol&&n.host===e.host}}():function(){return function(){return!0}}()},4362:function(e,t,n){t.nextTick=function(e){var t=Array.prototype.slice.call(arguments);t.shift(),setTimeout((function(){e.apply(null,t)}),0)},t.platform=t.arch=t.execPath=t.title="browser",t.pid=1,t.browser=!0,t.env={},t.argv=[],t.binding=function(e){throw new Error("No such module. (Possibly not yet loaded)")},function(){var e,r="/";t.cwd=function(){return r},t.chdir=function(t){e||(e=n("df7c")),r=e.resolve(t,r)}}(),t.exit=t.kill=t.umask=t.dlopen=t.uptime=t.memoryUsage=t.uvCounters=function(){},t.features={}},"44e7":function(e,t,n){var r=n("861d"),o=n("c6b6"),i=n("b622"),s=i("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[s])?!!t:"RegExp"==o(e))}},"467f":function(e,t,n){"use strict";var r=n("2d83");e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},"4a7b":function(e,t,n){"use strict";var r=n("c532");e.exports=function(e,t){t=t||{};var n={},o=["url","method","data"],i=["headers","auth","proxy","params"],s=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],a=["validateStatus"];function c(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function u(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(n[o]=c(void 0,e[o])):n[o]=c(e[o],t[o])}r.forEach(o,(function(e){r.isUndefined(t[e])||(n[e]=c(void 0,t[e]))})),r.forEach(i,u),r.forEach(s,(function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(n[o]=c(void 0,e[o])):n[o]=c(void 0,t[o])})),r.forEach(a,(function(r){r in t?n[r]=c(e[r],t[r]):r in e&&(n[r]=c(void 0,e[r]))}));var f=o.concat(i).concat(s).concat(a),l=Object.keys(e).concat(Object.keys(t)).filter((function(e){return-1===f.indexOf(e)}));return r.forEach(l,u),n}},5270:function(e,t,n){"use strict";var r=n("c532"),o=n("c401"),i=n("2e67"),s=n("2444");function a(e){e.cancelToken&&e.cancelToken.throwIfRequested()}e.exports=function(e){a(e),e.headers=e.headers||{},e.data=o(e.data,e.headers,e.transformRequest),e.headers=r.merge(e.headers.common||{},e.headers[e.method]||{},e.headers),r.forEach(["delete","get","head","post","put","patch","common"],(function(t){delete e.headers[t]}));var t=e.adapter||s.adapter;return t(e).then((function(t){return a(e),t.data=o(t.data,t.headers,e.transformResponse),t}),(function(t){return i(t)||(a(e),t&&t.response&&(t.response.data=o(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)}))}},"5f02":function(e,t,n){"use strict";e.exports=function(e){return"object"===typeof e&&!0===e.isAxiosError}},"7a77":function(e,t,n){"use strict";function r(e){this.message=e}r.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},r.prototype.__CANCEL__=!0,e.exports=r},"7aac":function(e,t,n){"use strict";var r=n("c532");e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,i,s){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(i)&&a.push("domain="+i),!0===s&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},"83b9":function(e,t,n){"use strict";var r=n("d925"),o=n("e683");e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},"8aa5":function(e,t,n){"use strict";var r=n("6547").charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},"8df4":function(e,t,n){"use strict";var r=n("7a77");function o(e){if("function"!==typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise((function(e){t=e}));var n=this;e((function(e){n.reason||(n.reason=new r(e),t(n.reason))}))}o.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},o.source=function(){var e,t=new o((function(t){e=t}));return{token:t,cancel:e}},e.exports=o},9263:function(e,t,n){"use strict";var r=n("ad6d"),o=n("9f7f"),i=n("5692"),s=n("7c73"),a=n("69f3").get,c=n("fce3"),u=n("107c"),f=RegExp.prototype.exec,l=i("native-string-replace",String.prototype.replace),p=f,d=function(){var e=/a/,t=/b*/g;return f.call(e,"a"),f.call(t,"a"),0!==e.lastIndex||0!==t.lastIndex}(),h=o.UNSUPPORTED_Y||o.BROKEN_CARET,m=void 0!==/()??/.exec("")[1],g=d||m||h||c||u;g&&(p=function(e){var t,n,o,i,c,u,g,v=this,x=a(v),y=x.raw;if(y)return y.lastIndex=v.lastIndex,t=p.call(y,e),v.lastIndex=y.lastIndex,t;var b=x.groups,w=h&&v.sticky,E=r.call(v),R=v.source,A=0,C=e;if(w&&(E=E.replace("y",""),-1===E.indexOf("g")&&(E+="g"),C=String(e).slice(v.lastIndex),v.lastIndex>0&&(!v.multiline||v.multiline&&"\n"!==e[v.lastIndex-1])&&(R="(?: "+R+")",C=" "+C,A++),n=new RegExp("^(?:"+R+")",E)),m&&(n=new RegExp("^"+R+"$(?!\\s)",E)),d&&(o=v.lastIndex),i=f.call(w?n:v,C),w?i?(i.input=i.input.slice(A),i[0]=i[0].slice(A),i.index=v.lastIndex,v.lastIndex+=i[0].length):v.lastIndex=0:d&&i&&(v.lastIndex=v.global?i.index+i[0].length:o),m&&i&&i.length>1&&l.call(i[0],n,(function(){for(c=1;c=0)return;s[t]="set-cookie"===t?(s[t]?s[t]:[]).concat([n]):s[t]?s[t]+", "+n:n}})),s):s}},c401:function(e,t,n){"use strict";var r=n("c532");e.exports=function(e,t,n){return r.forEach(n,(function(n){e=n(e,t)})),e}},c532:function(e,t,n){"use strict";var r=n("1d2b"),o=Object.prototype.toString;function i(e){return"[object Array]"===o.call(e)}function s(e){return"undefined"===typeof e}function a(e){return null!==e&&!s(e)&&null!==e.constructor&&!s(e.constructor)&&"function"===typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function c(e){return"[object ArrayBuffer]"===o.call(e)}function u(e){return"undefined"!==typeof FormData&&e instanceof FormData}function f(e){var t;return t="undefined"!==typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer,t}function l(e){return"string"===typeof e}function p(e){return"number"===typeof e}function d(e){return null!==e&&"object"===typeof e}function h(e){if("[object Object]"!==o.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function m(e){return"[object Date]"===o.call(e)}function g(e){return"[object File]"===o.call(e)}function v(e){return"[object Blob]"===o.call(e)}function x(e){return"[object Function]"===o.call(e)}function y(e){return d(e)&&x(e.pipe)}function b(e){return"undefined"!==typeof URLSearchParams&&e instanceof URLSearchParams}function w(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function E(){return("undefined"===typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!==typeof window&&"undefined"!==typeof document)}function R(e,t){if(null!==e&&"undefined"!==typeof e)if("object"!==typeof e&&(e=[e]),i(e))for(var n=0,r=e.length;n=0;r--){var o=e[r];"."===o?e.splice(r,1):".."===o?(e.splice(r,1),n++):n&&(e.splice(r,1),n--)}if(t)for(;n--;n)e.unshift("..");return e}function r(e){"string"!==typeof e&&(e+="");var t,n=0,r=-1,o=!0;for(t=e.length-1;t>=0;--t)if(47===e.charCodeAt(t)){if(!o){n=t+1;break}}else-1===r&&(o=!1,r=t+1);return-1===r?"":e.slice(n,r)}function o(e,t){if(e.filter)return e.filter(t);for(var n=[],r=0;r=-1&&!r;i--){var s=i>=0?arguments[i]:e.cwd();if("string"!==typeof s)throw new TypeError("Arguments to path.resolve must be strings");s&&(t=s+"/"+t,r="/"===s.charAt(0))}return t=n(o(t.split("/"),(function(e){return!!e})),!r).join("/"),(r?"/":"")+t||"."},t.normalize=function(e){var r=t.isAbsolute(e),s="/"===i(e,-1);return e=n(o(e.split("/"),(function(e){return!!e})),!r).join("/"),e||r||(e="."),e&&s&&(e+="/"),(r?"/":"")+e},t.isAbsolute=function(e){return"/"===e.charAt(0)},t.join=function(){var e=Array.prototype.slice.call(arguments,0);return t.normalize(o(e,(function(e,t){if("string"!==typeof e)throw new TypeError("Arguments to path.join must be strings");return e})).join("/"))},t.relative=function(e,n){function r(e){for(var t=0;t=0;n--)if(""!==e[n])break;return t>n?[]:e.slice(t,n-t+1)}e=t.resolve(e).substr(1),n=t.resolve(n).substr(1);for(var o=r(e.split("/")),i=r(n.split("/")),s=Math.min(o.length,i.length),a=s,c=0;c=1;--i)if(t=e.charCodeAt(i),47===t){if(!o){r=i;break}}else o=!1;return-1===r?n?"/":".":n&&1===r?"/":e.slice(0,r)},t.basename=function(e,t){var n=r(e);return t&&n.substr(-1*t.length)===t&&(n=n.substr(0,n.length-t.length)),n},t.extname=function(e){"string"!==typeof e&&(e+="");for(var t=-1,n=0,r=-1,o=!0,i=0,s=e.length-1;s>=0;--s){var a=e.charCodeAt(s);if(47!==a)-1===r&&(o=!1,r=s+1),46===a?-1===t?t=s:1!==i&&(i=1):-1!==t&&(i=-1);else if(!o){n=s+1;break}}return-1===t||-1===r||0===i||1===i&&t===r-1&&t===n+1?"":e.slice(t,r)};var i="b"==="ab".substr(-1)?function(e,t,n){return e.substr(t,n)}:function(e,t,n){return t<0&&(t=e.length+t),e.substr(t,n)}}).call(this,n("4362"))},e683:function(e,t,n){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},f6b4:function(e,t,n){"use strict";var r=n("c532");function o(){this.handlers=[]}o.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},o.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},o.prototype.forEach=function(e){r.forEach(this.handlers,(function(t){null!==t&&e(t)}))},e.exports=o},fce3:function(e,t,n){var r=n("d039");e.exports=r((function(){var e=RegExp(".","string".charAt(0));return!(e.dotAll&&e.exec("\n")&&"s"===e.flags)}))}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/bookshelf/js/app.e84ee963.js b/app/src/main/assets/web/bookshelf/js/app.e84ee963.js new file mode 100644 index 000000000..f59f100b4 --- /dev/null +++ b/app/src/main/assets/web/bookshelf/js/app.e84ee963.js @@ -0,0 +1 @@ +(function(e){function t(t){for(var o,r,i=t[0],c=t[1],l=t[2],f=0,d=[];f>>32-t}function o(e,t){var n,o,r,a,u;return r=2147483648&e,a=2147483648&t,n=1073741824&e,o=1073741824&t,u=(1073741823&e)+(1073741823&t),n&o?2147483648^u^r^a:n|o?1073741824&u?3221225472^u^r^a:1073741824^u^r^a:u^r^a}function r(e,t,n){return e&t|~e&n}function a(e,t,n){return e&n|t&~n}function u(e,t,n){return e^t^n}function i(e,t,n){return t^(e|~n)}function c(e,t,a,u,i,c,l){return e=o(e,o(o(r(t,a,u),i),l)),o(n(e,c),t)}function l(e,t,r,u,i,c,l){return e=o(e,o(o(a(t,r,u),i),l)),o(n(e,c),t)}function f(e,t,r,a,i,c,l){return e=o(e,o(o(u(t,r,a),i),l)),o(n(e,c),t)}function d(e,t,r,a,u,c,l){return e=o(e,o(o(i(t,r,a),u),l)),o(n(e,c),t)}function s(e){var t,n=e.length,o=n+8,r=(o-o%64)/64,a=16*(r+1),u=Array(a-1),i=0,c=0;while(c>>29,u}function p(e){var t,n,o="",r="";for(n=0;n<=3;n++)t=e>>>8*n&255,r="0"+t.toString(16),o+=r.substr(r.length-2,2);return o}var b,h,g,m,v,y,w,C,S,P=Array(),O=7,j=12,k=17,T=22,_=5,x=9,E=14,A=20,L=4,$=11,B=16,N=23,V=6,M=10,D=15,q=21;for(P=s(t),y=1732584193,w=4023233417,C=2562383102,S=271733878,b=0;bd;d++)if(h=C(e[d]),h&&h instanceof u)return h;return new u(!1)}c=f.call(e)}v=c.next;while(!(m=v.call(c)).done){try{h=C(m.value)}catch(S){throw l(c),S}if("object"==typeof h&&h&&h instanceof u)return h}return new u(!1)}},"23cb":function(e,t,n){var r=n("a691"),o=Math.max,i=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):i(n,t)}},"23e7":function(e,t,n){var r=n("da84"),o=n("06cf").f,i=n("9112"),a=n("6eeb"),s=n("ce4e"),l=n("e893"),u=n("94ca");e.exports=function(e,t){var n,c,f,d,p,h,v=e.target,m=e.global,y=e.stat;if(c=m?r:y?r[v]||s(v,{}):(r[v]||{}).prototype,c)for(f in t){if(p=t[f],e.noTargetGet?(h=o(c,f),d=h&&h.value):d=c[f],n=u(m?f:v+(y?".":"#")+f,e.forced),!n&&void 0!==d){if(typeof p===typeof d)continue;l(p,d)}(e.sham||d&&d.sham)&&i(p,"sham",!0),a(c,f,p,e)}}},"241c":function(e,t,n){var r=n("ca84"),o=n("7839"),i=o.concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},"25f0":function(e,t,n){"use strict";var r=n("6eeb"),o=n("825a"),i=n("d039"),a=n("ad6d"),s="toString",l=RegExp.prototype,u=l[s],c=i((function(){return"/a/b"!=u.call({source:"a",flags:"b"})})),f=u.name!=s;(c||f)&&r(RegExp.prototype,s,(function(){var e=o(this),t=String(e.source),n=e.flags,r=String(void 0===n&&e instanceof RegExp&&!("flags"in l)?a.call(e):n);return"/"+t+"/"+r}),{unsafe:!0})},2626:function(e,t,n){"use strict";var r=n("d066"),o=n("9bf2"),i=n("b622"),a=n("83ab"),s=i("species");e.exports=function(e){var t=r(e),n=o.f;a&&t&&!t[s]&&n(t,s,{configurable:!0,get:function(){return this}})}},2877:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},"299c":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=135)}({135:function(e,t,n){"use strict";n.r(t);var r=n(5),o=n.n(r),i=n(17),a=n.n(i),s=n(2),l=n(3),u=n(7),c=n.n(u),f={name:"ElTooltip",mixins:[o.a],props:{openDelay:{type:Number,default:0},disabled:Boolean,manual:Boolean,effect:{type:String,default:"dark"},arrowOffset:{type:Number,default:0},popperClass:String,content:String,visibleArrow:{default:!0},transition:{type:String,default:"el-fade-in-linear"},popperOptions:{default:function(){return{boundariesPadding:10,gpuAcceleration:!1}}},enterable:{type:Boolean,default:!0},hideAfter:{type:Number,default:0},tabindex:{type:Number,default:0}},data:function(){return{tooltipId:"el-tooltip-"+Object(l["generateId"])(),timeoutPending:null,focusing:!1}},beforeCreate:function(){var e=this;this.$isServer||(this.popperVM=new c.a({data:{node:""},render:function(e){return this.node}}).$mount(),this.debounceClose=a()(200,(function(){return e.handleClosePopper()})))},render:function(e){var t=this;this.popperVM&&(this.popperVM.node=e("transition",{attrs:{name:this.transition},on:{afterLeave:this.doDestroy}},[e("div",{on:{mouseleave:function(){t.setExpectedState(!1),t.debounceClose()},mouseenter:function(){t.setExpectedState(!0)}},ref:"popper",attrs:{role:"tooltip",id:this.tooltipId,"aria-hidden":this.disabled||!this.showPopper?"true":"false"},directives:[{name:"show",value:!this.disabled&&this.showPopper}],class:["el-tooltip__popper","is-"+this.effect,this.popperClass]},[this.$slots.content||this.content])]));var n=this.getFirstElement();if(!n)return null;var r=n.data=n.data||{};return r.staticClass=this.addTooltipClass(r.staticClass),n},mounted:function(){var e=this;this.referenceElm=this.$el,1===this.$el.nodeType&&(this.$el.setAttribute("aria-describedby",this.tooltipId),this.$el.setAttribute("tabindex",this.tabindex),Object(s["on"])(this.referenceElm,"mouseenter",this.show),Object(s["on"])(this.referenceElm,"mouseleave",this.hide),Object(s["on"])(this.referenceElm,"focus",(function(){if(e.$slots.default&&e.$slots.default.length){var t=e.$slots.default[0].componentInstance;t&&t.focus?t.focus():e.handleFocus()}else e.handleFocus()})),Object(s["on"])(this.referenceElm,"blur",this.handleBlur),Object(s["on"])(this.referenceElm,"click",this.removeFocusing)),this.value&&this.popperVM&&this.popperVM.$nextTick((function(){e.value&&e.updatePopper()}))},watch:{focusing:function(e){e?Object(s["addClass"])(this.referenceElm,"focusing"):Object(s["removeClass"])(this.referenceElm,"focusing")}},methods:{show:function(){this.setExpectedState(!0),this.handleShowPopper()},hide:function(){this.setExpectedState(!1),this.debounceClose()},handleFocus:function(){this.focusing=!0,this.show()},handleBlur:function(){this.focusing=!1,this.hide()},removeFocusing:function(){this.focusing=!1},addTooltipClass:function(e){return e?"el-tooltip "+e.replace("el-tooltip",""):"el-tooltip"},handleShowPopper:function(){var e=this;this.expectedState&&!this.manual&&(clearTimeout(this.timeout),this.timeout=setTimeout((function(){e.showPopper=!0}),this.openDelay),this.hideAfter>0&&(this.timeoutPending=setTimeout((function(){e.showPopper=!1}),this.hideAfter)))},handleClosePopper:function(){this.enterable&&this.expectedState||this.manual||(clearTimeout(this.timeout),this.timeoutPending&&clearTimeout(this.timeoutPending),this.showPopper=!1,this.disabled&&this.doDestroy())},setExpectedState:function(e){!1===e&&clearTimeout(this.timeoutPending),this.expectedState=e},getFirstElement:function(){var e=this.$slots.default;if(!Array.isArray(e))return null;for(var t=null,n=0;n=0&&Math.floor(t)===t&&isFinite(e)}function p(e){return o(e)&&"function"===typeof e.then&&"function"===typeof e.catch}function h(e){return null==e?"":Array.isArray(e)||c(e)&&e.toString===u?JSON.stringify(e,null,2):String(e)}function v(e){var t=parseFloat(e);return isNaN(t)?e:t}function m(e,t){for(var n=Object.create(null),r=e.split(","),o=0;o-1)return e.splice(n,1)}}var b=Object.prototype.hasOwnProperty;function _(e,t){return b.call(e,t)}function x(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}var w=/-(\w)/g,C=x((function(e){return e.replace(w,(function(e,t){return t?t.toUpperCase():""}))})),S=x((function(e){return e.charAt(0).toUpperCase()+e.slice(1)})),O=/\B([A-Z])/g,E=x((function(e){return e.replace(O,"-$1").toLowerCase()}));function k(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function $(e,t){return e.bind(t)}var j=Function.prototype.bind?$:k;function T(e,t){t=t||0;var n=e.length-t,r=new Array(n);while(n--)r[n]=e[n+t];return r}function A(e,t){for(var n in t)e[n]=t[n];return e}function M(e){for(var t={},n=0;n0,ne=Q&&Q.indexOf("edge/")>0,re=(Q&&Q.indexOf("android"),Q&&/iphone|ipad|ipod|ios/.test(Q)||"ios"===Z),oe=(Q&&/chrome\/\d+/.test(Q),Q&&/phantomjs/.test(Q),Q&&Q.match(/firefox\/(\d+)/)),ie={}.watch,ae=!1;if(Y)try{var se={};Object.defineProperty(se,"passive",{get:function(){ae=!0}}),window.addEventListener("test-passive",null,se)}catch(Sa){}var le=function(){return void 0===G&&(G=!Y&&!J&&"undefined"!==typeof e&&(e["process"]&&"server"===e["process"].env.VUE_ENV)),G},ue=Y&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function ce(e){return"function"===typeof e&&/native code/.test(e.toString())}var fe,de="undefined"!==typeof Symbol&&ce(Symbol)&&"undefined"!==typeof Reflect&&ce(Reflect.ownKeys);fe="undefined"!==typeof Set&&ce(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var pe=P,he=0,ve=function(){this.id=he++,this.subs=[]};ve.prototype.addSub=function(e){this.subs.push(e)},ve.prototype.removeSub=function(e){g(this.subs,e)},ve.prototype.depend=function(){ve.target&&ve.target.addDep(this)},ve.prototype.notify=function(){var e=this.subs.slice();for(var t=0,n=e.length;t-1)if(i&&!_(o,"default"))a=!1;else if(""===a||a===E(e)){var l=tt(String,o.type);(l<0||s0&&(a=$t(a,(t||"")+"_"+n),kt(a[0])&&kt(u)&&(c[l]=we(u.text+a[0].text),a.shift()),c.push.apply(c,a)):s(a)?kt(u)?c[l]=we(u.text+a):""!==a&&c.push(we(a)):kt(a)&&kt(u)?c[l]=we(u.text+a.text):(i(e._isVList)&&o(a.tag)&&r(a.key)&&o(t)&&(a.key="__vlist"+t+"_"+n+"__"),c.push(a)));return c}function jt(e){var t=e.$options.provide;t&&(e._provided="function"===typeof t?t.call(e):t)}function Tt(e){var t=At(e.$options.inject,e);t&&(je(!1),Object.keys(t).forEach((function(n){Le(e,n,t[n])})),je(!0))}function At(e,t){if(e){for(var n=Object.create(null),r=de?Reflect.ownKeys(e):Object.keys(e),o=0;o0,a=e?!!e.$stable:!i,s=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(a&&r&&r!==n&&s===r.$key&&!i&&!r.$hasNormal)return r;for(var l in o={},e)e[l]&&"$"!==l[0]&&(o[l]=Nt(t,l,e[l]))}else o={};for(var u in t)u in o||(o[u]=It(t,u));return e&&Object.isExtensible(e)&&(e._normalized=o),U(o,"$stable",a),U(o,"$key",s),U(o,"$hasNormal",i),o}function Nt(e,t,n){var r=function(){var e=arguments.length?n.apply(null,arguments):n({});e=e&&"object"===typeof e&&!Array.isArray(e)?[e]:Et(e);var t=e&&e[0];return e&&(!t||1===e.length&&t.isComment&&!Lt(t))?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function It(e,t){return function(){return e[t]}}function Ft(e,t){var n,r,i,a,s;if(Array.isArray(e)||"string"===typeof e)for(n=new Array(e.length),r=0,i=e.length;r1?T(n):n;for(var r=T(arguments,1),o='event handler for "'+e+'"',i=0,a=n.length;idocument.createEvent("Event").timeStamp&&(Xn=function(){return Yn.now()})}function Jn(){var e,t;for(Gn=Xn(),Un=!0,zn.sort((function(e,t){return e.id-t.id})),qn=0;qnqn&&zn[n].id>e.id)n--;zn.splice(n+1,0,e)}else zn.push(e);Vn||(Vn=!0,vt(Jn))}}var nr=0,rr=function(e,t,n,r,o){this.vm=e,o&&(e._watcher=this),e._watchers.push(this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++nr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new fe,this.newDepIds=new fe,this.expression="","function"===typeof t?this.getter=t:(this.getter=K(t),this.getter||(this.getter=P)),this.value=this.lazy?void 0:this.get()};rr.prototype.get=function(){var e;ye(this);var t=this.vm;try{e=this.getter.call(t,t)}catch(Sa){if(!this.user)throw Sa;nt(Sa,t,'getter for watcher "'+this.expression+'"')}finally{this.deep&&yt(e),ge(),this.cleanupDeps()}return e},rr.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},rr.prototype.cleanupDeps=function(){var e=this.deps.length;while(e--){var t=this.deps[e];this.newDepIds.has(t.id)||t.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},rr.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():tr(this)},rr.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||l(e)||this.deep){var t=this.value;if(this.value=e,this.user){var n='callback for watcher "'+this.expression+'"';rt(this.cb,this.vm,[e,t],this.vm,n)}else this.cb.call(this.vm,e,t)}}},rr.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},rr.prototype.depend=function(){var e=this.deps.length;while(e--)this.deps[e].depend()},rr.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||g(this.vm._watchers,this);var e=this.deps.length;while(e--)this.deps[e].removeSub(this);this.active=!1}};var or={enumerable:!0,configurable:!0,get:P,set:P};function ir(e,t,n){or.get=function(){return this[t][n]},or.set=function(e){this[t][n]=e},Object.defineProperty(e,n,or)}function ar(e){e._watchers=[];var t=e.$options;t.props&&sr(e,t.props),t.methods&&vr(e,t.methods),t.data?lr(e):Pe(e._data={},!0),t.computed&&fr(e,t.computed),t.watch&&t.watch!==ie&&mr(e,t.watch)}function sr(e,t){var n=e.$options.propsData||{},r=e._props={},o=e.$options._propKeys=[],i=!e.$parent;i||je(!1);var a=function(i){o.push(i);var a=Ye(i,t,n,e);Le(r,i,a),i in e||ir(e,"_props",i)};for(var s in t)a(s);je(!0)}function lr(e){var t=e.$options.data;t=e._data="function"===typeof t?ur(t,e):t||{},c(t)||(t={});var n=Object.keys(t),r=e.$options.props,o=(e.$options.methods,n.length);while(o--){var i=n[o];0,r&&_(r,i)||V(i)||ir(e,"_data",i)}Pe(t,!0)}function ur(e,t){ye();try{return e.call(t,t)}catch(Sa){return nt(Sa,t,"data()"),{}}finally{ge()}}var cr={lazy:!0};function fr(e,t){var n=e._computedWatchers=Object.create(null),r=le();for(var o in t){var i=t[o],a="function"===typeof i?i:i.get;0,r||(n[o]=new rr(e,a||P,P,cr)),o in e||dr(e,o,i)}}function dr(e,t,n){var r=!le();"function"===typeof n?(or.get=r?pr(t):hr(n),or.set=P):(or.get=n.get?r&&!1!==n.cache?pr(t):hr(n.get):P,or.set=n.set||P),Object.defineProperty(e,t,or)}function pr(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),ve.target&&t.depend(),t.value}}function hr(e){return function(){return e.call(this,this)}}function vr(e,t){e.$options.props;for(var n in t)e[n]="function"!==typeof t[n]?P:j(t[n],e)}function mr(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var o=0;o-1)return this;var n=T(arguments,1);return n.unshift(this),"function"===typeof e.install?e.install.apply(e,n):"function"===typeof e&&e.apply(null,n),t.push(e),this}}function Er(e){e.mixin=function(e){return this.options=Ge(this.options,e),this}}function kr(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=n.cid,o=e._Ctor||(e._Ctor={});if(o[r])return o[r];var i=e.name||n.options.name;var a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=Ge(n.options,e),a["super"]=n,a.options.props&&$r(a),a.options.computed&&jr(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,D.forEach((function(e){a[e]=n[e]})),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=A({},a.options),o[r]=a,a}}function $r(e){var t=e.options.props;for(var n in t)ir(e.prototype,"_props",n)}function jr(e){var t=e.options.computed;for(var n in t)dr(e.prototype,n,t[n])}function Tr(e){D.forEach((function(t){e[t]=function(e,n){return n?("component"===t&&c(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"===typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}))}function Ar(e){return e&&(e.Ctor.options.name||e.tag)}function Mr(e,t){return Array.isArray(e)?e.indexOf(t)>-1:"string"===typeof e?e.split(",").indexOf(t)>-1:!!f(e)&&e.test(t)}function Pr(e,t){var n=e.cache,r=e.keys,o=e._vnode;for(var i in n){var a=n[i];if(a){var s=a.name;s&&!t(s)&&Lr(n,i,r,o)}}}function Lr(e,t,n,r){var o=e[t];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),e[t]=null,g(n,t)}_r(Sr),gr(Sr),Tn(Sr),Ln(Sr),_n(Sr);var Rr=[String,RegExp,Array],Nr={name:"keep-alive",abstract:!0,props:{include:Rr,exclude:Rr,max:[String,Number]},methods:{cacheVNode:function(){var e=this,t=e.cache,n=e.keys,r=e.vnodeToCache,o=e.keyToCache;if(r){var i=r.tag,a=r.componentInstance,s=r.componentOptions;t[o]={name:Ar(s),tag:i,componentInstance:a},n.push(o),this.max&&n.length>parseInt(this.max)&&Lr(t,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var e in this.cache)Lr(this.cache,e,this.keys)},mounted:function(){var e=this;this.cacheVNode(),this.$watch("include",(function(t){Pr(e,(function(e){return Mr(t,e)}))})),this.$watch("exclude",(function(t){Pr(e,(function(e){return!Mr(t,e)}))}))},updated:function(){this.cacheVNode()},render:function(){var e=this.$slots.default,t=Sn(e),n=t&&t.componentOptions;if(n){var r=Ar(n),o=this,i=o.include,a=o.exclude;if(i&&(!r||!Mr(i,r))||a&&r&&Mr(a,r))return t;var s=this,l=s.cache,u=s.keys,c=null==t.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):t.key;l[c]?(t.componentInstance=l[c].componentInstance,g(u,c),u.push(c)):(this.vnodeToCache=t,this.keyToCache=c),t.data.keepAlive=!0}return t||e&&e[0]}},Ir={KeepAlive:Nr};function Fr(e){var t={get:function(){return B}};Object.defineProperty(e,"config",t),e.util={warn:pe,extend:A,mergeOptions:Ge,defineReactive:Le},e.set=Re,e.delete=Ne,e.nextTick=vt,e.observable=function(e){return Pe(e),e},e.options=Object.create(null),D.forEach((function(t){e.options[t+"s"]=Object.create(null)})),e.options._base=e,A(e.options.components,Ir),Or(e),Er(e),kr(e),Tr(e)}Fr(Sr),Object.defineProperty(Sr.prototype,"$isServer",{get:le}),Object.defineProperty(Sr.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Sr,"FunctionalRenderContext",{value:Qt}),Sr.version="2.6.14";var Hr=m("style,class"),Dr=m("input,textarea,option,select,progress"),zr=function(e,t,n){return"value"===n&&Dr(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Br=m("contenteditable,draggable,spellcheck"),Wr=m("events,caret,typing,plaintext-only"),Vr=function(e,t){return Xr(t)||"false"===t?"false":"contenteditable"===e&&Wr(t)?t:"true"},Ur=m("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qr="http://www.w3.org/1999/xlink",Kr=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Gr=function(e){return Kr(e)?e.slice(6,e.length):""},Xr=function(e){return null==e||!1===e};function Yr(e){var t=e.data,n=e,r=e;while(o(r.componentInstance))r=r.componentInstance._vnode,r&&r.data&&(t=Jr(r.data,t));while(o(n=n.parent))n&&n.data&&(t=Jr(t,n.data));return Zr(t.staticClass,t.class)}function Jr(e,t){return{staticClass:Qr(e.staticClass,t.staticClass),class:o(e.class)?[e.class,t.class]:t.class}}function Zr(e,t){return o(e)||o(t)?Qr(e,eo(t)):""}function Qr(e,t){return e?t?e+" "+t:e:t||""}function eo(e){return Array.isArray(e)?to(e):l(e)?no(e):"string"===typeof e?e:""}function to(e){for(var t,n="",r=0,i=e.length;r-1?lo[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:lo[e]=/HTMLUnknownElement/.test(t.toString())}var co=m("text,number,password,search,email,tel,url");function fo(e){if("string"===typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}function po(e,t){var n=document.createElement(e);return"select"!==e||t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n}function ho(e,t){return document.createElementNS(ro[e],t)}function vo(e){return document.createTextNode(e)}function mo(e){return document.createComment(e)}function yo(e,t,n){e.insertBefore(t,n)}function go(e,t){e.removeChild(t)}function bo(e,t){e.appendChild(t)}function _o(e){return e.parentNode}function xo(e){return e.nextSibling}function wo(e){return e.tagName}function Co(e,t){e.textContent=t}function So(e,t){e.setAttribute(t,"")}var Oo=Object.freeze({createElement:po,createElementNS:ho,createTextNode:vo,createComment:mo,insertBefore:yo,removeChild:go,appendChild:bo,parentNode:_o,nextSibling:xo,tagName:wo,setTextContent:Co,setStyleScope:So}),Eo={create:function(e,t){ko(t)},update:function(e,t){e.data.ref!==t.data.ref&&(ko(e,!0),ko(t))},destroy:function(e){ko(e,!0)}};function ko(e,t){var n=e.data.ref;if(o(n)){var r=e.context,i=e.componentInstance||e.elm,a=r.$refs;t?Array.isArray(a[n])?g(a[n],i):a[n]===i&&(a[n]=void 0):e.data.refInFor?Array.isArray(a[n])?a[n].indexOf(i)<0&&a[n].push(i):a[n]=[i]:a[n]=i}}var $o=new be("",{},[]),jo=["create","activate","update","remove","destroy"];function To(e,t){return e.key===t.key&&e.asyncFactory===t.asyncFactory&&(e.tag===t.tag&&e.isComment===t.isComment&&o(e.data)===o(t.data)&&Ao(e,t)||i(e.isAsyncPlaceholder)&&r(t.asyncFactory.error))}function Ao(e,t){if("input"!==e.tag)return!0;var n,r=o(n=e.data)&&o(n=n.attrs)&&n.type,i=o(n=t.data)&&o(n=n.attrs)&&n.type;return r===i||co(r)&&co(i)}function Mo(e,t,n){var r,i,a={};for(r=t;r<=n;++r)i=e[r].key,o(i)&&(a[i]=r);return a}function Po(e){var t,n,a={},l=e.modules,u=e.nodeOps;for(t=0;tv?(f=r(n[g+1])?null:n[g+1].elm,C(e,f,n,h,g,i)):h>g&&O(t,d,v)}function $(e,t,n,r){for(var i=n;i-1?Vo(e,t,n):Ur(t)?Xr(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Br(t)?e.setAttribute(t,Vr(t,n)):Kr(t)?Xr(n)?e.removeAttributeNS(qr,Gr(t)):e.setAttributeNS(qr,t,n):Vo(e,t,n)}function Vo(e,t,n){if(Xr(n))e.removeAttribute(t);else{if(ee&&!te&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,n)}}var Uo={create:Bo,update:Bo};function qo(e,t){var n=t.elm,i=t.data,a=e.data;if(!(r(i.staticClass)&&r(i.class)&&(r(a)||r(a.staticClass)&&r(a.class)))){var s=Yr(t),l=n._transitionClasses;o(l)&&(s=Qr(s,eo(l))),s!==n._prevClass&&(n.setAttribute("class",s),n._prevClass=s)}}var Ko,Go={create:qo,update:qo},Xo="__r",Yo="__c";function Jo(e){if(o(e[Xo])){var t=ee?"change":"input";e[t]=[].concat(e[Xo],e[t]||[]),delete e[Xo]}o(e[Yo])&&(e.change=[].concat(e[Yo],e.change||[]),delete e[Yo])}function Zo(e,t,n){var r=Ko;return function o(){var i=t.apply(null,arguments);null!==i&&ti(e,o,n,r)}}var Qo=st&&!(oe&&Number(oe[1])<=53);function ei(e,t,n,r){if(Qo){var o=Gn,i=t;t=i._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=o||e.timeStamp<=0||e.target.ownerDocument!==document)return i.apply(this,arguments)}}Ko.addEventListener(e,t,ae?{capture:n,passive:r}:n)}function ti(e,t,n,r){(r||Ko).removeEventListener(e,t._wrapper||t,n)}function ni(e,t){if(!r(e.data.on)||!r(t.data.on)){var n=t.data.on||{},o=e.data.on||{};Ko=t.elm,Jo(n),xt(n,o,ei,ti,Zo,t.context),Ko=void 0}}var ri,oi={create:ni,update:ni};function ii(e,t){if(!r(e.data.domProps)||!r(t.data.domProps)){var n,i,a=t.elm,s=e.data.domProps||{},l=t.data.domProps||{};for(n in o(l.__ob__)&&(l=t.data.domProps=A({},l)),s)n in l||(a[n]="");for(n in l){if(i=l[n],"textContent"===n||"innerHTML"===n){if(t.children&&(t.children.length=0),i===s[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n&&"PROGRESS"!==a.tagName){a._value=i;var u=r(i)?"":String(i);ai(a,u)&&(a.value=u)}else if("innerHTML"===n&&io(a.tagName)&&r(a.innerHTML)){ri=ri||document.createElement("div"),ri.innerHTML=""+i+"";var c=ri.firstChild;while(a.firstChild)a.removeChild(a.firstChild);while(c.firstChild)a.appendChild(c.firstChild)}else if(i!==s[n])try{a[n]=i}catch(Sa){}}}}function ai(e,t){return!e.composing&&("OPTION"===e.tagName||si(e,t)||li(e,t))}function si(e,t){var n=!0;try{n=document.activeElement!==e}catch(Sa){}return n&&e.value!==t}function li(e,t){var n=e.value,r=e._vModifiers;if(o(r)){if(r.number)return v(n)!==v(t);if(r.trim)return n.trim()!==t.trim()}return n!==t}var ui={create:ii,update:ii},ci=x((function(e){var t={},n=/;(?![^(]*\))/g,r=/:(.+)/;return e.split(n).forEach((function(e){if(e){var n=e.split(r);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}));function fi(e){var t=di(e.style);return e.staticStyle?A(e.staticStyle,t):t}function di(e){return Array.isArray(e)?M(e):"string"===typeof e?ci(e):e}function pi(e,t){var n,r={};if(t){var o=e;while(o.componentInstance)o=o.componentInstance._vnode,o&&o.data&&(n=fi(o.data))&&A(r,n)}(n=fi(e.data))&&A(r,n);var i=e;while(i=i.parent)i.data&&(n=fi(i.data))&&A(r,n);return r}var hi,vi=/^--/,mi=/\s*!important$/,yi=function(e,t,n){if(vi.test(t))e.style.setProperty(t,n);else if(mi.test(n))e.style.setProperty(E(t),n.replace(mi,""),"important");else{var r=bi(t);if(Array.isArray(n))for(var o=0,i=n.length;o-1?t.split(wi).forEach((function(t){return e.classList.add(t)})):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Si(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(wi).forEach((function(t){return e.classList.remove(t)})):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";while(n.indexOf(r)>=0)n=n.replace(r," ");n=n.trim(),n?e.setAttribute("class",n):e.removeAttribute("class")}}function Oi(e){if(e){if("object"===typeof e){var t={};return!1!==e.css&&A(t,Ei(e.name||"v")),A(t,e),t}return"string"===typeof e?Ei(e):void 0}}var Ei=x((function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}})),ki=Y&&!te,$i="transition",ji="animation",Ti="transition",Ai="transitionend",Mi="animation",Pi="animationend";ki&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Ti="WebkitTransition",Ai="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Mi="WebkitAnimation",Pi="webkitAnimationEnd"));var Li=Y?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Ri(e){Li((function(){Li(e)}))}function Ni(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),Ci(e,t))}function Ii(e,t){e._transitionClasses&&g(e._transitionClasses,t),Si(e,t)}function Fi(e,t,n){var r=Di(e,t),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s=o===$i?Ai:Pi,l=0,u=function(){e.removeEventListener(s,c),n()},c=function(t){t.target===e&&++l>=a&&u()};setTimeout((function(){l0&&(n=$i,c=a,f=i.length):t===ji?u>0&&(n=ji,c=u,f=l.length):(c=Math.max(a,u),n=c>0?a>u?$i:ji:null,f=n?n===$i?i.length:l.length:0);var d=n===$i&&Hi.test(r[Ti+"Property"]);return{type:n,timeout:c,propCount:f,hasTransform:d}}function zi(e,t){while(e.length1}function Ki(e,t){!0!==t.data.show&&Wi(t)}var Gi=Y?{create:Ki,activate:Ki,remove:function(e,t){!0!==e.data.show?Vi(e,t):t()}}:{},Xi=[Uo,Go,oi,ui,xi,Gi],Yi=Xi.concat(zo),Ji=Po({nodeOps:Oo,modules:Yi});te&&document.addEventListener("selectionchange",(function(){var e=document.activeElement;e&&e.vmodel&&ia(e,"input")}));var Zi={inserted:function(e,t,n,r){"select"===n.tag?(r.elm&&!r.elm._vOptions?wt(n,"postpatch",(function(){Zi.componentUpdated(e,t,n)})):Qi(e,t,n.context),e._vOptions=[].map.call(e.options,na)):("textarea"===n.tag||co(e.type))&&(e._vModifiers=t.modifiers,t.modifiers.lazy||(e.addEventListener("compositionstart",ra),e.addEventListener("compositionend",oa),e.addEventListener("change",oa),te&&(e.vmodel=!0)))},componentUpdated:function(e,t,n){if("select"===n.tag){Qi(e,t,n.context);var r=e._vOptions,o=e._vOptions=[].map.call(e.options,na);if(o.some((function(e,t){return!N(e,r[t])}))){var i=e.multiple?t.value.some((function(e){return ta(e,o)})):t.value!==t.oldValue&&ta(t.value,o);i&&ia(e,"change")}}}};function Qi(e,t,n){ea(e,t,n),(ee||ne)&&setTimeout((function(){ea(e,t,n)}),0)}function ea(e,t,n){var r=t.value,o=e.multiple;if(!o||Array.isArray(r)){for(var i,a,s=0,l=e.options.length;s-1,a.selected!==i&&(a.selected=i);else if(N(na(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));o||(e.selectedIndex=-1)}}function ta(e,t){return t.every((function(t){return!N(t,e)}))}function na(e){return"_value"in e?e._value:e.value}function ra(e){e.target.composing=!0}function oa(e){e.target.composing&&(e.target.composing=!1,ia(e.target,"input"))}function ia(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function aa(e){return!e.componentInstance||e.data&&e.data.transition?e:aa(e.componentInstance._vnode)}var sa={bind:function(e,t,n){var r=t.value;n=aa(n);var o=n.data&&n.data.transition,i=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;r&&o?(n.data.show=!0,Wi(n,(function(){e.style.display=i}))):e.style.display=r?i:"none"},update:function(e,t,n){var r=t.value,o=t.oldValue;if(!r!==!o){n=aa(n);var i=n.data&&n.data.transition;i?(n.data.show=!0,r?Wi(n,(function(){e.style.display=e.__vOriginalDisplay})):Vi(n,(function(){e.style.display="none"}))):e.style.display=r?e.__vOriginalDisplay:"none"}},unbind:function(e,t,n,r,o){o||(e.style.display=e.__vOriginalDisplay)}},la={model:Zi,show:sa},ua={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function ca(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?ca(Sn(t.children)):e}function fa(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var o=n._parentListeners;for(var i in o)t[C(i)]=o[i];return t}function da(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}function pa(e){while(e=e.parent)if(e.data.transition)return!0}function ha(e,t){return t.key===e.key&&t.tag===e.tag}var va=function(e){return e.tag||Lt(e)},ma=function(e){return"show"===e.name},ya={name:"transition",props:ua,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(va),n.length)){0;var r=this.mode;0;var o=n[0];if(pa(this.$vnode))return o;var i=ca(o);if(!i)return o;if(this._leaving)return da(e,o);var a="__transition-"+this._uid+"-";i.key=null==i.key?i.isComment?a+"comment":a+i.tag:s(i.key)?0===String(i.key).indexOf(a)?i.key:a+i.key:i.key;var l=(i.data||(i.data={})).transition=fa(this),u=this._vnode,c=ca(u);if(i.data.directives&&i.data.directives.some(ma)&&(i.data.show=!0),c&&c.data&&!ha(i,c)&&!Lt(c)&&(!c.componentInstance||!c.componentInstance._vnode.isComment)){var f=c.data.transition=A({},l);if("out-in"===r)return this._leaving=!0,wt(f,"afterLeave",(function(){t._leaving=!1,t.$forceUpdate()})),da(e,o);if("in-out"===r){if(Lt(i))return u;var d,p=function(){d()};wt(l,"afterEnter",p),wt(l,"enterCancelled",p),wt(f,"delayLeave",(function(e){d=e}))}}return o}}},ga=A({tag:String,moveClass:String},ua);delete ga.mode;var ba={props:ga,beforeMount:function(){var e=this,t=this._update;this._update=function(n,r){var o=Mn(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,o(),t.call(e,n,r)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=fa(this),s=0;sn)t.push(arguments[n++]);return _[++b]=function(){("function"==typeof e?e:Function(e)).apply(void 0,t)},r(b),b},v=function(e){delete _[e]},d?r=function(e){m.nextTick(C(e))}:g&&g.now?r=function(e){g.now(C(e))}:y&&!f?(o=new y,i=o.port2,o.port1.onmessage=S,r=l(i.postMessage,i,1)):a.addEventListener&&"function"==typeof postMessage&&!a.importScripts&&p&&"file:"!==p.protocol&&!s(O)?(r=O,a.addEventListener("message",S,!1)):r=x in c("script")?function(e){u.appendChild(c("script"))[x]=function(){u.removeChild(this),w(e)}}:function(e){setTimeout(C(e),0)}),e.exports={set:h,clear:v}},"2d00":function(e,t,n){var r,o,i=n("da84"),a=n("342f"),s=i.process,l=s&&s.versions,u=l&&l.v8;u?(r=u.split("."),o=r[0]<4?1:r[0]+r[1]):a&&(r=a.match(/Edge\/(\d+)/),(!r||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/),r&&(o=r[1]))),e.exports=o&&+o},"2f62":function(e,t,n){"use strict";(function(e){ +/*! + * vuex v3.6.2 + * (c) 2021 Evan You + * @license MIT + */ +function n(e){var t=Number(e.version.split(".")[0]);if(t>=2)e.mixin({beforeCreate:r});else{var n=e.prototype._init;e.prototype._init=function(e){void 0===e&&(e={}),e.init=e.init?[r].concat(e.init):r,n.call(this,e)}}function r(){var e=this.$options;e.store?this.$store="function"===typeof e.store?e.store():e.store:e.parent&&e.parent.$store&&(this.$store=e.parent.$store)}}var r="undefined"!==typeof window?window:"undefined"!==typeof e?e:{},o=r.__VUE_DEVTOOLS_GLOBAL_HOOK__;function i(e){o&&(e._devtoolHook=o,o.emit("vuex:init",e),o.on("vuex:travel-to-state",(function(t){e.replaceState(t)})),e.subscribe((function(e,t){o.emit("vuex:mutation",e,t)}),{prepend:!0}),e.subscribeAction((function(e,t){o.emit("vuex:action",e,t)}),{prepend:!0}))}function a(e,t){return e.filter(t)[0]}function s(e,t){if(void 0===t&&(t=[]),null===e||"object"!==typeof e)return e;var n=a(t,(function(t){return t.original===e}));if(n)return n.copy;var r=Array.isArray(e)?[]:{};return t.push({original:e,copy:r}),Object.keys(e).forEach((function(n){r[n]=s(e[n],t)})),r}function l(e,t){Object.keys(e).forEach((function(n){return t(e[n],n)}))}function u(e){return null!==e&&"object"===typeof e}function c(e){return e&&"function"===typeof e.then}function f(e,t){return function(){return e(t)}}var d=function(e,t){this.runtime=t,this._children=Object.create(null),this._rawModule=e;var n=e.state;this.state=("function"===typeof n?n():n)||{}},p={namespaced:{configurable:!0}};p.namespaced.get=function(){return!!this._rawModule.namespaced},d.prototype.addChild=function(e,t){this._children[e]=t},d.prototype.removeChild=function(e){delete this._children[e]},d.prototype.getChild=function(e){return this._children[e]},d.prototype.hasChild=function(e){return e in this._children},d.prototype.update=function(e){this._rawModule.namespaced=e.namespaced,e.actions&&(this._rawModule.actions=e.actions),e.mutations&&(this._rawModule.mutations=e.mutations),e.getters&&(this._rawModule.getters=e.getters)},d.prototype.forEachChild=function(e){l(this._children,e)},d.prototype.forEachGetter=function(e){this._rawModule.getters&&l(this._rawModule.getters,e)},d.prototype.forEachAction=function(e){this._rawModule.actions&&l(this._rawModule.actions,e)},d.prototype.forEachMutation=function(e){this._rawModule.mutations&&l(this._rawModule.mutations,e)},Object.defineProperties(d.prototype,p);var h=function(e){this.register([],e,!1)};function v(e,t,n){if(t.update(n),n.modules)for(var r in n.modules){if(!t.getChild(r))return void 0;v(e.concat(r),t.getChild(r),n.modules[r])}}h.prototype.get=function(e){return e.reduce((function(e,t){return e.getChild(t)}),this.root)},h.prototype.getNamespace=function(e){var t=this.root;return e.reduce((function(e,n){return t=t.getChild(n),e+(t.namespaced?n+"/":"")}),"")},h.prototype.update=function(e){v([],this.root,e)},h.prototype.register=function(e,t,n){var r=this;void 0===n&&(n=!0);var o=new d(t,n);if(0===e.length)this.root=o;else{var i=this.get(e.slice(0,-1));i.addChild(e[e.length-1],o)}t.modules&&l(t.modules,(function(t,o){r.register(e.concat(o),t,n)}))},h.prototype.unregister=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1],r=t.getChild(n);r&&r.runtime&&t.removeChild(n)},h.prototype.isRegistered=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1];return!!t&&t.hasChild(n)};var m;var y=function(e){var t=this;void 0===e&&(e={}),!m&&"undefined"!==typeof window&&window.Vue&&A(window.Vue);var n=e.plugins;void 0===n&&(n=[]);var r=e.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new h(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new m,this._makeLocalGettersCache=Object.create(null);var o=this,a=this,s=a.dispatch,l=a.commit;this.dispatch=function(e,t){return s.call(o,e,t)},this.commit=function(e,t,n){return l.call(o,e,t,n)},this.strict=r;var u=this._modules.root.state;w(this,u,[],this._modules.root),x(this,u),n.forEach((function(e){return e(t)}));var c=void 0!==e.devtools?e.devtools:m.config.devtools;c&&i(this)},g={state:{configurable:!0}};function b(e,t,n){return t.indexOf(e)<0&&(n&&n.prepend?t.unshift(e):t.push(e)),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function _(e,t){e._actions=Object.create(null),e._mutations=Object.create(null),e._wrappedGetters=Object.create(null),e._modulesNamespaceMap=Object.create(null);var n=e.state;w(e,n,[],e._modules.root,!0),x(e,n,t)}function x(e,t,n){var r=e._vm;e.getters={},e._makeLocalGettersCache=Object.create(null);var o=e._wrappedGetters,i={};l(o,(function(t,n){i[n]=f(t,e),Object.defineProperty(e.getters,n,{get:function(){return e._vm[n]},enumerable:!0})}));var a=m.config.silent;m.config.silent=!0,e._vm=new m({data:{$$state:t},computed:i}),m.config.silent=a,e.strict&&$(e),r&&(n&&e._withCommit((function(){r._data.$$state=null})),m.nextTick((function(){return r.$destroy()})))}function w(e,t,n,r,o){var i=!n.length,a=e._modules.getNamespace(n);if(r.namespaced&&(e._modulesNamespaceMap[a],e._modulesNamespaceMap[a]=r),!i&&!o){var s=j(t,n.slice(0,-1)),l=n[n.length-1];e._withCommit((function(){m.set(s,l,r.state)}))}var u=r.context=C(e,a,n);r.forEachMutation((function(t,n){var r=a+n;O(e,r,t,u)})),r.forEachAction((function(t,n){var r=t.root?n:a+n,o=t.handler||t;E(e,r,o,u)})),r.forEachGetter((function(t,n){var r=a+n;k(e,r,t,u)})),r.forEachChild((function(r,i){w(e,t,n.concat(i),r,o)}))}function C(e,t,n){var r=""===t,o={dispatch:r?e.dispatch:function(n,r,o){var i=T(n,r,o),a=i.payload,s=i.options,l=i.type;return s&&s.root||(l=t+l),e.dispatch(l,a)},commit:r?e.commit:function(n,r,o){var i=T(n,r,o),a=i.payload,s=i.options,l=i.type;s&&s.root||(l=t+l),e.commit(l,a,s)}};return Object.defineProperties(o,{getters:{get:r?function(){return e.getters}:function(){return S(e,t)}},state:{get:function(){return j(e.state,n)}}}),o}function S(e,t){if(!e._makeLocalGettersCache[t]){var n={},r=t.length;Object.keys(e.getters).forEach((function(o){if(o.slice(0,r)===t){var i=o.slice(r);Object.defineProperty(n,i,{get:function(){return e.getters[o]},enumerable:!0})}})),e._makeLocalGettersCache[t]=n}return e._makeLocalGettersCache[t]}function O(e,t,n,r){var o=e._mutations[t]||(e._mutations[t]=[]);o.push((function(t){n.call(e,r.state,t)}))}function E(e,t,n,r){var o=e._actions[t]||(e._actions[t]=[]);o.push((function(t){var o=n.call(e,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:e.getters,rootState:e.state},t);return c(o)||(o=Promise.resolve(o)),e._devtoolHook?o.catch((function(t){throw e._devtoolHook.emit("vuex:error",t),t})):o}))}function k(e,t,n,r){e._wrappedGetters[t]||(e._wrappedGetters[t]=function(e){return n(r.state,r.getters,e.state,e.getters)})}function $(e){e._vm.$watch((function(){return this._data.$$state}),(function(){0}),{deep:!0,sync:!0})}function j(e,t){return t.reduce((function(e,t){return e[t]}),e)}function T(e,t,n){return u(e)&&e.type&&(n=t,t=e,e=e.type),{type:e,payload:t,options:n}}function A(e){m&&e===m||(m=e,n(m))}g.state.get=function(){return this._vm._data.$$state},g.state.set=function(e){0},y.prototype.commit=function(e,t,n){var r=this,o=T(e,t,n),i=o.type,a=o.payload,s=(o.options,{type:i,payload:a}),l=this._mutations[i];l&&(this._withCommit((function(){l.forEach((function(e){e(a)}))})),this._subscribers.slice().forEach((function(e){return e(s,r.state)})))},y.prototype.dispatch=function(e,t){var n=this,r=T(e,t),o=r.type,i=r.payload,a={type:o,payload:i},s=this._actions[o];if(s){try{this._actionSubscribers.slice().filter((function(e){return e.before})).forEach((function(e){return e.before(a,n.state)}))}catch(u){0}var l=s.length>1?Promise.all(s.map((function(e){return e(i)}))):s[0](i);return new Promise((function(e,t){l.then((function(t){try{n._actionSubscribers.filter((function(e){return e.after})).forEach((function(e){return e.after(a,n.state)}))}catch(u){0}e(t)}),(function(e){try{n._actionSubscribers.filter((function(e){return e.error})).forEach((function(t){return t.error(a,n.state,e)}))}catch(u){0}t(e)}))}))}},y.prototype.subscribe=function(e,t){return b(e,this._subscribers,t)},y.prototype.subscribeAction=function(e,t){var n="function"===typeof e?{before:e}:e;return b(n,this._actionSubscribers,t)},y.prototype.watch=function(e,t,n){var r=this;return this._watcherVM.$watch((function(){return e(r.state,r.getters)}),t,n)},y.prototype.replaceState=function(e){var t=this;this._withCommit((function(){t._vm._data.$$state=e}))},y.prototype.registerModule=function(e,t,n){void 0===n&&(n={}),"string"===typeof e&&(e=[e]),this._modules.register(e,t),w(this,this.state,e,this._modules.get(e),n.preserveState),x(this,this.state)},y.prototype.unregisterModule=function(e){var t=this;"string"===typeof e&&(e=[e]),this._modules.unregister(e),this._withCommit((function(){var n=j(t.state,e.slice(0,-1));m.delete(n,e[e.length-1])})),_(this)},y.prototype.hasModule=function(e){return"string"===typeof e&&(e=[e]),this._modules.isRegistered(e)},y.prototype.hotUpdate=function(e){this._modules.update(e),_(this,!0)},y.prototype._withCommit=function(e){var t=this._committing;this._committing=!0,e(),this._committing=t},Object.defineProperties(y.prototype,g);var M=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=this.$store.state,n=this.$store.getters;if(e){var r=D(this.$store,"mapState",e);if(!r)return;t=r.context.state,n=r.context.getters}return"function"===typeof o?o.call(this,t,n):t[o]},n[r].vuex=!0})),n})),P=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=[],n=arguments.length;while(n--)t[n]=arguments[n];var r=this.$store.commit;if(e){var i=D(this.$store,"mapMutations",e);if(!i)return;r=i.context.commit}return"function"===typeof o?o.apply(this,[r].concat(t)):r.apply(this.$store,[o].concat(t))}})),n})),L=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;o=e+o,n[r]=function(){if(!e||D(this.$store,"mapGetters",e))return this.$store.getters[o]},n[r].vuex=!0})),n})),R=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=[],n=arguments.length;while(n--)t[n]=arguments[n];var r=this.$store.dispatch;if(e){var i=D(this.$store,"mapActions",e);if(!i)return;r=i.context.dispatch}return"function"===typeof o?o.apply(this,[r].concat(t)):r.apply(this.$store,[o].concat(t))}})),n})),N=function(e){return{mapState:M.bind(null,e),mapGetters:L.bind(null,e),mapMutations:P.bind(null,e),mapActions:R.bind(null,e)}};function I(e){return F(e)?Array.isArray(e)?e.map((function(e){return{key:e,val:e}})):Object.keys(e).map((function(t){return{key:t,val:e[t]}})):[]}function F(e){return Array.isArray(e)||u(e)}function H(e){return function(t,n){return"string"!==typeof t?(n=t,t=""):"/"!==t.charAt(t.length-1)&&(t+="/"),e(t,n)}}function D(e,t,n){var r=e._modulesNamespaceMap[n];return r}function z(e){void 0===e&&(e={});var t=e.collapsed;void 0===t&&(t=!0);var n=e.filter;void 0===n&&(n=function(e,t,n){return!0});var r=e.transformer;void 0===r&&(r=function(e){return e});var o=e.mutationTransformer;void 0===o&&(o=function(e){return e});var i=e.actionFilter;void 0===i&&(i=function(e,t){return!0});var a=e.actionTransformer;void 0===a&&(a=function(e){return e});var l=e.logMutations;void 0===l&&(l=!0);var u=e.logActions;void 0===u&&(u=!0);var c=e.logger;return void 0===c&&(c=console),function(e){var f=s(e.state);"undefined"!==typeof c&&(l&&e.subscribe((function(e,i){var a=s(i);if(n(e,f,a)){var l=V(),u=o(e),d="mutation "+e.type+l;B(c,d,t),c.log("%c prev state","color: #9E9E9E; font-weight: bold",r(f)),c.log("%c mutation","color: #03A9F4; font-weight: bold",u),c.log("%c next state","color: #4CAF50; font-weight: bold",r(a)),W(c)}f=a})),u&&e.subscribeAction((function(e,n){if(i(e,n)){var r=V(),o=a(e),s="action "+e.type+r;B(c,s,t),c.log("%c action","color: #03A9F4; font-weight: bold",o),W(c)}})))}}function B(e,t,n){var r=n?e.groupCollapsed:e.group;try{r.call(e,t)}catch(o){e.log(t)}}function W(e){try{e.groupEnd()}catch(t){e.log("—— log end ——")}}function V(){var e=new Date;return" @ "+q(e.getHours(),2)+":"+q(e.getMinutes(),2)+":"+q(e.getSeconds(),2)+"."+q(e.getMilliseconds(),3)}function U(e,t){return new Array(t+1).join(e)}function q(e,t){return U("0",t-e.toString().length)+e}var K={Store:y,install:A,version:"3.6.2",mapState:M,mapMutations:P,mapGetters:L,mapActions:R,createNamespacedHelpers:N,createLogger:z};t["a"]=K}).call(this,n("c8ba"))},"342f":function(e,t,n){var r=n("d066");e.exports=r("navigator","userAgent")||""},"35a1":function(e,t,n){var r=n("f5df"),o=n("3f8c"),i=n("b622"),a=i("iterator");e.exports=function(e){if(void 0!=e)return e[a]||e["@@iterator"]||o[r(e)]}},"37e8":function(e,t,n){var r=n("83ab"),o=n("9bf2"),i=n("825a"),a=n("df75");e.exports=r?Object.defineProperties:function(e,t){i(e);var n,r=a(t),s=r.length,l=0;while(s>l)o.f(e,n=r[l++],t[n]);return e}},"38a0":function(e,t,n){},"3bbe":function(e,t,n){var r=n("861d");e.exports=function(e){if(!r(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype");return e}},"3c4e":function(e,t,n){"use strict";var r=function(e){return o(e)&&!i(e)};function o(e){return!!e&&"object"===typeof e}function i(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||l(e)}var a="function"===typeof Symbol&&Symbol.for,s=a?Symbol.for("react.element"):60103;function l(e){return e.$$typeof===s}function u(e){return Array.isArray(e)?[]:{}}function c(e,t){var n=t&&!0===t.clone;return n&&r(e)?p(u(e),e,t):e}function f(e,t,n){var o=e.slice();return t.forEach((function(t,i){"undefined"===typeof o[i]?o[i]=c(t,n):r(t)?o[i]=p(e[i],t,n):-1===e.indexOf(t)&&o.push(c(t,n))})),o}function d(e,t,n){var o={};return r(e)&&Object.keys(e).forEach((function(t){o[t]=c(e[t],n)})),Object.keys(t).forEach((function(i){r(t[i])&&e[i]?o[i]=p(e[i],t[i],n):o[i]=c(t[i],n)})),o}function p(e,t,n){var r=Array.isArray(t),o=Array.isArray(e),i=n||{arrayMerge:f},a=r===o;if(a){if(r){var s=i.arrayMerge||f;return s(e,t,n)}return d(e,t,n)}return c(t,n)}p.all=function(e,t){if(!Array.isArray(e)||e.length<2)throw new Error("first argument should be an array with at least two elements");return e.reduce((function(e,n){return p(e,n,t)}))};var h=p;e.exports=h},"3ca3":function(e,t,n){"use strict";var r=n("6547").charAt,o=n("69f3"),i=n("7dd0"),a="String Iterator",s=o.set,l=o.getterFor(a);i(String,"String",(function(e){s(this,{type:a,string:String(e),index:0})}),(function(){var e,t=l(this),n=t.string,o=t.index;return o>=n.length?{value:void 0,done:!0}:(e=r(n,o),t.index+=e.length,{value:e,done:!1})}))},"3f8c":function(e,t){e.exports={}},4010:function(e,t,n){"use strict";t.__esModule=!0,t.removeResizeListener=t.addResizeListener=void 0;var r=n("6dd8"),o=i(r);function i(e){return e&&e.__esModule?e:{default:e}}var a="undefined"===typeof window,s=function(e){var t=e,n=Array.isArray(t),r=0;for(t=n?t:t[Symbol.iterator]();;){var o;if(n){if(r>=t.length)break;o=t[r++]}else{if(r=t.next(),r.done)break;o=r.value}var i=o,a=i.target.__resizeListeners__||[];a.length&&a.forEach((function(e){e()}))}};t.addResizeListener=function(e,t){a||(e.__resizeListeners__||(e.__resizeListeners__=[],e.__ro__=new o.default(s),e.__ro__.observe(e)),e.__resizeListeners__.push(t))},t.removeResizeListener=function(e,t){e&&e.__resizeListeners__&&(e.__resizeListeners__.splice(e.__resizeListeners__.indexOf(t),1),e.__resizeListeners__.length||e.__ro__.disconnect())}},"417f":function(e,t,n){"use strict";t.__esModule=!0;var r=n("2b0e"),o=a(r),i=n("5924");function a(e){return e&&e.__esModule?e:{default:e}}var s=[],l="@@clickoutsideContext",u=void 0,c=0;function f(e,t,n){return function(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!(n&&n.context&&r.target&&o.target)||e.contains(r.target)||e.contains(o.target)||e===r.target||n.context.popperElm&&(n.context.popperElm.contains(r.target)||n.context.popperElm.contains(o.target))||(t.expression&&e[l].methodName&&n.context[e[l].methodName]?n.context[e[l].methodName]():e[l].bindingFn&&e[l].bindingFn())}}!o.default.prototype.$isServer&&(0,i.on)(document,"mousedown",(function(e){return u=e})),!o.default.prototype.$isServer&&(0,i.on)(document,"mouseup",(function(e){s.forEach((function(t){return t[l].documentHandler(e,u)}))})),t.default={bind:function(e,t,n){s.push(e);var r=c++;e[l]={id:r,documentHandler:f(e,t,n),methodName:t.expression,bindingFn:t.value}},update:function(e,t,n){e[l].documentHandler=f(e,t,n),e[l].methodName=t.expression,e[l].bindingFn=t.value},unbind:function(e){for(var t=s.length,n=0;n0){var r=t[t.length-1];if(r.id===e){if(r.modalClass){var o=r.modalClass.trim().split(/\s+/);o.forEach((function(e){return(0,i.removeClass)(n,e)}))}t.pop(),t.length>0&&(n.style.zIndex=t[t.length-1].zIndex)}else for(var a=t.length-1;a>=0;a--)if(t[a].id===e){t.splice(a,1);break}}0===t.length&&(this.modalFade&&(0,i.addClass)(n,"v-modal-leave"),setTimeout((function(){0===t.length&&(n.parentNode&&n.parentNode.removeChild(n),n.style.display="none",d.modalDom=void 0),(0,i.removeClass)(n,"v-modal-leave")}),200))}};Object.defineProperty(d,"zIndex",{configurable:!0,get:function(){return l||(u=u||(o.default.prototype.$ELEMENT||{}).zIndex||2e3,l=!0),u},set:function(e){u=e}});var p=function(){if(!o.default.prototype.$isServer&&d.modalStack.length>0){var e=d.modalStack[d.modalStack.length-1];if(!e)return;var t=d.getInstance(e.id);return t}};o.default.prototype.$isServer||window.addEventListener("keydown",(function(e){if(27===e.keyCode){var t=p();t&&t.closeOnPressEscape&&(t.handleClose?t.handleClose():t.handleAction?t.handleAction("cancel"):t.close())}})),t.default=d},"4d64":function(e,t,n){var r=n("fc6a"),o=n("50c4"),i=n("23cb"),a=function(e){return function(t,n,a){var s,l=r(t),u=o(l.length),c=i(a,u);if(e&&n!=n){while(u>c)if(s=l[c++],s!=s)return!0}else for(;u>c;c++)if((e||c in l)&&l[c]===n)return e||c||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},"50c4":function(e,t,n){var r=n("a691"),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},5128:function(e,t,n){"use strict";t.__esModule=!0,t.PopupManager=void 0;var r=n("2b0e"),o=d(r),i=n("7f4d"),a=d(i),s=n("4b26"),l=d(s),u=n("e62d"),c=d(u),f=n("5924");function d(e){return e&&e.__esModule?e:{default:e}}var p=1,h=void 0;t.default={props:{visible:{type:Boolean,default:!1},openDelay:{},closeDelay:{},zIndex:{},modal:{type:Boolean,default:!1},modalFade:{type:Boolean,default:!0},modalClass:{},modalAppendToBody:{type:Boolean,default:!1},lockScroll:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!1},closeOnClickModal:{type:Boolean,default:!1}},beforeMount:function(){this._popupId="popup-"+p++,l.default.register(this._popupId,this)},beforeDestroy:function(){l.default.deregister(this._popupId),l.default.closeModal(this._popupId),this.restoreBodyStyle()},data:function(){return{opened:!1,bodyPaddingRight:null,computedBodyPaddingRight:0,withoutHiddenClass:!0,rendered:!1}},watch:{visible:function(e){var t=this;if(e){if(this._opening)return;this.rendered?this.open():(this.rendered=!0,o.default.nextTick((function(){t.open()})))}else this.close()}},methods:{open:function(e){var t=this;this.rendered||(this.rendered=!0);var n=(0,a.default)({},this.$props||this,e);this._closeTimer&&(clearTimeout(this._closeTimer),this._closeTimer=null),clearTimeout(this._openTimer);var r=Number(n.openDelay);r>0?this._openTimer=setTimeout((function(){t._openTimer=null,t.doOpen(n)}),r):this.doOpen(n)},doOpen:function(e){if(!this.$isServer&&(!this.willOpen||this.willOpen())&&!this.opened){this._opening=!0;var t=this.$el,n=e.modal,r=e.zIndex;if(r&&(l.default.zIndex=r),n&&(this._closing&&(l.default.closeModal(this._popupId),this._closing=!1),l.default.openModal(this._popupId,l.default.nextZIndex(),this.modalAppendToBody?void 0:t,e.modalClass,e.modalFade),e.lockScroll)){this.withoutHiddenClass=!(0,f.hasClass)(document.body,"el-popup-parent--hidden"),this.withoutHiddenClass&&(this.bodyPaddingRight=document.body.style.paddingRight,this.computedBodyPaddingRight=parseInt((0,f.getStyle)(document.body,"paddingRight"),10)),h=(0,c.default)();var o=document.documentElement.clientHeight0&&(o||"scroll"===i)&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.computedBodyPaddingRight+h+"px"),(0,f.addClass)(document.body,"el-popup-parent--hidden")}"static"===getComputedStyle(t).position&&(t.style.position="absolute"),t.style.zIndex=l.default.nextZIndex(),this.opened=!0,this.onOpen&&this.onOpen(),this.doAfterOpen()}},doAfterOpen:function(){this._opening=!1},close:function(){var e=this;if(!this.willClose||this.willClose()){null!==this._openTimer&&(clearTimeout(this._openTimer),this._openTimer=null),clearTimeout(this._closeTimer);var t=Number(this.closeDelay);t>0?this._closeTimer=setTimeout((function(){e._closeTimer=null,e.doClose()}),t):this.doClose()}},doClose:function(){this._closing=!0,this.onClose&&this.onClose(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose()},doAfterClose:function(){l.default.closeModal(this._popupId),this._closing=!1},restoreBodyStyle:function(){this.modal&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.bodyPaddingRight,(0,f.removeClass)(document.body,"el-popup-parent--hidden")),this.withoutHiddenClass=!0}}},t.PopupManager=l.default},5135:function(e,t,n){var r=n("7b0b"),o={}.hasOwnProperty;e.exports=Object.hasOwn||function(e,t){return o.call(r(e),t)}},5466:function(e,t,n){},5692:function(e,t,n){var r=n("c430"),o=n("c6cd");(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.15.2",mode:r?"pure":"global",copyright:"© 2021 Denis Pushkarev (zloirock.ru)"})},"56ef":function(e,t,n){var r=n("d066"),o=n("241c"),i=n("7418"),a=n("825a");e.exports=r("Reflect","ownKeys")||function(e){var t=o.f(a(e)),n=i.f;return n?t.concat(n(e)):t}},5924:function(e,t,n){"use strict";t.__esModule=!0,t.isInContainer=t.getScrollContainer=t.isScroll=t.getStyle=t.once=t.off=t.on=void 0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.hasClass=v,t.addClass=m,t.removeClass=y,t.setStyle=b;var o=n("2b0e"),i=a(o);function a(e){return e&&e.__esModule?e:{default:e}}var s=i.default.prototype.$isServer,l=/([\:\-\_]+(.))/g,u=/^moz([A-Z])/,c=s?0:Number(document.documentMode),f=function(e){return(e||"").replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g,"")},d=function(e){return e.replace(l,(function(e,t,n,r){return r?n.toUpperCase():n})).replace(u,"Moz$1")},p=t.on=function(){return!s&&document.addEventListener?function(e,t,n){e&&t&&n&&e.addEventListener(t,n,!1)}:function(e,t,n){e&&t&&n&&e.attachEvent("on"+t,n)}}(),h=t.off=function(){return!s&&document.removeEventListener?function(e,t,n){e&&t&&e.removeEventListener(t,n,!1)}:function(e,t,n){e&&t&&e.detachEvent("on"+t,n)}}();t.once=function(e,t,n){var r=function r(){n&&n.apply(this,arguments),h(e,t,r)};p(e,t,r)};function v(e,t){if(!e||!t)return!1;if(-1!==t.indexOf(" "))throw new Error("className should not contain space.");return e.classList?e.classList.contains(t):(" "+e.className+" ").indexOf(" "+t+" ")>-1}function m(e,t){if(e){for(var n=e.className,r=(t||"").split(" "),o=0,i=r.length;or.top&&n.right>r.left&&n.lefte?u():!0!==t&&(o=setTimeout(r?c:u,void 0===r?e-s:e))}return"boolean"!==typeof t&&(r=n,n=t,t=void 0),a}},"5c6c":function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},"605d":function(e,t,n){var r=n("c6b6"),o=n("da84");e.exports="process"==r(o.process)},6069:function(e,t){e.exports="object"==typeof window},"60da":function(e,t,n){"use strict";var r=n("83ab"),o=n("d039"),i=n("df75"),a=n("7418"),s=n("d1e7"),l=n("7b0b"),u=n("44ad"),c=Object.assign,f=Object.defineProperty;e.exports=!c||o((function(){if(r&&1!==c({b:1},c(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach((function(e){t[e]=e})),7!=c({},e)[n]||i(c({},t)).join("")!=o}))?function(e,t){var n=l(e),o=arguments.length,c=1,f=a.f,d=s.f;while(o>c){var p,h=u(arguments[c++]),v=f?i(h).concat(f(h)):i(h),m=v.length,y=0;while(m>y)p=v[y++],r&&!d.call(h,p)||(n[p]=h[p])}return n}:c},6167:function(e,t,n){"use strict";var r,o;"function"===typeof Symbol&&Symbol.iterator;(function(i,a){r=a,o="function"===typeof r?r.call(t,n,t,e):r,void 0===o||(e.exports=o)})(0,(function(){var e=window,t={placement:"bottom",gpuAcceleration:!0,offset:0,boundariesElement:"viewport",boundariesPadding:5,preventOverflowOrder:["left","right","top","bottom"],flipBehavior:"flip",arrowElement:"[x-arrow]",arrowOffset:0,modifiers:["shift","offset","preventOverflow","keepTogether","arrow","flip","applyStyle"],modifiersIgnored:[],forceAbsolute:!1};function n(e,n,r){this._reference=e.jquery?e[0]:e,this.state={};var o="undefined"===typeof n||null===n,i=n&&"[object Object]"===Object.prototype.toString.call(n);return this._popper=o||i?this.parse(i?n:{}):n.jquery?n[0]:n,this._options=Object.assign({},t,r),this._options.modifiers=this._options.modifiers.map(function(e){if(-1===this._options.modifiersIgnored.indexOf(e))return"applyStyle"===e&&this._popper.setAttribute("x-placement",this._options.placement),this.modifiers[e]||e}.bind(this)),this.state.position=this._getPosition(this._popper,this._reference),f(this._popper,{position:this.state.position,top:0}),this.update(),this._setupEventListeners(),this}function r(t){var n=t.style.display,r=t.style.visibility;t.style.display="block",t.style.visibility="hidden";t.offsetWidth;var o=e.getComputedStyle(t),i=parseFloat(o.marginTop)+parseFloat(o.marginBottom),a=parseFloat(o.marginLeft)+parseFloat(o.marginRight),s={width:t.offsetWidth+a,height:t.offsetHeight+i};return t.style.display=n,t.style.visibility=r,s}function o(e){var t={left:"right",right:"left",bottom:"top",top:"bottom"};return e.replace(/left|right|bottom|top/g,(function(e){return t[e]}))}function i(e){var t=Object.assign({},e);return t.right=t.left+t.width,t.bottom=t.top+t.height,t}function a(e,t){var n,r=0;for(n in e){if(e[n]===t)return r;r++}return null}function s(t,n){var r=e.getComputedStyle(t,null);return r[n]}function l(t){var n=t.offsetParent;return n!==e.document.body&&n?n:e.document.documentElement}function u(t){var n=t.parentNode;return n?n===e.document?e.document.body.scrollTop||e.document.body.scrollLeft?e.document.body:e.document.documentElement:-1!==["scroll","auto"].indexOf(s(n,"overflow"))||-1!==["scroll","auto"].indexOf(s(n,"overflow-x"))||-1!==["scroll","auto"].indexOf(s(n,"overflow-y"))?n:u(t.parentNode):t}function c(t){return t!==e.document.body&&("fixed"===s(t,"position")||(t.parentNode?c(t.parentNode):t))}function f(e,t){function n(e){return""!==e&&!isNaN(parseFloat(e))&&isFinite(e)}Object.keys(t).forEach((function(r){var o="";-1!==["width","height","top","right","bottom","left"].indexOf(r)&&n(t[r])&&(o="px"),e.style[r]=t[r]+o}))}function d(e){var t={};return e&&"[object Function]"===t.toString.call(e)}function p(e){var t={width:e.offsetWidth,height:e.offsetHeight,left:e.offsetLeft,top:e.offsetTop};return t.right=t.left+t.width,t.bottom=t.top+t.height,t}function h(e){var t=e.getBoundingClientRect(),n=-1!=navigator.userAgent.indexOf("MSIE"),r=n&&"HTML"===e.tagName?-e.scrollTop:t.top;return{left:t.left,top:r,right:t.right,bottom:t.bottom,width:t.right-t.left,height:t.bottom-r}}function v(e,t,n){var r=h(e),o=h(t);if(n){var i=u(t);o.top+=i.scrollTop,o.bottom+=i.scrollTop,o.left+=i.scrollLeft,o.right+=i.scrollLeft}var a={top:r.top-o.top,left:r.left-o.left,bottom:r.top-o.top+r.height,right:r.left-o.left+r.width,width:r.width,height:r.height};return a}function m(t){for(var n=["","ms","webkit","moz","o"],r=0;r1&&console.warn("WARNING: the given `parent` query("+t.parent+") matched more than one element, the first one will be used"),0===a.length)throw"ERROR: the given `parent` doesn't exists!";a=a[0]}return a.length>1&&a instanceof Element===!1&&(console.warn("WARNING: you have passed as parent a list of elements, the first one will be used"),a=a[0]),a.appendChild(o),o;function s(e,t){t.forEach((function(t){e.classList.add(t)}))}function l(e,t){t.forEach((function(t){e.setAttribute(t.split(":")[0],t.split(":")[1]||"")}))}},n.prototype._getPosition=function(e,t){var n=l(t);if(this._options.forceAbsolute)return"absolute";var r=c(t,n);return r?"fixed":"absolute"},n.prototype._getOffsets=function(e,t,n){n=n.split("-")[0];var o={};o.position=this.state.position;var i="fixed"===o.position,a=v(t,l(e),i),s=r(e);return-1!==["right","left"].indexOf(n)?(o.top=a.top+a.height/2-s.height/2,o.left="left"===n?a.left-s.width:a.right):(o.left=a.left+a.width/2-s.width/2,o.top="top"===n?a.top-s.height:a.bottom),o.width=s.width,o.height=s.height,{popper:o,reference:a}},n.prototype._setupEventListeners=function(){if(this.state.updateBound=this.update.bind(this),e.addEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement){var t=u(this._reference);t!==e.document.body&&t!==e.document.documentElement||(t=e),t.addEventListener("scroll",this.state.updateBound),this.state.scrollTarget=t}},n.prototype._removeEventListeners=function(){e.removeEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement&&this.state.scrollTarget&&(this.state.scrollTarget.removeEventListener("scroll",this.state.updateBound),this.state.scrollTarget=null),this.state.updateBound=null},n.prototype._getBoundaries=function(t,n,r){var o,i,a={};if("window"===r){var s=e.document.body,c=e.document.documentElement;i=Math.max(s.scrollHeight,s.offsetHeight,c.clientHeight,c.scrollHeight,c.offsetHeight),o=Math.max(s.scrollWidth,s.offsetWidth,c.clientWidth,c.scrollWidth,c.offsetWidth),a={top:0,right:o,bottom:i,left:0}}else if("viewport"===r){var f=l(this._popper),d=u(this._popper),h=p(f),v=function(e){return e==document.body?Math.max(document.documentElement.scrollTop,document.body.scrollTop):e.scrollTop},m=function(e){return e==document.body?Math.max(document.documentElement.scrollLeft,document.body.scrollLeft):e.scrollLeft},y="fixed"===t.offsets.popper.position?0:v(d),g="fixed"===t.offsets.popper.position?0:m(d);a={top:0-(h.top-y),right:e.document.documentElement.clientWidth-(h.left-g),bottom:e.document.documentElement.clientHeight-(h.top-y),left:0-(h.left-g)}}else a=l(this._popper)===r?{top:0,left:0,right:r.clientWidth,bottom:r.clientHeight}:p(r);return a.left+=n,a.right-=n,a.top=a.top+n,a.bottom=a.bottom-n,a},n.prototype.runModifiers=function(e,t,n){var r=t.slice();return void 0!==n&&(r=this._options.modifiers.slice(0,a(this._options.modifiers,n))),r.forEach(function(t){d(t)&&(e=t.call(this,e))}.bind(this)),e},n.prototype.isModifierRequired=function(e,t){var n=a(this._options.modifiers,e);return!!this._options.modifiers.slice(0,n).filter((function(e){return e===t})).length},n.prototype.modifiers={},n.prototype.modifiers.applyStyle=function(e){var t,n={position:e.offsets.popper.position},r=Math.round(e.offsets.popper.left),o=Math.round(e.offsets.popper.top);return this._options.gpuAcceleration&&(t=m("transform"))?(n[t]="translate3d("+r+"px, "+o+"px, 0)",n.top=0,n.left=0):(n.left=r,n.top=o),Object.assign(n,e.styles),f(this._popper,n),this._popper.setAttribute("x-placement",e.placement),this.isModifierRequired(this.modifiers.applyStyle,this.modifiers.arrow)&&e.offsets.arrow&&f(e.arrowElement,e.offsets.arrow),e},n.prototype.modifiers.shift=function(e){var t=e.placement,n=t.split("-")[0],r=t.split("-")[1];if(r){var o=e.offsets.reference,a=i(e.offsets.popper),s={y:{start:{top:o.top},end:{top:o.top+o.height-a.height}},x:{start:{left:o.left},end:{left:o.left+o.width-a.width}}},l=-1!==["bottom","top"].indexOf(n)?"x":"y";e.offsets.popper=Object.assign(a,s[l][r])}return e},n.prototype.modifiers.preventOverflow=function(e){var t=this._options.preventOverflowOrder,n=i(e.offsets.popper),r={left:function(){var t=n.left;return n.lefte.boundaries.right&&(t=Math.min(n.left,e.boundaries.right-n.width)),{left:t}},top:function(){var t=n.top;return n.tope.boundaries.bottom&&(t=Math.min(n.top,e.boundaries.bottom-n.height)),{top:t}}};return t.forEach((function(t){e.offsets.popper=Object.assign(n,r[t]())})),e},n.prototype.modifiers.keepTogether=function(e){var t=i(e.offsets.popper),n=e.offsets.reference,r=Math.floor;return t.rightr(n.right)&&(e.offsets.popper.left=r(n.right)),t.bottomr(n.bottom)&&(e.offsets.popper.top=r(n.bottom)),e},n.prototype.modifiers.flip=function(e){if(!this.isModifierRequired(this.modifiers.flip,this.modifiers.preventOverflow))return console.warn("WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!"),e;if(e.flipped&&e.placement===e._originalPlacement)return e;var t=e.placement.split("-")[0],n=o(t),r=e.placement.split("-")[1]||"",a=[];return a="flip"===this._options.flipBehavior?[t,n]:this._options.flipBehavior,a.forEach(function(s,l){if(t===s&&a.length!==l+1){t=e.placement.split("-")[0],n=o(t);var u=i(e.offsets.popper),c=-1!==["right","bottom"].indexOf(t);(c&&Math.floor(e.offsets.reference[t])>Math.floor(u[n])||!c&&Math.floor(e.offsets.reference[t])s[p]&&(e.offsets.popper[f]+=l[f]+h-s[p]);var v=l[f]+(n||l[c]/2-h/2),m=v-s[f];return m=Math.max(Math.min(s[c]-h-8,m),8),o[f]=m,o[d]="",e.offsets.arrow=o,e.arrowElement=t,e},Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var t=Object(e),n=1;n=u?e?"":void 0:(i=s.charCodeAt(l),i<55296||i>56319||l+1===u||(a=s.charCodeAt(l+1))<56320||a>57343?e?s.charAt(l):i:e?s.slice(l,l+2):a-56320+(i-55296<<10)+65536)}};e.exports={codeAt:i(!1),charAt:i(!0)}},"69f3":function(e,t,n){var r,o,i,a=n("7f9a"),s=n("da84"),l=n("861d"),u=n("9112"),c=n("5135"),f=n("c6cd"),d=n("f772"),p=n("d012"),h="Object already initialized",v=s.WeakMap,m=function(e){return i(e)?o(e):r(e,{})},y=function(e){return function(t){var n;if(!l(t)||(n=o(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}};if(a||f.state){var g=f.state||(f.state=new v),b=g.get,_=g.has,x=g.set;r=function(e,t){if(_.call(g,e))throw new TypeError(h);return t.facade=e,x.call(g,e,t),t},o=function(e){return b.call(g,e)||{}},i=function(e){return _.call(g,e)}}else{var w=d("state");p[w]=!0,r=function(e,t){if(c(e,w))throw new TypeError(h);return t.facade=e,u(e,w,t),t},o=function(e){return c(e,w)?e[w]:{}},i=function(e){return c(e,w)}}e.exports={set:r,get:o,has:i,enforce:m,getterFor:y}},"6ac9":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=73)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},2:function(e,t){e.exports=n("5924")},3:function(e,t){e.exports=n("8122")},5:function(e,t){e.exports=n("e974")},7:function(e,t){e.exports=n("2b0e")},73:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("span",[n("transition",{attrs:{name:e.transition},on:{"after-enter":e.handleAfterEnter,"after-leave":e.handleAfterLeave}},[n("div",{directives:[{name:"show",rawName:"v-show",value:!e.disabled&&e.showPopper,expression:"!disabled && showPopper"}],ref:"popper",staticClass:"el-popover el-popper",class:[e.popperClass,e.content&&"el-popover--plain"],style:{width:e.width+"px"},attrs:{role:"tooltip",id:e.tooltipId,"aria-hidden":e.disabled||!e.showPopper?"true":"false"}},[e.title?n("div",{staticClass:"el-popover__title",domProps:{textContent:e._s(e.title)}}):e._e(),e._t("default",[e._v(e._s(e.content))])],2)]),n("span",{ref:"wrapper",staticClass:"el-popover__reference-wrapper"},[e._t("reference")],2)],1)},o=[];r._withStripped=!0;var i=n(5),a=n.n(i),s=n(2),l=n(3),u={name:"ElPopover",mixins:[a.a],props:{trigger:{type:String,default:"click",validator:function(e){return["click","focus","hover","manual"].indexOf(e)>-1}},openDelay:{type:Number,default:0},closeDelay:{type:Number,default:200},title:String,disabled:Boolean,content:String,reference:{},popperClass:String,width:{},visibleArrow:{default:!0},arrowOffset:{type:Number,default:0},transition:{type:String,default:"fade-in-linear"},tabindex:{type:Number,default:0}},computed:{tooltipId:function(){return"el-popover-"+Object(l["generateId"])()}},watch:{showPopper:function(e){this.disabled||(e?this.$emit("show"):this.$emit("hide"))}},mounted:function(){var e=this,t=this.referenceElm=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$refs.wrapper.children&&(t=this.referenceElm=this.$refs.wrapper.children[0]),t&&(Object(s["addClass"])(t,"el-popover__reference"),t.setAttribute("aria-describedby",this.tooltipId),t.setAttribute("tabindex",this.tabindex),n.setAttribute("tabindex",0),"click"!==this.trigger&&(Object(s["on"])(t,"focusin",(function(){e.handleFocus();var n=t.__vue__;n&&"function"===typeof n.focus&&n.focus()})),Object(s["on"])(n,"focusin",this.handleFocus),Object(s["on"])(t,"focusout",this.handleBlur),Object(s["on"])(n,"focusout",this.handleBlur)),Object(s["on"])(t,"keydown",this.handleKeydown),Object(s["on"])(t,"click",this.handleClick)),"click"===this.trigger?(Object(s["on"])(t,"click",this.doToggle),Object(s["on"])(document,"click",this.handleDocumentClick)):"hover"===this.trigger?(Object(s["on"])(t,"mouseenter",this.handleMouseEnter),Object(s["on"])(n,"mouseenter",this.handleMouseEnter),Object(s["on"])(t,"mouseleave",this.handleMouseLeave),Object(s["on"])(n,"mouseleave",this.handleMouseLeave)):"focus"===this.trigger&&(this.tabindex<0&&console.warn("[Element Warn][Popover]a negative taindex means that the element cannot be focused by tab key"),t.querySelector("input, textarea")?(Object(s["on"])(t,"focusin",this.doShow),Object(s["on"])(t,"focusout",this.doClose)):(Object(s["on"])(t,"mousedown",this.doShow),Object(s["on"])(t,"mouseup",this.doClose)))},beforeDestroy:function(){this.cleanup()},deactivated:function(){this.cleanup()},methods:{doToggle:function(){this.showPopper=!this.showPopper},doShow:function(){this.showPopper=!0},doClose:function(){this.showPopper=!1},handleFocus:function(){Object(s["addClass"])(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!0)},handleClick:function(){Object(s["removeClass"])(this.referenceElm,"focusing")},handleBlur:function(){Object(s["removeClass"])(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!1)},handleMouseEnter:function(){var e=this;clearTimeout(this._timer),this.openDelay?this._timer=setTimeout((function(){e.showPopper=!0}),this.openDelay):this.showPopper=!0},handleKeydown:function(e){27===e.keyCode&&"manual"!==this.trigger&&this.doClose()},handleMouseLeave:function(){var e=this;clearTimeout(this._timer),this.closeDelay?this._timer=setTimeout((function(){e.showPopper=!1}),this.closeDelay):this.showPopper=!1},handleDocumentClick:function(e){var t=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$refs.wrapper.children&&(t=this.referenceElm=this.$refs.wrapper.children[0]),this.$el&&t&&!this.$el.contains(e.target)&&!t.contains(e.target)&&n&&!n.contains(e.target)&&(this.showPopper=!1)},handleAfterEnter:function(){this.$emit("after-enter")},handleAfterLeave:function(){this.$emit("after-leave"),this.doDestroy()},cleanup:function(){(this.openDelay||this.closeDelay)&&clearTimeout(this._timer)}},destroyed:function(){var e=this.reference;Object(s["off"])(e,"click",this.doToggle),Object(s["off"])(e,"mouseup",this.doClose),Object(s["off"])(e,"mousedown",this.doShow),Object(s["off"])(e,"focusin",this.doShow),Object(s["off"])(e,"focusout",this.doClose),Object(s["off"])(e,"mousedown",this.doShow),Object(s["off"])(e,"mouseup",this.doClose),Object(s["off"])(e,"mouseleave",this.handleMouseLeave),Object(s["off"])(e,"mouseenter",this.handleMouseEnter),Object(s["off"])(document,"click",this.handleDocumentClick)}},c=u,f=n(0),d=Object(f["a"])(c,r,o,!1,null,null,null);d.options.__file="packages/popover/src/main.vue";var p=d.exports,h=function(e,t,n){var r=t.expression?t.value:t.arg,o=n.context.$refs[r];o&&(Array.isArray(o)?o[0].$refs.reference=e:o.$refs.reference=e)},v={bind:function(e,t,n){h(e,t,n)},inserted:function(e,t,n){h(e,t,n)}},m=n(7),y=n.n(m);y.a.directive("popover",v),p.install=function(e){e.directive("popover",v),e.component(p.name,p)},p.directive=v;t["default"]=p}})},"6b7c":function(e,t,n){"use strict";t.__esModule=!0;var r=n("4897");t.default={methods:{t:function(){for(var e=arguments.length,t=Array(e),n=0;n0},e.prototype.connect_=function(){r&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),c?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){r&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t,r=u.some((function(e){return!!~n.indexOf(e)}));r&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),d=function(e,t){for(var n=0,r=Object.keys(t);n0},e}(),$="undefined"!==typeof WeakMap?new WeakMap:new n,j=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=f.getInstance(),r=new k(t,n,this);$.set(this,r)}return e}();["observe","unobserve","disconnect"].forEach((function(e){j.prototype[e]=function(){var t;return(t=$.get(this))[e].apply(t,arguments)}}));var T=function(){return"undefined"!==typeof o.ResizeObserver?o.ResizeObserver:j}();t["default"]=T}.call(this,n("c8ba"))},"6ed5":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=72)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},10:function(e,t){e.exports=n("f3ad")},13:function(e,t){e.exports=n("5128")},14:function(e,t){e.exports=n("eedf")},19:function(e,t){e.exports=n("4897")},2:function(e,t){e.exports=n("5924")},23:function(e,t){e.exports=n("41f8")},47:function(e,t){e.exports=n("722f")},6:function(e,t){e.exports=n("6b7c")},7:function(e,t){e.exports=n("2b0e")},72:function(e,t,n){"use strict";n.r(t);var r=n(7),o=n.n(r),i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"msgbox-fade"}},[n("div",{directives:[{name:"show",rawName:"v-show",value:e.visible,expression:"visible"}],staticClass:"el-message-box__wrapper",attrs:{tabindex:"-1",role:"dialog","aria-modal":"true","aria-label":e.title||"dialog"},on:{click:function(t){return t.target!==t.currentTarget?null:e.handleWrapperClick(t)}}},[n("div",{staticClass:"el-message-box",class:[e.customClass,e.center&&"el-message-box--center"]},[null!==e.title?n("div",{staticClass:"el-message-box__header"},[n("div",{staticClass:"el-message-box__title"},[e.icon&&e.center?n("div",{class:["el-message-box__status",e.icon]}):e._e(),n("span",[e._v(e._s(e.title))])]),e.showClose?n("button",{staticClass:"el-message-box__headerbtn",attrs:{type:"button","aria-label":"Close"},on:{click:function(t){e.handleAction(e.distinguishCancelAndClose?"close":"cancel")},keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction(e.distinguishCancelAndClose?"close":"cancel")}}},[n("i",{staticClass:"el-message-box__close el-icon-close"})]):e._e()]):e._e(),n("div",{staticClass:"el-message-box__content"},[n("div",{staticClass:"el-message-box__container"},[e.icon&&!e.center&&""!==e.message?n("div",{class:["el-message-box__status",e.icon]}):e._e(),""!==e.message?n("div",{staticClass:"el-message-box__message"},[e._t("default",[e.dangerouslyUseHTMLString?n("p",{domProps:{innerHTML:e._s(e.message)}}):n("p",[e._v(e._s(e.message))])])],2):e._e()]),n("div",{directives:[{name:"show",rawName:"v-show",value:e.showInput,expression:"showInput"}],staticClass:"el-message-box__input"},[n("el-input",{ref:"input",attrs:{type:e.inputType,placeholder:e.inputPlaceholder},nativeOn:{keydown:function(t){return!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleInputEnter(t)}},model:{value:e.inputValue,callback:function(t){e.inputValue=t},expression:"inputValue"}}),n("div",{staticClass:"el-message-box__errormsg",style:{visibility:e.editorErrorMessage?"visible":"hidden"}},[e._v(e._s(e.editorErrorMessage))])],1)]),n("div",{staticClass:"el-message-box__btns"},[e.showCancelButton?n("el-button",{class:[e.cancelButtonClasses],attrs:{loading:e.cancelButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("cancel")}},nativeOn:{click:function(t){e.handleAction("cancel")}}},[e._v("\n "+e._s(e.cancelButtonText||e.t("el.messagebox.cancel"))+"\n ")]):e._e(),n("el-button",{directives:[{name:"show",rawName:"v-show",value:e.showConfirmButton,expression:"showConfirmButton"}],ref:"confirm",class:[e.confirmButtonClasses],attrs:{loading:e.confirmButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("confirm")}},nativeOn:{click:function(t){e.handleAction("confirm")}}},[e._v("\n "+e._s(e.confirmButtonText||e.t("el.messagebox.confirm"))+"\n ")])],1)])])])},a=[];i._withStripped=!0;var s=n(13),l=n.n(s),u=n(6),c=n.n(u),f=n(10),d=n.n(f),p=n(14),h=n.n(p),v=n(2),m=n(19),y=n(47),g=n.n(y),b=void 0,_={success:"success",info:"info",warning:"warning",error:"error"},x={mixins:[l.a,c.a],props:{modal:{default:!0},lockScroll:{default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{default:!0},closeOnPressEscape:{default:!0},closeOnHashChange:{default:!0},center:{default:!1,type:Boolean},roundButton:{default:!1,type:Boolean}},components:{ElInput:d.a,ElButton:h.a},computed:{icon:function(){var e=this.type,t=this.iconClass;return t||(e&&_[e]?"el-icon-"+_[e]:"")},confirmButtonClasses:function(){return"el-button--primary "+this.confirmButtonClass},cancelButtonClasses:function(){return""+this.cancelButtonClass}},methods:{getSafeClose:function(){var e=this,t=this.uid;return function(){e.$nextTick((function(){t===e.uid&&e.doClose()}))}},doClose:function(){var e=this;this.visible&&(this.visible=!1,this._closing=!0,this.onClose&&this.onClose(),b.closeDialog(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose(),setTimeout((function(){e.action&&e.callback(e.action,e)})))},handleWrapperClick:function(){this.closeOnClickModal&&this.handleAction(this.distinguishCancelAndClose?"close":"cancel")},handleInputEnter:function(){if("textarea"!==this.inputType)return this.handleAction("confirm")},handleAction:function(e){("prompt"!==this.$type||"confirm"!==e||this.validate())&&(this.action=e,"function"===typeof this.beforeClose?(this.close=this.getSafeClose(),this.beforeClose(e,this,this.close)):this.doClose())},validate:function(){if("prompt"===this.$type){var e=this.inputPattern;if(e&&!e.test(this.inputValue||""))return this.editorErrorMessage=this.inputErrorMessage||Object(m["t"])("el.messagebox.error"),Object(v["addClass"])(this.getInputElement(),"invalid"),!1;var t=this.inputValidator;if("function"===typeof t){var n=t(this.inputValue);if(!1===n)return this.editorErrorMessage=this.inputErrorMessage||Object(m["t"])("el.messagebox.error"),Object(v["addClass"])(this.getInputElement(),"invalid"),!1;if("string"===typeof n)return this.editorErrorMessage=n,Object(v["addClass"])(this.getInputElement(),"invalid"),!1}}return this.editorErrorMessage="",Object(v["removeClass"])(this.getInputElement(),"invalid"),!0},getFirstFocus:function(){var e=this.$el.querySelector(".el-message-box__btns .el-button"),t=this.$el.querySelector(".el-message-box__btns .el-message-box__title");return e||t},getInputElement:function(){var e=this.$refs.input.$refs;return e.input||e.textarea},handleClose:function(){this.handleAction("close")}},watch:{inputValue:{immediate:!0,handler:function(e){var t=this;this.$nextTick((function(n){"prompt"===t.$type&&null!==e&&t.validate()}))}},visible:function(e){var t=this;e&&(this.uid++,"alert"!==this.$type&&"confirm"!==this.$type||this.$nextTick((function(){t.$refs.confirm.$el.focus()})),this.focusAfterClosed=document.activeElement,b=new g.a(this.$el,this.focusAfterClosed,this.getFirstFocus())),"prompt"===this.$type&&(e?setTimeout((function(){t.$refs.input&&t.$refs.input.$el&&t.getInputElement().focus()}),500):(this.editorErrorMessage="",Object(v["removeClass"])(this.getInputElement(),"invalid")))}},mounted:function(){var e=this;this.$nextTick((function(){e.closeOnHashChange&&window.addEventListener("hashchange",e.close)}))},beforeDestroy:function(){this.closeOnHashChange&&window.removeEventListener("hashchange",this.close),setTimeout((function(){b.closeDialog()}))},data:function(){return{uid:1,title:void 0,message:"",type:"",iconClass:"",customClass:"",showInput:!1,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,action:"",confirmButtonText:"",cancelButtonText:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonClass:"",confirmButtonDisabled:!1,cancelButtonClass:"",editorErrorMessage:null,callback:null,dangerouslyUseHTMLString:!1,focusAfterClosed:null,isOnComposition:!1,distinguishCancelAndClose:!1}}},w=x,C=n(0),S=Object(C["a"])(w,i,a,!1,null,null,null);S.options.__file="packages/message-box/src/main.vue";var O=S.exports,E=n(9),k=n.n(E),$=n(23),j="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},T={title:null,message:"",type:"",iconClass:"",showInput:!1,showClose:!0,modalFade:!0,lockScroll:!0,closeOnClickModal:!0,closeOnPressEscape:!0,closeOnHashChange:!0,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,confirmButtonPosition:"right",confirmButtonHighlight:!1,cancelButtonHighlight:!1,confirmButtonText:"",cancelButtonText:"",confirmButtonClass:"",cancelButtonClass:"",customClass:"",beforeClose:null,dangerouslyUseHTMLString:!1,center:!1,roundButton:!1,distinguishCancelAndClose:!1},A=o.a.extend(O),M=void 0,P=void 0,L=[],R=function(e){if(M){var t=M.callback;"function"===typeof t&&(P.showInput?t(P.inputValue,e):t(e)),M.resolve&&("confirm"===e?P.showInput?M.resolve({value:P.inputValue,action:e}):M.resolve(e):!M.reject||"cancel"!==e&&"close"!==e||M.reject(e))}},N=function(){P=new A({el:document.createElement("div")}),P.callback=R},I=function e(){if(P||N(),P.action="",(!P.visible||P.closeTimer)&&L.length>0){M=L.shift();var t=M.options;for(var n in t)t.hasOwnProperty(n)&&(P[n]=t[n]);void 0===t.callback&&(P.callback=R);var r=P.callback;P.callback=function(t,n){r(t,n),e()},Object($["isVNode"])(P.message)?(P.$slots.default=[P.message],P.message=null):delete P.$slots.default,["modal","showClose","closeOnClickModal","closeOnPressEscape","closeOnHashChange"].forEach((function(e){void 0===P[e]&&(P[e]=!0)})),document.body.appendChild(P.$el),o.a.nextTick((function(){P.visible=!0}))}},F=function e(t,n){if(!o.a.prototype.$isServer){if("string"===typeof t||Object($["isVNode"])(t)?(t={message:t},"string"===typeof arguments[1]&&(t.title=arguments[1])):t.callback&&!n&&(n=t.callback),"undefined"!==typeof Promise)return new Promise((function(r,o){L.push({options:k()({},T,e.defaults,t),callback:n,resolve:r,reject:o}),I()}));L.push({options:k()({},T,e.defaults,t),callback:n}),I()}};F.setDefaults=function(e){F.defaults=e},F.alert=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,$type:"alert",closeOnPressEscape:!1,closeOnClickModal:!1},n))},F.confirm=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,$type:"confirm",showCancelButton:!0},n))},F.prompt=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,showCancelButton:!0,showInput:!0,$type:"prompt"},n))},F.close=function(){P.doClose(),P.visible=!1,L=[],M=null};var H=F;t["default"]=H},9:function(e,t){e.exports=n("7f4d")}})},"6eeb":function(e,t,n){var r=n("da84"),o=n("9112"),i=n("5135"),a=n("ce4e"),s=n("8925"),l=n("69f3"),u=l.get,c=l.enforce,f=String(String).split("String");(e.exports=function(e,t,n,s){var l,u=!!s&&!!s.unsafe,d=!!s&&!!s.enumerable,p=!!s&&!!s.noTargetGet;"function"==typeof n&&("string"!=typeof t||i(n,"name")||o(n,"name",t),l=c(n),l.source||(l.source=f.join("string"==typeof t?t:""))),e!==r?(u?!p&&e[t]&&(d=!0):delete e[t],d?e[t]=n:o(e,t,n)):d?e[t]=n:a(t,n)})(Function.prototype,"toString",(function(){return"function"==typeof this&&u(this).source||s(this)}))},"722f":function(e,t,n){"use strict";t.__esModule=!0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n("e452"),i=a(o);function a(e){return e&&e.__esModule?e:{default:e}}var s,l=l||{};l.Dialog=function(e,t,n){var o=this;if(this.dialogNode=e,null===this.dialogNode||"dialog"!==this.dialogNode.getAttribute("role"))throw new Error("Dialog() requires a DOM element with ARIA role of dialog.");"string"===typeof t?this.focusAfterClosed=document.getElementById(t):"object"===("undefined"===typeof t?"undefined":r(t))?this.focusAfterClosed=t:this.focusAfterClosed=null,"string"===typeof n?this.focusFirst=document.getElementById(n):"object"===("undefined"===typeof n?"undefined":r(n))?this.focusFirst=n:this.focusFirst=null,this.focusFirst?this.focusFirst.focus():i.default.focusFirstDescendant(this.dialogNode),this.lastFocus=document.activeElement,s=function(e){o.trapFocus(e)},this.addListeners()},l.Dialog.prototype.addListeners=function(){document.addEventListener("focus",s,!0)},l.Dialog.prototype.removeListeners=function(){document.removeEventListener("focus",s,!0)},l.Dialog.prototype.closeDialog=function(){var e=this;this.removeListeners(),this.focusAfterClosed&&setTimeout((function(){e.focusAfterClosed.focus()}))},l.Dialog.prototype.trapFocus=function(e){i.default.IgnoreUtilFocusChanges||(this.dialogNode.contains(e.target)?this.lastFocus=e.target:(i.default.focusFirstDescendant(this.dialogNode),this.lastFocus===document.activeElement&&i.default.focusLastDescendant(this.dialogNode),this.lastFocus=document.activeElement))},t.default=l.Dialog},7418:function(e,t){t.f=Object.getOwnPropertySymbols},"76b9":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=108)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},108:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-breadcrumb",attrs:{"aria-label":"Breadcrumb",role:"navigation"}},[e._t("default")],2)},o=[];r._withStripped=!0;var i={name:"ElBreadcrumb",props:{separator:{type:String,default:"/"},separatorClass:{type:String,default:""}},provide:function(){return{elBreadcrumb:this}},mounted:function(){var e=this.$el.querySelectorAll(".el-breadcrumb__item");e.length&&e[e.length-1].setAttribute("aria-current","page")}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/breadcrumb/src/breadcrumb.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},7839:function(e,t){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},"7b0b":function(e,t,n){var r=n("1d80");e.exports=function(e){return Object(r(e))}},"7b3e":function(e,t,n){"use strict";var r,o=n("a3de"); +/** + * Checks if an event is supported in the current execution environment. + * + * NOTE: This will not work correctly for non-generic events such as `change`, + * `reset`, `load`, `error`, and `select`. + * + * Borrows from Modernizr. + * + * @param {string} eventNameSuffix Event name, e.g. "click". + * @param {?boolean} capture Check if the capture phase is supported. + * @return {boolean} True if the event is supported. + * @internal + * @license Modernizr 3.0.0pre (Custom Build) | MIT + */ +function i(e,t){if(!o.canUseDOM||t&&!("addEventListener"in document))return!1;var n="on"+e,i=n in document;if(!i){var a=document.createElement("div");a.setAttribute(n,"return;"),i="function"===typeof a[n]}return!i&&r&&"wheel"===e&&(i=document.implementation.hasFeature("Events.wheel","3.0")),i}o.canUseDOM&&(r=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("","")),e.exports=i},"7bc3":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=82)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},82:function(e,t,n){"use strict";n.r(t);var r=function(e,t){var n=t._c;return n("div",t._g(t._b({class:[t.data.staticClass,"el-divider","el-divider--"+t.props.direction]},"div",t.data.attrs,!1),t.listeners),[t.slots().default&&"vertical"!==t.props.direction?n("div",{class:["el-divider__text","is-"+t.props.contentPosition]},[t._t("default")],2):t._e()])},o=[];r._withStripped=!0;var i={name:"ElDivider",props:{direction:{type:String,default:"horizontal",validator:function(e){return-1!==["horizontal","vertical"].indexOf(e)}},contentPosition:{type:String,default:"center",validator:function(e){return-1!==["left","center","right"].indexOf(e)}}}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!0,null,null,null);l.options.__file="packages/divider/src/main.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},"7c73":function(e,t,n){var r,o=n("825a"),i=n("37e8"),a=n("7839"),s=n("d012"),l=n("1be4"),u=n("cc12"),c=n("f772"),f=">",d="<",p="prototype",h="script",v=c("IE_PROTO"),m=function(){},y=function(e){return d+h+f+e+d+"/"+h+f},g=function(e){e.write(y("")),e.close();var t=e.parentWindow.Object;return e=null,t},b=function(){var e,t=u("iframe"),n="java"+h+":";return t.style.display="none",l.appendChild(t),t.src=String(n),e=t.contentWindow.document,e.open(),e.write(y("document.F=Object")),e.close(),e.F},_=function(){try{r=document.domain&&new ActiveXObject("htmlfile")}catch(t){}_=r?g(r):b();var e=a.length;while(e--)delete _[p][a[e]];return _()};s[v]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(m[p]=o(e),n=new m,m[p]=null,n[v]=e):n=_(),void 0===t?n:i(n,t)}},"7dd0":function(e,t,n){"use strict";var r=n("23e7"),o=n("9ed3"),i=n("e163"),a=n("d2bb"),s=n("d44e"),l=n("9112"),u=n("6eeb"),c=n("b622"),f=n("c430"),d=n("3f8c"),p=n("ae93"),h=p.IteratorPrototype,v=p.BUGGY_SAFARI_ITERATORS,m=c("iterator"),y="keys",g="values",b="entries",_=function(){return this};e.exports=function(e,t,n,c,p,x,w){o(n,t,c);var C,S,O,E=function(e){if(e===p&&A)return A;if(!v&&e in j)return j[e];switch(e){case y:return function(){return new n(this,e)};case g:return function(){return new n(this,e)};case b:return function(){return new n(this,e)}}return function(){return new n(this)}},k=t+" Iterator",$=!1,j=e.prototype,T=j[m]||j["@@iterator"]||p&&j[p],A=!v&&T||E(p),M="Array"==t&&j.entries||T;if(M&&(C=i(M.call(new e)),h!==Object.prototype&&C.next&&(f||i(C)===h||(a?a(C,h):"function"!=typeof C[m]&&l(C,m,_)),s(C,k,!0,!0),f&&(d[k]=_))),p==g&&T&&T.name!==g&&($=!0,A=function(){return T.call(this)}),f&&!w||j[m]===A||l(j,m,A),d[t]=A,p)if(S={values:E(g),keys:x?A:E(y),entries:E(b)},w)for(O in S)(v||$||!(O in j))&&u(j,O,S[O]);else r({target:t,proto:!0,forced:v||$},S);return S}},"7f4d":function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e){for(var t=1,n=arguments.length;t0&&void 0!==arguments[0]?arguments[0]:"";return String(e).replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")};var h=t.arrayFindIndex=function(e,t){for(var n=0;n!==e.length;++n)if(t(e[n]))return n;return-1},v=(t.arrayFind=function(e,t){var n=h(e,t);return-1!==n?e[n]:void 0},t.coerceTruthyValueToArray=function(e){return Array.isArray(e)?e:e?[e]:[]},t.isIE=function(){return!i.default.prototype.$isServer&&!isNaN(Number(document.documentMode))},t.isEdge=function(){return!i.default.prototype.$isServer&&navigator.userAgent.indexOf("Edge")>-1},t.isFirefox=function(){return!i.default.prototype.$isServer&&!!window.navigator.userAgent.match(/firefox/i)},t.autoprefixer=function(e){if("object"!==("undefined"===typeof e?"undefined":r(e)))return e;var t=["transform","transition","animation"],n=["ms-","webkit-"];return t.forEach((function(t){var r=e[t];t&&r&&n.forEach((function(n){e[n+t]=r}))})),e},t.kebabCase=function(e){var t=/([^-])([A-Z])/g;return e.replace(t,"$1-$2").replace(t,"$1-$2").toLowerCase()},t.capitalize=function(e){return(0,a.isString)(e)?e.charAt(0).toUpperCase()+e.slice(1):e},t.looseEqual=function(e,t){var n=(0,a.isObject)(e),r=(0,a.isObject)(t);return n&&r?JSON.stringify(e)===JSON.stringify(t):!n&&!r&&String(e)===String(t)}),m=t.arrayEquals=function(e,t){if(e=e||[],t=t||[],e.length!==t.length)return!1;for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};if(!o.a.prototype.$isServer){if(e=_()({},w,e),"string"===typeof e.target&&(e.target=document.querySelector(e.target)),e.target=e.target||document.body,e.target!==document.body?e.fullscreen=!1:e.body=!0,e.fullscreen&&C)return C;var t=e.body?document.body:e.target,n=new x({el:document.createElement("div"),data:e});return S(e,t,n),"absolute"!==n.originalPosition&&"fixed"!==n.originalPosition&&Object(d["addClass"])(t,"el-loading-parent--relative"),e.fullscreen&&e.lock&&Object(d["addClass"])(t,"el-loading-parent--hidden"),t.appendChild(n.$el),o.a.nextTick((function(){n.visible=!0})),e.fullscreen&&(C=n),n}},E=O;t["default"]={install:function(e){e.use(g),e.prototype.$loading=E},directive:g,service:E}},9:function(e,t){e.exports=n("7f4d")}})},"8bbc":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=127)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},127:function(e,t,n){"use strict";n.r(t);var r,o,i={name:"ElTag",props:{text:String,closable:Boolean,type:String,hit:Boolean,disableTransitions:Boolean,color:String,size:String,effect:{type:String,default:"light",validator:function(e){return-1!==["dark","light","plain"].indexOf(e)}}},methods:{handleClose:function(e){e.stopPropagation(),this.$emit("close",e)},handleClick:function(e){this.$emit("click",e)}},computed:{tagSize:function(){return this.size||(this.$ELEMENT||{}).size}},render:function(e){var t=this.type,n=this.tagSize,r=this.hit,o=this.effect,i=["el-tag",t?"el-tag--"+t:"",n?"el-tag--"+n:"",o?"el-tag--"+o:"",r&&"is-hit"],a=e("span",{class:i,style:{backgroundColor:this.color},on:{click:this.handleClick}},[this.$slots.default,this.closable&&e("i",{class:"el-tag__close el-icon-close",on:{click:this.handleClose}})]);return this.disableTransitions?a:e("transition",{attrs:{name:"el-zoom-in-center"}},[a])}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/tag/src/tag.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},"8c4f":function(e,t,n){"use strict"; +/*! + * vue-router v3.5.2 + * (c) 2021 Evan You + * @license MIT + */function r(e,t){0}function o(e,t){for(var n in t)e[n]=t[n];return e}var i=/[!'()*]/g,a=function(e){return"%"+e.charCodeAt(0).toString(16)},s=/%2C/g,l=function(e){return encodeURIComponent(e).replace(i,a).replace(s,",")};function u(e){try{return decodeURIComponent(e)}catch(t){0}return e}function c(e,t,n){void 0===t&&(t={});var r,o=n||d;try{r=o(e||"")}catch(s){r={}}for(var i in t){var a=t[i];r[i]=Array.isArray(a)?a.map(f):f(a)}return r}var f=function(e){return null==e||"object"===typeof e?e:String(e)};function d(e){var t={};return e=e.trim().replace(/^(\?|#|&)/,""),e?(e.split("&").forEach((function(e){var n=e.replace(/\+/g," ").split("="),r=u(n.shift()),o=n.length>0?u(n.join("=")):null;void 0===t[r]?t[r]=o:Array.isArray(t[r])?t[r].push(o):t[r]=[t[r],o]})),t):t}function p(e){var t=e?Object.keys(e).map((function(t){var n=e[t];if(void 0===n)return"";if(null===n)return l(t);if(Array.isArray(n)){var r=[];return n.forEach((function(e){void 0!==e&&(null===e?r.push(l(t)):r.push(l(t)+"="+l(e)))})),r.join("&")}return l(t)+"="+l(n)})).filter((function(e){return e.length>0})).join("&"):null;return t?"?"+t:""}var h=/\/?$/;function v(e,t,n,r){var o=r&&r.options.stringifyQuery,i=t.query||{};try{i=m(i)}catch(s){}var a={name:t.name||e&&e.name,meta:e&&e.meta||{},path:t.path||"/",hash:t.hash||"",query:i,params:t.params||{},fullPath:b(t,o),matched:e?g(e):[]};return n&&(a.redirectedFrom=b(n,o)),Object.freeze(a)}function m(e){if(Array.isArray(e))return e.map(m);if(e&&"object"===typeof e){var t={};for(var n in e)t[n]=m(e[n]);return t}return e}var y=v(null,{path:"/"});function g(e){var t=[];while(e)t.unshift(e),e=e.parent;return t}function b(e,t){var n=e.path,r=e.query;void 0===r&&(r={});var o=e.hash;void 0===o&&(o="");var i=t||p;return(n||"/")+i(r)+o}function _(e,t,n){return t===y?e===t:!!t&&(e.path&&t.path?e.path.replace(h,"")===t.path.replace(h,"")&&(n||e.hash===t.hash&&x(e.query,t.query)):!(!e.name||!t.name)&&(e.name===t.name&&(n||e.hash===t.hash&&x(e.query,t.query)&&x(e.params,t.params))))}function x(e,t){if(void 0===e&&(e={}),void 0===t&&(t={}),!e||!t)return e===t;var n=Object.keys(e).sort(),r=Object.keys(t).sort();return n.length===r.length&&n.every((function(n,o){var i=e[n],a=r[o];if(a!==n)return!1;var s=t[n];return null==i||null==s?i===s:"object"===typeof i&&"object"===typeof s?x(i,s):String(i)===String(s)}))}function w(e,t){return 0===e.path.replace(h,"/").indexOf(t.path.replace(h,"/"))&&(!t.hash||e.hash===t.hash)&&C(e.query,t.query)}function C(e,t){for(var n in t)if(!(n in e))return!1;return!0}function S(e){for(var t=0;t=0&&(t=e.slice(r),e=e.slice(0,r));var o=e.indexOf("?");return o>=0&&(n=e.slice(o+1),e=e.slice(0,o)),{path:e,query:n,hash:t}}function T(e){return e.replace(/\/\//g,"/")}var A=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)},M=J,P=F,L=H,R=B,N=Y,I=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function F(e,t){var n,r=[],o=0,i=0,a="",s=t&&t.delimiter||"/";while(null!=(n=I.exec(e))){var l=n[0],u=n[1],c=n.index;if(a+=e.slice(i,c),i=c+l.length,u)a+=u[1];else{var f=e[i],d=n[2],p=n[3],h=n[4],v=n[5],m=n[6],y=n[7];a&&(r.push(a),a="");var g=null!=d&&null!=f&&f!==d,b="+"===m||"*"===m,_="?"===m||"*"===m,x=n[2]||s,w=h||v;r.push({name:p||o++,prefix:d||"",delimiter:x,optional:_,repeat:b,partial:g,asterisk:!!y,pattern:w?V(w):y?".*":"[^"+W(x)+"]+?"})}}return i1||!S.length)return 0===S.length?e():e("span",{},S)}if("a"===this.tag)C.on=x,C.attrs={href:l,"aria-current":g};else{var O=se(this.$slots.default);if(O){O.isStatic=!1;var E=O.data=o({},O.data);for(var k in E.on=E.on||{},E.on){var $=E.on[k];k in x&&(E.on[k]=Array.isArray($)?$:[$])}for(var j in x)j in E.on?E.on[j].push(x[j]):E.on[j]=b;var T=O.data.attrs=o({},O.data.attrs);T.href=l,T["aria-current"]=g}else C.on=x}return e(this.tag,C,this.$slots.default)}};function ae(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(void 0===e.button||0===e.button)){if(e.currentTarget&&e.currentTarget.getAttribute){var t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function se(e){if(e)for(var t,n=0;n-1&&(s.params[f]=n.params[f]);return s.path=Q(u.path,s.params,'named route "'+l+'"'),d(u,s,a)}if(s.path){s.params={};for(var p=0;p=e.length?n():e[o]?t(e[o],(function(){r(o+1)})):r(o+1)};r(0)}var He={redirected:2,aborted:4,cancelled:8,duplicated:16};function De(e,t){return Ve(e,t,He.redirected,'Redirected when going from "'+e.fullPath+'" to "'+qe(t)+'" via a navigation guard.')}function ze(e,t){var n=Ve(e,t,He.duplicated,'Avoided redundant navigation to current location: "'+e.fullPath+'".');return n.name="NavigationDuplicated",n}function Be(e,t){return Ve(e,t,He.cancelled,'Navigation cancelled from "'+e.fullPath+'" to "'+t.fullPath+'" with a new navigation.')}function We(e,t){return Ve(e,t,He.aborted,'Navigation aborted from "'+e.fullPath+'" to "'+t.fullPath+'" via a navigation guard.')}function Ve(e,t,n,r){var o=new Error(r);return o._isRouter=!0,o.from=e,o.to=t,o.type=n,o}var Ue=["params","query","hash"];function qe(e){if("string"===typeof e)return e;if("path"in e)return e.path;var t={};return Ue.forEach((function(n){n in e&&(t[n]=e[n])})),JSON.stringify(t,null,2)}function Ke(e){return Object.prototype.toString.call(e).indexOf("Error")>-1}function Ge(e,t){return Ke(e)&&e._isRouter&&(null==t||e.type===t)}function Xe(e){return function(t,n,r){var o=!1,i=0,a=null;Ye(e,(function(e,t,n,s){if("function"===typeof e&&void 0===e.cid){o=!0,i++;var l,u=et((function(t){Qe(t)&&(t=t.default),e.resolved="function"===typeof t?t:te.extend(t),n.components[s]=t,i--,i<=0&&r()})),c=et((function(e){var t="Failed to resolve async component "+s+": "+e;a||(a=Ke(e)?e:new Error(t),r(a))}));try{l=e(u,c)}catch(d){c(d)}if(l)if("function"===typeof l.then)l.then(u,c);else{var f=l.component;f&&"function"===typeof f.then&&f.then(u,c)}}})),o||r()}}function Ye(e,t){return Je(e.map((function(e){return Object.keys(e.components).map((function(n){return t(e.components[n],e.instances[n],e,n)}))})))}function Je(e){return Array.prototype.concat.apply([],e)}var Ze="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag;function Qe(e){return e.__esModule||Ze&&"Module"===e[Symbol.toStringTag]}function et(e){var t=!1;return function(){var n=[],r=arguments.length;while(r--)n[r]=arguments[r];if(!t)return t=!0,e.apply(this,n)}}var tt=function(e,t){this.router=e,this.base=nt(t),this.current=y,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function nt(e){if(!e)if(ue){var t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^https?:\/\/[^\/]+/,"")}else e="/";return"/"!==e.charAt(0)&&(e="/"+e),e.replace(/\/$/,"")}function rt(e,t){var n,r=Math.max(e.length,t.length);for(n=0;n0)){var t=this.router,n=t.options.scrollBehavior,r=Re&&n;r&&this.listeners.push(Ce());var o=function(){var n=e.current,o=dt(e.base);e.current===y&&o===e._startLocation||e.transitionTo(o,(function(e){r&&Se(t,e,n,!0)}))};window.addEventListener("popstate",o),this.listeners.push((function(){window.removeEventListener("popstate",o)}))}},t.prototype.go=function(e){window.history.go(e)},t.prototype.push=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){Ne(T(r.base+e.fullPath)),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){Ie(T(r.base+e.fullPath)),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.ensureURL=function(e){if(dt(this.base)!==this.current.fullPath){var t=T(this.base+this.current.fullPath);e?Ne(t):Ie(t)}},t.prototype.getCurrentLocation=function(){return dt(this.base)},t}(tt);function dt(e){var t=window.location.pathname,n=t.toLowerCase(),r=e.toLowerCase();return!e||n!==r&&0!==n.indexOf(T(r+"/"))||(t=t.slice(e.length)),(t||"/")+window.location.search+window.location.hash}var pt=function(e){function t(t,n,r){e.call(this,t,n),r&&ht(this.base)||vt()}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.setupListeners=function(){var e=this;if(!(this.listeners.length>0)){var t=this.router,n=t.options.scrollBehavior,r=Re&&n;r&&this.listeners.push(Ce());var o=function(){var t=e.current;vt()&&e.transitionTo(mt(),(function(n){r&&Se(e.router,n,t,!0),Re||bt(n.fullPath)}))},i=Re?"popstate":"hashchange";window.addEventListener(i,o),this.listeners.push((function(){window.removeEventListener(i,o)}))}},t.prototype.push=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){gt(e.fullPath),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){bt(e.fullPath),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.go=function(e){window.history.go(e)},t.prototype.ensureURL=function(e){var t=this.current.fullPath;mt()!==t&&(e?gt(t):bt(t))},t.prototype.getCurrentLocation=function(){return mt()},t}(tt);function ht(e){var t=dt(e);if(!/^\/#/.test(t))return window.location.replace(T(e+"/#"+t)),!0}function vt(){var e=mt();return"/"===e.charAt(0)||(bt("/"+e),!1)}function mt(){var e=window.location.href,t=e.indexOf("#");return t<0?"":(e=e.slice(t+1),e)}function yt(e){var t=window.location.href,n=t.indexOf("#"),r=n>=0?t.slice(0,n):t;return r+"#"+e}function gt(e){Re?Ne(yt(e)):window.location.hash=e}function bt(e){Re?Ie(yt(e)):window.location.replace(yt(e))}var _t=function(e){function t(t,n){e.call(this,t,n),this.stack=[],this.index=-1}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.push=function(e,t,n){var r=this;this.transitionTo(e,(function(e){r.stack=r.stack.slice(0,r.index+1).concat(e),r.index++,t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this;this.transitionTo(e,(function(e){r.stack=r.stack.slice(0,r.index).concat(e),t&&t(e)}),n)},t.prototype.go=function(e){var t=this,n=this.index+e;if(!(n<0||n>=this.stack.length)){var r=this.stack[n];this.confirmTransition(r,(function(){var e=t.current;t.index=n,t.updateRoute(r),t.router.afterHooks.forEach((function(t){t&&t(r,e)}))}),(function(e){Ge(e,He.duplicated)&&(t.index=n)}))}},t.prototype.getCurrentLocation=function(){var e=this.stack[this.stack.length-1];return e?e.fullPath:"/"},t.prototype.ensureURL=function(){},t}(tt),xt=function(e){void 0===e&&(e={}),this.app=null,this.apps=[],this.options=e,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=he(e.routes||[],this);var t=e.mode||"hash";switch(this.fallback="history"===t&&!Re&&!1!==e.fallback,this.fallback&&(t="hash"),ue||(t="abstract"),this.mode=t,t){case"history":this.history=new ft(this,e.base);break;case"hash":this.history=new pt(this,e.base,this.fallback);break;case"abstract":this.history=new _t(this,e.base);break;default:0}},wt={currentRoute:{configurable:!0}};function Ct(e,t){return e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function St(e,t,n){var r="hash"===n?"#"+t:t;return e?T(e+"/"+r):r}xt.prototype.match=function(e,t,n){return this.matcher.match(e,t,n)},wt.currentRoute.get=function(){return this.history&&this.history.current},xt.prototype.init=function(e){var t=this;if(this.apps.push(e),e.$once("hook:destroyed",(function(){var n=t.apps.indexOf(e);n>-1&&t.apps.splice(n,1),t.app===e&&(t.app=t.apps[0]||null),t.app||t.history.teardown()})),!this.app){this.app=e;var n=this.history;if(n instanceof ft||n instanceof pt){var r=function(e){var r=n.current,o=t.options.scrollBehavior,i=Re&&o;i&&"fullPath"in e&&Se(t,e,r,!1)},o=function(e){n.setupListeners(),r(e)};n.transitionTo(n.getCurrentLocation(),o,o)}n.listen((function(e){t.apps.forEach((function(t){t._route=e}))}))}},xt.prototype.beforeEach=function(e){return Ct(this.beforeHooks,e)},xt.prototype.beforeResolve=function(e){return Ct(this.resolveHooks,e)},xt.prototype.afterEach=function(e){return Ct(this.afterHooks,e)},xt.prototype.onReady=function(e,t){this.history.onReady(e,t)},xt.prototype.onError=function(e){this.history.onError(e)},xt.prototype.push=function(e,t,n){var r=this;if(!t&&!n&&"undefined"!==typeof Promise)return new Promise((function(t,n){r.history.push(e,t,n)}));this.history.push(e,t,n)},xt.prototype.replace=function(e,t,n){var r=this;if(!t&&!n&&"undefined"!==typeof Promise)return new Promise((function(t,n){r.history.replace(e,t,n)}));this.history.replace(e,t,n)},xt.prototype.go=function(e){this.history.go(e)},xt.prototype.back=function(){this.go(-1)},xt.prototype.forward=function(){this.go(1)},xt.prototype.getMatchedComponents=function(e){var t=e?e.matched?e:this.resolve(e).route:this.currentRoute;return t?[].concat.apply([],t.matched.map((function(e){return Object.keys(e.components).map((function(t){return e.components[t]}))}))):[]},xt.prototype.resolve=function(e,t,n){t=t||this.history.current;var r=ee(e,t,n,this),o=this.match(r,t),i=o.redirectedFrom||o.fullPath,a=this.history.base,s=St(a,i,this.mode);return{location:r,route:o,href:s,normalizedTo:r,resolved:o}},xt.prototype.getRoutes=function(){return this.matcher.getRoutes()},xt.prototype.addRoute=function(e,t){this.matcher.addRoute(e,t),this.history.current!==y&&this.history.transitionTo(this.history.getCurrentLocation())},xt.prototype.addRoutes=function(e){this.matcher.addRoutes(e),this.history.current!==y&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(xt.prototype,wt),xt.install=le,xt.version="3.5.2",xt.isNavigationFailure=Ge,xt.NavigationFailureType=He,xt.START_LOCATION=y,ue&&window.Vue&&window.Vue.use(xt),t["a"]=xt},"8eb7":function(e,t){var n,r,o,i,a,s,l,u,c,f,d,p,h,v,m,y=!1;function g(){if(!y){y=!0;var e=navigator.userAgent,t=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(e),g=/(Mac OS X)|(Windows)|(Linux)/.exec(e);if(p=/\b(iPhone|iP[ao]d)/.exec(e),h=/\b(iP[ao]d)/.exec(e),f=/Android/i.exec(e),v=/FBAN\/\w+;/i.exec(e),m=/Mobile/i.exec(e),d=!!/Win64/.exec(e),t){n=t[1]?parseFloat(t[1]):t[5]?parseFloat(t[5]):NaN,n&&document&&document.documentMode&&(n=document.documentMode);var b=/(?:Trident\/(\d+.\d+))/.exec(e);s=b?parseFloat(b[1])+4:n,r=t[2]?parseFloat(t[2]):NaN,o=t[3]?parseFloat(t[3]):NaN,i=t[4]?parseFloat(t[4]):NaN,i?(t=/(?:Chrome\/(\d+\.\d+))/.exec(e),a=t&&t[1]?parseFloat(t[1]):NaN):a=NaN}else n=r=o=a=i=NaN;if(g){if(g[1]){var _=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(e);l=!_||parseFloat(_[1].replace("_","."))}else l=!1;u=!!g[2],c=!!g[3]}else l=u=c=!1}}var b={ie:function(){return g()||n},ieCompatibilityMode:function(){return g()||s>n},ie64:function(){return b.ie()&&d},firefox:function(){return g()||r},opera:function(){return g()||o},webkit:function(){return g()||i},safari:function(){return b.webkit()},chrome:function(){return g()||a},windows:function(){return g()||u},osx:function(){return g()||l},linux:function(){return g()||c},iphone:function(){return g()||p},mobile:function(){return g()||p||h||f||m},nativeApp:function(){return g()||v},android:function(){return g()||f},ipad:function(){return g()||h}};e.exports=b},"8f24":function(e,t,n){},"90e3":function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol("+String(void 0===e?"":e)+")_"+(++n+r).toString(36)}},9112:function(e,t,n){var r=n("83ab"),o=n("9bf2"),i=n("5c6c");e.exports=r?function(e,t,n){return o.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},"94ca":function(e,t,n){var r=n("d039"),o=/#|\.prototype\./,i=function(e,t){var n=s[a(e)];return n==u||n!=l&&("function"==typeof t?r(t):!!t)},a=i.normalize=function(e){return String(e).replace(o,".").toLowerCase()},s=i.data={},l=i.NATIVE="N",u=i.POLYFILL="P";e.exports=i},9619:function(e,t,n){var r=n("597f"),o=n("0e15");e.exports={throttle:r,debounce:o}},"9bf2":function(e,t,n){var r=n("83ab"),o=n("0cfb"),i=n("825a"),a=n("c04e"),s=Object.defineProperty;t.f=r?s:function(e,t,n){if(i(e),t=a(t,!0),i(n),o)try{return s(e,t,n)}catch(r){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},"9d7e":function(e,t,n){"use strict";t.__esModule=!0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.default=function(e){function t(e){for(var t=arguments.length,n=Array(t>1?t-1:0),a=1;a0?r:n)(e)}},a742:function(e,t,n){"use strict";t.__esModule=!0,t.isDefined=t.isUndefined=t.isFunction=void 0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.isString=s,t.isObject=l,t.isHtmlElement=u;var o=n("2b0e"),i=a(o);function a(e){return e&&e.__esModule?e:{default:e}}function s(e){return"[object String]"===Object.prototype.toString.call(e)}function l(e){return"[object Object]"===Object.prototype.toString.call(e)}function u(e){return e&&e.nodeType===Node.ELEMENT_NODE}var c=function(e){var t={};return e&&"[object Function]"===t.toString.call(e)};"object"===("undefined"===typeof Int8Array?"undefined":r(Int8Array))||!i.default.prototype.$isServer&&"function"===typeof document.childNodes||(t.isFunction=c=function(e){return"function"===typeof e||!1}),t.isFunction=c;t.isUndefined=function(e){return void 0===e},t.isDefined=function(e){return void 0!==e&&null!==e}},a79d:function(e,t,n){"use strict";var r=n("23e7"),o=n("c430"),i=n("fea9"),a=n("d039"),s=n("d066"),l=n("4840"),u=n("cdf9"),c=n("6eeb"),f=!!i&&a((function(){i.prototype["finally"].call({then:function(){}},(function(){}))}));if(r({target:"Promise",proto:!0,real:!0,forced:f},{finally:function(e){var t=l(this,s("Promise")),n="function"==typeof e;return this.then(n?function(n){return u(t,e()).then((function(){return n}))}:e,n?function(n){return u(t,e()).then((function(){throw n}))}:e)}}),!o&&"function"==typeof i){var d=s("Promise").prototype["finally"];i.prototype["finally"]!==d&&c(i.prototype,"finally",d,{unsafe:!0})}},ad41:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=56)}([function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},,function(e,t){e.exports=n("5924")},function(e,t){e.exports=n("8122")},,function(e,t){e.exports=n("e974")},function(e,t){e.exports=n("6b7c")},function(e,t){e.exports=n("2b0e")},function(e,t,n){"use strict";n.d(t,"b",(function(){return i})),n.d(t,"i",(function(){return s})),n.d(t,"d",(function(){return l})),n.d(t,"e",(function(){return u})),n.d(t,"c",(function(){return c})),n.d(t,"g",(function(){return f})),n.d(t,"f",(function(){return d})),n.d(t,"h",(function(){return h})),n.d(t,"l",(function(){return v})),n.d(t,"k",(function(){return m})),n.d(t,"j",(function(){return y})),n.d(t,"a",(function(){return g})),n.d(t,"m",(function(){return b})),n.d(t,"n",(function(){return _}));var r=n(3),o="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i=function(e){var t=e.target;while(t&&"HTML"!==t.tagName.toUpperCase()){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},a=function(e){return null!==e&&"object"===("undefined"===typeof e?"undefined":o(e))},s=function(e,t,n,o,i){if(!t&&!o&&(!i||Array.isArray(i)&&!i.length))return e;n="string"===typeof n?"descending"===n?-1:1:n&&n<0?-1:1;var s=o?null:function(n,o){return i?(Array.isArray(i)||(i=[i]),i.map((function(t){return"string"===typeof t?Object(r["getValueByPath"])(n,t):t(n,o,e)}))):("$key"!==t&&a(n)&&"$value"in n&&(n=n.$value),[a(n)?Object(r["getValueByPath"])(n,t):n])},l=function(e,t){if(o)return o(e.value,t.value);for(var n=0,r=e.key.length;nt.key[n])return 1}return 0};return e.map((function(e,t){return{value:e,index:t,key:s?s(e,t):null}})).sort((function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*n})).map((function(e){return e.value}))},l=function(e,t){var n=null;return e.columns.forEach((function(e){e.id===t&&(n=e)})),n},u=function(e,t){for(var n=null,r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",o=function(e){return!(Array.isArray(e)&&e.length)};function i(e,a,s){t(e,a,s),a.forEach((function(e){if(e[r])t(e,null,s+1);else{var a=e[n];o(a)||i(e,a,s+1)}}))}e.forEach((function(e){if(e[r])t(e,null,0);else{var a=e[n];o(a)||i(e,a,0)}}))}},function(e,t){e.exports=n("7f4d")},,function(e,t){e.exports=n("2bb5")},function(e,t){e.exports=n("417f")},function(e,t){e.exports=n("5128")},,function(e,t){e.exports=n("14e9")},function(e,t){e.exports=n("4010")},function(e,t){e.exports=n("0e15")},function(e,t){e.exports=n("dcdc")},,,,,,,,,,,function(e,t){e.exports=n("299c")},,,,,,,,,function(e,t){e.exports=n("e62d")},function(e,t){e.exports=n("7fc1")},,,,function(e,t){e.exports=n("9619")},,,function(e,t){e.exports=n("c098")},,,,,,,,,,function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-table",class:[{"el-table--fit":e.fit,"el-table--striped":e.stripe,"el-table--border":e.border||e.isGroup,"el-table--hidden":e.isHidden,"el-table--group":e.isGroup,"el-table--fluid-height":e.maxHeight,"el-table--scrollable-x":e.layout.scrollX,"el-table--scrollable-y":e.layout.scrollY,"el-table--enable-row-hover":!e.store.states.isComplex,"el-table--enable-row-transition":0!==(e.store.states.data||[]).length&&(e.store.states.data||[]).length<100},e.tableSize?"el-table--"+e.tableSize:""],on:{mouseleave:function(t){e.handleMouseLeave(t)}}},[n("div",{ref:"hiddenColumns",staticClass:"hidden-columns"},[e._t("default")],2),e.showHeader?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"headerWrapper",staticClass:"el-table__header-wrapper"},[n("table-header",{ref:"tableHeader",style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"default-sort":e.defaultSort}})],1):e._e(),n("div",{ref:"bodyWrapper",staticClass:"el-table__body-wrapper",class:[e.layout.scrollX?"is-scrolling-"+e.scrollPosition:"is-scrolling-none"],style:[e.bodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{context:e.context,store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.data&&0!==e.data.length?e._e():n("div",{ref:"emptyBlock",staticClass:"el-table__empty-block",style:e.emptyBlockStyle},[n("span",{staticClass:"el-table__empty-text"},[e._t("empty",[e._v(e._s(e.emptyText||e.t("el.table.emptyText")))])],2)]),e.$slots.append?n("div",{ref:"appendWrapper",staticClass:"el-table__append-wrapper"},[e._t("append")],2):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"},{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"footerWrapper",staticClass:"el-table__footer-wrapper"},[n("table-footer",{style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,"default-sort":e.defaultSort}})],1):e._e(),e.fixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"fixedWrapper",staticClass:"el-table__fixed",style:[{width:e.layout.fixedWidth?e.layout.fixedWidth+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"fixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"fixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"fixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"left",store:e.store,stripe:e.stripe,highlight:e.highlightCurrentRow,"row-class-name":e.rowClassName,"row-style":e.rowStyle}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"fixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"rightFixedWrapper",staticClass:"el-table__fixed-right",style:[{width:e.layout.rightFixedWidth?e.layout.rightFixedWidth+"px":"",right:e.layout.scrollY?(e.border?e.layout.gutterWidth:e.layout.gutterWidth||0)+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"rightFixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"rightFixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"rightFixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"right",store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"rightFixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{ref:"rightFixedPatch",staticClass:"el-table__fixed-right-patch",style:{width:e.layout.scrollY?e.layout.gutterWidth+"px":"0",height:e.layout.headerHeight+"px"}}):e._e(),n("div",{directives:[{name:"show",rawName:"v-show",value:e.resizeProxyVisible,expression:"resizeProxyVisible"}],ref:"resizeProxy",staticClass:"el-table__column-resize-proxy"})])},o=[];r._withStripped=!0;var i=n(18),a=n.n(i),s=n(43),l=n(16),u=n(46),c=n.n(u),f="undefined"!==typeof navigator&&navigator.userAgent.toLowerCase().indexOf("firefox")>-1,d=function(e,t){e&&e.addEventListener&&e.addEventListener(f?"DOMMouseScroll":"mousewheel",(function(e){var n=c()(e);t&&t.apply(this,[e,n])}))},p={bind:function(e,t){d(e,t.value)}},h=n(6),v=n.n(h),m=n(11),y=n.n(m),g=n(7),b=n.n(g),_=n(9),x=n.n(_),w=n(8),C={data:function(){return{states:{defaultExpandAll:!1,expandRows:[]}}},methods:{updateExpandRows:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,r=e.rowKey,o=e.defaultExpandAll,i=e.expandRows;if(o)this.states.expandRows=n.slice();else if(r){var a=Object(w["f"])(i,r);this.states.expandRows=n.reduce((function(e,t){var n=Object(w["g"])(t,r),o=a[n];return o&&e.push(t),e}),[])}else this.states.expandRows=[]},toggleRowExpansion:function(e,t){var n=Object(w["m"])(this.states.expandRows,e,t);n&&(this.table.$emit("expand-change",e,this.states.expandRows.slice()),this.scheduleLayout())},setExpandRowKeys:function(e){this.assertRowKey();var t=this.states,n=t.data,r=t.rowKey,o=Object(w["f"])(n,r);this.states.expandRows=e.reduce((function(e,t){var n=o[t];return n&&e.push(n.row),e}),[])},isRowExpanded:function(e){var t=this.states,n=t.expandRows,r=void 0===n?[]:n,o=t.rowKey;if(o){var i=Object(w["f"])(r,o);return!!i[Object(w["g"])(e,o)]}return-1!==r.indexOf(e)}}},S=n(3),O={data:function(){return{states:{_currentRowKey:null,currentRow:null}}},methods:{setCurrentRowKey:function(e){this.assertRowKey(),this.states._currentRowKey=e,this.setCurrentRowByKey(e)},restoreCurrentRowKey:function(){this.states._currentRowKey=null},setCurrentRowByKey:function(e){var t=this.states,n=t.data,r=void 0===n?[]:n,o=t.rowKey,i=null;o&&(i=Object(S["arrayFind"])(r,(function(t){return Object(w["g"])(t,o)===e}))),t.currentRow=i},updateCurrentRow:function(e){var t=this.states,n=this.table,r=t.currentRow;if(e&&e!==r)return t.currentRow=e,void n.$emit("current-change",e,r);!e&&r&&(t.currentRow=null,n.$emit("current-change",null,r))},updateCurrentRowData:function(){var e=this.states,t=this.table,n=e.rowKey,r=e._currentRowKey,o=e.data||[],i=e.currentRow;if(-1===o.indexOf(i)&&i){if(n){var a=Object(w["g"])(i,n);this.setCurrentRowByKey(a)}else e.currentRow=null;null===e.currentRow&&t.$emit("current-change",null,i)}else r&&(this.setCurrentRowByKey(r),this.restoreCurrentRowKey())}}},E=Object.assign||function(e){for(var t=1;t0&&t[0]&&"selection"===t[0].type&&!t[0].fixed&&(t[0].fixed=!0,e.fixedColumns.unshift(t[0]));var n=t.filter((function(e){return!e.fixed}));e.originColumns=[].concat(e.fixedColumns).concat(n).concat(e.rightFixedColumns);var r=j(n),o=j(e.fixedColumns),i=j(e.rightFixedColumns);e.leafColumnsLength=r.length,e.fixedLeafColumnsLength=o.length,e.rightFixedLeafColumnsLength=i.length,e.columns=[].concat(o).concat(r).concat(i),e.isComplex=e.fixedColumns.length>0||e.rightFixedColumns.length>0},scheduleLayout:function(e){e&&this.updateColumns(),this.table.debouncedUpdateLayout()},isSelected:function(e){var t=this.states.selection,n=void 0===t?[]:t;return n.indexOf(e)>-1},clearSelection:function(){var e=this.states;e.isAllSelected=!1;var t=e.selection;t.length&&(e.selection=[],this.table.$emit("selection-change",[]))},cleanSelection:function(){var e=this.states,t=e.data,n=e.rowKey,r=e.selection,o=void 0;if(n){o=[];var i=Object(w["f"])(r,n),a=Object(w["f"])(t,n);for(var s in i)i.hasOwnProperty(s)&&!a[s]&&o.push(i[s].row)}else o=r.filter((function(e){return-1===t.indexOf(e)}));if(o.length){var l=r.filter((function(e){return-1===o.indexOf(e)}));e.selection=l,this.table.$emit("selection-change",l.slice())}},toggleRowSelection:function(e,t){var n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],r=Object(w["m"])(this.states.selection,e,t);if(r){var o=(this.states.selection||[]).slice();n&&this.table.$emit("select",o,e),this.table.$emit("selection-change",o)}},_toggleAllSelection:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,r=e.selection,o=e.selectOnIndeterminate?!e.isAllSelected:!(e.isAllSelected||r.length);e.isAllSelected=o;var i=!1;n.forEach((function(t,n){e.selectable?e.selectable.call(null,t,n)&&Object(w["m"])(r,t,o)&&(i=!0):Object(w["m"])(r,t,o)&&(i=!0)})),i&&this.table.$emit("selection-change",r?r.slice():[]),this.table.$emit("select-all",r)},updateSelectionByRowKey:function(){var e=this.states,t=e.selection,n=e.rowKey,r=e.data,o=Object(w["f"])(t,n);r.forEach((function(e){var r=Object(w["g"])(e,n),i=o[r];i&&(t[i.index]=e)}))},updateAllSelected:function(){var e=this.states,t=e.selection,n=e.rowKey,r=e.selectable,o=e.data||[];if(0!==o.length){var i=void 0;n&&(i=Object(w["f"])(t,n));for(var a=function(e){return i?!!i[Object(w["g"])(e,n)]:-1!==t.indexOf(e)},s=!0,l=0,u=0,c=o.length;u1?n-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:{};if(!e)throw new Error("Table is required.");var n=new A;return n.table=e,n.toggleAllSelection=P()(10,n._toggleAllSelection),Object.keys(t).forEach((function(e){n.states[e]=t[e]})),n}function R(e){var t={};return Object.keys(e).forEach((function(n){var r=e[n],o=void 0;"string"===typeof r?o=function(){return this.store.states[r]}:"function"===typeof r?o=function(){return r.call(this,this.store.states)}:console.error("invalid value type"),o&&(t[n]=o)})),t}var N=n(38),I=n.n(N);function F(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var H=function(){function e(t){for(var n in F(this,e),this.observers=[],this.table=null,this.store=null,this.columns=null,this.fit=!0,this.showHeader=!0,this.height=null,this.scrollX=!1,this.scrollY=!1,this.bodyWidth=null,this.fixedWidth=null,this.rightFixedWidth=null,this.tableHeight=null,this.headerHeight=44,this.appendHeight=0,this.footerHeight=44,this.viewportHeight=null,this.bodyHeight=null,this.fixedBodyHeight=null,this.gutterWidth=I()(),t)t.hasOwnProperty(n)&&(this[n]=t[n]);if(!this.table)throw new Error("table is required for Table Layout");if(!this.store)throw new Error("store is required for Table Layout")}return e.prototype.updateScrollY=function(){var e=this.height;if(null===e)return!1;var t=this.table.bodyWrapper;if(this.table.$el&&t){var n=t.querySelector(".el-table__body"),r=this.scrollY,o=n.offsetHeight>this.bodyHeight;return this.scrollY=o,r!==o}return!1},e.prototype.setHeight=function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"height";if(!b.a.prototype.$isServer){var r=this.table.$el;if(e=Object(w["j"])(e),this.height=e,!r&&(e||0===e))return b.a.nextTick((function(){return t.setHeight(e,n)}));"number"===typeof e?(r.style[n]=e+"px",this.updateElsHeight()):"string"===typeof e&&(r.style[n]=e,this.updateElsHeight())}},e.prototype.setMaxHeight=function(e){this.setHeight(e,"max-height")},e.prototype.getFlattenColumns=function(){var e=[],t=this.table.columns;return t.forEach((function(t){t.isColumnGroup?e.push.apply(e,t.columns):e.push(t)})),e},e.prototype.updateElsHeight=function(){var e=this;if(!this.table.$ready)return b.a.nextTick((function(){return e.updateElsHeight()}));var t=this.table.$refs,n=t.headerWrapper,r=t.appendWrapper,o=t.footerWrapper;if(this.appendHeight=r?r.offsetHeight:0,!this.showHeader||n){var i=n?n.querySelector(".el-table__header tr"):null,a=this.headerDisplayNone(i),s=this.headerHeight=this.showHeader?n.offsetHeight:0;if(this.showHeader&&!a&&n.offsetWidth>0&&(this.table.columns||[]).length>0&&s<2)return b.a.nextTick((function(){return e.updateElsHeight()}));var l=this.tableHeight=this.table.$el.clientHeight,u=this.footerHeight=o?o.offsetHeight:0;null!==this.height&&(this.bodyHeight=l-s-u+(o?1:0)),this.fixedBodyHeight=this.scrollX?this.bodyHeight-this.gutterWidth:this.bodyHeight;var c=!(this.store.states.data&&this.store.states.data.length);this.viewportHeight=this.scrollX?l-(c?0:this.gutterWidth):l,this.updateScrollY(),this.notifyObservers("scrollable")}},e.prototype.headerDisplayNone=function(e){if(!e)return!0;var t=e;while("DIV"!==t.tagName){if("none"===getComputedStyle(t).display)return!0;t=t.parentElement}return!1},e.prototype.updateColumnsWidth=function(){if(!b.a.prototype.$isServer){var e=this.fit,t=this.table.$el.clientWidth,n=0,r=this.getFlattenColumns(),o=r.filter((function(e){return"number"!==typeof e.width}));if(r.forEach((function(e){"number"===typeof e.width&&e.realWidth&&(e.realWidth=null)})),o.length>0&&e){r.forEach((function(e){n+=e.width||e.minWidth||80}));var i=this.scrollY?this.gutterWidth:0;if(n<=t-i){this.scrollX=!1;var a=t-i-n;if(1===o.length)o[0].realWidth=(o[0].minWidth||80)+a;else{var s=o.reduce((function(e,t){return e+(t.minWidth||80)}),0),l=a/s,u=0;o.forEach((function(e,t){if(0!==t){var n=Math.floor((e.minWidth||80)*l);u+=n,e.realWidth=(e.minWidth||80)+n}})),o[0].realWidth=(o[0].minWidth||80)+a-u}}else this.scrollX=!0,o.forEach((function(e){e.realWidth=e.minWidth}));this.bodyWidth=Math.max(n,t),this.table.resizeState.width=this.bodyWidth}else r.forEach((function(e){e.width||e.minWidth?e.realWidth=e.width||e.minWidth:e.realWidth=80,n+=e.realWidth})),this.scrollX=n>t,this.bodyWidth=n;var c=this.store.states.fixedColumns;if(c.length>0){var f=0;c.forEach((function(e){f+=e.realWidth||e.width})),this.fixedWidth=f}var d=this.store.states.rightFixedColumns;if(d.length>0){var p=0;d.forEach((function(e){p+=e.realWidth||e.width})),this.rightFixedWidth=p}this.notifyObservers("columns")}},e.prototype.addObserver=function(e){this.observers.push(e)},e.prototype.removeObserver=function(e){var t=this.observers.indexOf(e);-1!==t&&this.observers.splice(t,1)},e.prototype.notifyObservers=function(e){var t=this,n=this.observers;n.forEach((function(n){switch(e){case"columns":n.onColumnsChange(t);break;case"scrollable":n.onScrollableChange(t);break;default:throw new Error("Table Layout don't have event "+e+".")}}))},e}(),D=H,z=n(2),B=n(29),W=n.n(B),V={created:function(){this.tableLayout.addObserver(this)},destroyed:function(){this.tableLayout.removeObserver(this)},computed:{tableLayout:function(){var e=this.layout;if(!e&&this.table&&(e=this.table.layout),!e)throw new Error("Can not find table layout.");return e}},mounted:function(){this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout)},updated:function(){this.__updated__||(this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout),this.__updated__=!0)},methods:{onColumnsChange:function(e){var t=this.$el.querySelectorAll("colgroup > col");if(t.length){var n=e.getFlattenColumns(),r={};n.forEach((function(e){r[e.id]=e}));for(var o=0,i=t.length;o col[name=gutter]"),n=0,r=t.length;n=this.leftFixedLeafCount:"right"===this.fixed?e=this.columnsCount-this.rightFixedLeafCount},getSpan:function(e,t,n,r){var o=1,i=1,a=this.table.spanMethod;if("function"===typeof a){var s=a({row:e,column:t,rowIndex:n,columnIndex:r});Array.isArray(s)?(o=s[0],i=s[1]):"object"===("undefined"===typeof s?"undefined":U(s))&&(o=s.rowspan,i=s.colspan)}return{rowspan:o,colspan:i}},getRowStyle:function(e,t){var n=this.table.rowStyle;return"function"===typeof n?n.call(null,{row:e,rowIndex:t}):n||null},getRowClass:function(e,t){var n=["el-table__row"];this.table.highlightCurrentRow&&e===this.store.states.currentRow&&n.push("current-row"),this.stripe&&t%2===1&&n.push("el-table__row--striped");var r=this.table.rowClassName;return"string"===typeof r?n.push(r):"function"===typeof r&&n.push(r.call(null,{row:e,rowIndex:t})),this.store.states.expandRows.indexOf(e)>-1&&n.push("expanded"),n},getCellStyle:function(e,t,n,r){var o=this.table.cellStyle;return"function"===typeof o?o.call(null,{rowIndex:e,columnIndex:t,row:n,column:r}):o},getCellClass:function(e,t,n,r){var o=[r.id,r.align,r.className];this.isColumnHidden(t)&&o.push("is-hidden");var i=this.table.cellClassName;return"string"===typeof i?o.push(i):"function"===typeof i&&o.push(i.call(null,{rowIndex:e,columnIndex:t,row:n,column:r})),o.join(" ")},getColspanRealWidth:function(e,t,n){if(t<1)return e[n].realWidth;var r=e.map((function(e){var t=e.realWidth;return t})).slice(n,n+t);return r.reduce((function(e,t){return e+t}),-1)},handleCellMouseEnter:function(e,t){var n=this.table,r=Object(w["b"])(e);if(r){var o=Object(w["c"])(n,r),i=n.hoverState={cell:r,column:o,row:t};n.$emit("cell-mouse-enter",i.row,i.column,i.cell,e)}var a=e.target.querySelector(".cell");if(Object(z["hasClass"])(a,"el-tooltip")&&a.childNodes.length){var s=document.createRange();s.setStart(a,0),s.setEnd(a,a.childNodes.length);var l=s.getBoundingClientRect().width,u=(parseInt(Object(z["getStyle"])(a,"paddingLeft"),10)||0)+(parseInt(Object(z["getStyle"])(a,"paddingRight"),10)||0);if((l+u>a.offsetWidth||a.scrollWidth>a.offsetWidth)&&this.$refs.tooltip){var c=this.$refs.tooltip;this.tooltipContent=r.innerText||r.textContent,c.referenceElm=r,c.$refs.popper&&(c.$refs.popper.style.display="none"),c.doDestroy(),c.setExpectedState(!0),this.activateTooltip(c)}}},handleCellMouseLeave:function(e){var t=this.$refs.tooltip;t&&(t.setExpectedState(!1),t.handleClosePopper());var n=Object(w["b"])(e);if(n){var r=this.table.hoverState||{};this.table.$emit("cell-mouse-leave",r.row,r.column,r.cell,e)}},handleMouseEnter:P()(30,(function(e){this.store.commit("setHoverRow",e)})),handleMouseLeave:P()(30,(function(){this.store.commit("setHoverRow",null)})),handleContextMenu:function(e,t){this.handleEvent(e,t,"contextmenu")},handleDoubleClick:function(e,t){this.handleEvent(e,t,"dblclick")},handleClick:function(e,t){this.store.commit("setCurrentRow",t),this.handleEvent(e,t,"click")},handleEvent:function(e,t,n){var r=this.table,o=Object(w["b"])(e),i=void 0;o&&(i=Object(w["c"])(r,o),i&&r.$emit("cell-"+n,t,i,o,e)),r.$emit("row-"+n,t,i,e)},rowRender:function(e,t,n){var r=this,o=this.$createElement,i=this.treeIndent,a=this.columns,s=this.firstDefaultColumnIndex,l=a.map((function(e,t){return r.isColumnHidden(t)})),u=this.getRowClass(e,t),c=!0;n&&(u.push("el-table__row--level-"+n.level),c=n.display);var f=c?null:{display:"none"};return o("tr",{style:[f,this.getRowStyle(e,t)],class:u,key:this.getKeyOfRow(e,t),on:{dblclick:function(t){return r.handleDoubleClick(t,e)},click:function(t){return r.handleClick(t,e)},contextmenu:function(t){return r.handleContextMenu(t,e)},mouseenter:function(e){return r.handleMouseEnter(t)},mouseleave:this.handleMouseLeave}},[a.map((function(u,c){var f=r.getSpan(e,u,t,c),d=f.rowspan,p=f.colspan;if(!d||!p)return null;var h=q({},u);h.realWidth=r.getColspanRealWidth(a,p,c);var v={store:r.store,_self:r.context||r.table.$vnode.context,column:h,row:e,$index:t};return c===s&&n&&(v.treeNode={indent:n.level*i,level:n.level},"boolean"===typeof n.expanded&&(v.treeNode.expanded=n.expanded,"loading"in n&&(v.treeNode.loading=n.loading),"noLazyChildren"in n&&(v.treeNode.noLazyChildren=n.noLazyChildren))),o("td",{style:r.getCellStyle(t,c,e,u),class:r.getCellClass(t,c,e,u),attrs:{rowspan:d,colspan:p},on:{mouseenter:function(t){return r.handleCellMouseEnter(t,e)},mouseleave:r.handleCellMouseLeave}},[u.renderCell.call(r._renderProxy,r.$createElement,v,l[c])])}))])},wrappedRowRender:function(e,t){var n=this,r=this.$createElement,o=this.store,i=o.isRowExpanded,a=o.assertRowKey,s=o.states,l=s.treeData,u=s.lazyTreeNodeMap,c=s.childrenColumnName,f=s.rowKey;if(this.hasExpandColumn&&i(e)){var d=this.table.renderExpanded,p=this.rowRender(e,t);return d?[[p,r("tr",{key:"expanded-row__"+p.key},[r("td",{attrs:{colspan:this.columnsCount},class:"el-table__expanded-cell"},[d(this.$createElement,{row:e,$index:t,store:this.store})])])]]:(console.error("[Element Error]renderExpanded is required."),p)}if(Object.keys(l).length){a();var h=Object(w["g"])(e,f),v=l[h],m=null;v&&(m={expanded:v.expanded,level:v.level,display:!0},"boolean"===typeof v.lazy&&("boolean"===typeof v.loaded&&v.loaded&&(m.noLazyChildren=!(v.children&&v.children.length)),m.loading=v.loading));var y=[this.rowRender(e,t,m)];if(v){var g=0,b=function e(r,o){r&&r.length&&o&&r.forEach((function(r){var i={display:o.display&&o.expanded,level:o.level+1},a=Object(w["g"])(r,f);if(void 0===a||null===a)throw new Error("for nested data item, row-key is required.");if(v=q({},l[a]),v&&(i.expanded=v.expanded,v.level=v.level||i.level,v.display=!(!v.expanded||!i.display),"boolean"===typeof v.lazy&&("boolean"===typeof v.loaded&&v.loaded&&(i.noLazyChildren=!(v.children&&v.children.length)),i.loading=v.loading)),g++,y.push(n.rowRender(r,t+g,i)),v){var s=u[a]||r[c];e(s,v)}}))};v.display=!0;var _=u[h]||e[c];b(_,v)}return y}return this.rowRender(e,t)}}},G=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"el-zoom-in-top"}},[e.multiple?n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("div",{staticClass:"el-table-filter__content"},[n("el-scrollbar",{attrs:{"wrap-class":"el-table-filter__wrap"}},[n("el-checkbox-group",{staticClass:"el-table-filter__checkbox-group",model:{value:e.filteredValue,callback:function(t){e.filteredValue=t},expression:"filteredValue"}},e._l(e.filters,(function(t){return n("el-checkbox",{key:t.value,attrs:{label:t.value}},[e._v(e._s(t.text))])})),1)],1)],1),n("div",{staticClass:"el-table-filter__bottom"},[n("button",{class:{"is-disabled":0===e.filteredValue.length},attrs:{disabled:0===e.filteredValue.length},on:{click:e.handleConfirm}},[e._v(e._s(e.t("el.table.confirmFilter")))]),n("button",{on:{click:e.handleReset}},[e._v(e._s(e.t("el.table.resetFilter")))])])]):n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("ul",{staticClass:"el-table-filter__list"},[n("li",{staticClass:"el-table-filter__list-item",class:{"is-active":void 0===e.filterValue||null===e.filterValue},on:{click:function(t){e.handleSelect(null)}}},[e._v(e._s(e.t("el.table.clearFilter")))]),e._l(e.filters,(function(t){return n("li",{key:t.value,staticClass:"el-table-filter__list-item",class:{"is-active":e.isActive(t)},attrs:{label:t.value},on:{click:function(n){e.handleSelect(t.value)}}},[e._v(e._s(t.text))])}))],2)])])},X=[];G._withStripped=!0;var Y=n(5),J=n.n(Y),Z=n(13),Q=n(12),ee=n.n(Q),te=[];!b.a.prototype.$isServer&&document.addEventListener("click",(function(e){te.forEach((function(t){var n=e.target;t&&t.$el&&(n===t.$el||t.$el.contains(n)||t.handleOutsideClick&&t.handleOutsideClick(e))}))}));var ne={open:function(e){e&&te.push(e)},close:function(e){var t=te.indexOf(e);-1!==t&&te.splice(e,1)}},re=n(39),oe=n.n(re),ie=n(15),ae=n.n(ie),se={name:"ElTableFilterPanel",mixins:[J.a,v.a],directives:{Clickoutside:ee.a},components:{ElCheckbox:a.a,ElCheckboxGroup:oe.a,ElScrollbar:ae.a},props:{placement:{type:String,default:"bottom-end"}},methods:{isActive:function(e){return e.value===this.filterValue},handleOutsideClick:function(){var e=this;setTimeout((function(){e.showPopper=!1}),16)},handleConfirm:function(){this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleReset:function(){this.filteredValue=[],this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleSelect:function(e){this.filterValue=e,"undefined"!==typeof e&&null!==e?this.confirmFilter(this.filteredValue):this.confirmFilter([]),this.handleOutsideClick()},confirmFilter:function(e){this.table.store.commit("filterChange",{column:this.column,values:e}),this.table.store.updateAllSelected()}},data:function(){return{table:null,cell:null,column:null}},computed:{filters:function(){return this.column&&this.column.filters},filterValue:{get:function(){return(this.column.filteredValue||[])[0]},set:function(e){this.filteredValue&&("undefined"!==typeof e&&null!==e?this.filteredValue.splice(0,1,e):this.filteredValue.splice(0,1))}},filteredValue:{get:function(){return this.column&&this.column.filteredValue||[]},set:function(e){this.column&&(this.column.filteredValue=e)}},multiple:function(){return!this.column||this.column.filterMultiple}},mounted:function(){var e=this;this.popperElm=this.$el,this.referenceElm=this.cell,this.table.bodyWrapper.addEventListener("scroll",(function(){e.updatePopper()})),this.$watch("showPopper",(function(t){e.column&&(e.column.filterOpened=t),t?ne.open(e):ne.close(e)}))},watch:{showPopper:function(e){!0===e&&parseInt(this.popperJS._popper.style.zIndex,10)1;return o&&(this.$parent.isGroup=!0),e("table",{class:"el-table__header",attrs:{cellspacing:"0",cellpadding:"0",border:"0"}},[e("colgroup",[this.columns.map((function(t){return e("col",{attrs:{name:t.id},key:t.id})})),this.hasGutter?e("col",{attrs:{name:"gutter"}}):""]),e("thead",{class:[{"is-group":o,"has-gutter":this.hasGutter}]},[this._l(r,(function(n,r){return e("tr",{style:t.getHeaderRowStyle(r),class:t.getHeaderRowClass(r)},[n.map((function(o,i){return e("th",{attrs:{colspan:o.colSpan,rowspan:o.rowSpan},on:{mousemove:function(e){return t.handleMouseMove(e,o)},mouseout:t.handleMouseOut,mousedown:function(e){return t.handleMouseDown(e,o)},click:function(e){return t.handleHeaderClick(e,o)},contextmenu:function(e){return t.handleHeaderContextMenu(e,o)}},style:t.getHeaderCellStyle(r,i,n,o),class:t.getHeaderCellClass(r,i,n,o),key:o.id},[e("div",{class:["cell",o.filteredValue&&o.filteredValue.length>0?"highlight":"",o.labelClassName]},[o.renderHeader?o.renderHeader.call(t._renderProxy,e,{column:o,$index:i,store:t.store,_self:t.$parent.$vnode.context}):o.label,o.sortable?e("span",{class:"caret-wrapper",on:{click:function(e){return t.handleSortClick(e,o)}}},[e("i",{class:"sort-caret ascending",on:{click:function(e){return t.handleSortClick(e,o,"ascending")}}}),e("i",{class:"sort-caret descending",on:{click:function(e){return t.handleSortClick(e,o,"descending")}}})]):"",o.filterable?e("span",{class:"el-table__column-filter-trigger",on:{click:function(e){return t.handleFilterClick(e,o)}}},[e("i",{class:["el-icon-arrow-down",o.filterOpened?"el-icon-arrow-up":""]})]):""])])})),t.hasGutter?e("th",{class:"gutter"}):""])}))])])},props:{fixed:String,store:{required:!0},border:Boolean,defaultSort:{type:Object,default:function(){return{prop:"",order:""}}}},components:{ElCheckbox:a.a},computed:de({table:function(){return this.$parent},hasGutter:function(){return!this.fixed&&this.tableLayout.gutterWidth}},R({columns:"columns",isAllSelected:"isAllSelected",leftFixedLeafCount:"fixedLeafColumnsLength",rightFixedLeafCount:"rightFixedLeafColumnsLength",columnsCount:function(e){return e.columns.length},leftFixedCount:function(e){return e.fixedColumns.length},rightFixedCount:function(e){return e.rightFixedColumns.length}})),created:function(){this.filterPanels={}},mounted:function(){var e=this;this.$nextTick((function(){var t=e.defaultSort,n=t.prop,r=t.order,o=!0;e.store.commit("sort",{prop:n,order:r,init:o})}))},beforeDestroy:function(){var e=this.filterPanels;for(var t in e)e.hasOwnProperty(t)&&e[t]&&e[t].$destroy(!0)},methods:{isCellHidden:function(e,t){for(var n=0,r=0;r=this.leftFixedLeafCount:"right"===this.fixed?n=this.columnsCount-this.rightFixedLeafCount},getHeaderRowStyle:function(e){var t=this.table.headerRowStyle;return"function"===typeof t?t.call(null,{rowIndex:e}):t},getHeaderRowClass:function(e){var t=[],n=this.table.headerRowClassName;return"string"===typeof n?t.push(n):"function"===typeof n&&t.push(n.call(null,{rowIndex:e})),t.join(" ")},getHeaderCellStyle:function(e,t,n,r){var o=this.table.headerCellStyle;return"function"===typeof o?o.call(null,{rowIndex:e,columnIndex:t,row:n,column:r}):o},getHeaderCellClass:function(e,t,n,r){var o=[r.id,r.order,r.headerAlign,r.className,r.labelClassName];0===e&&this.isCellHidden(t,n)&&o.push("is-hidden"),r.children||o.push("is-leaf"),r.sortable&&o.push("is-sortable");var i=this.table.headerCellClassName;return"string"===typeof i?o.push(i):"function"===typeof i&&o.push(i.call(null,{rowIndex:e,columnIndex:t,row:n,column:r})),o.join(" ")},toggleAllSelection:function(e){e.stopPropagation(),this.store.commit("toggleAllSelection")},handleFilterClick:function(e,t){e.stopPropagation();var n=e.target,r="TH"===n.tagName?n:n.parentNode;if(!Object(z["hasClass"])(r,"noclick")){r=r.querySelector(".el-table__column-filter-trigger")||r;var o=this.$parent,i=this.filterPanels[t.id];i&&t.filterOpened?i.showPopper=!1:(i||(i=new b.a(fe),this.filterPanels[t.id]=i,t.filterPlacement&&(i.placement=t.filterPlacement),i.table=o,i.cell=r,i.column=t,!this.$isServer&&i.$mount(document.createElement("div"))),setTimeout((function(){i.showPopper=!0}),16))}},handleHeaderClick:function(e,t){!t.filters&&t.sortable?this.handleSortClick(e,t):t.filterable&&!t.sortable&&this.handleFilterClick(e,t),this.$parent.$emit("header-click",t,e)},handleHeaderContextMenu:function(e,t){this.$parent.$emit("header-contextmenu",t,e)},handleMouseDown:function(e,t){var n=this;if(!this.$isServer&&!(t.children&&t.children.length>0)&&this.draggingColumn&&this.border){this.dragging=!0,this.$parent.resizeProxyVisible=!0;var r=this.$parent,o=r.$el,i=o.getBoundingClientRect().left,a=this.$el.querySelector("th."+t.id),s=a.getBoundingClientRect(),l=s.left-i+30;Object(z["addClass"])(a,"noclick"),this.dragState={startMouseLeft:e.clientX,startLeft:s.right-i,startColumnLeft:s.left-i,tableLeft:i};var u=r.$refs.resizeProxy;u.style.left=this.dragState.startLeft+"px",document.onselectstart=function(){return!1},document.ondragstart=function(){return!1};var c=function(e){var t=e.clientX-n.dragState.startMouseLeft,r=n.dragState.startLeft+t;u.style.left=Math.max(l,r)+"px"},f=function o(){if(n.dragging){var i=n.dragState,s=i.startColumnLeft,l=i.startLeft,f=parseInt(u.style.left,10),d=f-s;t.width=t.realWidth=d,r.$emit("header-dragend",t.width,l-s,t,e),n.store.scheduleLayout(),document.body.style.cursor="",n.dragging=!1,n.draggingColumn=null,n.dragState={},r.resizeProxyVisible=!1}document.removeEventListener("mousemove",c),document.removeEventListener("mouseup",o),document.onselectstart=null,document.ondragstart=null,setTimeout((function(){Object(z["removeClass"])(a,"noclick")}),0)};document.addEventListener("mousemove",c),document.addEventListener("mouseup",f)}},handleMouseMove:function(e,t){if(!(t.children&&t.children.length>0)){var n=e.target;while(n&&"TH"!==n.tagName)n=n.parentNode;if(t&&t.resizable&&!this.dragging&&this.border){var r=n.getBoundingClientRect(),o=document.body.style;r.width>12&&r.right-e.pageX<8?(o.cursor="col-resize",Object(z["hasClass"])(n,"is-sortable")&&(n.style.cursor="col-resize"),this.draggingColumn=t):this.dragging||(o.cursor="",Object(z["hasClass"])(n,"is-sortable")&&(n.style.cursor="pointer"),this.draggingColumn=null)}}},handleMouseOut:function(){this.$isServer||(document.body.style.cursor="")},toggleOrder:function(e){var t=e.order,n=e.sortOrders;if(""===t)return n[0];var r=n.indexOf(t||null);return n[r>n.length-2?0:r+1]},handleSortClick:function(e,t,n){e.stopPropagation();var r=t.order===n?null:n||this.toggleOrder(t),o=e.target;while(o&&"TH"!==o.tagName)o=o.parentNode;if(o&&"TH"===o.tagName&&Object(z["hasClass"])(o,"noclick"))Object(z["removeClass"])(o,"noclick");else if(t.sortable){var i=this.store.states,a=i.sortProp,s=void 0,l=i.sortingColumn;(l!==t||l===t&&null===l.order)&&(l&&(l.order=null),i.sortingColumn=t,a=t.property),s=t.order=r||null,i.sortProp=a,i.sortOrder=s,this.store.commit("changeSortCondition")}}},data:function(){return{draggingColumn:null,dragging:!1,dragState:{}}}},me=Object.assign||function(e){for(var t=1;t=this.leftFixedLeafCount;if("right"===this.fixed){for(var r=0,o=0;o=this.columnsCount-this.rightFixedCount)},getRowClasses:function(e,t){var n=[e.id,e.align,e.labelClassName];return e.className&&n.push(e.className),this.isCellHidden(t,this.columns,e)&&n.push("is-hidden"),e.children||n.push("is-leaf"),n}}},ge=Object.assign||function(e){for(var t=1;t0){var r=n.scrollTop;t.pixelY<0&&0!==r&&e.preventDefault(),t.pixelY>0&&n.scrollHeight-n.clientHeight>r&&e.preventDefault(),n.scrollTop+=Math.ceil(t.pixelY/5)}else n.scrollLeft+=Math.ceil(t.pixelX/5)},handleHeaderFooterMousewheel:function(e,t){var n=t.pixelX,r=t.pixelY;Math.abs(n)>=Math.abs(r)&&(this.bodyWrapper.scrollLeft+=t.pixelX/5)},syncPostion:Object(s["throttle"])(20,(function(){var e=this.bodyWrapper,t=e.scrollLeft,n=e.scrollTop,r=e.offsetWidth,o=e.scrollWidth,i=this.$refs,a=i.headerWrapper,s=i.footerWrapper,l=i.fixedBodyWrapper,u=i.rightFixedBodyWrapper;a&&(a.scrollLeft=t),s&&(s.scrollLeft=t),l&&(l.scrollTop=n),u&&(u.scrollTop=n);var c=o-r-1;this.scrollPosition=t>=c?"right":0===t?"left":"middle"})),bindEvents:function(){this.bodyWrapper.addEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(l["addResizeListener"])(this.$el,this.resizeListener)},unbindEvents:function(){this.bodyWrapper.removeEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(l["removeResizeListener"])(this.$el,this.resizeListener)},resizeListener:function(){if(this.$ready){var e=!1,t=this.$el,n=this.resizeState,r=n.width,o=n.height,i=t.offsetWidth;r!==i&&(e=!0);var a=t.offsetHeight;(this.height||this.shouldUpdateHeight)&&o!==a&&(e=!0),e&&(this.resizeState.width=i,this.resizeState.height=a,this.doLayout())}},doLayout:function(){this.shouldUpdateHeight&&this.layout.updateElsHeight(),this.layout.updateColumnsWidth()},sort:function(e,t){this.store.commit("sort",{prop:e,order:t})},toggleAllSelection:function(){this.store.commit("toggleAllSelection")}},computed:ge({tableSize:function(){return this.size||(this.$ELEMENT||{}).size},bodyWrapper:function(){return this.$refs.bodyWrapper},shouldUpdateHeight:function(){return this.height||this.maxHeight||this.fixedColumns.length>0||this.rightFixedColumns.length>0},bodyWidth:function(){var e=this.layout,t=e.bodyWidth,n=e.scrollY,r=e.gutterWidth;return t?t-(n?r:0)+"px":""},bodyHeight:function(){var e=this.layout,t=e.headerHeight,n=void 0===t?0:t,r=e.bodyHeight,o=e.footerHeight,i=void 0===o?0:o;if(this.height)return{height:r?r+"px":""};if(this.maxHeight){var a=Object(w["j"])(this.maxHeight);if("number"===typeof a)return{"max-height":a-i-(this.showHeader?n:0)+"px"}}return{}},fixedBodyHeight:function(){if(this.height)return{height:this.layout.fixedBodyHeight?this.layout.fixedBodyHeight+"px":""};if(this.maxHeight){var e=Object(w["j"])(this.maxHeight);if("number"===typeof e)return e=this.layout.scrollX?e-this.layout.gutterWidth:e,this.showHeader&&(e-=this.layout.headerHeight),e-=this.layout.footerHeight,{"max-height":e+"px"}}return{}},fixedHeight:function(){return this.maxHeight?this.showSummary?{bottom:0}:{bottom:this.layout.scrollX&&this.data.length?this.layout.gutterWidth+"px":""}:this.showSummary?{height:this.layout.tableHeight?this.layout.tableHeight+"px":""}:{height:this.layout.viewportHeight?this.layout.viewportHeight+"px":""}},emptyBlockStyle:function(){if(this.data&&this.data.length)return null;var e="100%";return this.layout.appendHeight&&(e="calc(100% - "+this.layout.appendHeight+"px)"),{width:this.bodyWidth,height:e}}},R({selection:"selection",columns:"columns",tableData:"data",fixedColumns:"fixedColumns",rightFixedColumns:"rightFixedColumns"})),watch:{height:{immediate:!0,handler:function(e){this.layout.setHeight(e)}},maxHeight:{immediate:!0,handler:function(e){this.layout.setMaxHeight(e)}},currentRowKey:{immediate:!0,handler:function(e){this.rowKey&&this.store.setCurrentRowKey(e)}},data:{immediate:!0,handler:function(e){this.store.commit("setData",e)}},expandRowKeys:{immediate:!0,handler:function(e){e&&this.store.setExpandRowKeysAdapter(e)}}},created:function(){var e=this;this.tableId="el-table_"+be++,this.debouncedUpdateLayout=Object(s["debounce"])(50,(function(){return e.doLayout()}))},mounted:function(){var e=this;this.bindEvents(),this.store.updateColumns(),this.doLayout(),this.resizeState={width:this.$el.offsetWidth,height:this.$el.offsetHeight},this.store.states.columns.forEach((function(t){t.filteredValue&&t.filteredValue.length&&e.store.commit("filterChange",{column:t,values:t.filteredValue,silent:!0})})),this.$ready=!0},destroyed:function(){this.unbindEvents()},data:function(){var e=this.treeProps,t=e.hasChildren,n=void 0===t?"hasChildren":t,r=e.children,o=void 0===r?"children":r;this.store=L(this,{rowKey:this.rowKey,defaultExpandAll:this.defaultExpandAll,selectOnIndeterminate:this.selectOnIndeterminate,indent:this.indent,lazy:this.lazy,lazyColumnIdentifier:n,childrenColumnName:o});var i=new D({store:this.store,table:this,fit:this.fit,showHeader:this.showHeader});return{layout:i,isHidden:!1,renderExpanded:null,resizeProxyVisible:!1,resizeState:{width:null,height:null},isGroup:!1,scrollPosition:"left"}}},xe=_e,we=Object(ue["a"])(xe,r,o,!1,null,null,null);we.options.__file="packages/table/src/table.vue";var Ce=we.exports;Ce.install=function(e){e.component(Ce.name,Ce)};t["default"]=Ce}])},ad6d:function(e,t,n){"use strict";var r=n("825a");e.exports=function(){var e=r(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},ae93:function(e,t,n){"use strict";var r,o,i,a=n("d039"),s=n("e163"),l=n("9112"),u=n("5135"),c=n("b622"),f=n("c430"),d=c("iterator"),p=!1,h=function(){return this};[].keys&&(i=[].keys(),"next"in i?(o=s(s(i)),o!==Object.prototype&&(r=o)):p=!0);var v=void 0==r||a((function(){var e={};return r[d].call(e)!==e}));v&&(r={}),f&&!v||u(r,d)||l(r,d,h),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:p}},b041:function(e,t,n){"use strict";var r=n("00ee"),o=n("f5df");e.exports=r?{}.toString:function(){return"[object "+o(this)+"]"}},b575:function(e,t,n){var r,o,i,a,s,l,u,c,f=n("da84"),d=n("06cf").f,p=n("2cf4").set,h=n("1cdc"),v=n("a4b4"),m=n("605d"),y=f.MutationObserver||f.WebKitMutationObserver,g=f.document,b=f.process,_=f.Promise,x=d(f,"queueMicrotask"),w=x&&x.value;w||(r=function(){var e,t;m&&(e=b.domain)&&e.exit();while(o){t=o.fn,o=o.next;try{t()}catch(n){throw o?a():i=void 0,n}}i=void 0,e&&e.enter()},h||m||v||!y||!g?_&&_.resolve?(u=_.resolve(void 0),u.constructor=_,c=u.then,a=function(){c.call(u,r)}):a=m?function(){b.nextTick(r)}:function(){p.call(f,r)}:(s=!0,l=g.createTextNode(""),new y(r).observe(l,{characterData:!0}),a=function(){l.data=s=!s})),e.exports=w||function(e){var t={fn:e,next:void 0};i&&(i.next=t),o||(o=t,a()),i=t}},b622:function(e,t,n){var r=n("da84"),o=n("5692"),i=n("5135"),a=n("90e3"),s=n("4930"),l=n("fdbf"),u=o("wks"),c=r.Symbol,f=l?c:c&&c.withoutSetter||a;e.exports=function(e){return i(u,e)&&(s||"string"==typeof u[e])||(s&&i(c,e)?u[e]=c[e]:u[e]=f("Symbol."+e)),u[e]}},b84d:function(e,t,n){},be4f:function(e,t,n){},c04e:function(e,t,n){var r=n("861d");e.exports=function(e,t){if(!r(e))return e;var n,o;if(t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;if("function"==typeof(n=e.valueOf)&&!r(o=n.call(e)))return o;if(!t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;throw TypeError("Can't convert object to primitive value")}},c098:function(e,t,n){e.exports=n("d4af")},c216:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=106)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},106:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("span",{staticClass:"el-breadcrumb__item"},[n("span",{ref:"link",class:["el-breadcrumb__inner",e.to?"is-link":""],attrs:{role:"link"}},[e._t("default")],2),e.separatorClass?n("i",{staticClass:"el-breadcrumb__separator",class:e.separatorClass}):n("span",{staticClass:"el-breadcrumb__separator",attrs:{role:"presentation"}},[e._v(e._s(e.separator))])])},o=[];r._withStripped=!0;var i={name:"ElBreadcrumbItem",props:{to:{},replace:Boolean},data:function(){return{separator:"",separatorClass:""}},inject:["elBreadcrumb"],mounted:function(){var e=this;this.separator=this.elBreadcrumb.separator,this.separatorClass=this.elBreadcrumb.separatorClass;var t=this.$refs.link;t.setAttribute("role","link"),t.addEventListener("click",(function(t){var n=e.to,r=e.$router;n&&r&&(e.replace?r.replace(n):r.push(n))}))}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/breadcrumb/src/breadcrumb-item.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},c430:function(e,t){e.exports=!1},c56a:function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:300,r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!e||!t)throw new Error("instance & callback is required");var o=!1,i=function(){o||(o=!0,t&&t.apply(null,arguments))};r?e.$once("after-leave",i):e.$on("after-leave",i),setTimeout((function(){i()}),n+100)}},c6b6:function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},c6cd:function(e,t,n){var r=n("da84"),o=n("ce4e"),i="__core-js_shared__",a=r[i]||o(i,{});e.exports=a},c8ba:function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}e.exports=n},ca84:function(e,t,n){var r=n("5135"),o=n("fc6a"),i=n("4d64").indexOf,a=n("d012");e.exports=function(e,t){var n,s=o(e),l=0,u=[];for(n in s)!r(a,n)&&r(s,n)&&u.push(n);while(t.length>l)r(s,n=t[l++])&&(~i(u,n)||u.push(n));return u}},cbb5:function(e,t,n){},cc12:function(e,t,n){var r=n("da84"),o=n("861d"),i=r.document,a=o(i)&&o(i.createElement);e.exports=function(e){return a?i.createElement(e):{}}},cca6:function(e,t,n){var r=n("23e7"),o=n("60da");r({target:"Object",stat:!0,forced:Object.assign!==o},{assign:o})},cdf9:function(e,t,n){var r=n("825a"),o=n("861d"),i=n("f069");e.exports=function(e,t){if(r(e),o(t)&&t.constructor===e)return t;var n=i.f(e),a=n.resolve;return a(t),n.promise}},ce4e:function(e,t,n){var r=n("da84"),o=n("9112");e.exports=function(e,t){try{o(r,e,t)}catch(n){r[e]=t}return t}},d010:function(e,t,n){"use strict";function r(e,t,n){this.$children.forEach((function(o){var i=o.$options.componentName;i===e?o.$emit.apply(o,[t].concat(n)):r.apply(o,[e,t].concat([n]))}))}t.__esModule=!0,t.default={methods:{dispatch:function(e,t,n){var r=this.$parent||this.$root,o=r.$options.componentName;while(r&&(!o||o!==e))r=r.$parent,r&&(o=r.$options.componentName);r&&r.$emit.apply(r,[t].concat(n))},broadcast:function(e,t,n){r.call(this,e,t,n)}}}},d012:function(e,t){e.exports={}},d039:function(e,t){e.exports=function(e){try{return!!e()}catch(t){return!0}}},d066:function(e,t,n){var r=n("428f"),o=n("da84"),i=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?i(r[e])||i(o[e]):r[e]&&r[e][t]||o[e]&&o[e][t]}},d1e7:function(e,t,n){"use strict";var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);t.f=i?function(e){var t=o(this,e);return!!t&&t.enumerable}:r},d2bb:function(e,t,n){var r=n("825a"),o=n("3bbe");e.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,n={};try{e=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set,e.call(n,[]),t=n instanceof Array}catch(i){}return function(n,i){return r(n),o(i),t?e.call(n,i):n.__proto__=i,n}}():void 0)},d397:function(e,t,n){"use strict";function r(e){return void 0!==e&&null!==e}function o(e){var t=/([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi;return t.test(e)}t.__esModule=!0,t.isDef=r,t.isKorean=o},d3b7:function(e,t,n){var r=n("00ee"),o=n("6eeb"),i=n("b041");r||o(Object.prototype,"toString",i,{unsafe:!0})},d44e:function(e,t,n){var r=n("9bf2").f,o=n("5135"),i=n("b622"),a=i("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,a)&&r(e,a,{configurable:!0,value:t})}},d4af:function(e,t,n){"use strict";var r=n("8eb7"),o=n("7b3e"),i=10,a=40,s=800;function l(e){var t=0,n=0,r=0,o=0;return"detail"in e&&(n=e.detail),"wheelDelta"in e&&(n=-e.wheelDelta/120),"wheelDeltaY"in e&&(n=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=n,n=0),r=t*i,o=n*i,"deltaY"in e&&(o=e.deltaY),"deltaX"in e&&(r=e.deltaX),(r||o)&&e.deltaMode&&(1==e.deltaMode?(r*=a,o*=a):(r*=s,o*=s)),r&&!t&&(t=r<1?-1:1),o&&!n&&(n=o<1?-1:1),{spinX:t,spinY:n,pixelX:r,pixelY:o}}l.getEventType=function(){return r.firefox()?"DOMMouseScroll":o("wheel")?"wheel":"mousewheel"},e.exports=l},da84:function(e,t,n){(function(t){var n=function(e){return e&&e.Math==Math&&e};e.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof t&&t)||function(){return this}()||Function("return this")()}).call(this,n("c8ba"))},dcdc:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=119)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},119:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("label",{staticClass:"el-checkbox",class:[e.border&&e.checkboxSize?"el-checkbox--"+e.checkboxSize:"",{"is-disabled":e.isDisabled},{"is-bordered":e.border},{"is-checked":e.isChecked}],attrs:{id:e.id}},[n("span",{staticClass:"el-checkbox__input",class:{"is-disabled":e.isDisabled,"is-checked":e.isChecked,"is-indeterminate":e.indeterminate,"is-focus":e.focus},attrs:{tabindex:!!e.indeterminate&&0,role:!!e.indeterminate&&"checkbox","aria-checked":!!e.indeterminate&&"mixed"}},[n("span",{staticClass:"el-checkbox__inner"}),e.trueLabel||e.falseLabel?n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",name:e.name,disabled:e.isDisabled,"true-value":e.trueLabel,"false-value":e.falseLabel},domProps:{checked:Array.isArray(e.model)?e._i(e.model,null)>-1:e._q(e.model,e.trueLabel)},on:{change:[function(t){var n=e.model,r=t.target,o=r.checked?e.trueLabel:e.falseLabel;if(Array.isArray(n)){var i=null,a=e._i(n,i);r.checked?a<0&&(e.model=n.concat([i])):a>-1&&(e.model=n.slice(0,a).concat(n.slice(a+1)))}else e.model=o},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}}):n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",disabled:e.isDisabled,name:e.name},domProps:{value:e.label,checked:Array.isArray(e.model)?e._i(e.model,e.label)>-1:e.model},on:{change:[function(t){var n=e.model,r=t.target,o=!!r.checked;if(Array.isArray(n)){var i=e.label,a=e._i(n,i);r.checked?a<0&&(e.model=n.concat([i])):a>-1&&(e.model=n.slice(0,a).concat(n.slice(a+1)))}else e.model=o},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}})]),e.$slots.default||e.label?n("span",{staticClass:"el-checkbox__label"},[e._t("default"),e.$slots.default?e._e():[e._v(e._s(e.label))]],2):e._e()])},o=[];r._withStripped=!0;var i=n(4),a=n.n(i),s={name:"ElCheckbox",mixins:[a.a],inject:{elForm:{default:""},elFormItem:{default:""}},componentName:"ElCheckbox",data:function(){return{selfModel:!1,focus:!1,isLimitExceeded:!1}},computed:{model:{get:function(){return this.isGroup?this.store:void 0!==this.value?this.value:this.selfModel},set:function(e){this.isGroup?(this.isLimitExceeded=!1,void 0!==this._checkboxGroup.min&&e.lengththis._checkboxGroup.max&&(this.isLimitExceeded=!0),!1===this.isLimitExceeded&&this.dispatch("ElCheckboxGroup","input",[e])):(this.$emit("input",e),this.selfModel=e)}},isChecked:function(){return"[object Boolean]"==={}.toString.call(this.model)?this.model:Array.isArray(this.model)?this.model.indexOf(this.label)>-1:null!==this.model&&void 0!==this.model?this.model===this.trueLabel:void 0},isGroup:function(){var e=this.$parent;while(e){if("ElCheckboxGroup"===e.$options.componentName)return this._checkboxGroup=e,!0;e=e.$parent}return!1},store:function(){return this._checkboxGroup?this._checkboxGroup.value:this.value},isLimitDisabled:function(){var e=this._checkboxGroup,t=e.max,n=e.min;return!(!t&&!n)&&this.model.length>=t&&!this.isChecked||this.model.length<=n&&this.isChecked},isDisabled:function(){return this.isGroup?this._checkboxGroup.disabled||this.disabled||(this.elForm||{}).disabled||this.isLimitDisabled:this.disabled||(this.elForm||{}).disabled},_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},checkboxSize:function(){var e=this.size||this._elFormItemSize||(this.$ELEMENT||{}).size;return this.isGroup&&this._checkboxGroup.checkboxGroupSize||e}},props:{value:{},label:{},indeterminate:Boolean,disabled:Boolean,checked:Boolean,name:String,trueLabel:[String,Number],falseLabel:[String,Number],id:String,controls:String,border:Boolean,size:String},methods:{addToStore:function(){Array.isArray(this.model)&&-1===this.model.indexOf(this.label)?this.model.push(this.label):this.model=this.trueLabel||!0},handleChange:function(e){var t=this;if(!this.isLimitExceeded){var n=void 0;n=e.target.checked?void 0===this.trueLabel||this.trueLabel:void 0!==this.falseLabel&&this.falseLabel,this.$emit("change",n,e),this.$nextTick((function(){t.isGroup&&t.dispatch("ElCheckboxGroup","change",[t._checkboxGroup.value])}))}}},created:function(){this.checked&&this.addToStore()},mounted:function(){this.indeterminate&&this.$el.setAttribute("aria-controls",this.controls)},watch:{value:function(e){this.dispatch("ElFormItem","el.form.change",e)}}},l=s,u=n(0),c=Object(u["a"])(l,r,o,!1,null,null,null);c.options.__file="packages/checkbox/src/checkbox.vue";var f=c.exports;f.install=function(e){e.component(f.name,f)};t["default"]=f},4:function(e,t){e.exports=n("d010")}})},ddb0:function(e,t,n){var r=n("da84"),o=n("fdbc"),i=n("e260"),a=n("9112"),s=n("b622"),l=s("iterator"),u=s("toStringTag"),c=i.values;for(var f in o){var d=r[f],p=d&&d.prototype;if(p){if(p[l]!==c)try{a(p,l,c)}catch(v){p[l]=c}if(p[u]||a(p,u,f),o[f])for(var h in i)if(p[h]!==i[h])try{a(p,h,i[h])}catch(v){p[h]=i[h]}}}},df75:function(e,t,n){var r=n("ca84"),o=n("7839");e.exports=Object.keys||function(e){return r(e,o)}},e163:function(e,t,n){var r=n("5135"),o=n("7b0b"),i=n("f772"),a=n("e177"),s=i("IE_PROTO"),l=Object.prototype;e.exports=a?Object.getPrototypeOf:function(e){return e=o(e),r(e,s)?e[s]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?l:null}},e177:function(e,t,n){var r=n("d039");e.exports=!r((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},e260:function(e,t,n){"use strict";var r=n("fc6a"),o=n("44d2"),i=n("3f8c"),a=n("69f3"),s=n("7dd0"),l="Array Iterator",u=a.set,c=a.getterFor(l);e.exports=s(Array,"Array",(function(e,t){u(this,{type:l,target:r(e),index:0,kind:t})}),(function(){var e=c(this),t=e.target,n=e.kind,r=e.index++;return!t||r>=t.length?(e.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:r,done:!1}:"values"==n?{value:t[r],done:!1}:{value:[r,t[r]],done:!1}}),"values"),i.Arguments=i.Array,o("keys"),o("values"),o("entries")},e2cc:function(e,t,n){var r=n("6eeb");e.exports=function(e,t,n){for(var o in t)r(e,o,t[o],n);return e}},e3ea:function(e,t,n){},e452:function(e,t,n){"use strict";t.__esModule=!0;var r=r||{};r.Utils=r.Utils||{},r.Utils.focusFirstDescendant=function(e){for(var t=0;t=0;t--){var n=e.childNodes[t];if(r.Utils.attemptFocus(n)||r.Utils.focusLastDescendant(n))return!0}return!1},r.Utils.attemptFocus=function(e){if(!r.Utils.isFocusable(e))return!1;r.Utils.IgnoreUtilFocusChanges=!0;try{e.focus()}catch(t){}return r.Utils.IgnoreUtilFocusChanges=!1,document.activeElement===e},r.Utils.isFocusable=function(e){if(e.tabIndex>0||0===e.tabIndex&&null!==e.getAttribute("tabIndex"))return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&"ignore"!==e.rel;case"INPUT":return"hidden"!==e.type&&"file"!==e.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},r.Utils.triggerEvent=function(e,t){var n=void 0;n=/^mouse|click/.test(t)?"MouseEvents":/^key/.test(t)?"KeyboardEvent":"HTMLEvents";for(var r=document.createEvent(n),o=arguments.length,i=Array(o>2?o-2:0),a=2;a=51&&/native code/.test(e))return!1;var n=new B((function(e){e(1)})),r=function(e){e((function(){}),(function(){}))},o=n.constructor={};return o[N]=r,oe=n.then((function(){}))instanceof r,!oe||!t&&P&&!Y})),ae=ie||!w((function(e){B.all(e)["catch"]((function(){}))})),se=function(e){var t;return!(!y(e)||"function"!=typeof(t=e.then))&&t},le=function(e,t){if(!e.notified){e.notified=!0;var n=e.reactions;O((function(){var r=e.value,o=e.state==ee,i=0;while(n.length>i){var a,s,l,u=n[i++],c=o?u.ok:u.fail,f=u.resolve,d=u.reject,p=u.domain;try{c?(o||(e.rejection===re&&de(e),e.rejection=ne),!0===c?a=r:(p&&p.enter(),a=c(r),p&&(p.exit(),l=!0)),a===u.promise?d(V("Promise-chain cycle")):(s=se(a))?s.call(a,f,d):f(a)):d(r)}catch(h){p&&!l&&p.exit(),d(h)}}e.reactions=[],e.notified=!1,t&&!e.rejection&&ce(e)}))}},ue=function(e,t,n){var r,o;X?(r=U.createEvent("Event"),r.promise=t,r.reason=n,r.initEvent(e,!1,!0),u.dispatchEvent(r)):r={promise:t,reason:n},!Y&&(o=u["on"+e])?o(r):e===J&&k("Unhandled promise rejection",n)},ce=function(e){S.call(u,(function(){var t,n=e.facade,r=e.value,o=fe(e);if(o&&(t=j((function(){L?q.emit("unhandledRejection",r,n):ue(J,n,r)})),e.rejection=L||fe(e)?re:ne,t.error))throw t.value}))},fe=function(e){return e.rejection!==ne&&!e.parent},de=function(e){S.call(u,(function(){var t=e.facade;L?q.emit("rejectionHandled",t):ue(Z,t,e.value)}))},pe=function(e,t,n){return function(r){e(t,r,n)}},he=function(e,t,n){e.done||(e.done=!0,n&&(e=n),e.value=t,e.state=te,le(e,!0))},ve=function(e,t,n){if(!e.done){e.done=!0,n&&(e=n);try{if(e.facade===t)throw V("Promise can't be resolved itself");var r=se(t);r?O((function(){var n={done:!1};try{r.call(t,pe(ve,n,e),pe(he,n,e))}catch(o){he(n,o,e)}})):(e.value=t,e.state=ee,le(e,!1))}catch(o){he({done:!1},o,e)}}};if(ie&&(B=function(e){b(this,B,I),g(e),r.call(this);var t=F(this);try{e(pe(ve,t),pe(he,t))}catch(n){he(t,n)}},W=B.prototype,r=function(e){H(this,{type:I,done:!1,notified:!1,parent:!1,reactions:[],rejection:!1,state:Q,value:void 0})},r.prototype=p(W,{then:function(e,t){var n=D(this),r=K(C(this,B));return r.ok="function"!=typeof e||e,r.fail="function"==typeof t&&t,r.domain=L?q.domain:void 0,n.parent=!0,n.reactions.push(r),n.state!=Q&&le(n,!1),r.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new r,t=F(e);this.promise=e,this.resolve=pe(ve,t),this.reject=pe(he,t)},$.f=K=function(e){return e===B||e===i?new o(e):G(e)},!l&&"function"==typeof f&&z!==Object.prototype)){a=z.then,oe||(d(z,"then",(function(e,t){var n=this;return new B((function(e,t){a.call(n,e,t)})).then(e,t)}),{unsafe:!0}),d(z,"catch",W["catch"],{unsafe:!0}));try{delete z.constructor}catch(me){}h&&h(z,W)}s({global:!0,wrap:!0,forced:ie},{Promise:B}),v(B,I,!1,!0),m(I),i=c(I),s({target:I,stat:!0,forced:ie},{reject:function(e){var t=K(this);return t.reject.call(void 0,e),t.promise}}),s({target:I,stat:!0,forced:l||ie},{resolve:function(e){return E(l&&this===i?B:this,e)}}),s({target:I,stat:!0,forced:ae},{all:function(e){var t=this,n=K(t),r=n.resolve,o=n.reject,i=j((function(){var n=g(t.resolve),i=[],a=0,s=1;x(e,(function(e){var l=a++,u=!1;i.push(void 0),s++,n.call(t,e).then((function(e){u||(u=!0,i[l]=e,--s||r(i))}),o)})),--s||r(i)}));return i.error&&o(i.value),n.promise},race:function(e){var t=this,n=K(t),r=n.reject,o=j((function(){var o=g(t.resolve);x(e,(function(e){o.call(t,e).then(n.resolve,r)}))}));return o.error&&r(o.value),n.promise}})},e893:function(e,t,n){var r=n("5135"),o=n("56ef"),i=n("06cf"),a=n("9bf2");e.exports=function(e,t){for(var n=o(t),s=a.f,l=i.f,u=0;u-1?"center "+n:n+" center"}},appendArrow:function(e){var t=void 0;if(!this.appended){for(var n in this.appended=!0,e.attributes)if(/^_v-/.test(e.attributes[n].name)){t=e.attributes[n].name;break}var r=document.createElement("div");t&&r.setAttribute(t,""),r.setAttribute("x-arrow",""),r.className="popper__arrow",e.appendChild(r)}}},beforeDestroy:function(){this.doDestroy(!0),this.popperElm&&this.popperElm.parentNode===document.body&&(this.popperElm.removeEventListener("click",l),document.body.removeChild(this.popperElm))},deactivated:function(){this.$options.beforeDestroy[0].call(this)}}},ecdf:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=133)}({133:function(e,t,n){"use strict";n.r(t);var r=n(3),o={default:{order:""},selection:{width:48,minWidth:48,realWidth:48,order:"",className:"el-table-column--selection"},expand:{width:48,minWidth:48,realWidth:48,order:""},index:{width:48,minWidth:48,realWidth:48,order:""}},i={selection:{renderHeader:function(e,t){var n=t.store;return e("el-checkbox",{attrs:{disabled:n.states.data&&0===n.states.data.length,indeterminate:n.states.selection.length>0&&!this.isAllSelected,value:this.isAllSelected},nativeOn:{click:this.toggleAllSelection}})},renderCell:function(e,t){var n=t.row,r=t.column,o=t.store,i=t.$index;return e("el-checkbox",{nativeOn:{click:function(e){return e.stopPropagation()}},attrs:{value:o.isSelected(n),disabled:!!r.selectable&&!r.selectable.call(null,n,i)},on:{input:function(){o.commit("rowSelectedChanged",n)}}})},sortable:!1,resizable:!1},index:{renderHeader:function(e,t){var n=t.column;return n.label||"#"},renderCell:function(e,t){var n=t.$index,r=t.column,o=n+1,i=r.index;return"number"===typeof i?o=n+i:"function"===typeof i&&(o=i(n)),e("div",[o])},sortable:!1},expand:{renderHeader:function(e,t){var n=t.column;return n.label||""},renderCell:function(e,t){var n=t.row,r=t.store,o=["el-table__expand-icon"];r.states.expandRows.indexOf(n)>-1&&o.push("el-table__expand-icon--expanded");var i=function(e){e.stopPropagation(),r.toggleRowExpansion(n)};return e("div",{class:o,on:{click:i}},[e("i",{class:"el-icon el-icon-arrow-right"})])},sortable:!1,resizable:!1,className:"el-table__expand-column"}};function a(e,t){var n=t.row,o=t.column,i=t.$index,a=o.property,s=a&&Object(r["getPropByPath"])(n,a).v;return o&&o.formatter?o.formatter(n,o,s,i):s}function s(e,t){var n=t.row,r=t.treeNode,o=t.store;if(!r)return null;var i=[],a=function(e){e.stopPropagation(),o.loadOrToggle(n)};if(r.indent&&i.push(e("span",{class:"el-table__indent",style:{"padding-left":r.indent+"px"}})),"boolean"!==typeof r.expanded||r.noLazyChildren)i.push(e("span",{class:"el-table__placeholder"}));else{var s=["el-table__expand-icon",r.expanded?"el-table__expand-icon--expanded":""],l=["el-icon-arrow-right"];r.loading&&(l=["el-icon-loading"]),i.push(e("div",{class:s,on:{click:a}},[e("i",{class:l})]))}return i}var l=n(8),u=n(18),c=n.n(u),f=Object.assign||function(e){for(var t=1;t-1}))}}},data:function(){return{isSubColumn:!1,columns:[]}},computed:{owner:function(){var e=this.$parent;while(e&&!e.tableId)e=e.$parent;return e},columnOrTableParent:function(){var e=this.$parent;while(e&&!e.tableId&&!e.columnId)e=e.$parent;return e},realWidth:function(){return Object(l["l"])(this.width)},realMinWidth:function(){return Object(l["k"])(this.minWidth)},realAlign:function(){return this.align?"is-"+this.align:null},realHeaderAlign:function(){return this.headerAlign?"is-"+this.headerAlign:this.realAlign}},methods:{getPropsData:function(){for(var e=this,t=arguments.length,n=Array(t),r=0;rt.key[n])return 1}return 0};return e.map((function(e,t){return{value:e,index:t,key:s?s(e,t):null}})).sort((function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*n})).map((function(e){return e.value}))},l=function(e,t){var n=null;return e.columns.forEach((function(e){e.id===t&&(n=e)})),n},u=function(e,t){for(var n=null,r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",o=function(e){return!(Array.isArray(e)&&e.length)};function i(e,a,s){t(e,a,s),a.forEach((function(e){if(e[r])t(e,null,s+1);else{var a=e[n];o(a)||i(e,a,s+1)}}))}e.forEach((function(e){if(e[r])t(e,null,0);else{var a=e[n];o(a)||i(e,a,0)}}))}}})},eedf:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=86)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},86:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("button",{staticClass:"el-button",class:[e.type?"el-button--"+e.type:"",e.buttonSize?"el-button--"+e.buttonSize:"",{"is-disabled":e.buttonDisabled,"is-loading":e.loading,"is-plain":e.plain,"is-round":e.round,"is-circle":e.circle}],attrs:{disabled:e.buttonDisabled||e.loading,autofocus:e.autofocus,type:e.nativeType},on:{click:e.handleClick}},[e.loading?n("i",{staticClass:"el-icon-loading"}):e._e(),e.icon&&!e.loading?n("i",{class:e.icon}):e._e(),e.$slots.default?n("span",[e._t("default")],2):e._e()])},o=[];r._withStripped=!0;var i={name:"ElButton",inject:{elForm:{default:""},elFormItem:{default:""}},props:{type:{type:String,default:"default"},size:String,icon:{type:String,default:""},nativeType:{type:String,default:"button"},loading:Boolean,disabled:Boolean,plain:Boolean,autofocus:Boolean,round:Boolean,circle:Boolean},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},buttonSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},buttonDisabled:function(){return this.disabled||(this.elForm||{}).disabled}},methods:{handleClick:function(e){this.$emit("click",e)}}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/button/src/button.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},f069:function(e,t,n){"use strict";var r=n("1c0b"),o=function(e){var t,n;this.promise=new e((function(e,r){if(void 0!==t||void 0!==n)throw TypeError("Bad Promise constructor");t=e,n=r})),this.resolve=r(t),this.reject=r(n)};e.exports.f=function(e){return new o(e)}},f0d9:function(e,t,n){"use strict";t.__esModule=!0,t.default={el:{colorpicker:{confirm:"确定",clear:"清空"},datepicker:{now:"此刻",today:"今天",cancel:"取消",clear:"清空",confirm:"确定",selectDate:"选择日期",selectTime:"选择时间",startDate:"开始日期",startTime:"开始时间",endDate:"结束日期",endTime:"结束时间",prevYear:"前一年",nextYear:"后一年",prevMonth:"上个月",nextMonth:"下个月",year:"年",month1:"1 月",month2:"2 月",month3:"3 月",month4:"4 月",month5:"5 月",month6:"6 月",month7:"7 月",month8:"8 月",month9:"9 月",month10:"10 月",month11:"11 月",month12:"12 月",weeks:{sun:"日",mon:"一",tue:"二",wed:"三",thu:"四",fri:"五",sat:"六"},months:{jan:"一月",feb:"二月",mar:"三月",apr:"四月",may:"五月",jun:"六月",jul:"七月",aug:"八月",sep:"九月",oct:"十月",nov:"十一月",dec:"十二月"}},select:{loading:"加载中",noMatch:"无匹配数据",noData:"无数据",placeholder:"请选择"},cascader:{noMatch:"无匹配数据",loading:"加载中",placeholder:"请选择",noData:"暂无数据"},pagination:{goto:"前往",pagesize:"条/页",total:"共 {total} 条",pageClassifier:"页"},messagebox:{title:"提示",confirm:"确定",cancel:"取消",error:"输入的数据不合法!"},upload:{deleteTip:"按 delete 键可删除",delete:"删除",preview:"查看图片",continue:"继续上传"},table:{emptyText:"暂无数据",confirmFilter:"筛选",resetFilter:"重置",clearFilter:"全部",sumText:"合计"},tree:{emptyText:"暂无数据"},transfer:{noMatch:"无匹配数据",noData:"无数据",titles:["列表 1","列表 2"],filterPlaceholder:"请输入搜索内容",noCheckedFormat:"共 {total} 项",hasCheckedFormat:"已选 {checked}/{total} 项"},image:{error:"加载失败"},pageHeader:{title:"返回"},popconfirm:{confirmButtonText:"确定",cancelButtonText:"取消"},empty:{description:"暂无数据"}}}},f3ad:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=79)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},11:function(e,t){e.exports=n("2bb5")},21:function(e,t){e.exports=n("d397")},4:function(e,t){e.exports=n("d010")},79:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{class:["textarea"===e.type?"el-textarea":"el-input",e.inputSize?"el-input--"+e.inputSize:"",{"is-disabled":e.inputDisabled,"is-exceed":e.inputExceed,"el-input-group":e.$slots.prepend||e.$slots.append,"el-input-group--append":e.$slots.append,"el-input-group--prepend":e.$slots.prepend,"el-input--prefix":e.$slots.prefix||e.prefixIcon,"el-input--suffix":e.$slots.suffix||e.suffixIcon||e.clearable||e.showPassword}],on:{mouseenter:function(t){e.hovering=!0},mouseleave:function(t){e.hovering=!1}}},["textarea"!==e.type?[e.$slots.prepend?n("div",{staticClass:"el-input-group__prepend"},[e._t("prepend")],2):e._e(),"textarea"!==e.type?n("input",e._b({ref:"input",staticClass:"el-input__inner",attrs:{tabindex:e.tabindex,type:e.showPassword?e.passwordVisible?"text":"password":e.type,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"input",e.$attrs,!1)):e._e(),e.$slots.prefix||e.prefixIcon?n("span",{staticClass:"el-input__prefix"},[e._t("prefix"),e.prefixIcon?n("i",{staticClass:"el-input__icon",class:e.prefixIcon}):e._e()],2):e._e(),e.getSuffixVisible()?n("span",{staticClass:"el-input__suffix"},[n("span",{staticClass:"el-input__suffix-inner"},[e.showClear&&e.showPwdVisible&&e.isWordLimitVisible?e._e():[e._t("suffix"),e.suffixIcon?n("i",{staticClass:"el-input__icon",class:e.suffixIcon}):e._e()],e.showClear?n("i",{staticClass:"el-input__icon el-icon-circle-close el-input__clear",on:{mousedown:function(e){e.preventDefault()},click:e.clear}}):e._e(),e.showPwdVisible?n("i",{staticClass:"el-input__icon el-icon-view el-input__clear",on:{click:e.handlePasswordVisible}}):e._e(),e.isWordLimitVisible?n("span",{staticClass:"el-input__count"},[n("span",{staticClass:"el-input__count-inner"},[e._v("\n "+e._s(e.textLength)+"/"+e._s(e.upperLimit)+"\n ")])]):e._e()],2),e.validateState?n("i",{staticClass:"el-input__icon",class:["el-input__validateIcon",e.validateIcon]}):e._e()]):e._e(),e.$slots.append?n("div",{staticClass:"el-input-group__append"},[e._t("append")],2):e._e()]:n("textarea",e._b({ref:"textarea",staticClass:"el-textarea__inner",style:e.textareaStyle,attrs:{tabindex:e.tabindex,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"textarea",e.$attrs,!1)),e.isWordLimitVisible&&"textarea"===e.type?n("span",{staticClass:"el-input__count"},[e._v(e._s(e.textLength)+"/"+e._s(e.upperLimit))]):e._e()],2)},o=[];r._withStripped=!0;var i=n(4),a=n.n(i),s=n(11),l=n.n(s),u=void 0,c="\n height:0 !important;\n visibility:hidden !important;\n overflow:hidden !important;\n position:absolute !important;\n z-index:-1000 !important;\n top:0 !important;\n right:0 !important\n",f=["letter-spacing","line-height","padding-top","padding-bottom","font-family","font-weight","font-size","text-rendering","text-transform","width","text-indent","padding-left","padding-right","border-width","box-sizing"];function d(e){var t=window.getComputedStyle(e),n=t.getPropertyValue("box-sizing"),r=parseFloat(t.getPropertyValue("padding-bottom"))+parseFloat(t.getPropertyValue("padding-top")),o=parseFloat(t.getPropertyValue("border-bottom-width"))+parseFloat(t.getPropertyValue("border-top-width")),i=f.map((function(e){return e+":"+t.getPropertyValue(e)})).join(";");return{contextStyle:i,paddingSize:r,borderSize:o,boxSizing:n}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;u||(u=document.createElement("textarea"),document.body.appendChild(u));var r=d(e),o=r.paddingSize,i=r.borderSize,a=r.boxSizing,s=r.contextStyle;u.setAttribute("style",s+";"+c),u.value=e.value||e.placeholder||"";var l=u.scrollHeight,f={};"border-box"===a?l+=i:"content-box"===a&&(l-=o),u.value="";var p=u.scrollHeight-o;if(null!==t){var h=p*t;"border-box"===a&&(h=h+o+i),l=Math.max(h,l),f.minHeight=h+"px"}if(null!==n){var v=p*n;"border-box"===a&&(v=v+o+i),l=Math.min(v,l)}return f.height=l+"px",u.parentNode&&u.parentNode.removeChild(u),u=null,f}var h=n(9),v=n.n(h),m=n(21),y={name:"ElInput",componentName:"ElInput",mixins:[a.a,l.a],inheritAttrs:!1,inject:{elForm:{default:""},elFormItem:{default:""}},data:function(){return{textareaCalcStyle:{},hovering:!1,focused:!1,isComposing:!1,passwordVisible:!1}},props:{value:[String,Number],size:String,resize:String,form:String,disabled:Boolean,readonly:Boolean,type:{type:String,default:"text"},autosize:{type:[Boolean,Object],default:!1},autocomplete:{type:String,default:"off"},autoComplete:{type:String,validator:function(e){return!0}},validateEvent:{type:Boolean,default:!0},suffixIcon:String,prefixIcon:String,label:String,clearable:{type:Boolean,default:!1},showPassword:{type:Boolean,default:!1},showWordLimit:{type:Boolean,default:!1},tabindex:String},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},validateState:function(){return this.elFormItem?this.elFormItem.validateState:""},needStatusIcon:function(){return!!this.elForm&&this.elForm.statusIcon},validateIcon:function(){return{validating:"el-icon-loading",success:"el-icon-circle-check",error:"el-icon-circle-close"}[this.validateState]},textareaStyle:function(){return v()({},this.textareaCalcStyle,{resize:this.resize})},inputSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},inputDisabled:function(){return this.disabled||(this.elForm||{}).disabled},nativeInputValue:function(){return null===this.value||void 0===this.value?"":String(this.value)},showClear:function(){return this.clearable&&!this.inputDisabled&&!this.readonly&&this.nativeInputValue&&(this.focused||this.hovering)},showPwdVisible:function(){return this.showPassword&&!this.inputDisabled&&!this.readonly&&(!!this.nativeInputValue||this.focused)},isWordLimitVisible:function(){return this.showWordLimit&&this.$attrs.maxlength&&("text"===this.type||"textarea"===this.type)&&!this.inputDisabled&&!this.readonly&&!this.showPassword},upperLimit:function(){return this.$attrs.maxlength},textLength:function(){return"number"===typeof this.value?String(this.value).length:(this.value||"").length},inputExceed:function(){return this.isWordLimitVisible&&this.textLength>this.upperLimit}},watch:{value:function(e){this.$nextTick(this.resizeTextarea),this.validateEvent&&this.dispatch("ElFormItem","el.form.change",[e])},nativeInputValue:function(){this.setNativeInputValue()},type:function(){var e=this;this.$nextTick((function(){e.setNativeInputValue(),e.resizeTextarea(),e.updateIconOffset()}))}},methods:{focus:function(){this.getInput().focus()},blur:function(){this.getInput().blur()},getMigratingConfig:function(){return{props:{icon:"icon is removed, use suffix-icon / prefix-icon instead.","on-icon-click":"on-icon-click is removed."},events:{click:"click is removed."}}},handleBlur:function(e){this.focused=!1,this.$emit("blur",e),this.validateEvent&&this.dispatch("ElFormItem","el.form.blur",[this.value])},select:function(){this.getInput().select()},resizeTextarea:function(){if(!this.$isServer){var e=this.autosize,t=this.type;if("textarea"===t)if(e){var n=e.minRows,r=e.maxRows;this.textareaCalcStyle=p(this.$refs.textarea,n,r)}else this.textareaCalcStyle={minHeight:p(this.$refs.textarea).minHeight}}},setNativeInputValue:function(){var e=this.getInput();e&&e.value!==this.nativeInputValue&&(e.value=this.nativeInputValue)},handleFocus:function(e){this.focused=!0,this.$emit("focus",e)},handleCompositionStart:function(){this.isComposing=!0},handleCompositionUpdate:function(e){var t=e.target.value,n=t[t.length-1]||"";this.isComposing=!Object(m["isKorean"])(n)},handleCompositionEnd:function(e){this.isComposing&&(this.isComposing=!1,this.handleInput(e))},handleInput:function(e){this.isComposing||e.target.value!==this.nativeInputValue&&(this.$emit("input",e.target.value),this.$nextTick(this.setNativeInputValue))},handleChange:function(e){this.$emit("change",e.target.value)},calcIconOffset:function(e){var t=[].slice.call(this.$el.querySelectorAll(".el-input__"+e)||[]);if(t.length){for(var n=null,r=0;r0&&(this.timer=setTimeout((function(){e.closed||e.close()}),this.duration))},keydown:function(e){27===e.keyCode&&(this.closed||this.close())}},mounted:function(){this.startTimer(),document.addEventListener("keydown",this.keydown)},beforeDestroy:function(){document.removeEventListener("keydown",this.keydown)}},u=l,c=n(0),f=Object(c["a"])(u,i,a,!1,null,null,null);f.options.__file="packages/message/src/main.vue";var d=f.exports,p=n(13),h=n(23),v=o.a.extend(d),m=void 0,y=[],g=1,b=function e(t){if(!o.a.prototype.$isServer){t=t||{},"string"===typeof t&&(t={message:t});var n=t.onClose,r="message_"+g++;t.onClose=function(){e.close(r,n)},m=new v({data:t}),m.id=r,Object(h["isVNode"])(m.message)&&(m.$slots.default=[m.message],m.message=null),m.$mount(),document.body.appendChild(m.$el);var i=t.offset||20;return y.forEach((function(e){i+=e.$el.offsetHeight+16})),m.verticalOffset=i,m.visible=!0,m.$el.style.zIndex=p["PopupManager"].nextZIndex(),y.push(m),m}};["success","warning","info","error"].forEach((function(e){b[e]=function(t){return"string"===typeof t&&(t={message:t}),t.type=e,b(t)}})),b.close=function(e,t){for(var n=y.length,r=-1,o=void 0,i=0;iy.length-1))for(var a=r;a=0;e--)y[e].close()};var _=b;t["default"]=_}})},f5df:function(e,t,n){var r=n("00ee"),o=n("c6b6"),i=n("b622"),a=i("toStringTag"),s="Arguments"==o(function(){return arguments}()),l=function(e,t){try{return e[t]}catch(n){}};e.exports=r?o:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=l(t=Object(e),a))?n:s?o(t):"Object"==(r=o(t))&&"function"==typeof t.callee?"Arguments":r}},f772:function(e,t,n){var r=n("5692"),o=n("90e3"),i=r("keys");e.exports=function(e){return i[e]||(i[e]=o(e))}},fc6a:function(e,t,n){var r=n("44ad"),o=n("1d80");e.exports=function(e){return r(o(e))}},fdbc:function(e,t){e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},fdbf:function(e,t,n){var r=n("4930");e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},fea9:function(e,t,n){var r=n("da84");e.exports=r.Promise}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/bookshelf/js/detail.4e6a53a9.js b/app/src/main/assets/web/bookshelf/js/detail.4e6a53a9.js new file mode 100644 index 000000000..eaefb9b7f --- /dev/null +++ b/app/src/main/assets/web/bookshelf/js/detail.4e6a53a9.js @@ -0,0 +1 @@ +(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["detail"],{"057f":function(t,e,n){var o=n("fc6a"),i=n("241c").f,s={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],r=function(t){try{return i(t)}catch(e){return a.slice()}};t.exports.f=function(t){return a&&"[object Window]"==s.call(t)?r(t):i(o(t))}},"05b3":function(t,e,n){},"0827":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg=="},1276:function(t,e,n){"use strict";var o=n("d784"),i=n("44e7"),s=n("825a"),a=n("1d80"),r=n("4840"),c=n("8aa5"),A=n("50c4"),l=n("14c3"),u=n("9263"),f=n("9f7f"),g=n("d039"),d=f.UNSUPPORTED_Y,h=[].push,p=Math.min,m=4294967295,b=!g((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));o("split",(function(t,e,n){var o;return o="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var o=String(a(this)),s=void 0===n?m:n>>>0;if(0===s)return[];if(void 0===t)return[o];if(!i(t))return e.call(o,t,s);var r,c,A,l=[],f=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),g=0,d=new RegExp(t.source,f+"g");while(r=u.call(d,o)){if(c=d.lastIndex,c>g&&(l.push(o.slice(g,r.index)),r.length>1&&r.index=s))break;d.lastIndex===r.index&&d.lastIndex++}return g===o.length?!A&&d.test("")||l.push(""):l.push(o.slice(g)),l.length>s?l.slice(0,s):l}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var i=a(this),s=void 0==e?void 0:e[t];return void 0!==s?s.call(e,i,n):o.call(String(i),e,n)},function(t,i){var a=n(o,this,t,i,o!==e);if(a.done)return a.value;var u=s(this),f=String(t),g=r(u,RegExp),h=u.unicode,b=(u.ignoreCase?"i":"")+(u.multiline?"m":"")+(u.unicode?"u":"")+(d?"g":"y"),v=new g(d?"^(?:"+u.source+")":u,b),C=void 0===i?m:i>>>0;if(0===C)return[];if(0===f.length)return null===l(v,f)?[f]:[];var y=0,B=0,S=[];while(B=51||!o((function(){var e=[],n=e.constructor={};return n[a]=function(){return{foo:1}},1!==e[t](Boolean).foo}))}},"1e75":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXM2t7O3ODQ3uLR4OTDp25yAAACdUlEQVQozw3P70sTcQDH8c/3/M7NG+j35mnHwjwh4hRy/QFK3zvPNbeIG1koPZmxfj2IDAwihL53zj0JYisfmEHcZJZOiBUG60lZiI8T/ANusuftgQ+kCPIPeMP7hS5mUrV9c1g6MQCAEZ8tDLHwofImAGRlX+SZK3Vu9rRRPuO4PK6/9nA4GIATsxlODS+rdCMhkAZivpYV0LWoQHSLSA4NfUg+6mY+7BKL2++F9LvnrBDYm6JO9i/YO3i/HJTGQ4pdIV82TbEDFG6vGYCd4wZchgK5J2CrKTLE+Tx0v+YGlIbdWJFcQl4ptBN8fUJQN1MCJLcZLYwUVVo+famGGty8EXJF5ofOEDzcodT3/Fb0I5sHmc1ZG7CcSl8COgxlXx09jT05OafjCZLIHJhGIaU6wDZHsuMQ41wbdjmQXbhKnMq1zlXSYrjCnyZblqexA7fC8RxS74tq2P3OxSQwTuJSApH8OZLzBBp1pOe0i3rdyDUA47GySZ31YmC4EQYSXvFSvieORGBxXF9aeVtUWKGS9WMC4Z9Y2uXnJ2nCUXVMbPOYqNYNmGWWQ7Evr+BWC+a0JAMTImcq/S4Z5INdQMeuOqDIMa9beilxfA60iC6sP1INcPDpmHBW8drZHNmqwyddJtVje9q8WGUgWAOzmbU4FCQBFi8B2Wk6pickBnYhJMenmJGuRmtt2IoKq9NuFGbNFR99sHnvrnLsLysKANDIsxbp6RNMAsoDSKuRpMwZbAAzI68QatIjmZ0aImyM3O8/4e2MNlOHZomFsa/fLDsysliHS+nlYLQJMnynxrH8QO4PaAV2Li8B/+52UgeGIVNFYf8B1XG/kFSmLcUAAAAASUVORK5CYII="},"356c":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXPz8/R0dHT09PU1NToNyAhAAACdElEQVQozw3NP0xTQQDH8d9d7sFrG+QeKVgQ4aoFCwFkYERyLY//0UB8GNGg1WAC0RBGJrzW4mCXQmpgvCYOwEAYiulSpYtza2KiW7s5FgNJFSV2/CzfL7RwpoJ20iadmgA8owOyaxmusKE44scBeb4vIv00dqYgmf6jzWcr7W6INbDQeZbQL9ytXeYgtFfzmW1Fek5msxJlwhyt6qDDxOLQzpVPompYrMPnEnhvLm7M5BxY5nowAj3zkydAkpC0FIG6g7AK+Ub25ybyNWVYwtpseP2rfrQwiGRpfqrnMuPeuvr2dA0p2YsHF2XghkrXKtZ8tLBjR7S2qIaYbKmyLd/QP+EogLjqqwNw5Lq1pDlMLkM5+gNoSvdq+Pxmz9/61EFq6GYM6GqaGvlN95zy3gsmEWI8K3k8OP9OmRLEPO6DP3Wv3g42COinJTZ33dcIvs4ESp6opMTjDs6mcYTEbFeUifuxh989yZrIx4lkpuixxz0nHLCekKbE17suKhYkMGhoYhTZtVBvg4bfq/1L1Im0AGMVpBFwumM0zwyuKiCMi5dqR4Flx47AGyF2xTbxqUdTwCH94BT3DozpLV5WuAL/N8rGtHKjotBOOuOtCJ9E21uqsyBoLOzaXbHPrK5PQBP+fBfeidvJAeMIAmzVt5IkJJ9DBWaZDAepYUhlQqHt0h72SJ3j8TZHom64f516xx9T5evgMPgwG82jZdJaJIDyWp6LAjOCclVyzNA3iTKzIULlBQEPaTXlPHok5gISclmyaWZlqY2aTHdRHpJOwTdDEQ3ZfKtbpclcNhyVClagmY+fIfyKukntPqBgnx5QvZHk/D/MK8JMClrSigAAAABJRU5ErkJggg=="},4039:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX0/PTx+fH2/vbz+/P4//htSO9OAAAC5UlEQVQ4yyWT0QGjMAxDZTsDWKQDmJQBYrgBUsr+M517x0+LRWw9CyA+pC1YzndrMgHaNXVKQ+di13Of1qbur48nWhuRjj8i6ON8e7pNm7zyag/DBTfS9Z4Hup1fUuXMKY4HEE8QOHCByXkIkl7lDT239RtL9quO4JItmmhOAHXg45QuYKrQFLyGJcRvaTw6kQqZy6mkR6JAPFH/XqsQjEDRmUOA+MNLHGyMUT7AHApoAhjgjIJmCxy6XHdf648AWCdGe57IUDazCeTImQOY4/z+eVYVX2IjOw9RydeAeJwl79iGi4HpgQgHEchWraUZLtayu8scq0lHHHUKMY3Ml8hB7CS1jOckDLG9ccgNeX3124phOcjL9fPnWJhTXpLHeG9DRmHnTxHEaHakS2J51lwAJcUraNbuU7q4gMTDQj3Eripc/x+qFM5VEKAB1roQfAkX5/PxqnS2QpOrxfK1Zft0/omV5T+xCSBUAIbEIwUQgvAfxFE1O8dnk233+1UZiqJ1mAbsue6Yt8tF+yOrxC/YrUhzC4qPlE3EbR5hGKhhHdlrg7J9WunV7L7BcYQwAeE59u2tnN1c6gfVYrQiLSZ9OxZdWDXQq0+r0Pbarh3UqGCwauVvbiXuDsNxCtLDdW9rTF8oQYN4EoXXdfmwNguQP26n/tRjDeo+F2W7PjWtfSr6Bn/z+cXOLp4NnMV4RytvSW4B68m+XN9XfZTFGhO/S+cHTuTqZDC21ccA0N7QsePALaDQC3D1f94U9CWo+aq6BjB3v0rxIimBM12296M3aKPHjXLQE9KQKH4By8RHraJ3AgVto2r4xdFqlaPaiAHLl1ZF4P2pI6cYc+K8UZdcmxy7lqGc1IoPxLmIFuIeEZ6j2sQT88muEg1zwrEDTIX5U/ZmcsqfgVlBumiBLF4sAyhf9BFlXOPKLZ4H0iFb3VoHrGhtHTldKrOvP2/reu2zfV8CXMPqzRdlgd0a5eI7WwB/AYcgavcqxXWEAAAAAElFTkSuQmCC"},4400:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAElBMVEUQERMODxESFBYWGBkaHB0eICLm6ozJAAACkUlEQVQ4yyWTUdLbMAiEASfvoOkBkBy/O5keIE0v8E/uf5h+68qZWALELgu2MG9PP9qyvCzTVhrrsPGOCjvTfXQZvtp/W3Gy6LCITqs4q/DZ+KYl76zKzHVYpY2wNY27nqN1sbLGcrLH3/ENH4oWlGctsDu8AO+HzTLlsYdh8MzP1m6YDMz0ACfcimvakBj+mwO/+5Uta5teOD379sxK1fUxmUhv8MU3jUT5gs26PMephFznkLcpQZ6/dPL9C/GWHcCxDN6oZhD5xBm5qoYBPA+PFE/H1tXDWcWl8TW7rS+4dUzAVy0BIrvC4/HcqW2TkG1HO8q9dC23INAg7NA4AFRFkDTM2lfELPyFzi1VddcpX2z0KjHBUDmdLNJ6dDps4ytrX+FPsZwE31wSL+6OWfHOAJ3+Y0Rk/MiKfmWNPg7oVP/U3Ck9FoCkC2gBpALOiqbMNTkOe8P4FWkTD2Y9Q3+5VmV0uLUJBl68U5uAK2Kl6QDXvLxbwweOL2sixW78uU8p0ysfc7cWrF1j6B1sPJ4WgclYSnJN1bzozrhEcFHmRzBkbJWqqdG+EYJXRFmT5jnLXPUNF6WBdoFbTxYsmDXVLU/WA7MExNc93sJS5hIXDeLxzMScHzdhKvEkibr6cQXYPrmtmTA7JcInISrTzRDvShTdka0uVGrsJAAR6tSn1sKziZtfKVjAxPrJsYgZO0bye+vKTZ/DgoAoLGNO6jYHimZYTL/3pLJHawquJukjBpfz8WOGVSVIWx9ywUfS5iENutidRM4NzkAmxgUSQ68xgNOU+ZLalr4TS2V+D2xqukZig+Z9DilR7Nouzwp1cp/3E5q6Rdlf08obKvAM4qZ6pMr+w3PmQALSSBfjyZn5DwrNRVbywBQiAAAAAElFTkSuQmCC"},"477e":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAACVBMVEX28ef48+n69esoK7jYAAAB4UlEQVQozw2OsW4bQQxEhwLXkDrysGdEqRRgVShfQQq8wOr2jD0jSpXCLvwXbtKfADlFqgSwC/9ljqweZgYzQFnb/QGepYhA9jzmTc1WaSEtQpbFgjWATI00ZZtIckXx8q2Oe5yEByBy+RHOTcM+VVTadULsvxvRC/q8WTwgcWGD+Mnaqa0oy2gw2pKFzK+PzEsus5hP9AHojKslVynLlioVTBEN8cjDNnZoR1uMGTiZAAN47HxMtEkGUE9b8HWzkqNX5Lpk0yVziAJOs46rK1pG/xNuXLjz95fSDoJE5IqG23MAYPtWoeWPvfVtIV/Ng9oH3W0gGMPIOqd4MK4QZ55dV61gOb8Zxp7I9qayaGxp6Q91cmC0ZRdBwEQVHWzSAanlZwVWc9yljeTCeaHjBVvlPSLeyeBUT2rPdJegQI103jVS3uYkyIx1il6mslMDedZuOkwzolsagvPuQAfp7cYg7k9V1NOxfq64PNSvMdwONV4VYEmqlbpZy5OAakRKkjPnL4CBv5/OZRgoWHBmNbxB0LgB1I4vXFj93UoF2/0TPEsWwV9EhbIiTPqYoTHYoMn3enTDjmrFeDTIzaL1bUC/PBIMuF+vSSYSaxoVt90EO3Gu1zrMuMRGUk7Ffv3L+A931Gsb/yBoIgAAAABJRU5ErkJggg=="},"537b":function(t,e,n){"use strict";n.r(e);var o=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"chapter-wrapper",class:{night:t.isNight,day:!t.isNight},style:t.bodyTheme},[n("div",{staticClass:"tool-bar",style:t.leftBarTheme},[n("div",{staticClass:"tools"},[n("el-popover",{attrs:{placement:"right",width:t.cataWidth,trigger:"click","visible-arrow":!1,"popper-class":"pop-cata"},model:{value:t.popCataVisible,callback:function(e){t.popCataVisible=e},expression:"popCataVisible"}},[n("PopCata",{ref:"popCata",staticClass:"popup",on:{getContent:t.getContent}}),n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},attrs:{slot:"reference"},slot:"reference"},[n("div",{staticClass:"iconfont"},[t._v("  ")]),n("div",{staticClass:"icon-text"},[t._v("目录")])])],1),n("el-popover",{attrs:{placement:"bottom-right",width:"470",trigger:"click","visible-arrow":!1,"popper-class":"pop-setting"},model:{value:t.readSettingsVisible,callback:function(e){t.readSettingsVisible=e},expression:"readSettingsVisible"}},[n("ReadSettings",{staticClass:"popup"}),n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},attrs:{slot:"reference"},slot:"reference"},[n("div",{staticClass:"iconfont"},[t._v("  ")]),n("div",{staticClass:"icon-text"},[t._v("设置")])])],1),n("div",{staticClass:"tool-icon",on:{click:t.toShelf}},[n("div",{staticClass:"iconfont"},[t._v("  ")]),n("div",{staticClass:"icon-text"},[t._v("书架")])]),n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},on:{click:t.toTop}},[n("div",{staticClass:"iconfont"},[t._v("  ")]),n("div",{staticClass:"icon-text"},[t._v("顶部")])]),n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},on:{click:t.toBottom}},[n("div",{staticClass:"iconfont"},[t._v("  ")]),n("div",{staticClass:"icon-text"},[t._v("底部")])])],1)]),n("div",{staticClass:"read-bar",style:t.rightBarTheme},[n("div",{staticClass:"tools"},[n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},on:{click:t.toLastChapter}},[n("div",{staticClass:"iconfont"},[t._v("  ")])]),n("div",{staticClass:"tool-icon",class:{"no-point":t.noPoint},on:{click:t.toNextChapter}},[n("div",{staticClass:"iconfont"},[t._v("  ")])])])]),n("div",{staticClass:"chapter-bar"}),n("div",{ref:"content",staticClass:"chapter",style:t.chapterTheme},[n("div",{staticClass:"content"},[n("div",{ref:"top",staticClass:"top-bar"}),t.show?n("div",{ref:"title",staticClass:"title"},[t._v(t._s(t.title))]):t._e(),n("Pcontent",{attrs:{carray:t.content}}),n("div",{ref:"bottom",staticClass:"bottom-bar"})],1)])])},i=[],s=(n("ac1f"),n("1276"),function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"cata-wrapper",style:t.popupTheme},[n("div",{staticClass:"title"},[t._v(" 目录 ")]),n("div",{ref:"cataData",staticClass:"data-wrapper",class:{night:t.isNight,day:!t.isNight}},[n("div",{staticClass:"cata"},t._l(t.catalog,(function(e,o){return n("div",{key:e.durChapterIndex,ref:"cata",refInFor:!0,staticClass:"log",class:{selected:t.isSelected(o)},on:{click:function(n){return t.gotoChapter(e)}}},[n("div",{staticClass:"log-text"},[t._v(" "+t._s(e.title)+" ")])])})),0)])])}),a=[];n("a4d3"),n("e01a"),n("d3b7"),n("d28b"),n("3ca3"),n("ddb0");function r(t){return r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}var c,A,l=function(t,e,n,o){return t/=o/2,t<1?n/2*t*t+e:(t--,-n/2*(t*(t-2)-1)+e)},u=function(){var t,e,n,o,i,s,a,c,A,u,f,g,d;function h(){var e=t.scrollTop||t.scrollY||t.pageYOffset;return e="undefined"===typeof e?0:e,e}function p(e){var o=e.getBoundingClientRect().top,i=t.getBoundingClientRect?t.getBoundingClientRect().top:0;return o-i+n}function m(e){t.scrollTo?t.scrollTo(0,e):t.scrollTop=e}function b(t){u||(u=t),f=t-u,g=s(f,n,c,A),m(g),f1&&void 0!==arguments[1]?arguments[1]:{};switch(A=f.duration||1e3,i=f.offset||0,d=f.callback,s=f.easing||l,a=f.a11y||!1,r(f.container)){case"object":t=f.container;break;case"string":t=document.querySelector(f.container);break;default:t=window}switch(n=h(),r(u)){case"number":e=void 0,a=!1,o=n+u;break;case"object":e=u,o=p(e);break;case"string":e=document.querySelector(u),o=p(e);break}switch(c=o-n+i,r(f.duration)){case"number":A=f.duration;break;case"function":A=f.duration(c);break}requestAnimationFrame(b)}return C},f=u(),g=f,d=n("7286"),h=n.n(d),p=n("477e"),m=n.n(p),b=n("e160"),v=n.n(b),C=n("df5e"),y=n.n(C),B=n("ec0f"),S=n.n(B),I=n("b671"),k=n.n(I),w=n("5629"),x=n.n(w),E=n("d0e3"),Q=n.n(E),U=n("4039"),D=n.n(U),V=n("1e75"),O=n.n(V),F=n("1632"),P=n.n(F),R=n("7abd"),M=n.n(R),N=n("356c"),K=n.n(N),H=n("b165"),J=n.n(H),z=n("cf68"),L=n.n(z),W=n("4400"),T=n.n(W),q=n("802e"),G=n.n(q),Z=n("0827"),Y=n.n(Z),j={themes:[{body:"#ede7da url("+h.a+") repeat",content:"#ede7da url("+m.a+") repeat",popup:"#ede7da url("+v.a+") repeat"},{body:"#ede7da url("+y.a+") repeat",content:"#ede7da url("+S.a+") repeat",popup:"#ede7da url("+k.a+") repeat"},{body:"#ede7da url("+x.a+") repeat",content:"#ede7da url("+Q.a+") repeat",popup:"#ede7da url("+D.a+") repeat"},{body:"#ede7da url("+O.a+") repeat",content:"#ede7da url("+P.a+") repeat",popup:"#ede7da url("+M.a+") repeat"},{body:"#ebcece repeat",content:"#f5e4e4 repeat",popup:"#faeceb repeat"},{body:"#ede7da url("+K.a+") repeat",content:"#ede7da url("+J.a+") repeat",popup:"#ede7da url("+L.a+") repeat"},{body:"#ede7da url("+T.a+") repeat",content:"#ede7da url("+G.a+") repeat",popup:"#ede7da url("+Y.a+") repeat"}],fonts:[{fontFamily:"Microsoft YaHei, PingFangSC-Regular, HelveticaNeue-Light, Helvetica Neue Light, sans-serif"},{fontFamily:"PingFangSC-Regular, -apple-system, Simsun"},{fontFamily:"Kaiti"}]},X=j,$=(n("05b3"),{name:"PopCata",data:function(){return{isNight:6==this.$store.state.config.theme,index:this.$store.state.readingBook.index}},computed:{catalog:function(){return this.$store.state.readingBook.catalog},popCataVisible:function(){return this.$store.state.popCataVisible},theme:function(){return this.$store.state.config.theme},popupTheme:function(){return{background:X.themes[this.theme].popup}}},mounted:function(){},watch:{theme:function(t){this.isNight=6==t},popCataVisible:function(){this.$nextTick((function(){var t=this.$store.state.readingBook.index,e=this.$refs.cataData;g(this.$refs.cata[t],{container:e,duration:0})}))}},methods:{isSelected:function(t){return t==this.$store.state.readingBook.index},gotoChapter:function(t){this.index=this.catalog.indexOf(t),this.$store.commit("setPopCataVisible",!1),this.$store.commit("setContentLoading",!0),this.$emit("getContent",this.index)}}}),_=$,tt=(n("9078"),n("2877")),et=Object(tt["a"])(_,s,a,!1,null,"22f8c37b",null),nt=et.exports,ot=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"settings-wrapper",class:{night:t.isNight,day:!t.isNight},style:t.popupTheme},[n("div",{staticClass:"settings-title"},[t._v("设置")]),n("div",{staticClass:"setting-list"},[n("ul",[n("li",{staticClass:"theme-list"},[n("i",[t._v("阅读主题")]),t._l(t.themeColors,(function(e,o){return n("span",{key:o,ref:"themes",refInFor:!0,staticClass:"theme-item",class:{selected:t.selectedTheme==o},style:e,on:{click:function(e){return t.setTheme(o)}}},[o<6?n("em",{staticClass:"iconfont"},[t._v("")]):n("em",{staticClass:"moon-icon"},[t._v(t._s(t.moonIcon))])])}))],2),n("li",{staticClass:"font-list"},[n("i",[t._v("正文字体")]),t._l(t.fonts,(function(e,o){return n("span",{key:o,staticClass:"font-item",class:{selected:t.selectedFont==o},on:{click:function(e){return t.setFont(o)}}},[t._v(t._s(e))])}))],2),n("li",{staticClass:"font-size"},[n("i",[t._v("字体大小")]),n("div",{staticClass:"resize"},[n("span",{staticClass:"less",on:{click:t.lessFontSize}},[n("em",{staticClass:"iconfont"},[t._v("")])]),n("b"),t._v(" "),n("span",{staticClass:"lang"},[t._v(t._s(t.fontSize))]),n("b"),n("span",{staticClass:"more",on:{click:t.moreFontSize}},[n("em",{staticClass:"iconfont"},[t._v("")])])])]),n("li",{staticClass:"read-width"},[n("i",[t._v("页面宽度")]),n("div",{staticClass:"resize"},[n("span",{staticClass:"less",on:{click:t.lessReadWidth}},[n("em",{staticClass:"iconfont"},[t._v("")])]),n("b"),t._v(" "),n("span",{staticClass:"lang"},[t._v(t._s(t.readWidth))]),n("b"),n("span",{staticClass:"more",on:{click:t.moreReadWidth}},[n("em",{staticClass:"iconfont"},[t._v("")])])])])])])])},it=[],st=(n("82da"),{name:"ReadSettings",data:function(){return{theme:0,isNight:6==this.$store.state.config.theme,moonIcon:"",themeColors:[{background:"rgba(250, 245, 235, 0.8)"},{background:"rgba(245, 234, 204, 0.8)"},{background:"rgba(230, 242, 230, 0.8)"},{background:"rgba(228, 241, 245, 0.8)"},{background:"rgba(245, 228, 228, 0.8)"},{background:"rgba(224, 224, 224, 0.8)"},{background:"rgba(0, 0, 0, 0.5)"}],moonIconStyle:{display:"inline",color:"rgba(255,255,255,0.2)"},fonts:["雅黑","宋体","楷书"]}},mounted:function(){var t=this.$store.state.config;this.theme=t.theme,6==this.theme?this.moonIcon="":this.moonIcon=""},computed:{config:function(){return this.$store.state.config},popupTheme:function(){return{background:X.themes[this.config.theme].popup}},selectedTheme:function(){return this.$store.state.config.theme},selectedFont:function(){return this.$store.state.config.font},fontSize:function(){return this.$store.state.config.fontSize},readWidth:function(){return this.$store.state.config.readWidth}},methods:{setTheme:function(t){6==t?(this.isNight=!0,this.moonIcon="",this.moonIconStyle.color="#ed4259"):(this.isNight=!1,this.moonIcon="",this.moonIconStyle.color="rgba(255,255,255,0.2)");var e=this.config;e.theme=t,this.$store.commit("setConfig",e)},setFont:function(t){var e=this.config;e.font=t,this.$store.commit("setConfig",e)},moreFontSize:function(){var t=this.config;t.fontSize<48&&(t.fontSize+=2),this.$store.commit("setConfig",t)},lessFontSize:function(){var t=this.config;t.fontSize>12&&(t.fontSize-=2),this.$store.commit("setConfig",t)},moreReadWidth:function(){var t=this.config;t.readWidth<960&&(t.readWidth+=160),this.$store.commit("setConfig",t)},lessReadWidth:function(){var t=this.config;t.readWidth>640&&(t.readWidth-=160),this.$store.commit("setConfig",t)}}}),at=st,rt=(n("75ab"),Object(tt["a"])(at,ot,it,!1,null,"36dafd56",null)),ct=rt.exports,At=(n("d81d"),{name:"pcontent",data:function(){return{}},props:["carray"],render:function(){var t=arguments[0],e=this.fontFamily,n=this.fontSize,o=e;return o.fontSize=n,this.show?t("div",[this.carray.map((function(e){return t("p",{style:o,domProps:{innerHTML:e}})}))]):t("div")},computed:{show:function(){return this.$store.state.showContent},fontFamily:function(){return X.fonts[this.$store.state.config.font]},fontSize:function(){return this.$store.state.config.fontSize+"px"}},watch:{fontSize:function(){var t=this;t.$store.commit("setShowContent",!1),this.$nextTick((function(){t.$store.commit("setShowContent",!0)}))}}}),lt=At,ut=(n("ca36"),Object(tt["a"])(lt,c,A,!1,null,"7b03cca0",null)),ft=ut.exports,gt=n("bc3a"),dt=n.n(gt),ht={components:{PopCata:nt,Pcontent:ft,ReadSettings:ct},created:function(){var t=JSON.parse(localStorage.getItem("config"));null!=t&&this.$store.commit("setConfig",t)},beforeCreate:function(){var t=JSON.parse(localStorage.getItem("config"));null!=t&&this.$store.commit("setConfig",t)},mounted:function(){var t=this;this.loading=this.$loading({target:this.$refs.content,lock:!0,text:"正在获取内容",spinner:"el-icon-loading",background:"rgba(0,0,0,0)"});var e=this,n=sessionStorage.getItem("bookUrl"),o=sessionStorage.getItem("bookName"),i=sessionStorage.getItem("chapterIndex")||0,s=JSON.parse(localStorage.getItem(n));(null==s||i>0)&&(s={bookName:o,bookUrl:n,index:i},localStorage.setItem(n,JSON.stringify(s))),this.func_keyup=function(t){switch(t.key){case"ArrowLeft":t.stopPropagation(),t.preventDefault(),e.toLastChapter();break;case"ArrowRight":t.stopPropagation(),t.preventDefault(),e.toNextChapter();break;case"ArrowUp":t.stopPropagation(),t.preventDefault(),0===document.documentElement.scrollTop?e.$message.warning("已到达页面顶部"):g(0-document.documentElement.clientHeight+100);break;case"ArrowDown":t.stopPropagation(),t.preventDefault(),document.documentElement.clientHeight+document.documentElement.scrollTop===document.documentElement.scrollHeight?e.$message.warning("已到达页面底部"):g(document.documentElement.clientHeight-100);break}},this.getCatalog(n).then((function(n){var o=n.data.data;s.catalog=o,e.$store.commit("setReadingBook",s);var i=e.$store.state.readingBook.index||0;t.getContent(i),window.addEventListener("keyup",t.func_keyup)}),(function(t){throw e.loading.close(),e.$message.error("获取书籍目录失败"),t}))},destroyed:function(){window.removeEventListener("keyup",this.func_keyup)},watch:{chapterName:function(t){this.title=t},content:function(){this.$store.commit("setContentLoading",!1)},theme:function(t){this.isNight=6==t},bodyColor:function(t){this.bodyTheme.background=t},chapterColor:function(t){this.chapterTheme.background=t},readWidth:function(t){this.chapterTheme.width=t;var e=-((parseInt(t)+130)/2+68)+"px",n=-((parseInt(t)+130)/2+52)+"px";this.leftBarTheme.marginLeft=e,this.rightBarTheme.marginRight=n},popupColor:function(t){this.leftBarTheme.background=t,this.rightBarTheme.background=t},readSettingsVisible:function(t){if(!t){var e=JSON.stringify(this.$store.state.config);localStorage.setItem("config",e)}}},data:function(){return{title:"",content:[],noPoint:!0,isNight:6==this.$store.state.config.theme,bodyTheme:{background:X.themes[this.$store.state.config.theme].body},chapterTheme:{background:X.themes[this.$store.state.config.theme].content,width:this.$store.state.config.readWidth-130+"px"},leftBarTheme:{background:X.themes[this.$store.state.config.theme].popup,marginLeft:-(this.$store.state.config.readWidth/2+68)+"px"},rightBarTheme:{background:X.themes[this.$store.state.config.theme].popup,marginRight:-(this.$store.state.config.readWidth/2+52)+"px"}}},computed:{catalog:function(){return this.$store.state.catalog},windowHeight:function(){return window.innerHeight},contentHeight:function(){return this.$refs.content.offsetHeight},popCataVisible:{get:function(){return this.$store.state.popCataVisible},set:function(t){this.$store.commit("setPopCataVisible",t)}},readSettingsVisible:{get:function(){return this.$store.state.readSettingsVisible},set:function(t){this.$store.commit("setReadSettingsVisible",t)}},config:function(){return this.$store.state.config},theme:function(){return this.config.theme},bodyColor:function(){return X.themes[this.config.theme].body},chapterColor:function(){return X.themes[this.config.theme].content},popupColor:function(){return X.themes[this.config.theme].popup},readWidth:function(){return this.$store.state.config.readWidth-130+"px"},cataWidth:function(){return this.$store.state.config.readWidth-33},show:function(){return this.$store.state.showContent}},methods:{getCatalog:function(t){return dt.a.get("/getChapterList?url="+encodeURIComponent(t))},getContent:function(t){var e=this;this.$store.commit("setShowContent",!1),this.loading.visible||(this.loading=this.$loading({target:this.$refs.content,lock:!0,text:"正在获取内容",spinner:"el-icon-loading",background:"rgba(0,0,0,0)"}));var n=sessionStorage.getItem("bookUrl"),o=JSON.parse(localStorage.getItem(n));o.index=t,localStorage.setItem(n,JSON.stringify(o)),this.$store.state.readingBook.index=t;var i=this.$store.state.readingBook.catalog[t].title,s=this.$store.state.readingBook.catalog[t].index;this.title=i,g(this.$refs.top,{duration:0});var a=this;dt.a.get("/getBookContent?url="+encodeURIComponent(n)+"&index="+s).then((function(t){var n=t.data.data;a.content=n.split(/\n+/),e.$store.commit("setContentLoading",!0),a.loading.close(),a.noPoint=!1,a.$store.commit("setShowContent",!0)}),(function(t){throw a.$message.error("获取章节内容失败"),a.content="  获取章节内容失败!",t}))},toTop:function(){g(this.$refs.top)},toBottom:function(){g(this.$refs.bottom)},toNextChapter:function(){this.$store.commit("setContentLoading",!0);var t=this.$store.state.readingBook.index;t++,"undefined"!==typeof this.$store.state.readingBook.catalog[t]?(this.$message.info("下一章"),this.getContent(t)):this.$message.error("本章是最后一章")},toLastChapter:function(){this.$store.commit("setContentLoading",!0);var t=this.$store.state.readingBook.index;t--,"undefined"!==typeof this.$store.state.readingBook.catalog[t]?(this.$message.info("上一章"),this.getContent(t)):this.$message.error("本章是第一章")},toShelf:function(){this.$router.push("/")}}},pt=ht,mt=(n("ace6"),Object(tt["a"])(pt,o,i,!1,null,"0405dcaf",null));e["default"]=mt.exports},5629:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXN383Q4tDP4c/R5NEInCCXAAACVElEQVQozw3Hv2sTYRwH4M/79pJ7bZL2bXqtERJ97zjUpbZDhg6pfC8qibi8hLR0EaJ0EFxaCSWDxjfpj1zrYBcRBKE6SAfBJWsx9i8IQfdQxDlKtA6t2OnhQfN3lbG7ytYRywF8rVoPCNO0X2sQOKDpAnSDK2VwkHgmh5yLGT8qASt+2KofnNt2Xg1gf1UF8AoM6052cRMNaloLZb7RKQGrKKji2OefsZF+VqIvos5ZLVIZCX61JcwUdk56wASVkgQvzPfvmT2twTSwyYaC/Pl/UhAHorFhBgZtL6XdAZRp1tkPwC1NLa9CWs5prLhI85NBQsLdXvjDymG3/EbYfQhVNYqc3TtktQhWLY3ko0QsdMbSEp+64v0NfxyqLbIGdh6M2xHHlLBGqKTyQo4E/nebBgBfe1GpdeywYXc8CT7D3cKXuMXkBy4xN6o5OuKamYp3DVI6uccO9lxgd2CAlJgI2BGgaAgIJV/TYwKqu3WFccjbMuA+bVkWgS2bfnlRbD1Eb1sDyWMmjKYIBgGAWbqKRicfvzBkBIz3V5AKnguWdglQEysQsSuVzOg6ALy1pitA5ykGCsc857BRYcgCSZyFOdvoOigSGoPc5Ta73mgxshIcQE5sHMHd9D7yqITw7JO+GHVMxjhzYLcKPSEgmz3fU+BRy3iYNtiXLaBssCW8KguReqkQOTb3MStV0Ugt4U1eIs1RZWRII6Ww8xeNNItyGGQI4ZMlpg/3lQtkl2JFnBp1imRyFe0kK2Id3PCslMgiQNMS77gvFeDhG3cSkYvheeg/e7ClIh5oh+IAAAAASUVORK5CYII="},"58c8":function(t,e,n){},"5ca7":function(t,e,n){"use strict";n("a7bd")},"63f0":function(t,e,n){},"65f0":function(t,e,n){var o=n("861d"),i=n("e8b5"),s=n("b622"),a=s("species");t.exports=function(t,e){var n;return i(t)&&(n=t.constructor,"function"!=typeof n||n!==Array&&!i(n.prototype)?o(n)&&(n=n[a],null===n&&(n=void 0)):n=void 0),new(void 0===n?Array:n)(0===e?0:e)}},7286:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXr5djn4dTp49bt59rT6LKxAAACnElEQVQozw3NUUwScRzA8d8R6MF8YMIx8uk47hDSJbj14IPzOGc7jPLvwTGg5uAYDbe2tt56cLtznvEnS6yDqCcEaWi91DvrbLJZz7b1aFtz1aO+2OZWvn+/+4CHeB6BMYaqBLfjPNRY6RFT2JJYby+uAk4WUTrtlmJ4hgPYb2q1XGDQjaK8pgJHvqNaAX+KyuIkDXpgQinb46nOulnn4b5laUHTxLfseeArAoNOeJlOIjdoal0n1FA7tKFv5roK+YaHOqP3P0XyKHPHY+MhTRe5uCZnKhtJKw2eSrSoBDPLtpZuNcFNJcFyiCMxOaaHIfXz1e8HQbWLySrBQ4x0x1qlhnHlnz2HQEC6TNb0gTHXa7IKhcaHqkE015hk9whA0YeWiLIXf7Fa2CZo3DjqjB4tTuF8jIcbfcEx5z/w4sXpQhXW+ju0cqh7icTFmRMaG+v6CIvTjcSpHcH8JEsF3EPh3fRthYdVLLgI2fWXm85/pGFE4l046s70L+yKCcirGFR+jbpy3kMmiCGHrSezVONsn1RBixncyk2PcVWk7DlgxHo8iZwDyq5uAUD854dZhdIFYzKoQig2haUKi1lVufz2RZUZPZ41n/hrOQB6h0Hhg8I367FNoEHgeM/KY7szSeQwD8q2WE3HM35ZLl0K1MJiOtHIkBclRQUwZnyOWcNsRQQgVLj1PSqkjF9DsoOSaSg3iinKzvfmgsNFFfpP/2T3GLGvL4fHEfwIX1sVvXcPqLztehWGcfn9nI2U9nTfCgJPe/jFPLZwgVEzimBgAm0VIyK2tt1cE/AzQdLK+SxLSQ4aDCZnnId94OG2S1XwvnTbNk/ZnhyRCQT+sZM6z9g6LXL1BOBe+zJySiFkHAINCtnQokbCJ/apCv0foqPiZVfhpywAAAAASUVORK5CYII="},"746f":function(t,e,n){var o=n("428f"),i=n("5135"),s=n("e538"),a=n("9bf2").f;t.exports=function(t){var e=o.Symbol||(o.Symbol={});i(e,t)||a(e,t,{value:s.f(t)})}},"75ab":function(t,e,n){"use strict";n("63f0")},"7abd":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXm9PXq+Pno9vfs+vttWKBGAAACPElEQVQozw3RQWrbQACF4TfCMjPqZgIj4RRaxsZKE0PuMBZ2cLKaCI9RDAXFmJJknUWWI1O1UlamOMHJSjGkuFn3AD2Cr9CepDrAg+/xIxK4QwIqHHQkUhQ/WuphInVIFBojl8QXc012Tgq4RTtVHWVLZVFh1tEoI91uiN4joCqde8Ukn/zGM1B2W4ari2PtTwyw55Ld+Wways54qhGPyS6FzbIT3lIY8WwWdCq56Yolx6KmSKzoqrsCB5heAp4TGNQWJ1Pc6XlE5jQD5OlIX9I47A9uiUQcPQxcury/ToyxWJG/za6ki88crxKPocKS59Sl3EtBG7C89fCGflpfqoSzCeC4crioJA7F0V5+8MaSIk4qSCdwzpogmbqzEirVpGiS2dOVJvUuuqFEmhHao06KEpq+8lvHI14NJk3Qrmi9vBuRLwAz0qZB4hsDXQFXgtnlpDX3C6ug9BquSw/CYtwAzuTz5vuQNdr/YibhR68378ehZH30FSpjh71LpQkrsj+Q062h5WwZ5wlRoD6uQJy1DqvSYuCUapMBqT5YA4ZFw4KlWapxoUGlKWrx0eDQvmigu4WMYt97ruru98fYL8/0lG6CTOFcFWBhFK5gKw19h2JN808nh7xhkU6sWKLXdtkqBL6h+lULK5k19wFB/FldnGYf3LDeuf6IC2/MzJOSOP0qPxLqzaGIqtBcFIItrstkazONOkrc1D1czjuwEGESB4JJnjgSMN7PXAu7fZQpl1C236C+9mM4Af8P98Ch4R2TRl8AAAAASUVORK5CYII="},"802e":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg=="},"82da":function(t,e,n){},9078:function(t,e,n){"use strict";n("c0bc")},a4d3:function(t,e,n){"use strict";var o=n("23e7"),i=n("da84"),s=n("d066"),a=n("c430"),r=n("83ab"),c=n("4930"),A=n("fdbf"),l=n("d039"),u=n("5135"),f=n("e8b5"),g=n("861d"),d=n("825a"),h=n("7b0b"),p=n("fc6a"),m=n("c04e"),b=n("5c6c"),v=n("7c73"),C=n("df75"),y=n("241c"),B=n("057f"),S=n("7418"),I=n("06cf"),k=n("9bf2"),w=n("d1e7"),x=n("9112"),E=n("6eeb"),Q=n("5692"),U=n("f772"),D=n("d012"),V=n("90e3"),O=n("b622"),F=n("e538"),P=n("746f"),R=n("d44e"),M=n("69f3"),N=n("b727").forEach,K=U("hidden"),H="Symbol",J="prototype",z=O("toPrimitive"),L=M.set,W=M.getterFor(H),T=Object[J],q=i.Symbol,G=s("JSON","stringify"),Z=I.f,Y=k.f,j=B.f,X=w.f,$=Q("symbols"),_=Q("op-symbols"),tt=Q("string-to-symbol-registry"),et=Q("symbol-to-string-registry"),nt=Q("wks"),ot=i.QObject,it=!ot||!ot[J]||!ot[J].findChild,st=r&&l((function(){return 7!=v(Y({},"a",{get:function(){return Y(this,"a",{value:7}).a}})).a}))?function(t,e,n){var o=Z(T,e);o&&delete T[e],Y(t,e,n),o&&t!==T&&Y(T,e,o)}:Y,at=function(t,e){var n=$[t]=v(q[J]);return L(n,{type:H,tag:t,description:e}),r||(n.description=e),n},rt=A?function(t){return"symbol"==typeof t}:function(t){return Object(t)instanceof q},ct=function(t,e,n){t===T&&ct(_,e,n),d(t);var o=m(e,!0);return d(n),u($,o)?(n.enumerable?(u(t,K)&&t[K][o]&&(t[K][o]=!1),n=v(n,{enumerable:b(0,!1)})):(u(t,K)||Y(t,K,b(1,{})),t[K][o]=!0),st(t,o,n)):Y(t,o,n)},At=function(t,e){d(t);var n=p(e),o=C(n).concat(dt(n));return N(o,(function(e){r&&!ut.call(n,e)||ct(t,e,n[e])})),t},lt=function(t,e){return void 0===e?v(t):At(v(t),e)},ut=function(t){var e=m(t,!0),n=X.call(this,e);return!(this===T&&u($,e)&&!u(_,e))&&(!(n||!u(this,e)||!u($,e)||u(this,K)&&this[K][e])||n)},ft=function(t,e){var n=p(t),o=m(e,!0);if(n!==T||!u($,o)||u(_,o)){var i=Z(n,o);return!i||!u($,o)||u(n,K)&&n[K][o]||(i.enumerable=!0),i}},gt=function(t){var e=j(p(t)),n=[];return N(e,(function(t){u($,t)||u(D,t)||n.push(t)})),n},dt=function(t){var e=t===T,n=j(e?_:p(t)),o=[];return N(n,(function(t){!u($,t)||e&&!u(T,t)||o.push($[t])})),o};if(c||(q=function(){if(this instanceof q)throw TypeError("Symbol is not a constructor");var t=arguments.length&&void 0!==arguments[0]?String(arguments[0]):void 0,e=V(t),n=function(t){this===T&&n.call(_,t),u(this,K)&&u(this[K],e)&&(this[K][e]=!1),st(this,e,b(1,t))};return r&&it&&st(T,e,{configurable:!0,set:n}),at(e,t)},E(q[J],"toString",(function(){return W(this).tag})),E(q,"withoutSetter",(function(t){return at(V(t),t)})),w.f=ut,k.f=ct,I.f=ft,y.f=B.f=gt,S.f=dt,F.f=function(t){return at(O(t),t)},r&&(Y(q[J],"description",{configurable:!0,get:function(){return W(this).description}}),a||E(T,"propertyIsEnumerable",ut,{unsafe:!0}))),o({global:!0,wrap:!0,forced:!c,sham:!c},{Symbol:q}),N(C(nt),(function(t){P(t)})),o({target:H,stat:!0,forced:!c},{for:function(t){var e=String(t);if(u(tt,e))return tt[e];var n=q(e);return tt[e]=n,et[n]=e,n},keyFor:function(t){if(!rt(t))throw TypeError(t+" is not a symbol");if(u(et,t))return et[t]},useSetter:function(){it=!0},useSimple:function(){it=!1}}),o({target:"Object",stat:!0,forced:!c,sham:!r},{create:lt,defineProperty:ct,defineProperties:At,getOwnPropertyDescriptor:ft}),o({target:"Object",stat:!0,forced:!c},{getOwnPropertyNames:gt,getOwnPropertySymbols:dt}),o({target:"Object",stat:!0,forced:l((function(){S.f(1)}))},{getOwnPropertySymbols:function(t){return S.f(h(t))}}),G){var ht=!c||l((function(){var t=q();return"[null]"!=G([t])||"{}"!=G({a:t})||"{}"!=G(Object(t))}));o({target:"JSON",stat:!0,forced:ht},{stringify:function(t,e,n){var o,i=[t],s=1;while(arguments.length>s)i.push(arguments[s++]);if(o=e,(g(e)||void 0!==t)&&!rt(t))return f(e)||(e=function(t,e){if("function"==typeof o&&(e=o.call(this,t,e)),!rt(e))return e}),i[1]=e,G.apply(null,i)}})}q[J][z]||x(q[J],z,q[J].valueOf),R(q,H),D[K]=!0},a7bd:function(t,e,n){},ace6:function(t,e,n){"use strict";n("fc0c")},b165:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXe3t7a2trc3Nzg4OCXP9lCAAACoklEQVQozwXBzU/TYBwA4N+QEr4CNbSFwcFuowSqMRvEAwShHWAYNsu7dS0dLnGUSWT4kZB4lGzE4VtcwgIDJqcOWLJxcv4BOoQZuCPxSNSD4WSWLJGL8XmAIiyo2RgJ4A1pxQQlOxRAszLTdnPu2oQGb05RC5slJld7ZAIfo4O44Bn1ud59F0BcjnYOa17Jhwc6EdiKettncsXjT1f8KUBZUW41pK0Jc1Az4dEV3rkkPBtDSZ83Blyt0kSf2PRjzIykoBwINisPbPPtljdVE9iAXRfUPkXLVIgYrCccp5g687NdZbcJ+xa5VE/HhTtT23IKsN5jj/pcUd0dTZNAqCVw72n4gOwnTOC0vvHfaauT8d9zAoRRfPpISZRVyUiw8ELzOG1b2DZpFzkSrHLhq52twDEdyZHwvp2j4uv/bjvOf23/AcEtTuJbY5Cp4YcAer1IGkUzOo2rn8LQOKjFJw3NTw24nprQXY5aF4wxcqcSdbFQ00H4xFl8Drx4X4CikvAM1tuR8bKIBCBoLnKN10KJG4zKAsc7c9WEB9gnCi6BhVjqoco6t20ILAJuVctvaEZK732cRHDRmGfuihOam0o2CHByUZ/epCcVlRs2wmCnMqsd6aSim3ibBJtm1LGyXW3Bb7tJCPlFtUG+SvPdeEUAB60lNdo+VQbLcwRNVtT68FsLcr1+NotgNihlpExS1V2SFgNbeC8bEhgm8sM17wSi6Us2gxVWJU/5GKBpandvfyYbU1yHCLpCgWGbbPXn40rehEsUXKIJr9DMKgICfjc4bl1YfvUhE/YIECGRqjCxSM9hrybAIkND5OeWfFZsXkxB+qDzb7pUQ3EfQ3Ml6EChEt3D+iS01VqC7EQ/Z/DuPQcz4yChoFQJce2Qr+NNAv0HxofmpXGqgHkAAAAASUVORK5CYII="},b671:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX48dr48Nf58tv379X17NJtIBxUAAACFUlEQVQ4y1XRUZakMAgF0Af2AiDWApDZgHZqAV1nZv9rGh7Rj7Y8McUFEg1wvcMESMNVD/neU8Xcaz7nYYkYlYO6Ti82PBI4BvIEg1aj3wKwRvIMgZsUy5LdhCawPFh1sZs4SrlyN9fQKpv8s5dgZ2eLyqqJiu+WkCmUEybXkm3INS01WAiv0PapJ0CZc0SJQUzcWnZYbOOY20iFD8Bk+/j2A3wNxH7GdShFYS5ff237kXh9I9zSkQmIAhOsOSVfJ6DIXTMDaPnzkRJ92S1BQQmXl5LdirgRLLDdcYqcGPwe3QN4xCBiGNbrqq9wpW1XCecChwaQdVOsRDpPCpeoolPdxeXp3WNB9PHVzWBHlygy4NJCCrFHREv6bDt0VGwJZASkpONmm1UseGeFKAQexgaAkrfYWl3AGxWOLL2AIMBNbCXpktmS3k3vHeYjGCPBa43wJTurO3ZFVpQSJdAZGLoHTyk1upkjxMEaIxum3iIARcCa5kSkFAW5fi1mUlL9eyOsaanFmOMruwvEdE3ZYzsRSzo5ewRLXyVPPEvknt8ij4DvCg2O7xOgBCUprEzV4z1WekSpUgI8DT2mrnSOXKRfQavwuKA1F+tFnMKdJSUpMA7wQAifWRkMgjUKKZE4lBl6MCM4B1pq1P4uIjDE6Pq6rL0FnW1nIFmta5vrSvq/Ch4tpqG/ZNyyWa5jZPktq81eYv8Bt5s4iFITOp4AAAAASUVORK5CYII="},b727:function(t,e,n){var o=n("0366"),i=n("44ad"),s=n("7b0b"),a=n("50c4"),r=n("65f0"),c=[].push,A=function(t){var e=1==t,n=2==t,A=3==t,l=4==t,u=6==t,f=7==t,g=5==t||u;return function(d,h,p,m){for(var b,v,C=s(d),y=i(C),B=o(h,p,3),S=a(y.length),I=0,k=m||r,w=e?k(d,S):n||f?k(d,0):void 0;S>I;I++)if((g||I in y)&&(b=y[I],v=B(b,I,C),t))if(e)w[I]=v;else if(v)switch(t){case 3:return!0;case 5:return b;case 6:return I;case 2:c.call(w,b)}else switch(t){case 4:return!1;case 7:c.call(w,b)}return u?-1:A||l?l:w}};t.exports={forEach:A(0),map:A(1),filter:A(2),some:A(3),every:A(4),find:A(5),findIndex:A(6),filterOut:A(7)}},c0bc:function(t,e,n){},c84b:function(t,e,n){"use strict";n.r(e);var o=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"detail-wrapper"},[n("div",{staticClass:"detail"},[n("div",{staticClass:"bar"},[n("el-breadcrumb",{attrs:{separator:"/"}},[n("el-breadcrumb-item",{staticClass:"index",attrs:{to:{path:"/"}}},[t._v("书架")]),n("el-breadcrumb-item",{staticClass:"sub"},[t._v("目录")])],1)],1),n("el-divider"),n("div",{staticClass:"catalog"},t._l(this.$store.state.catalog,(function(e){return n("div",{key:e.index,staticClass:"note",on:{click:function(n){return t.toChapter(e.url,e.title,e.index)}}},[t._v(" "+t._s(e.title)+" ")])})),0)],1)])},i=[],s=n("bc3a"),a=n.n(s),r={data:function(){return{key:"value"}},mounted:function(){var t=this;a.a.get("/getChapterList?url="+encodeURIComponent(sessionStorage.getItem("bookUrl"))).then((function(e){t.$store.commit("setCatalog",e.data.data),sessionStorage.setItem("catalog",JSON.stringify(e.data.data))}),(function(t){throw t}))},methods:{toChapter:function(t,e,n){sessionStorage.setItem("chapterID",n),this.$router.push({path:"/chapter"})}}},c=r,A=(n("5ca7"),n("2877")),l=Object(A["a"])(c,o,i,!1,null,"57115a66",null);e["default"]=l.exports},ca36:function(t,e,n){"use strict";n("58c8")},cf68:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEXm5ubo6Ojp6enr6+vt7e1FnZagAAACrklEQVQ4yx1SixUbMQgT3AKAFwDcAfzpBN1/qMrJS5w7bCQhC6IGSUGYQJd6Ox9ZPXi1AGJBavhUTT0JjYPGAab9WcDYIxsmlnxkayX8mhxCmKHA75az5cfRbWybEExiu08xDSgGym0mwuf3j4SvHeQxDJJzh2zp4iOlrD8iOb4SXyC1wiOLRTcnrje+nGamFeXVKWkmzbFIPChkmJ6Fg7mBpV8n+JGOVCd4jv1thThkjeQGNeafpeV3rsEWLfyWc8tC9jOv6FQ8rRzHOOVB+jCYEUAJpDvh8xHNFm/Tm5p5lw94Pp3NhtKEfQsGvnXhowdZE73hPwxKvjDd4i4PCdd0fe3W5fO8ktAsUAacLgstpUw60JCiPLg2XpkgiqPIYYXJd9ksGIT3q+LlevypzItvO+s0F1dBzVr2QDMUkYmuyGcrIS44mVJ7JVKwQXjYuBYp0Uetecbswzsikzu3gUR8bJC/C8Gd/NAzI/xdUGOYQQHDZ8X2d5XuzGRUiXAi9si5CRgoiToRZPtzLJkd0FUHRHZwJf0BHT1sE7gcnh0jmKKlSSF4/GBirGk5+K9NKlGDCfc9JtPhg78JdabH0YQRKNZnJ8tFnPfXHJb4xum1TTCeEmyEdbyEJLjznMLHuFD2Y9NEkSleIBs7SiCbblhgctVi9ch++kDYnn1C9DA5TvdPsToXM55wI6k+8eKT1blwPTqWb5CFJ+7dTBmab+KHy+xwNtItXhZNSpHD2fxnynrxG3ZBKRe8KBpXk11AnadlccEhr9w1nBBvBylNkv7A8eqpGBCDqhitmWQXBjjdS6idr/QjXWLDeMzMbVDoJuM8zN7WenMZWXgZ2vX3F01J3jHZbwk1LRP+DWEvDJtOUoh/AIaBUz5VpWyhuyx4QtgL/NmgC6kM/JvNe+R/C/5aL7BKIbYAAAAASUVORK5CYII="},d0e3:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXh7eHl8eXj7+Pn8+eTbH1KAAACPElEQVQozxWPQWrbQABF/0xn3JFKQRTZOIuUsbCCbOgdRoYEOauxkYPcTRyTlPQWIxEltrsRwQ6hK9nEQek6F+gNTE/Q3qLLusv34cN7SH3mFicdYW4gNIhJWXPBRVXzjcFD0IqeU4o4PRbAIVjyico0vJpIifqPfL80QN9DAQY5ucRHE/hpHxBldXe9GilaHKcKMlj6pho2zXgkNdBl0oJ8kiF1DSiJF1ZHBJkQr0Dbux/5I42Zp4cFahJDFGeW6/QjBwmFY/Q7vZ2SnoOdW2parv/Cnm81+m0xrEfiVXQ3W4nOXIqVYi3l6AAQBwMFkViVBANMto4enXHPNTkHBB0oVj4r5vHzCWayrgBvxtygDlDB2CNDjd80ZInY69aKVYZcfJ8DW+fWuc+syEODALx+ojqoafHsthTI+ZW27PGpIeo/cR6YKcbqIuIFhHmBrzAovzIOOJk1ucvcDzrMRYGVBH2yvcAOf0KiKwfRovBI3tm/kW1eemtfNWwIIXE2mJNhvoszfmMBfRCv0OPwd2321uDW3nx2q/BDxFVeoN1g7a6Im8yRnoawa8kbdXnU0cHeTMxKfZGlJgvLb3sKsxgglQnDdAfvj9LUnqWRDo0GiUmPwyU7TAsD7wHeIW3Nfy1qVGKoE9NgJCdYCAexNRob9yCn4DAQmXtQuUtera6bEmTTXhZy6h856xi4mnEl6BI9mfISkLbtJyZIMJIAUd5ZOBEu88KRAk71yxfItj/hpIB0Errv4gO1os4/UICf+o3kkqwAAAAASUVORK5CYII="},d28b:function(t,e,n){var o=n("746f");o("iterator")},d81d:function(t,e,n){"use strict";var o=n("23e7"),i=n("b727").map,s=n("1dde"),a=s("map");o({target:"Array",proto:!0,forced:!a},{map:function(t){return i(this,t,arguments.length>1?arguments[1]:void 0)}})},df5e:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAATlBMVEXdzaHh0KPgz6LdzKDezqLczJ7ezZ/fz6Dcy5zi0aXdzZ3fz6Tfz57h0KDg0aLcyZrg0KXi0qPfzZ3j06bh0qbdyJbfzJrhz5/cxpLZwo0vDconAAAFn0lEQVRIxxyPW5LjMAwDAT5FybLl2JnM3P+i6+wXWVC1GoQGaD0h4XM3Q5o4T0HgABHBi6pZ4CDXXcUOFd6VhqC3Kch4EI8w9oMXwvU6m5LOOvcxKMOhuu8i5+5cMjcgb0t4F2uvOoeI3/MlT4IqsbtM9UG2AGSXUOsxzPevnXzK1CSHytZLvx7VdQmUcJsJCxJh2nmHW12Qod1qPjt8pih47uQ9aGpoNWF+yElCt60oH7vdIU/MnlRPSBLC/VwqxcKR8PFqnADN9ih5ufqnTlG9KwCofvs7kKYqOPHTNMQ93j9qNImFw9vjHPZ0F1m8hUUVB/Q/TrRYDMXr9++APMFARAt6sPh6wVAXzxUGhZsFUwCNfPZ8/72TAHebAhvuOuT3gO1Vn5d9Jd5sBRkg0p2seL9B7ulkjFJFIt9HPpLzdSzzMP3UcodAfMqC6pBuET2heHK1itZf1GZ1bi0BwOSxiCS8f/JBHMPMM4XCu3Mt1uz9lJbDJRqsKDZuikzkvskQEz6hanfDfO494azY5JpqPqOF1RhxD9XYEdaNxiqWqakKgmPfmrsta8KAiwF4HBxGVUJAgeSqQaiRRZJ7D2jedhw5t1CIAKxag0CBA60BpoBE6DcUi8O5AuM4pLfN0kHLmeu2B4e6HofqbgxsTWUw3PAODqa1oDtyzgXBlusi1KFdclMPE8O3jvLJ8RNi5/RxDQVzVmXA233XQ4KummunfxvLOZo+iH37964YjP06995CTdu9hsvErqJNzmf4wTrZ5DL7+qW9EoLnadrx67b8dUtrJnBXaT1N1uvPaYRKpWkq52xNsMN7vv4Sdryt/f4MhQoMCKnvVxikai1CQ6ZsnwJDc8+3Y/z8HcfvYQNq66pnAu1Hwa+3KNSwbNu8h3nDPqTl9fl7tx8fBhFfdS0o0F3JUKEZtZG9b/LZEM95lzaR30OnWPzroMxyZYdBIMoMnpN0J+m7/40+/P4soFSUjgzE7yY5zrMJuoZv0CmpVguYx1pprfb5HOviRVhHUVi/352shxCYrYBZxGtVaxiAz/MsaGSIsB7R1t4zJXH//n7RTTQQwxqcGEqEvklFHUgiO2GvJV+jAIPR+N29usWDoiSOVrN3XuqT1egQJAAU9EwslVJC8u0rGcy+WPqktJhjfMpatIG6CDAb0v5H34MGKqiVRue7GGLZ9Otxtt4JIrAhxBDwDuqI9JavcO0A7GlqFt219tH/bln9jBXzaKWAEqJV0CBxs5TwM8EvUPHaa8S86vN303MVWOsl3goDBHPWSoQ9c0kQmCKljfsKNH1+ofEOHW8a9a7glZGS8fPieL/SRSs0LAhI4FDTnXs1QYtubv2+IXPZpHB4bhivRexBkYKsSrYXNjvMUbVXpVJ+N6haV72c1k2zrnv5IYBMJBYTSZx0KTkoM3vY93rU/qs7zHplc/3d2ACadhFWByrn9LUk2IWb5JywvawTQc3F0iz+lgsBmInAIemBJtft2plKIlAFOgcroigrG2XlDsAzywQECNyaI8yr2ogoh7D4qJOYmZBzQgoZAM1PAcB8sDrr1uE5CDMR+nWSSVUGUCHAs8Vd21HOE0FzNj37pX0sLp9p3K8k++xxpkmzDxK64rmTSJnDUuIgTeslui6lg92jonZXI4jqNiUuzN4IagcKMjCniMGCODoo8T4tGDprn2hRww+NrnYiCwokd9iiWrkmbRfXYGLAoZrjO1lVQKExjUy5fIkgJURmz2uGFdASwwlWx5gDVTMK7hP6ISRVsFbYNmqtZL9MQtio285PaekyzDhZmtdexCYB0SZcTmBdhvdbmAEonk8hwcHQuZN1kVqrhyKoHHsnQhQAjF7SG533Da2S4LGjx1LoZqp7XeKQLDUBmYmydG0NQHpMeR5lRIRQc1PQ2ASMQflF4YBDMt0/GFlEHeRwCcEAAAAASUVORK5CYII="},e01a:function(t,e,n){"use strict";var o=n("23e7"),i=n("83ab"),s=n("da84"),a=n("5135"),r=n("861d"),c=n("9bf2").f,A=n("e893"),l=s.Symbol;if(i&&"function"==typeof l&&(!("description"in l.prototype)||void 0!==l().description)){var u={},f=function(){var t=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),e=this instanceof f?new l(t):void 0===t?l():l(t);return""===t&&(u[e]=!0),e};A(f,l);var g=f.prototype=l.prototype;g.constructor=f;var d=g.toString,h="Symbol(test)"==String(l("test")),p=/^Symbol\((.*)\)[^)]+$/;c(g,"description",{configurable:!0,get:function(){var t=r(this)?this.valueOf():this,e=d.call(t);if(a(u,t))return"";var n=h?e.slice(7,-1):e.replace(p,"$1");return""===n?void 0:n}}),o({global:!0,forced:!0},{Symbol:f})}},e160:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEX6+fP8+/X+/ff///kbczPAAAACeElEQVQozxXHQUgUUQAG4P8936yzs6VvZNZmN9QxVxiF9OLBoOjtOC6rQq6ygXjI2fCQBdXBg4egtzFGdqkoI+zgBFbqkm3hQSxhFYLotOcubeKhOnVYoqQy+m4f5g5TvpX0xHLbLY9j8SMhJp+Jk4LfAUS2kVRIjILmnwGBTX42PhCVlDJQkIiy2nWAvaJ1h+oFIpJ0hMSYVbyyrgDWshcMpMyL1brPDQKWmduO+KTJ6XeXAMK9Yc3FpD7atyNwg6kt5XgFpLPhjUTFSYVn2abDiugGShwD8JTVRJVo/2ecuKtRb/qc4BK+9TboFfokog4T2Fn6Oqdnsjk90NMS76Rji6E0NmwkPBAZ4Xbkw8KoDAkAbEhkc78e9omxxgxg6qa5HvMv+UZbCV0qmHnSHKl5TxeA2XTCGWekR581mwC5crBH81PznASqB9va3TbkYAjJPLfg5uBfXaJgIgIBv9eessRIhxe7PA7kj6uUMeMaQ/OEQOYRaaHlqH2Gxwsl6E/pwVY5FH7uCypBZPKvDQyVziYBrAkMURe2MOOOxG/eQpp5PF+bFzUV5HtPj9GeiVSNZDELleifYTp9NAjsoiXg4cW+4ZORkdSMB/B74aAdjhsVakhgkugsbmqcDSLEoWp8zRjrux3tli6Q5uM3E+maT99Wy0RiP7tboiuRZle2c6CYeL2kcUc1KvPtQKucogMadKVTQOJYCeyCYlhQQ/Q7Etfd/vBygy9iqy+LyHeF46saCYvW6ingsbA9RBWtdi8GgUXW+oQx9/wP6bAAX1TWeV+CbShZDlQ9xT6SoSxZmKRAkmXb60kzEzkRF+Ccb94BGspGJoN/UzmyR4wjXHAAAAAASUVORK5CYII="},e538:function(t,e,n){var o=n("b622");e.f=o},e8b5:function(t,e,n){var o=n("c6b6");t.exports=Array.isArray||function(t){return"Array"==o(t)}},ec0f:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAALVBMVEXx58b168ny6Mjz6sn06sf27Mvw5sTz6cbw5cLy58T37svv47/168v37s7t4Ltrv0//AAAEjUlEQVQ4yw2Ty2sTURxGf3dmOqmPxb0zmaStCnfmZpL6gpmbxIpUSMZGrSJkxsZiVZimNVaqMklrUnWTRq2KIDFWWx+IFrIRFxXEB4KIgqu6EBdu7M6FIPg32PW3+DhwDmBaYrK56KP4HGIsvg/uvOV0wK+qgBMlO9BujuH4DSJlOseqV5a/BEF97gt0ChyIPqBhXI9BtqtIB8vJB/LdCQ3OVjaLNX0g7+OmoI4e7nkemAqX6o8vg0yyQAyQS7IfgvFbI+6QyI3R4KELxw7kwM2ooQfyQigYnwY5MZbMlHI1DvnQVCoVcrt+R+bO7vPDif3ybNajwqAAe443dpfDsPt379VMWZzGRuqM79mQF+DUz9nt74bQ8J/O80MtVR51U02JKKmTCvTzLVf+vuxP/aHnPo9+2bW+zVsJ0Y630/CrfzX+b+UL+7O68Rczv+7lrMh5etfKXvhc2rk6KforxuoO2xB2tcxKfeXHt18rHOiHI/0RRjW/YGRDkHiwo3nzqL60o58C/bgRuaj7vk+QOwOhpnFNdjuWpKMCGP8Yapu9Ty5FTHKQLGSEFikjd9ADwP9ciaNNjc5qMH6w50AF/LKOsOYqsOG9GjKgc7ZXolqntm6fysJ6Ma6ll2CiqmOgE6O7x1wXExklbeqMYcwsmJmOoigt8SBg2WfilDSsAZJcBxDcrqtBXzFQJqZNHfscyIhoZlygAtyYAceah+elrFbI+46gEHDGiW878Kj7JpWyfhg6iyRMymV1MKBSeVpfgLHIohyTojI6sRyK1VpcqzVZeEBLOnA9unhGKUXPJDYtV9Dxuz4iA5xSkSWhCJdAiJR9PHlvfvbntbrR14FDqUNRAYDJmSnv3oKxuz5+7fiblgVJyYLTbgUM05P7LESkoXvyWNfb0aUU6FZizgQIa25VqKQZqFrk6v6BsqqIHlQmkQ9KrBhkC20/DrFsAFEEYLjM+lj2wYHXCwnNvZQR42XJ2iVK+UBXnI+OBE6oXpUUHiQ1yg0MhA03iwGbnOdQYc1CMiPIPQrCQJFH4L4BMFktAtKd9PN5gnU2Gra4KuK+V+mjtBRpAGIqDVe4wnSnajiFGO5d7smvhVQEMEYwqshrENIEaY7YeblJYtsb3QhAHWZCEKK67swwPMKw0If1Ta+6DgHmlgPzcUTSbi3rrv1Y64/BYEMPQ5SDHUOR022B4QRF6xLUPAaPX/V4IDI5N2BMwx4LqO1uO4j6uW7NvM7lATqGAxY/ZHVgoGZbu7SvkNR75x6qGSB23FdouENVwN7sCbewTdsXGrrnQ5ZZKOCOFtMTIzxlPu6eYmtL+nMFmoK7OeXajn86r9sqWbfmvHC4IagE5qfCPGZvLSq5F55hHIxJFa4/vRxHBlz0og4TojU1l/MOHJX17lybdF0mQhFO44JYUNt3UA473IXw/iPfDWtKG5oFSXIF5iU/VnyDSjxxeDk3jAXRyVyGTNB9FxH9qcFDNJpVbt2y9LytUXkK7Py6+z1RezHQqnoY8XcLimmd8dCnBhQCuaGpJCq3SoIlmYvLz8UkWhJw7T8k+Db/DYEKwgAAAABJRU5ErkJggg=="},fc0c:function(t,e,n){}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/new/manifest.json b/app/src/main/assets/web/bookshelf/manifest.json similarity index 100% rename from app/src/main/assets/web/new/manifest.json rename to app/src/main/assets/web/bookshelf/manifest.json diff --git a/app/src/main/assets/web/bookshelf/precache-manifest.5ae9ceec57e7f0f3cc808807b7fe5f32.js b/app/src/main/assets/web/bookshelf/precache-manifest.5ae9ceec57e7f0f3cc808807b7fe5f32.js new file mode 100644 index 000000000..f122b5e28 --- /dev/null +++ b/app/src/main/assets/web/bookshelf/precache-manifest.5ae9ceec57e7f0f3cc808807b7fe5f32.js @@ -0,0 +1,70 @@ +self.__precacheManifest = (self.__precacheManifest || []).concat([ + { + "revision": "c6913d775f2f965ac5f3", + "url": "css/about.b9bb4fe0.css" + }, + { + "revision": "c7b3e35a4e0391b1ed37", + "url": "css/app.e4c919b7.css" + }, + { + "revision": "9a65f05f9810a3ea7f46", + "url": "css/chunk-vendors.8a465a1d.css" + }, + { + "revision": "5ab5c6be15b21e2d609b", + "url": "css/detail.e03dc50b.css" + }, + { + "revision": "535877f50039c0cb49a6196a5b7517cd", + "url": "fonts/element-icons.535877f5.woff" + }, + { + "revision": "732389ded34cb9c52dd88271f1345af9", + "url": "fonts/element-icons.732389de.ttf" + }, + { + "revision": "f9a3fb0e145017e166dd4d91d9280cc4", + "url": "fonts/iconfont.f9a3fb0e.woff" + }, + { + "revision": "f39ecc1a1d2a1eff3aca8aadd818bb61", + "url": "fonts/popfont.f39ecc1a.ttf" + }, + { + "revision": "6c094b6d4ae9404dbed273c41b06fae8", + "url": "fonts/shelffont.6c094b6d.ttf" + }, + { + "revision": "8a8424347500238b7b6c08a98d0f89af", + "url": "index.html" + }, + { + "revision": "c6913d775f2f965ac5f3", + "url": "js/about.9f8f9ac0.js" + }, + { + "revision": "5998ccb313ed338c15e1", + "url": "js/about~detail.8270a871.js" + }, + { + "revision": "c7b3e35a4e0391b1ed37", + "url": "js/app.e84ee963.js" + }, + { + "revision": "9a65f05f9810a3ea7f46", + "url": "js/chunk-vendors.3ef7796f.js" + }, + { + "revision": "5ab5c6be15b21e2d609b", + "url": "js/detail.4e6a53a9.js" + }, + { + "revision": "b46d04eb43bc31ca0f9f95121646440d", + "url": "manifest.json" + }, + { + "revision": "b6216d61c03e6ce0c9aea6ca7808f7ca", + "url": "robots.txt" + } +]); \ No newline at end of file diff --git a/app/src/main/assets/web/bookshelf/robots.txt b/app/src/main/assets/web/bookshelf/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/app/src/main/assets/web/bookshelf/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/app/src/main/assets/web/new/service-worker.js b/app/src/main/assets/web/bookshelf/service-worker.js similarity index 94% rename from app/src/main/assets/web/new/service-worker.js rename to app/src/main/assets/web/bookshelf/service-worker.js index 9f58a3efa..e2e43f1cc 100644 --- a/app/src/main/assets/web/new/service-worker.js +++ b/app/src/main/assets/web/bookshelf/service-worker.js @@ -14,7 +14,7 @@ importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); importScripts( - "precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js" + "precache-manifest.5ae9ceec57e7f0f3cc808807b7fe5f32.js" ); workbox.core.setCacheNameDetails({prefix: "yd-web-tool"}); diff --git a/app/src/main/assets/web/images/bg.jpg b/app/src/main/assets/web/images/bg.jpg new file mode 100644 index 000000000..91194db75 Binary files /dev/null and b/app/src/main/assets/web/images/bg.jpg differ diff --git a/app/src/main/assets/web/index.css b/app/src/main/assets/web/index.css deleted file mode 100644 index a3e76ba06..000000000 --- a/app/src/main/assets/web/index.css +++ /dev/null @@ -1,148 +0,0 @@ -body { - margin: 0; -} -.editor { - display: flex; - align-items: stretch; -} -.setbox, -.menu, -.outbox { - flex: 1; - display: flex; - flex-flow: column; - max-height: 100vh; - overflow-y: auto; -} -.menu { - justify-content: center; - max-width: 90px; - margin: 0 5px; -} -.menu .button { - width: 90px; - height: 30px; - min-height: 30px; - margin: 5px 0px; - cursor: pointer; -} -@keyframes stroker { - 0% { - stroke-dashoffset: 0 - } - 100% { - stroke-dashoffset: -240 - } -} -.button rect { - width: 100%; - height: 100%; - fill: transparent; - stroke: #666; - stroke-width: 2px; -} -.button rect.busy { - stroke: #fD1850; - stroke-dasharray: 30 90; - animation: stroker 1s linear infinite; -} -.button text { - text-anchor: middle; - dominant-baseline: middle; -} -.setbox { - min-width: 40em; -} -.rules, -.tabbox { - flex: 1; - display: flex; - flex-flow: column; -} -.rules>* { - display: flex; - margin: 2px 0; -} -.rules textarea { - flex: 1; - margin-left: 5px; -} -.rules>*, -.rules>*>div, -.rules textarea { - min-height: 1em; -} -textarea { - word-break: break-all; -} -.tabtitle { - display: flex; - z-index: 1; - justify-content: flex-end; -} -.tabtitle>div { - cursor: pointer; - padding: 1px 10px 0 10px; - border-bottom: 3px solid transparent; - font-weight: bold; -} -.tabtitle>.this { - color: #4f9da6; - border-bottom-color: #4EBBE4; -} -.tabbody { - flex: 1; - display: flex; - margin-top: -1px; - border: 1px solid #A9A9A9; - height: 0; -} -.tabbody>* { - flex: 1; - flex-flow: column; - display: none; -} -.tabbody>.this { - display: flex; -} -.tabbody>*>.titlebar{ - display: flex; -} -.tabbody>*>.titlebar>*{ - flex: 1; - margin: 1px 1px 1px 1px; -} -.tabbody>*>.context { - flex: 1; - flex-flow: column; - border: 0; - padding: 5px; - overflow-y: auto; -} -.tabbody>*>.inputbox{ - border: 0; - border-bottom: #A9A9A9 solid 1px; - height: 15px; - text-align:center; -} -.link>* { - display: flex; - margin: 5px; - border-bottom: 1px solid; - text-decoration: none; -} -#RuleList>label>* { - background: #eee; - padding-left: 3px; - margin: 2px 0; - cursor: pointer; -} -#RuleList input[type=radio] { - display: none; -} -#RuleList input[type="radio"]:checked+* { - background: #15cda8; -} -.isError { - color: #FF0000; -} \ No newline at end of file diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html index bd4945790..55bb88a18 100644 --- a/app/src/main/assets/web/index.html +++ b/app/src/main/assets/web/index.html @@ -1,378 +1,65 @@ - + + - - 阅读3.0书源编辑器_V4.0 - - + Legado web 导航 + + + - -
-
-
-
基本
-
-
源URL :
- -
-
-
源类型 :
- -
-
-
源名称 :
- -
-
-
源分组 :
- -
-
-
登录地址:
- -
-
-
注释:
- -
-
-
链接验证:
- -
-
-
请求头 :
- -
-

-
搜索
-
-
搜索地址:
- -
-
-
列表规则:
- -
-
-
书名规则:
- -
-
-
作者规则:
- -
-
-
分类规则:
- -
-
-
字数规则:
- -
-
-
最新章节:
- -
-
-
简介规则:
- -
-
-
封面规则:
- -
-
-
详情地址:
- -
-

-
发现
-
-
发现地址:
- -
-
-
列表规则:
- -
-
-
书名规则:
- -
-
-
作者规则:
- -
-
-
分类规则:
- -
-
-
字数规则:
- -
-
-
最新章节:
- -
-
-
简介规则:
- -
-
-
封面规则:
- -
-
-
详情地址:
- -
-

-
详情
-
-
预处理 :
- -
-
-
书名规则:
- -
-
-
作者规则:
- -
-
-
分类规则:
- -
-
-
字数规则:
- -
-
-
最新章节:
- -
-
-
简介规则:
- -
-
-
封面规则:
- -
-
-
目录地址:
- -
-

-
目录
-
-
列表规则:
- -
-
-
章节名称:
- -
-
-
章节地址:
- -
-
-
收费标识:
- -
-
-
章节信息:
- -
-
-
翻页规则:
- -
-

-
正文
-
-
正文规则:
- -
-
-
翻页规则:
- -
-
-
脚本注入:
- -
-
-
资源正则:
- -
-

-
其它规则
-
-
启用搜索:
- -
-
-
启用发现:
- -
-
-
搜索权重:
- -
-
-
排序编号:
- -
-
-
更新时间:
- -
-
-
- -
-
-
-
编辑书源
-
调试书源
-
书源列表
-
帮助信息
-
-
-
- -
-
- - -
-
- -
- - - - -
-
-
-
- -
-
+ + + +
+ + + + + + + + +
- + +
+ + + + \ No newline at end of file diff --git a/app/src/main/assets/web/new/bookshelf.html b/app/src/main/assets/web/new/bookshelf.html deleted file mode 100644 index 8b8bb1852..000000000 --- a/app/src/main/assets/web/new/bookshelf.html +++ /dev/null @@ -1,3 +0,0 @@ -yd-web-tool
\ No newline at end of file diff --git a/app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css b/app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css deleted file mode 100644 index 997434314..000000000 --- a/app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css +++ /dev/null @@ -1 +0,0 @@ -.el-message__closeBtn:focus,.el-message__content:focus{outline-width:0}.el-message{min-width:380px;-webkit-box-sizing:border-box;box-sizing:border-box;border-radius:4px;border-width:1px;border-style:solid;border-color:#ebeef5;position:fixed;left:50%;top:20px;-webkit-transform:translateX(-50%);transform:translateX(-50%);background-color:#edf2fc;-webkit-transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,top .4s,-webkit-transform .4s;transition:opacity .3s,transform .4s,top .4s;transition:opacity .3s,transform .4s,top .4s,-webkit-transform .4s;overflow:hidden;padding:15px 15px 15px 20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-message.is-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message.is-closable .el-message__content{padding-right:16px}.el-message p{margin:0}.el-message--info .el-message__content{color:#909399}.el-message--success{background-color:#f0f9eb;border-color:#e1f3d8}.el-message--success .el-message__content{color:#67c23a}.el-message--warning{background-color:#fdf6ec;border-color:#faecd8}.el-message--warning .el-message__content{color:#e6a23c}.el-message--error{background-color:#fef0f0;border-color:#fde2e2}.el-message--error .el-message__content{color:#f56c6c}.el-message__icon{margin-right:10px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__closeBtn{position:absolute;top:50%;right:15px;-webkit-transform:translateY(-50%);transform:translateY(-50%);cursor:pointer;color:#c0c4cc;font-size:16px}.el-message__closeBtn:hover{color:#909399}.el-message .el-icon-success{color:#67c23a}.el-message .el-icon-error{color:#f56c6c}.el-message .el-icon-info{color:#909399}.el-message .el-icon-warning{color:#e6a23c}.el-message-fade-enter,.el-message-fade-leave-active{opacity:0;-webkit-transform:translate(-50%,-100%);transform:translate(-50%,-100%)}.el-fade-in-enter,.el-fade-in-leave-active,.el-fade-in-linear-enter,.el-fade-in-linear-leave,.el-fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active,.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.el-fade-in-enter-active,.el-fade-in-leave-active,.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter,.el-zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.el-zoom-in-top-enter,.el-zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.el-zoom-in-bottom-enter,.el-zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1);transform:scale(1);-webkit-transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.el-zoom-in-left-enter,.el-zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45);transform:scale(.45)}.collapse-transition{-webkit-transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out;transition:height .3s ease-in-out,padding-top .3s ease-in-out,padding-bottom .3s ease-in-out}.horizontal-collapse-transition{-webkit-transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out;transition:width .3s ease-in-out,padding-left .3s ease-in-out,padding-right .3s ease-in-out}.el-list-enter-active,.el-list-leave-active{-webkit-transition:all 1s;transition:all 1s}.el-list-enter,.el-list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.el-opacity-transition{-webkit-transition:opacity .3s cubic-bezier(.55,0,.1,1);transition:opacity .3s cubic-bezier(.55,0,.1,1)}@font-face{font-family:element-icons;src:url(../fonts/element-icons.535877f5.woff) format("woff"),url(../fonts/element-icons.732389de.ttf) format("truetype");font-weight:400;font-display:"auto";font-style:normal}[class*=" el-icon-"],[class^=el-icon-]{font-family:element-icons!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;vertical-align:baseline;display:inline-block;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.el-icon-ice-cream-round:before{content:"\e6a0"}.el-icon-ice-cream-square:before{content:"\e6a3"}.el-icon-lollipop:before{content:"\e6a4"}.el-icon-potato-strips:before{content:"\e6a5"}.el-icon-milk-tea:before{content:"\e6a6"}.el-icon-ice-drink:before{content:"\e6a7"}.el-icon-ice-tea:before{content:"\e6a9"}.el-icon-coffee:before{content:"\e6aa"}.el-icon-orange:before{content:"\e6ab"}.el-icon-pear:before{content:"\e6ac"}.el-icon-apple:before{content:"\e6ad"}.el-icon-cherry:before{content:"\e6ae"}.el-icon-watermelon:before{content:"\e6af"}.el-icon-grape:before{content:"\e6b0"}.el-icon-refrigerator:before{content:"\e6b1"}.el-icon-goblet-square-full:before{content:"\e6b2"}.el-icon-goblet-square:before{content:"\e6b3"}.el-icon-goblet-full:before{content:"\e6b4"}.el-icon-goblet:before{content:"\e6b5"}.el-icon-cold-drink:before{content:"\e6b6"}.el-icon-coffee-cup:before{content:"\e6b8"}.el-icon-water-cup:before{content:"\e6b9"}.el-icon-hot-water:before{content:"\e6ba"}.el-icon-ice-cream:before{content:"\e6bb"}.el-icon-dessert:before{content:"\e6bc"}.el-icon-sugar:before{content:"\e6bd"}.el-icon-tableware:before{content:"\e6be"}.el-icon-burger:before{content:"\e6bf"}.el-icon-knife-fork:before{content:"\e6c1"}.el-icon-fork-spoon:before{content:"\e6c2"}.el-icon-chicken:before{content:"\e6c3"}.el-icon-food:before{content:"\e6c4"}.el-icon-dish-1:before{content:"\e6c5"}.el-icon-dish:before{content:"\e6c6"}.el-icon-moon-night:before{content:"\e6ee"}.el-icon-moon:before{content:"\e6f0"}.el-icon-cloudy-and-sunny:before{content:"\e6f1"}.el-icon-partly-cloudy:before{content:"\e6f2"}.el-icon-cloudy:before{content:"\e6f3"}.el-icon-sunny:before{content:"\e6f6"}.el-icon-sunset:before{content:"\e6f7"}.el-icon-sunrise-1:before{content:"\e6f8"}.el-icon-sunrise:before{content:"\e6f9"}.el-icon-heavy-rain:before{content:"\e6fa"}.el-icon-lightning:before{content:"\e6fb"}.el-icon-light-rain:before{content:"\e6fc"}.el-icon-wind-power:before{content:"\e6fd"}.el-icon-baseball:before{content:"\e712"}.el-icon-soccer:before{content:"\e713"}.el-icon-football:before{content:"\e715"}.el-icon-basketball:before{content:"\e716"}.el-icon-ship:before{content:"\e73f"}.el-icon-truck:before{content:"\e740"}.el-icon-bicycle:before{content:"\e741"}.el-icon-mobile-phone:before{content:"\e6d3"}.el-icon-service:before{content:"\e6d4"}.el-icon-key:before{content:"\e6e2"}.el-icon-unlock:before{content:"\e6e4"}.el-icon-lock:before{content:"\e6e5"}.el-icon-watch:before{content:"\e6fe"}.el-icon-watch-1:before{content:"\e6ff"}.el-icon-timer:before{content:"\e702"}.el-icon-alarm-clock:before{content:"\e703"}.el-icon-map-location:before{content:"\e704"}.el-icon-delete-location:before{content:"\e705"}.el-icon-add-location:before{content:"\e706"}.el-icon-location-information:before{content:"\e707"}.el-icon-location-outline:before{content:"\e708"}.el-icon-location:before{content:"\e79e"}.el-icon-place:before{content:"\e709"}.el-icon-discover:before{content:"\e70a"}.el-icon-first-aid-kit:before{content:"\e70b"}.el-icon-trophy-1:before{content:"\e70c"}.el-icon-trophy:before{content:"\e70d"}.el-icon-medal:before{content:"\e70e"}.el-icon-medal-1:before{content:"\e70f"}.el-icon-stopwatch:before{content:"\e710"}.el-icon-mic:before{content:"\e711"}.el-icon-copy-document:before{content:"\e718"}.el-icon-full-screen:before{content:"\e719"}.el-icon-switch-button:before{content:"\e71b"}.el-icon-aim:before{content:"\e71c"}.el-icon-crop:before{content:"\e71d"}.el-icon-odometer:before{content:"\e71e"}.el-icon-time:before{content:"\e71f"}.el-icon-bangzhu:before{content:"\e724"}.el-icon-close-notification:before{content:"\e726"}.el-icon-microphone:before{content:"\e727"}.el-icon-turn-off-microphone:before{content:"\e728"}.el-icon-position:before{content:"\e729"}.el-icon-postcard:before{content:"\e72a"}.el-icon-message:before{content:"\e72b"}.el-icon-chat-line-square:before{content:"\e72d"}.el-icon-chat-dot-square:before{content:"\e72e"}.el-icon-chat-dot-round:before{content:"\e72f"}.el-icon-chat-square:before{content:"\e730"}.el-icon-chat-line-round:before{content:"\e731"}.el-icon-chat-round:before{content:"\e732"}.el-icon-set-up:before{content:"\e733"}.el-icon-turn-off:before{content:"\e734"}.el-icon-open:before{content:"\e735"}.el-icon-connection:before{content:"\e736"}.el-icon-link:before{content:"\e737"}.el-icon-cpu:before{content:"\e738"}.el-icon-thumb:before{content:"\e739"}.el-icon-female:before{content:"\e73a"}.el-icon-male:before{content:"\e73b"}.el-icon-guide:before{content:"\e73c"}.el-icon-news:before{content:"\e73e"}.el-icon-price-tag:before{content:"\e744"}.el-icon-discount:before{content:"\e745"}.el-icon-wallet:before{content:"\e747"}.el-icon-coin:before{content:"\e748"}.el-icon-money:before{content:"\e749"}.el-icon-bank-card:before{content:"\e74a"}.el-icon-box:before{content:"\e74b"}.el-icon-present:before{content:"\e74c"}.el-icon-sell:before{content:"\e6d5"}.el-icon-sold-out:before{content:"\e6d6"}.el-icon-shopping-bag-2:before{content:"\e74d"}.el-icon-shopping-bag-1:before{content:"\e74e"}.el-icon-shopping-cart-2:before{content:"\e74f"}.el-icon-shopping-cart-1:before{content:"\e750"}.el-icon-shopping-cart-full:before{content:"\e751"}.el-icon-smoking:before{content:"\e752"}.el-icon-no-smoking:before{content:"\e753"}.el-icon-house:before{content:"\e754"}.el-icon-table-lamp:before{content:"\e755"}.el-icon-school:before{content:"\e756"}.el-icon-office-building:before{content:"\e757"}.el-icon-toilet-paper:before{content:"\e758"}.el-icon-notebook-2:before{content:"\e759"}.el-icon-notebook-1:before{content:"\e75a"}.el-icon-files:before{content:"\e75b"}.el-icon-collection:before{content:"\e75c"}.el-icon-receiving:before{content:"\e75d"}.el-icon-suitcase-1:before{content:"\e760"}.el-icon-suitcase:before{content:"\e761"}.el-icon-film:before{content:"\e763"}.el-icon-collection-tag:before{content:"\e765"}.el-icon-data-analysis:before{content:"\e766"}.el-icon-pie-chart:before{content:"\e767"}.el-icon-data-board:before{content:"\e768"}.el-icon-data-line:before{content:"\e76d"}.el-icon-reading:before{content:"\e769"}.el-icon-magic-stick:before{content:"\e76a"}.el-icon-coordinate:before{content:"\e76b"}.el-icon-mouse:before{content:"\e76c"}.el-icon-brush:before{content:"\e76e"}.el-icon-headset:before{content:"\e76f"}.el-icon-umbrella:before{content:"\e770"}.el-icon-scissors:before{content:"\e771"}.el-icon-mobile:before{content:"\e773"}.el-icon-attract:before{content:"\e774"}.el-icon-monitor:before{content:"\e775"}.el-icon-search:before{content:"\e778"}.el-icon-takeaway-box:before{content:"\e77a"}.el-icon-paperclip:before{content:"\e77d"}.el-icon-printer:before{content:"\e77e"}.el-icon-document-add:before{content:"\e782"}.el-icon-document:before{content:"\e785"}.el-icon-document-checked:before{content:"\e786"}.el-icon-document-copy:before{content:"\e787"}.el-icon-document-delete:before{content:"\e788"}.el-icon-document-remove:before{content:"\e789"}.el-icon-tickets:before{content:"\e78b"}.el-icon-folder-checked:before{content:"\e77f"}.el-icon-folder-delete:before{content:"\e780"}.el-icon-folder-remove:before{content:"\e781"}.el-icon-folder-add:before{content:"\e783"}.el-icon-folder-opened:before{content:"\e784"}.el-icon-folder:before{content:"\e78a"}.el-icon-edit-outline:before{content:"\e764"}.el-icon-edit:before{content:"\e78c"}.el-icon-date:before{content:"\e78e"}.el-icon-c-scale-to-original:before{content:"\e7c6"}.el-icon-view:before{content:"\e6ce"}.el-icon-loading:before{content:"\e6cf"}.el-icon-rank:before{content:"\e6d1"}.el-icon-sort-down:before{content:"\e7c4"}.el-icon-sort-up:before{content:"\e7c5"}.el-icon-sort:before{content:"\e6d2"}.el-icon-finished:before{content:"\e6cd"}.el-icon-refresh-left:before{content:"\e6c7"}.el-icon-refresh-right:before{content:"\e6c8"}.el-icon-refresh:before{content:"\e6d0"}.el-icon-video-play:before{content:"\e7c0"}.el-icon-video-pause:before{content:"\e7c1"}.el-icon-d-arrow-right:before{content:"\e6dc"}.el-icon-d-arrow-left:before{content:"\e6dd"}.el-icon-arrow-up:before{content:"\e6e1"}.el-icon-arrow-down:before{content:"\e6df"}.el-icon-arrow-right:before{content:"\e6e0"}.el-icon-arrow-left:before{content:"\e6de"}.el-icon-top-right:before{content:"\e6e7"}.el-icon-top-left:before{content:"\e6e8"}.el-icon-top:before{content:"\e6e6"}.el-icon-bottom:before{content:"\e6eb"}.el-icon-right:before{content:"\e6e9"}.el-icon-back:before{content:"\e6ea"}.el-icon-bottom-right:before{content:"\e6ec"}.el-icon-bottom-left:before{content:"\e6ed"}.el-icon-caret-top:before{content:"\e78f"}.el-icon-caret-bottom:before{content:"\e790"}.el-icon-caret-right:before{content:"\e791"}.el-icon-caret-left:before{content:"\e792"}.el-icon-d-caret:before{content:"\e79a"}.el-icon-share:before{content:"\e793"}.el-icon-menu:before{content:"\e798"}.el-icon-s-grid:before{content:"\e7a6"}.el-icon-s-check:before{content:"\e7a7"}.el-icon-s-data:before{content:"\e7a8"}.el-icon-s-opportunity:before{content:"\e7aa"}.el-icon-s-custom:before{content:"\e7ab"}.el-icon-s-claim:before{content:"\e7ad"}.el-icon-s-finance:before{content:"\e7ae"}.el-icon-s-comment:before{content:"\e7af"}.el-icon-s-flag:before{content:"\e7b0"}.el-icon-s-marketing:before{content:"\e7b1"}.el-icon-s-shop:before{content:"\e7b4"}.el-icon-s-open:before{content:"\e7b5"}.el-icon-s-management:before{content:"\e7b6"}.el-icon-s-ticket:before{content:"\e7b7"}.el-icon-s-release:before{content:"\e7b8"}.el-icon-s-home:before{content:"\e7b9"}.el-icon-s-promotion:before{content:"\e7ba"}.el-icon-s-operation:before{content:"\e7bb"}.el-icon-s-unfold:before{content:"\e7bc"}.el-icon-s-fold:before{content:"\e7a9"}.el-icon-s-platform:before{content:"\e7bd"}.el-icon-s-order:before{content:"\e7be"}.el-icon-s-cooperation:before{content:"\e7bf"}.el-icon-bell:before{content:"\e725"}.el-icon-message-solid:before{content:"\e799"}.el-icon-video-camera:before{content:"\e772"}.el-icon-video-camera-solid:before{content:"\e796"}.el-icon-camera:before{content:"\e779"}.el-icon-camera-solid:before{content:"\e79b"}.el-icon-download:before{content:"\e77c"}.el-icon-upload2:before{content:"\e77b"}.el-icon-upload:before{content:"\e7c3"}.el-icon-picture-outline-round:before{content:"\e75f"}.el-icon-picture-outline:before{content:"\e75e"}.el-icon-picture:before{content:"\e79f"}.el-icon-close:before{content:"\e6db"}.el-icon-check:before{content:"\e6da"}.el-icon-plus:before{content:"\e6d9"}.el-icon-minus:before{content:"\e6d8"}.el-icon-help:before{content:"\e73d"}.el-icon-s-help:before{content:"\e7b3"}.el-icon-circle-close:before{content:"\e78d"}.el-icon-circle-check:before{content:"\e720"}.el-icon-circle-plus-outline:before{content:"\e723"}.el-icon-remove-outline:before{content:"\e722"}.el-icon-zoom-out:before{content:"\e776"}.el-icon-zoom-in:before{content:"\e777"}.el-icon-error:before{content:"\e79d"}.el-icon-success:before{content:"\e79c"}.el-icon-circle-plus:before{content:"\e7a0"}.el-icon-remove:before{content:"\e7a2"}.el-icon-info:before{content:"\e7a1"}.el-icon-question:before{content:"\e7a4"}.el-icon-warning-outline:before{content:"\e6c9"}.el-icon-warning:before{content:"\e7a3"}.el-icon-goods:before{content:"\e7c2"}.el-icon-s-goods:before{content:"\e7b2"}.el-icon-star-off:before{content:"\e717"}.el-icon-star-on:before{content:"\e797"}.el-icon-more-outline:before{content:"\e6cc"}.el-icon-more:before{content:"\e794"}.el-icon-phone-outline:before{content:"\e6cb"}.el-icon-phone:before{content:"\e795"}.el-icon-user:before{content:"\e6e3"}.el-icon-user-solid:before{content:"\e7a5"}.el-icon-setting:before{content:"\e6ca"}.el-icon-s-tools:before{content:"\e7ac"}.el-icon-delete:before{content:"\e6d7"}.el-icon-delete-solid:before{content:"\e7c9"}.el-icon-eleme:before{content:"\e7c7"}.el-icon-platform-eleme:before{content:"\e7ca"}.el-icon-loading{-webkit-animation:rotating 2s linear infinite;animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@-webkit-keyframes rotating{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotating{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.el-button,.el-input__inner{-webkit-appearance:none;outline:0}.el-message-box,.el-popup-parent--hidden{overflow:hidden}.v-modal-enter{-webkit-animation:v-modal-in .2s ease;animation:v-modal-in .2s ease}.v-modal-leave{-webkit-animation:v-modal-out .2s ease forwards;animation:v-modal-out .2s ease forwards}@-webkit-keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-in{0%{opacity:0}}@-webkit-keyframes v-modal-out{to{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{position:fixed;left:0;top:0;width:100%;height:100%;opacity:.5;background:#000}.el-message-box{display:inline-block;width:420px;padding-bottom:10px;vertical-align:middle;background-color:#fff;border-radius:4px;border:1px solid #ebeef5;font-size:18px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);text-align:left;-webkit-backface-visibility:hidden;backface-visibility:hidden}.el-message-box__wrapper{position:fixed;top:0;bottom:0;left:0;right:0;text-align:center}.el-message-box__wrapper:after{content:"";display:inline-block;height:100%;width:0;vertical-align:middle}.el-message-box__header{position:relative;padding:15px 15px 10px}.el-message-box__title{padding-left:0;margin-bottom:0;font-size:18px;line-height:1;color:#303133}.el-message-box__headerbtn{position:absolute;top:15px;right:15px;padding:0;border:none;outline:0;background:0 0;font-size:16px;cursor:pointer}.el-message-box__headerbtn .el-message-box__close{color:#909399}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:#409eff}.el-message-box__content{padding:10px 15px;color:#606266;font-size:14px}.el-message-box__container{position:relative}.el-message-box__input{padding-top:15px}.el-message-box__input input.invalid,.el-message-box__input input.invalid:focus{border-color:#f56c6c}.el-message-box__status{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);font-size:24px!important}.el-message-box__status:before{padding-left:1px}.el-message-box__status+.el-message-box__message{padding-left:36px;padding-right:12px}.el-message-box__status.el-icon-success{color:#67c23a}.el-message-box__status.el-icon-info{color:#909399}.el-message-box__status.el-icon-warning{color:#e6a23c}.el-message-box__status.el-icon-error{color:#f56c6c}.el-message-box__message{margin:0}.el-message-box__message p{margin:0;line-height:24px}.el-message-box__errormsg{color:#f56c6c;font-size:12px;min-height:18px;margin-top:2px}.el-message-box__btns{padding:5px 15px 0;text-align:right}.el-message-box__btns button:nth-child(2){margin-left:10px}.el-message-box__btns-reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.el-message-box--center{padding-bottom:30px}.el-message-box--center .el-message-box__header{padding-top:30px}.el-message-box--center .el-message-box__title{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.el-message-box--center .el-message-box__status{position:relative;top:auto;padding-right:5px;text-align:center;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.el-message-box--center .el-message-box__message{margin-left:0}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__content{text-align:center}.el-message-box--center .el-message-box__content{padding-left:27px;padding-right:27px}.msgbox-fade-enter-active{-webkit-animation:msgbox-fade-in .3s;animation:msgbox-fade-in .3s}.msgbox-fade-leave-active{-webkit-animation:msgbox-fade-out .3s;animation:msgbox-fade-out .3s}@-webkit-keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes msgbox-fade-in{0%{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@-webkit-keyframes msgbox-fade-out{0%{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}to{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}@keyframes msgbox-fade-out{0%{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}to{-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0);opacity:0}}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{position:absolute;z-index:2000;background-color:hsla(0,0%,100%,.9);margin:0;top:0;right:0;bottom:0;left:0;-webkit-transition:opacity .3s;transition:opacity .3s}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:-25px}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:50px;width:50px}.el-loading-spinner{top:50%;margin-top:-21px;width:100%;text-align:center;position:absolute}.el-loading-spinner .el-loading-text{color:#409eff;margin:3px 0;font-size:14px}.el-loading-spinner .circular{height:42px;width:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.el-loading-spinner .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#409eff;stroke-linecap:round}.el-loading-spinner i{color:#409eff}.el-loading-fade-enter,.el-loading-fade-leave-active{opacity:0}@-webkit-keyframes loading-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes loading-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.el-textarea{position:relative;display:inline-block;width:100%;vertical-align:bottom;font-size:14px}.el-textarea__inner{display:block;resize:vertical;padding:5px 15px;line-height:1.5;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:inherit;color:#606266;background-color:#fff;background-image:none;border:1px solid #dcdfe6;border-radius:4px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.el-textarea__inner::-webkit-input-placeholder{color:#c0c4cc}.el-textarea__inner:-ms-input-placeholder{color:#c0c4cc}.el-textarea__inner::-ms-input-placeholder{color:#c0c4cc}.el-textarea__inner::-moz-placeholder{color:#c0c4cc}.el-textarea__inner::placeholder{color:#c0c4cc}.el-textarea__inner:hover{border-color:#c0c4cc}.el-textarea__inner:focus{outline:0;border-color:#409eff}.el-textarea .el-input__count{color:#909399;background:#fff;position:absolute;font-size:12px;bottom:5px;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:#f5f7fa;border-color:#e4e7ed;color:#c0c4cc;cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-webkit-input-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner:-ms-input-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner::-ms-input-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner::-moz-placeholder{color:#c0c4cc}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:#c0c4cc}.el-textarea.is-exceed .el-textarea__inner{border-color:#f56c6c}.el-textarea.is-exceed .el-input__count{color:#f56c6c}.el-input{position:relative;font-size:14px;display:inline-block;width:100%}.el-input::-webkit-scrollbar{z-index:11;width:6px}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{border-radius:5px;width:6px;background:#b4bccc}.el-input::-webkit-scrollbar-corner,.el-input::-webkit-scrollbar-track{background:#fff}.el-input::-webkit-scrollbar-track-piece{background:#fff;width:6px}.el-input .el-input__clear{color:#c0c4cc;font-size:14px;cursor:pointer;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1)}.el-input .el-input__clear:hover{color:#909399}.el-input .el-input__count{height:100%;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#909399;font-size:12px}.el-input .el-input__count .el-input__count-inner{background:#fff;line-height:normal;display:inline-block;padding:0 5px}.el-input__inner{-webkit-appearance:none;background-color:#fff;background-image:none;border-radius:4px;border:1px solid #dcdfe6;-webkit-box-sizing:border-box;box-sizing:border-box;color:#606266;display:inline-block;font-size:inherit;height:40px;line-height:40px;outline:0;padding:0 15px;-webkit-transition:border-color .2s cubic-bezier(.645,.045,.355,1);transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%}.el-input__prefix,.el-input__suffix{position:absolute;top:0;-webkit-transition:all .3s;text-align:center;height:100%;color:#c0c4cc}.el-input__inner::-webkit-input-placeholder{color:#c0c4cc}.el-input__inner:-ms-input-placeholder{color:#c0c4cc}.el-input__inner::-ms-input-placeholder{color:#c0c4cc}.el-input__inner::-moz-placeholder{color:#c0c4cc}.el-input__inner::placeholder{color:#c0c4cc}.el-input__inner:hover{border-color:#c0c4cc}.el-input.is-active .el-input__inner,.el-input__inner:focus{border-color:#409eff;outline:0}.el-input__suffix{right:5px;-webkit-transition:all .3s;transition:all .3s;pointer-events:none}.el-input__suffix-inner{pointer-events:all}.el-input__prefix{left:5px}.el-input__icon,.el-input__prefix{-webkit-transition:all .3s;transition:all .3s}.el-input__icon{height:100%;width:25px;text-align:center;line-height:40px}.el-input__icon:after{content:"";height:100%;width:0;display:inline-block;vertical-align:middle}.el-input__validateIcon{pointer-events:none}.el-input.is-disabled .el-input__inner{background-color:#f5f7fa;border-color:#e4e7ed;color:#c0c4cc;cursor:not-allowed}.el-input.is-disabled .el-input__inner::-webkit-input-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner:-ms-input-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner::-ms-input-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner::-moz-placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__inner::placeholder{color:#c0c4cc}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-exceed .el-input__inner{border-color:#f56c6c}.el-input.is-exceed .el-input__suffix .el-input__count{color:#f56c6c}.el-input--suffix .el-input__inner{padding-right:30px}.el-input--prefix .el-input__inner{padding-left:30px}.el-input--medium{font-size:14px}.el-input--medium .el-input__inner{height:36px;line-height:36px}.el-input--medium .el-input__icon{line-height:36px}.el-input--small{font-size:13px}.el-input--small .el-input__inner{height:32px;line-height:32px}.el-input--small .el-input__icon{line-height:32px}.el-input--mini{font-size:12px}.el-input--mini .el-input__inner{height:28px;line-height:28px}.el-input--mini .el-input__icon{line-height:28px}.el-input-group{line-height:normal;display:inline-table;width:100%;border-collapse:separate;border-spacing:0}.el-input-group>.el-input__inner{vertical-align:middle;display:table-cell}.el-input-group__append,.el-input-group__prepend{background-color:#f5f7fa;color:#909399;vertical-align:middle;display:table-cell;position:relative;border:1px solid #dcdfe6;border-radius:4px;padding:0 20px;width:1px;white-space:nowrap}.el-input-group--prepend .el-input__inner,.el-input-group__append{border-top-left-radius:0;border-bottom-left-radius:0}.el-input-group--append .el-input__inner,.el-input-group__prepend{border-top-right-radius:0;border-bottom-right-radius:0}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:0}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:-10px -20px}.el-input-group__append button.el-button,.el-input-group__append div.el-select .el-input__inner,.el-input-group__append div.el-select:hover .el-input__inner,.el-input-group__prepend button.el-button,.el-input-group__prepend div.el-select .el-input__inner,.el-input-group__prepend div.el-select:hover .el-input__inner{border-color:transparent;background-color:transparent;color:inherit;border-top:0;border-bottom:0}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-right:0}.el-input-group__append{border-left:0}.el-input-group--append .el-select .el-input.is-focus .el-input__inner,.el-input-group--prepend .el-select .el-input.is-focus .el-input__inner{border-color:transparent}.el-input__inner::-ms-clear{display:none;width:0;height:0}.el-popper .popper__arrow,.el-popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-popper .popper__arrow{border-width:6px;-webkit-filter:drop-shadow(0 2px 12px rgba(0,0,0,.03));filter:drop-shadow(0 2px 12px rgba(0,0,0,.03))}.el-popper .popper__arrow:after{content:" ";border-width:6px}.el-popper[x-placement^=top]{margin-bottom:12px}.el-popper[x-placement^=top] .popper__arrow{bottom:-6px;left:50%;margin-right:3px;border-top-color:#ebeef5;border-bottom-width:0}.el-popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-6px;border-top-color:#fff;border-bottom-width:0}.el-popper[x-placement^=bottom]{margin-top:12px}.el-popper[x-placement^=bottom] .popper__arrow{top:-6px;left:50%;margin-right:3px;border-top-width:0;border-bottom-color:#ebeef5}.el-popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-6px;border-top-width:0;border-bottom-color:#fff}.el-popper[x-placement^=right]{margin-left:12px}.el-popper[x-placement^=right] .popper__arrow{top:50%;left:-6px;margin-bottom:3px;border-right-color:#ebeef5;border-left-width:0}.el-popper[x-placement^=right] .popper__arrow:after{bottom:-6px;left:1px;border-right-color:#fff;border-left-width:0}.el-popper[x-placement^=left]{margin-right:12px}.el-popper[x-placement^=left] .popper__arrow{top:50%;right:-6px;margin-bottom:3px;border-right-width:0;border-left-color:#ebeef5}.el-popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-6px;margin-left:-6px;border-right-width:0;border-left-color:#fff}.el-popover{position:absolute;background:#fff;min-width:150px;border-radius:4px;border:1px solid #ebeef5;padding:12px;z-index:2000;color:#606266;line-height:1.4;text-align:justify;font-size:14px;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);word-break:break-all}.el-popover--plain{padding:18px 20px}.el-popover__title{color:#303133;font-size:16px;line-height:1;margin-bottom:12px}.el-popover:focus,.el-popover:focus:active,.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}.el-checkbox-button__inner,.el-tag{-webkit-box-sizing:border-box;white-space:nowrap}.el-checkbox-button__inner{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.el-table-column--selection .cell{padding-left:14px;padding-right:14px}.el-table-filter{border:1px solid #ebeef5;border-radius:2px;background-color:#fff;-webkit-box-shadow:0 2px 12px 0 rgba(0,0,0,.1);box-shadow:0 2px 12px 0 rgba(0,0,0,.1);-webkit-box-sizing:border-box;box-sizing:border-box;margin:2px 0}.el-table-filter__list{padding:5px 0;margin:0;list-style:none;min-width:100px}.el-table-filter__list-item{line-height:36px;padding:0 10px;cursor:pointer;font-size:14px}.el-table-filter__list-item:hover{background-color:#ecf5ff;color:#66b1ff}.el-table-filter__list-item.is-active{background-color:#409eff;color:#fff}.el-table-filter__content{min-width:100px}.el-table-filter__bottom{border-top:1px solid #ebeef5;padding:8px}.el-table-filter__bottom button{background:0 0;border:none;color:#606266;cursor:pointer;font-size:13px;padding:0 3px}.el-table-filter__bottom button:hover{color:#409eff}.el-table-filter__bottom button:focus{outline:0}.el-table-filter__bottom button.is-disabled{color:#c0c4cc;cursor:not-allowed}.el-table-filter__wrap{max-height:280px}.el-table-filter__checkbox-group{padding:10px}.el-table-filter__checkbox-group label.el-checkbox{display:block;margin-right:5px;margin-bottom:8px;margin-left:5px}.el-table-filter__checkbox-group .el-checkbox:last-child{margin-bottom:0}.el-checkbox,.el-checkbox__input{display:inline-block;position:relative;white-space:nowrap}.el-table,.el-table__append-wrapper{overflow:hidden}.el-table--hidden,.el-table td.is-hidden>*,.el-table th.is-hidden>*{visibility:hidden}.el-checkbox{color:#606266;font-weight:500;font-size:14px;cursor:pointer;user-select:none;margin-right:30px}.el-checkbox,.el-checkbox-button__inner,.el-table th{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.el-checkbox.is-bordered{padding:9px 20px 9px 10px;border-radius:4px;border:1px solid #dcdfe6;-webkit-box-sizing:border-box;box-sizing:border-box;line-height:normal;height:40px}.el-checkbox.is-bordered.is-checked{border-color:#409eff}.el-checkbox.is-bordered.is-disabled{border-color:#ebeef5;cursor:not-allowed}.el-checkbox.is-bordered+.el-checkbox.is-bordered{margin-left:10px}.el-checkbox.is-bordered.el-checkbox--medium{padding:7px 20px 7px 10px;border-radius:4px;height:36px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__label{line-height:17px;font-size:14px}.el-checkbox.is-bordered.el-checkbox--medium .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:5px 15px 5px 10px;border-radius:3px;height:32px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{line-height:15px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox.is-bordered.el-checkbox--mini{padding:3px 15px 3px 10px;border-radius:3px;height:28px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__label{line-height:12px;font-size:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--mini .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox__input{cursor:pointer;outline:0;line-height:1;vertical-align:middle}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:#edf2fc;border-color:#dcdfe6;cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:#c0c4cc}.el-checkbox__input.is-disabled .el-checkbox__inner+.el-checkbox__label{cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:#c0c4cc}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:#f2f6fc;border-color:#dcdfe6}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:#c0c4cc;border-color:#c0c4cc}.el-checkbox__input.is-checked .el-checkbox__inner,.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:#409eff;border-color:#409eff}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:#c0c4cc;cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner:after{-webkit-transform:rotate(45deg) scaleY(1);transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:#409eff}.el-checkbox__input.is-focus .el-checkbox__inner{border-color:#409eff}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";position:absolute;display:block;background-color:#fff;height:2px;-webkit-transform:scale(.5);transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:1px solid #dcdfe6;border-radius:2px;-webkit-box-sizing:border-box;box-sizing:border-box;width:14px;height:14px;background-color:#fff;z-index:1;-webkit-transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:#409eff}.el-checkbox__inner:after{-webkit-box-sizing:content-box;box-sizing:content-box;content:"";border:1px solid #fff;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;-webkit-transform:rotate(45deg) scaleY(0);transform:rotate(45deg) scaleY(0);width:3px;-webkit-transition:-webkit-transform .15s ease-in .05s;transition:-webkit-transform .15s ease-in .05s;transition:transform .15s ease-in .05s;transition:transform .15s ease-in .05s,-webkit-transform .15s ease-in .05s;-webkit-transform-origin:center;transform-origin:center}.el-checkbox__original{opacity:0;outline:0;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox-button,.el-checkbox-button__inner{position:relative;display:inline-block}.el-checkbox__label{display:inline-block;padding-left:10px;line-height:19px;font-size:14px}.el-checkbox:last-of-type{margin-right:0}.el-checkbox-button__inner{line-height:1;font-weight:500;white-space:nowrap;vertical-align:middle;cursor:pointer;background:#fff;border:1px solid #dcdfe6;border-left:0;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);padding:12px 20px;font-size:14px;border-radius:0}.el-checkbox-button__inner.is-round{padding:12px 20px}.el-checkbox-button__inner:hover{color:#409eff}.el-checkbox-button__inner [class*=el-icon-]{line-height:.9}.el-checkbox-button__inner [class*=el-icon-]+span{margin-left:5px}.el-checkbox-button__original{opacity:0;outline:0;position:absolute;margin:0;z-index:-1}.el-checkbox-button.is-checked .el-checkbox-button__inner{color:#fff;background-color:#409eff;border-color:#409eff;-webkit-box-shadow:-1px 0 0 0 #8cc5ff;box-shadow:-1px 0 0 0 #8cc5ff}.el-checkbox-button.is-checked:first-child .el-checkbox-button__inner{border-left-color:#409eff}.el-checkbox-button.is-disabled .el-checkbox-button__inner{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5;-webkit-box-shadow:none;box-shadow:none}.el-checkbox-button.is-disabled:first-child .el-checkbox-button__inner{border-left-color:#ebeef5}.el-checkbox-button:first-child .el-checkbox-button__inner{border-left:1px solid #dcdfe6;border-radius:4px 0 0 4px;-webkit-box-shadow:none!important;box-shadow:none!important}.el-checkbox-button.is-focus .el-checkbox-button__inner{border-color:#409eff}.el-checkbox-button:last-child .el-checkbox-button__inner{border-radius:0 4px 4px 0}.el-checkbox-button--medium .el-checkbox-button__inner{padding:10px 20px;font-size:14px;border-radius:0}.el-checkbox-button--medium .el-checkbox-button__inner.is-round{padding:10px 20px}.el-checkbox-button--small .el-checkbox-button__inner{padding:9px 15px;font-size:12px;border-radius:0}.el-checkbox-button--small .el-checkbox-button__inner.is-round{padding:9px 15px}.el-checkbox-button--mini .el-checkbox-button__inner{padding:7px 15px;font-size:12px;border-radius:0}.el-checkbox-button--mini .el-checkbox-button__inner.is-round{padding:7px 15px}.el-checkbox-group{font-size:0}.el-tag{background-color:#ecf5ff;border-color:#d9ecff;display:inline-block;height:32px;padding:0 10px;line-height:30px;font-size:12px;color:#409eff;border-width:1px;border-style:solid;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box;white-space:nowrap}.el-tag.is-hit{border-color:#409eff}.el-tag .el-tag__close{color:#409eff}.el-tag .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag.el-tag--info{background-color:#f4f4f5;border-color:#e9e9eb;color:#909399}.el-tag.el-tag--info.is-hit{border-color:#909399}.el-tag.el-tag--info .el-tag__close{color:#909399}.el-tag.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag.el-tag--success{background-color:#f0f9eb;border-color:#e1f3d8;color:#67c23a}.el-tag.el-tag--success.is-hit{border-color:#67c23a}.el-tag.el-tag--success .el-tag__close{color:#67c23a}.el-tag.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag.el-tag--warning{background-color:#fdf6ec;border-color:#faecd8;color:#e6a23c}.el-tag.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag.el-tag--danger{background-color:#fef0f0;border-color:#fde2e2;color:#f56c6c}.el-tag.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag .el-icon-close{border-radius:50%;text-align:center;position:relative;cursor:pointer;font-size:12px;height:16px;width:16px;line-height:16px;vertical-align:middle;top:-1px;right:-5px}.el-tag .el-icon-close:before{display:block}.el-tag--dark{background-color:#409eff;color:#fff}.el-tag--dark,.el-tag--dark.is-hit{border-color:#409eff}.el-tag--dark .el-tag__close{color:#fff}.el-tag--dark .el-tag__close:hover{color:#fff;background-color:#66b1ff}.el-tag--dark.el-tag--info{background-color:#909399;border-color:#909399;color:#fff}.el-tag--dark.el-tag--info.is-hit{border-color:#909399}.el-tag--dark.el-tag--info .el-tag__close{color:#fff}.el-tag--dark.el-tag--info .el-tag__close:hover{color:#fff;background-color:#a6a9ad}.el-tag--dark.el-tag--success{background-color:#67c23a;border-color:#67c23a;color:#fff}.el-tag--dark.el-tag--success.is-hit{border-color:#67c23a}.el-tag--dark.el-tag--success .el-tag__close{color:#fff}.el-tag--dark.el-tag--success .el-tag__close:hover{color:#fff;background-color:#85ce61}.el-tag--dark.el-tag--warning{background-color:#e6a23c;border-color:#e6a23c;color:#fff}.el-tag--dark.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--dark.el-tag--warning .el-tag__close{color:#fff}.el-tag--dark.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#ebb563}.el-tag--dark.el-tag--danger{background-color:#f56c6c;border-color:#f56c6c;color:#fff}.el-tag--dark.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--dark.el-tag--danger .el-tag__close{color:#fff}.el-tag--dark.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f78989}.el-tag--plain{background-color:#fff;border-color:#b3d8ff;color:#409eff}.el-tag--plain.is-hit{border-color:#409eff}.el-tag--plain .el-tag__close{color:#409eff}.el-tag--plain .el-tag__close:hover{color:#fff;background-color:#409eff}.el-tag--plain.el-tag--info{background-color:#fff;border-color:#d3d4d6;color:#909399}.el-tag--plain.el-tag--info.is-hit{border-color:#909399}.el-tag--plain.el-tag--info .el-tag__close{color:#909399}.el-tag--plain.el-tag--info .el-tag__close:hover{color:#fff;background-color:#909399}.el-tag--plain.el-tag--success{background-color:#fff;border-color:#c2e7b0;color:#67c23a}.el-tag--plain.el-tag--success.is-hit{border-color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close{color:#67c23a}.el-tag--plain.el-tag--success .el-tag__close:hover{color:#fff;background-color:#67c23a}.el-tag--plain.el-tag--warning{background-color:#fff;border-color:#f5dab1;color:#e6a23c}.el-tag--plain.el-tag--warning.is-hit{border-color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close{color:#e6a23c}.el-tag--plain.el-tag--warning .el-tag__close:hover{color:#fff;background-color:#e6a23c}.el-tag--plain.el-tag--danger{background-color:#fff;border-color:#fbc4c4;color:#f56c6c}.el-tag--plain.el-tag--danger.is-hit{border-color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close{color:#f56c6c}.el-tag--plain.el-tag--danger .el-tag__close:hover{color:#fff;background-color:#f56c6c}.el-tag--medium{height:28px;line-height:26px}.el-tag--medium .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--small{height:24px;padding:0 8px;line-height:22px}.el-tag--small .el-icon-close{-webkit-transform:scale(.8);transform:scale(.8)}.el-tag--mini{height:20px;padding:0 5px;line-height:19px}.el-tag--mini .el-icon-close{margin-left:-3px;-webkit-transform:scale(.7);transform:scale(.7)}.el-tooltip:focus:hover,.el-tooltip:focus:not(.focusing){outline-width:0}.el-tooltip__popper{position:absolute;border-radius:4px;padding:10px;z-index:2000;font-size:12px;line-height:1.2;min-width:10px;word-wrap:break-word}.el-tooltip__popper .popper__arrow,.el-tooltip__popper .popper__arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.el-tooltip__popper .popper__arrow{border-width:6px}.el-tooltip__popper .popper__arrow:after{content:" ";border-width:5px}.el-tooltip__popper[x-placement^=top]{margin-bottom:12px}.el-tooltip__popper[x-placement^=top] .popper__arrow{bottom:-6px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=top] .popper__arrow:after{bottom:1px;margin-left:-5px;border-top-color:#303133;border-bottom-width:0}.el-tooltip__popper[x-placement^=bottom]{margin-top:12px}.el-tooltip__popper[x-placement^=bottom] .popper__arrow{top:-6px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=bottom] .popper__arrow:after{top:1px;margin-left:-5px;border-top-width:0;border-bottom-color:#303133}.el-tooltip__popper[x-placement^=right]{margin-left:12px}.el-tooltip__popper[x-placement^=right] .popper__arrow{left:-6px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=right] .popper__arrow:after{bottom:-5px;left:1px;border-right-color:#303133;border-left-width:0}.el-tooltip__popper[x-placement^=left]{margin-right:12px}.el-tooltip__popper[x-placement^=left] .popper__arrow{right:-6px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper[x-placement^=left] .popper__arrow:after{right:1px;bottom:-5px;margin-left:-5px;border-right-width:0;border-left-color:#303133}.el-tooltip__popper.is-dark{background:#303133;color:#fff}.el-table,.el-table__expanded-cell{background-color:#fff}.el-tooltip__popper.is-light{background:#fff;border:1px solid #303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow{border-top-color:#303133}.el-tooltip__popper.is-light[x-placement^=top] .popper__arrow:after{border-top-color:#fff}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow{border-bottom-color:#303133}.el-tooltip__popper.is-light[x-placement^=bottom] .popper__arrow:after{border-bottom-color:#fff}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow{border-left-color:#303133}.el-tooltip__popper.is-light[x-placement^=left] .popper__arrow:after{border-left-color:#fff}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow{border-right-color:#303133}.el-tooltip__popper.is-light[x-placement^=right] .popper__arrow:after{border-right-color:#fff}.el-table{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-flex:1;-ms-flex:1;flex:1;width:100%;max-width:100%;font-size:14px;color:#606266}.el-table--mini,.el-table--small,.el-table__expand-icon{font-size:12px}.el-table__empty-block{min-height:60px;text-align:center;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.el-table__empty-text{line-height:60px;width:50%;color:#909399}.el-table__expand-column .cell{padding:0;text-align:center}.el-table__expand-icon{position:relative;cursor:pointer;color:#666;-webkit-transition:-webkit-transform .2s ease-in-out;transition:-webkit-transform .2s ease-in-out;transition:transform .2s ease-in-out;transition:transform .2s ease-in-out,-webkit-transform .2s ease-in-out;height:20px}.el-table__expand-icon--expanded{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.el-table__expand-icon>.el-icon{position:absolute;left:50%;top:50%;margin-left:-5px;margin-top:-5px}.el-table__expanded-cell[class*=cell]{padding:20px 50px}.el-table__expanded-cell:hover{background-color:transparent!important}.el-table__placeholder{display:inline-block;width:20px}.el-table--fit{border-right:0;border-bottom:0}.el-table--fit td.gutter,.el-table--fit th.gutter{border-right-width:1px}.el-table--scrollable-x .el-table__body-wrapper{overflow-x:auto}.el-table--scrollable-y .el-table__body-wrapper{overflow-y:auto}.el-table thead{color:#909399;font-weight:500}.el-table thead.is-group th{background:#f5f7fa}.el-table th,.el-table tr{background-color:#fff}.el-table td,.el-table th{padding:12px 0;min-width:0;-webkit-box-sizing:border-box;box-sizing:border-box;text-overflow:ellipsis;vertical-align:middle;position:relative;text-align:left}.el-table td.is-center,.el-table th.is-center{text-align:center}.el-table td.is-right,.el-table th.is-right{text-align:right}.el-table td.gutter,.el-table th.gutter{width:15px;border-right-width:0;border-bottom-width:0;padding:0}.el-table--medium td,.el-table--medium th{padding:10px 0}.el-table--small td,.el-table--small th{padding:8px 0}.el-table--mini td,.el-table--mini th{padding:6px 0}.el-table--border td:first-child .cell,.el-table--border th:first-child .cell,.el-table .cell{padding-left:10px}.el-table tr input[type=checkbox]{margin:0}.el-table td,.el-table th.is-leaf{border-bottom:1px solid #ebeef5}.el-table th.is-sortable{cursor:pointer}.el-table th{overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.el-table th>.cell{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;vertical-align:middle;padding-left:10px;padding-right:10px;width:100%}.el-table th>.cell.highlight{color:#409eff}.el-table th.required>div:before{display:inline-block;content:"";width:8px;height:8px;border-radius:50%;background:#ff4d51;margin-right:5px;vertical-align:middle}.el-table td div{-webkit-box-sizing:border-box;box-sizing:border-box}.el-table td.gutter{width:0}.el-table .cell{-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;white-space:normal;word-break:break-all;line-height:23px;padding-right:10px}.el-table .cell.el-tooltip{white-space:nowrap;min-width:50px}.el-table--border,.el-table--group{border:1px solid #ebeef5}.el-table--border:after,.el-table--group:after,.el-table:before{content:"";position:absolute;background-color:#ebeef5;z-index:1}.el-table--border:after,.el-table--group:after{top:0;right:0;width:1px;height:100%}.el-table:before{left:0;bottom:0;width:100%;height:1px}.el-table--border{border-right:none;border-bottom:none}.el-table--border.el-loading-parent--relative{border-color:transparent}.el-table--border td,.el-table--border th,.el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{border-right:1px solid #ebeef5}.el-table--border th.gutter:last-of-type{border-bottom:1px solid #ebeef5;border-bottom-width:1px}.el-table--border th,.el-table__fixed-right-patch{border-bottom:1px solid #ebeef5}.el-table__fixed,.el-table__fixed-right{position:absolute;top:0;left:0;overflow-x:hidden;overflow-y:hidden;-webkit-box-shadow:0 0 10px rgba(0,0,0,.12);box-shadow:0 0 10px rgba(0,0,0,.12)}.el-table__fixed-right:before,.el-table__fixed:before{content:"";position:absolute;left:0;bottom:0;width:100%;height:1px;background-color:#ebeef5;z-index:4}.el-table__fixed-right-patch{position:absolute;top:-1px;right:0;background-color:#fff}.el-table__fixed-right{top:0;left:auto;right:0}.el-table__fixed-right .el-table__fixed-body-wrapper,.el-table__fixed-right .el-table__fixed-footer-wrapper,.el-table__fixed-right .el-table__fixed-header-wrapper{left:auto;right:0}.el-table__fixed-header-wrapper{position:absolute;left:0;top:0;z-index:3}.el-table__fixed-footer-wrapper{position:absolute;left:0;bottom:0;z-index:3}.el-table__fixed-footer-wrapper tbody td{border-top:1px solid #ebeef5;background-color:#f5f7fa;color:#606266}.el-table__fixed-body-wrapper{position:absolute;left:0;top:37px;overflow:hidden;z-index:3}.el-table__body-wrapper,.el-table__footer-wrapper,.el-table__header-wrapper{width:100%}.el-table__footer-wrapper{margin-top:-1px}.el-table__footer-wrapper td{border-top:1px solid #ebeef5}.el-table__body,.el-table__footer,.el-table__header{table-layout:fixed;border-collapse:separate}.el-table__footer-wrapper,.el-table__header-wrapper{overflow:hidden}.el-table__footer-wrapper tbody td,.el-table__header-wrapper tbody td{background-color:#f5f7fa;color:#606266}.el-table__body-wrapper{overflow:hidden;position:relative}.el-table__body-wrapper.is-scrolling-left~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed,.el-table__body-wrapper.is-scrolling-none~.el-table__fixed-right,.el-table__body-wrapper.is-scrolling-right~.el-table__fixed-right{-webkit-box-shadow:none;box-shadow:none}.el-table__body-wrapper .el-table--border.is-scrolling-right~.el-table__fixed-right{border-left:1px solid #ebeef5}.el-table .caret-wrapper{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:34px;width:24px;vertical-align:middle;cursor:pointer;overflow:initial;position:relative}.el-table .sort-caret{width:0;height:0;border:5px solid transparent;position:absolute;left:7px}.el-table .sort-caret.ascending{border-bottom-color:#c0c4cc;top:5px}.el-table .sort-caret.descending{border-top-color:#c0c4cc;bottom:7px}.el-table .ascending .sort-caret.ascending{border-bottom-color:#409eff}.el-table .descending .sort-caret.descending{border-top-color:#409eff}.el-table .hidden-columns{visibility:hidden;position:absolute;z-index:-1}.el-table--striped .el-table__body tr.el-table__row--striped td{background:#fafafa}.el-table--striped .el-table__body tr.el-table__row--striped.current-row td{background-color:#ecf5ff}.el-table__body tr.hover-row.current-row>td,.el-table__body tr.hover-row.el-table__row--striped.current-row>td,.el-table__body tr.hover-row.el-table__row--striped>td,.el-table__body tr.hover-row>td{background-color:#f5f7fa}.el-table__body tr.current-row>td{background-color:#ecf5ff}.el-table__column-resize-proxy{position:absolute;left:200px;top:0;bottom:0;width:0;border-left:1px solid #ebeef5;z-index:10}.el-table__column-filter-trigger{display:inline-block;line-height:34px;cursor:pointer}.el-table__column-filter-trigger i{color:#909399;font-size:12px;-webkit-transform:scale(.75);transform:scale(.75)}.el-table--enable-row-transition .el-table__body td{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}.el-table--enable-row-hover .el-table__body tr:hover>td{background-color:#f5f7fa}.el-table--fluid-height .el-table__fixed,.el-table--fluid-height .el-table__fixed-right{bottom:0;overflow:hidden}.el-table [class*=el-table__row--level] .el-table__expand-icon{display:inline-block;width:20px;line-height:20px;height:20px;text-align:center;margin-right:3px}.el-breadcrumb{font-size:14px;line-height:1}.el-breadcrumb:after,.el-breadcrumb:before{display:table;content:""}.el-breadcrumb:after{clear:both}.el-breadcrumb__separator{margin:0 9px;font-weight:700;color:#c0c4cc}.el-breadcrumb__separator[class*=icon]{margin:0 6px;font-weight:400}.el-breadcrumb__item{float:left}.el-breadcrumb__inner{color:#606266}.el-breadcrumb__inner.is-link,.el-breadcrumb__inner a{font-weight:700;text-decoration:none;-webkit-transition:color .2s cubic-bezier(.645,.045,.355,1);transition:color .2s cubic-bezier(.645,.045,.355,1);color:#303133}.el-breadcrumb__inner.is-link:hover,.el-breadcrumb__inner a:hover{color:#409eff;cursor:pointer}.el-breadcrumb__item:last-child .el-breadcrumb__inner,.el-breadcrumb__item:last-child .el-breadcrumb__inner:hover,.el-breadcrumb__item:last-child .el-breadcrumb__inner a,.el-breadcrumb__item:last-child .el-breadcrumb__inner a:hover{font-weight:400;color:#606266;cursor:text}.el-breadcrumb__item:last-child .el-breadcrumb__separator{display:none}.el-divider{background-color:#dcdfe6;position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative}.el-divider__text{position:absolute;background-color:#fff;padding:0 20px;font-weight:500;color:#303133;font-size:14px}.el-divider__text.is-left{left:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-divider__text.is-center{left:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.el-button-group>.el-button.is-active,.el-button-group>.el-button.is-disabled,.el-button-group>.el-button:active,.el-button-group>.el-button:focus,.el-button-group>.el-button:hover{z-index:1}.el-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;border:1px solid #dcdfe6;color:#606266;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;-webkit-transition:.1s;transition:.1s;font-weight:500;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:12px 20px;font-size:14px;border-radius:4px}.el-button+.el-button{margin-left:10px}.el-button:focus,.el-button:hover{color:#409eff;border-color:#c6e2ff;background-color:#ecf5ff}.el-button:active{color:#3a8ee6;border-color:#3a8ee6;outline:0}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon-]+span{margin-left:5px}.el-button.is-plain:focus,.el-button.is-plain:hover{background:#fff;border-color:#409eff;color:#409eff}.el-button.is-active,.el-button.is-plain:active{color:#3a8ee6;border-color:#3a8ee6}.el-button.is-plain:active{background:#fff;outline:0}.el-button.is-disabled,.el-button.is-disabled:focus,.el-button.is-disabled:hover{color:#c0c4cc;cursor:not-allowed;background-image:none;background-color:#fff;border-color:#ebeef5}.el-button.is-disabled.el-button--text{background-color:transparent}.el-button.is-disabled.is-plain,.el-button.is-disabled.is-plain:focus,.el-button.is-disabled.is-plain:hover{background-color:#fff;border-color:#ebeef5;color:#c0c4cc}.el-button.is-loading{position:relative;pointer-events:none}.el-button.is-loading:before{pointer-events:none;content:"";position:absolute;left:-1px;top:-1px;right:-1px;bottom:-1px;border-radius:inherit;background-color:hsla(0,0%,100%,.35)}.el-button.is-round{border-radius:20px;padding:12px 23px}.el-button.is-circle{border-radius:50%;padding:12px}.el-button--primary{color:#fff;background-color:#409eff;border-color:#409eff}.el-button--primary:focus,.el-button--primary:hover{background:#66b1ff;border-color:#66b1ff;color:#fff}.el-button--primary.is-active,.el-button--primary:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff}.el-button--primary:active{outline:0}.el-button--primary.is-disabled,.el-button--primary.is-disabled:active,.el-button--primary.is-disabled:focus,.el-button--primary.is-disabled:hover{color:#fff;background-color:#a0cfff;border-color:#a0cfff}.el-button--primary.is-plain{color:#409eff;background:#ecf5ff;border-color:#b3d8ff}.el-button--primary.is-plain:focus,.el-button--primary.is-plain:hover{background:#409eff;border-color:#409eff;color:#fff}.el-button--primary.is-plain:active{background:#3a8ee6;border-color:#3a8ee6;color:#fff;outline:0}.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover{color:#8cc5ff;background-color:#ecf5ff;border-color:#d9ecff}.el-button--success{color:#fff;background-color:#67c23a;border-color:#67c23a}.el-button--success:focus,.el-button--success:hover{background:#85ce61;border-color:#85ce61;color:#fff}.el-button--success.is-active,.el-button--success:active{background:#5daf34;border-color:#5daf34;color:#fff}.el-button--success:active{outline:0}.el-button--success.is-disabled,.el-button--success.is-disabled:active,.el-button--success.is-disabled:focus,.el-button--success.is-disabled:hover{color:#fff;background-color:#b3e19d;border-color:#b3e19d}.el-button--success.is-plain{color:#67c23a;background:#f0f9eb;border-color:#c2e7b0}.el-button--success.is-plain:focus,.el-button--success.is-plain:hover{background:#67c23a;border-color:#67c23a;color:#fff}.el-button--success.is-plain:active{background:#5daf34;border-color:#5daf34;color:#fff;outline:0}.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover{color:#a4da89;background-color:#f0f9eb;border-color:#e1f3d8}.el-button--warning{color:#fff;background-color:#e6a23c;border-color:#e6a23c}.el-button--warning:focus,.el-button--warning:hover{background:#ebb563;border-color:#ebb563;color:#fff}.el-button--warning.is-active,.el-button--warning:active{background:#cf9236;border-color:#cf9236;color:#fff}.el-button--warning:active{outline:0}.el-button--warning.is-disabled,.el-button--warning.is-disabled:active,.el-button--warning.is-disabled:focus,.el-button--warning.is-disabled:hover{color:#fff;background-color:#f3d19e;border-color:#f3d19e}.el-button--warning.is-plain{color:#e6a23c;background:#fdf6ec;border-color:#f5dab1}.el-button--warning.is-plain:focus,.el-button--warning.is-plain:hover{background:#e6a23c;border-color:#e6a23c;color:#fff}.el-button--warning.is-plain:active{background:#cf9236;border-color:#cf9236;color:#fff;outline:0}.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover{color:#f0c78a;background-color:#fdf6ec;border-color:#faecd8}.el-button--danger{color:#fff;background-color:#f56c6c;border-color:#f56c6c}.el-button--danger:focus,.el-button--danger:hover{background:#f78989;border-color:#f78989;color:#fff}.el-button--danger.is-active,.el-button--danger:active{background:#dd6161;border-color:#dd6161;color:#fff}.el-button--danger:active{outline:0}.el-button--danger.is-disabled,.el-button--danger.is-disabled:active,.el-button--danger.is-disabled:focus,.el-button--danger.is-disabled:hover{color:#fff;background-color:#fab6b6;border-color:#fab6b6}.el-button--danger.is-plain{color:#f56c6c;background:#fef0f0;border-color:#fbc4c4}.el-button--danger.is-plain:focus,.el-button--danger.is-plain:hover{background:#f56c6c;border-color:#f56c6c;color:#fff}.el-button--danger.is-plain:active{background:#dd6161;border-color:#dd6161;color:#fff;outline:0}.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover{color:#f9a7a7;background-color:#fef0f0;border-color:#fde2e2}.el-button--info{color:#fff;background-color:#909399;border-color:#909399}.el-button--info:focus,.el-button--info:hover{background:#a6a9ad;border-color:#a6a9ad;color:#fff}.el-button--info.is-active,.el-button--info:active{background:#82848a;border-color:#82848a;color:#fff}.el-button--info:active{outline:0}.el-button--info.is-disabled,.el-button--info.is-disabled:active,.el-button--info.is-disabled:focus,.el-button--info.is-disabled:hover{color:#fff;background-color:#c8c9cc;border-color:#c8c9cc}.el-button--info.is-plain{color:#909399;background:#f4f4f5;border-color:#d3d4d6}.el-button--info.is-plain:focus,.el-button--info.is-plain:hover{background:#909399;border-color:#909399;color:#fff}.el-button--info.is-plain:active{background:#82848a;border-color:#82848a;color:#fff;outline:0}.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover{color:#bcbec2;background-color:#f4f4f5;border-color:#e9e9eb}.el-button--text,.el-button--text.is-disabled,.el-button--text.is-disabled:focus,.el-button--text.is-disabled:hover,.el-button--text:active{border-color:transparent}.el-button--medium{padding:10px 20px;font-size:14px;border-radius:4px}.el-button--mini,.el-button--small{font-size:12px;border-radius:3px}.el-button--medium.is-round{padding:10px 20px}.el-button--medium.is-circle{padding:10px}.el-button--small,.el-button--small.is-round{padding:9px 15px}.el-button--small.is-circle{padding:9px}.el-button--mini,.el-button--mini.is-round{padding:7px 15px}.el-button--mini.is-circle{padding:7px}.el-button--text{color:#409eff;background:0 0;padding-left:0;padding-right:0}.el-button--text:focus,.el-button--text:hover{color:#66b1ff;border-color:transparent;background-color:transparent}.el-button--text:active{color:#3a8ee6;background-color:transparent}.el-button-group{display:inline-block;vertical-align:middle}.el-button-group:after,.el-button-group:before{display:table;content:""}.el-button-group:after{clear:both}.el-button-group>.el-button{float:left;position:relative}.el-button-group>.el-button+.el-button{margin-left:0}.el-button-group>.el-button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.el-button-group>.el-button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.el-button-group>.el-button:first-child:last-child{border-radius:4px}.el-button-group>.el-button:first-child:last-child.is-round{border-radius:20px}.el-button-group>.el-button:first-child:last-child.is-circle{border-radius:50%}.el-button-group>.el-button:not(:first-child):not(:last-child){border-radius:0}.el-button-group>.el-button:not(:last-child){margin-right:-1px}.el-button-group>.el-dropdown>.el-button{border-top-left-radius:0;border-bottom-left-radius:0;border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--primary:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--success:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--warning:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--danger:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:first-child{border-right-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:last-child{border-left-color:hsla(0,0%,100%,.5)}.el-button-group .el-button--info:not(:first-child):not(:last-child){border-left-color:hsla(0,0%,100%,.5);border-right-color:hsla(0,0%,100%,.5)} \ No newline at end of file diff --git a/app/src/main/assets/web/new/css/detail.fb767a87.css b/app/src/main/assets/web/new/css/detail.fb767a87.css deleted file mode 100644 index 42ecf3753..000000000 --- a/app/src/main/assets/web/new/css/detail.fb767a87.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";.detail-wrapper[data-v-6d476c76]{padding:2% 0}.detail-wrapper .detail .bar .el-breadcrumb[data-v-6d476c76]{font-size:24px;font-weight:500;line-height:48px}.detail-wrapper .detail .bar .el-breadcrumb .index[data-v-6d476c76]{color:#333}.detail-wrapper .detail .bar .el-breadcrumb .sub[data-v-6d476c76]{color:#676767}.detail-wrapper .detail .el-divider[data-v-6d476c76]{margin-top:2%}.detail-wrapper .detail .catalog[data-v-6d476c76]{margin-top:2%;display:grid;grid-template-columns:repeat(auto-fill,300px);-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.detail-wrapper .detail .catalog .note[data-v-6d476c76]{width:200px;font-weight:500;font-size:14px;line-height:40px;padding-left:12px;padding-right:12px;word-wrap:break-word;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;text-align:left}@font-face{font-family:FZZCYSK;src:local("☺"),url(../fonts/popfont.f39ecc1a.ttf);font-style:normal;font-weight:400}.cata-wrapper[data-v-8c647fa4]{margin:-16px;padding:18px 0 24px 25px}.cata-wrapper .title[data-v-8c647fa4]{font-size:18px;font-weight:400;font-family:FZZCYSK;margin:0 0 20px 0;color:#ed4259;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;border-bottom:1px solid #ed4259}.cata-wrapper .data-wrapper[data-v-8c647fa4]{height:300px;overflow:auto}.cata-wrapper .data-wrapper .cata[data-v-8c647fa4]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.cata-wrapper .data-wrapper .cata .selected[data-v-8c647fa4]{color:#eb4259}.cata-wrapper .data-wrapper .cata .log[data-v-8c647fa4]{width:50%;height:40px;cursor:pointer;float:left;font:16px/40px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}.cata-wrapper .data-wrapper .cata .log .log-text[data-v-8c647fa4]{margin-right:26px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.cata-wrapper .night[data-v-8c647fa4] .log{border-bottom:1px solid #666}.cata-wrapper .day[data-v-8c647fa4] .log{border-bottom:1px solid #f2f2f2}@font-face{font-family:iconfont;src:url(../fonts/iconfont.f9a3fb0e.woff) format("woff")}[data-v-3d7e2fe5] .iconfont,[data-v-3d7e2fe5] .moon-icon{font-family:iconfont;font-style:normal}.settings-wrapper[data-v-3d7e2fe5]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin:-13px;width:478px;height:300px;text-align:left;padding:40px 0 40px 24px;background:#ede7da url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX48dr48Nf58tv379X17NJtIBxUAAACFUlEQVQ4y1XRUZakMAgF0Af2AiDWApDZgHZqAV1nZv9rGh7Rj7Y8McUFEg1wvcMESMNVD/neU8Xcaz7nYYkYlYO6Ti82PBI4BvIEg1aj3wKwRvIMgZsUy5LdhCawPFh1sZs4SrlyN9fQKpv8s5dgZ2eLyqqJiu+WkCmUEybXkm3INS01WAiv0PapJ0CZc0SJQUzcWnZYbOOY20iFD8Bk+/j2A3wNxH7GdShFYS5ff237kXh9I9zSkQmIAhOsOSVfJ6DIXTMDaPnzkRJ92S1BQQmXl5LdirgRLLDdcYqcGPwe3QN4xCBiGNbrqq9wpW1XCecChwaQdVOsRDpPCpeoolPdxeXp3WNB9PHVzWBHlygy4NJCCrFHREv6bDt0VGwJZASkpONmm1UseGeFKAQexgaAkrfYWl3AGxWOLL2AIMBNbCXpktmS3k3vHeYjGCPBa43wJTurO3ZFVpQSJdAZGLoHTyk1upkjxMEaIxum3iIARcCa5kSkFAW5fi1mUlL9eyOsaanFmOMruwvEdE3ZYzsRSzo5ewRLXyVPPEvknt8ij4DvCg2O7xOgBCUprEzV4z1WekSpUgI8DT2mrnSOXKRfQavwuKA1F+tFnMKdJSUpMA7wQAifWRkMgjUKKZE4lBl6MCM4B1pq1P4uIjDE6Pq6rL0FnW1nIFmta5vrSvq/Ch4tpqG/ZNyyWa5jZPktq81eYv8Bt5s4iFITOp4AAAAASUVORK5CYII=) repeat}.settings-wrapper .settings-title[data-v-3d7e2fe5]{font-size:18px;line-height:22px;margin-bottom:28px;font-family:FZZCYSK;font-weight:400}.settings-wrapper .setting-list ul[data-v-3d7e2fe5]{list-style:none outside none;margin:0;padding:0}.settings-wrapper .setting-list ul li[data-v-3d7e2fe5]{list-style:none outside none}.settings-wrapper .setting-list ul li i[data-v-3d7e2fe5]{font:12px/16px PingFangSC-Regular,-apple-system,Simsun;display:inline-block;min-width:48px;margin-right:16px;vertical-align:middle;color:#666}.settings-wrapper .setting-list ul li .theme-item[data-v-3d7e2fe5]{line-height:32px;width:34px;height:34px;margin-right:16px;border-radius:100%;display:inline-block;cursor:pointer;text-align:center;vertical-align:middle}.settings-wrapper .setting-list ul li .theme-item .iconfont[data-v-3d7e2fe5]{display:none}.settings-wrapper .setting-list ul li .selected[data-v-3d7e2fe5]{color:#ed4259}.settings-wrapper .setting-list ul li .selected .iconfont[data-v-3d7e2fe5]{display:inline}.settings-wrapper .setting-list ul .font-list[data-v-3d7e2fe5]{margin-top:28px}.settings-wrapper .setting-list ul .font-list .font-item[data-v-3d7e2fe5]{width:78px;height:34px;cursor:pointer;margin-right:16px;border-radius:2px;text-align:center;vertical-align:middle;display:inline-block;font:14px/34px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}.settings-wrapper .setting-list ul .font-list .font-item[data-v-3d7e2fe5]:hover,.settings-wrapper .setting-list ul .font-list .selected[data-v-3d7e2fe5]{color:#ed4259;border:1px solid #ed4259}.settings-wrapper .setting-list ul .font-size[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width[data-v-3d7e2fe5]{margin-top:28px}.settings-wrapper .setting-list ul .font-size .resize[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width .resize[data-v-3d7e2fe5]{display:inline-block;width:274px;height:34px;vertical-align:middle;border-radius:2px}.settings-wrapper .setting-list ul .font-size .resize span[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width .resize span[data-v-3d7e2fe5]{width:89px;height:34px;line-height:34px;display:inline-block;cursor:pointer;text-align:center;vertical-align:middle}.settings-wrapper .setting-list ul .font-size .resize span em[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width .resize span em[data-v-3d7e2fe5]{font-style:normal}.settings-wrapper .setting-list ul .font-size .resize .less[data-v-3d7e2fe5]:hover,.settings-wrapper .setting-list ul .font-size .resize .more[data-v-3d7e2fe5]:hover,.settings-wrapper .setting-list ul .read-width .resize .less[data-v-3d7e2fe5]:hover,.settings-wrapper .setting-list ul .read-width .resize .more[data-v-3d7e2fe5]:hover{color:#ed4259}.settings-wrapper .setting-list ul .font-size .resize .lang[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width .resize .lang[data-v-3d7e2fe5]{color:#a6a6a6;font-weight:400;font-family:FZZCYSK}.settings-wrapper .setting-list ul .font-size .resize b[data-v-3d7e2fe5],.settings-wrapper .setting-list ul .read-width .resize b[data-v-3d7e2fe5]{display:inline-block;height:20px;vertical-align:middle}.night[data-v-3d7e2fe5] .selected,.night[data-v-3d7e2fe5] .theme-item{border:1px solid #666}.night[data-v-3d7e2fe5] .moon-icon{color:#ed4259}.night[data-v-3d7e2fe5] .font-list .font-item,.night[data-v-3d7e2fe5] .resize{border:1px solid #666;background:rgba(45,45,45,.5)}.night[data-v-3d7e2fe5] .resize b{border-right:1px solid #666}.day[data-v-3d7e2fe5] .theme-item{border:1px solid #e5e5e5}.day[data-v-3d7e2fe5] .selected{border:1px solid #ed4259}.day[data-v-3d7e2fe5] .moon-icon{display:inline;color:hsla(0,0%,100%,.2)}.day[data-v-3d7e2fe5] .font-list .font-item{background:hsla(0,0%,100%,.5);border:1px solid rgba(0,0,0,.1)}.day[data-v-3d7e2fe5] .resize{border:1px solid #e5e5e5;background:hsla(0,0%,100%,.5)}.day[data-v-3d7e2fe5] .resize b{border-right:1px solid #e5e5e5}p[data-v-6ee085ae]{display:block;word-wrap:break-word;word-break:break-all}[data-v-3d823984] .pop-setting{margin-left:68px;top:0}[data-v-3d823984] .pop-cata{margin-left:10px}.chapter-wrapper[data-v-3d823984]{padding:0 4%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.chapter-wrapper[data-v-3d823984] .no-point{pointer-events:none}.chapter-wrapper .tool-bar[data-v-3d823984]{position:fixed;top:0;left:50%;z-index:100}.chapter-wrapper .tool-bar .tools[data-v-3d823984]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.chapter-wrapper .tool-bar .tools .tool-icon[data-v-3d823984]{font-size:18px;width:58px;height:48px;text-align:center;padding-top:12px;cursor:pointer;outline:none}.chapter-wrapper .tool-bar .tools .tool-icon .iconfont[data-v-3d823984]{font-family:iconfont;width:16px;height:16px;font-size:16px;margin:0 auto 6px}.chapter-wrapper .tool-bar .tools .tool-icon .icon-text[data-v-3d823984]{font-size:12px}.chapter-wrapper .read-bar[data-v-3d823984]{position:fixed;bottom:0;right:50%;z-index:100}.chapter-wrapper .read-bar .tools[data-v-3d823984]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.chapter-wrapper .read-bar .tools .tool-icon[data-v-3d823984]{font-size:18px;width:42px;height:31px;padding-top:12px;text-align:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;outline:none;margin-top:-1px}.chapter-wrapper .read-bar .tools .tool-icon .iconfont[data-v-3d823984]{font-family:iconfont;width:16px;height:16px;font-size:16px;margin:0 auto 6px}.chapter-wrapper .chapter-bar .el-breadcrumb .item[data-v-3d823984]{font-size:14px;color:#606266}.chapter-wrapper .chapter[data-v-3d823984]{font-family:Microsoft YaHei,PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,sans-serif;text-align:left;padding:0 65px;min-height:100vh;width:670px;margin:0 auto}.chapter-wrapper .chapter[data-v-3d823984] .el-icon-loading{font-size:36px;color:#b5b5b5}.chapter-wrapper .chapter[data-v-3d823984] .el-loading-text{font-weight:500;color:#b5b5b5}.chapter-wrapper .chapter .content[data-v-3d823984]{font-size:18px;line-height:1.8;overflow:hidden;font-family:Microsoft YaHei,PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,sans-serif}.chapter-wrapper .chapter .content .title[data-v-3d823984]{margin-bottom:57px;font:24px/32px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}.chapter-wrapper .chapter .content .bottom-bar[data-v-3d823984],.chapter-wrapper .chapter .content .top-bar[data-v-3d823984]{height:64px}.day[data-v-3d823984] .popup{-webkit-box-shadow:0 2px 4px rgba(0,0,0,.12),0 0 6px rgba(0,0,0,.04);box-shadow:0 2px 4px rgba(0,0,0,.12),0 0 6px rgba(0,0,0,.04)}.day[data-v-3d823984] .tool-icon{border:1px solid rgba(0,0,0,.1);margin-top:-1px;color:#000}.day[data-v-3d823984] .tool-icon .icon-text{color:rgba(0,0,0,.4)}.day[data-v-3d823984] .chapter{border:1px solid #d8d8d8;color:#262626}.night[data-v-3d823984] .popup{-webkit-box-shadow:0 2px 4px rgba(0,0,0,.48),0 0 6px rgba(0,0,0,.16);box-shadow:0 2px 4px rgba(0,0,0,.48),0 0 6px rgba(0,0,0,.16)}.night[data-v-3d823984] .tool-icon{border:1px solid #444;margin-top:-1px;color:#666}.night[data-v-3d823984] .tool-icon .icon-text{color:#666}.night[data-v-3d823984] .chapter{border:1px solid #444;color:#666}.night[data-v-3d823984] .popper__arrow{background:#666} \ No newline at end of file diff --git a/app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg b/app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg deleted file mode 100644 index 996bbd412..000000000 Binary files a/app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg and /dev/null differ diff --git a/app/src/main/assets/web/new/js/about.a0534951.js b/app/src/main/assets/web/new/js/about.a0534951.js deleted file mode 100644 index 9880f7f2f..000000000 --- a/app/src/main/assets/web/new/js/about.a0534951.js +++ /dev/null @@ -1 +0,0 @@ -(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["about"],{"1e11":function(t,e,a){"use strict";var s=a("f088"),n=a.n(s);n.a},"7b5b":function(t,e,a){},"7e43":function(t,e,a){t.exports=a.p+"img/noCover.b5c48bc1.jpeg"},d504:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"index-wrapper"},[s("div",{staticClass:"navigation-wrapper"},[s("div",{staticClass:"navigation-title"},[t._v(" 阅读 ")]),s("div",{staticClass:"navigation-sub-title"},[t._v(" 清风不识字,何故乱翻书 ")]),s("div",{staticClass:"search-wrapper"},[s("el-input",{staticClass:"search-input",attrs:{size:"mini",placeholder:"搜索书籍"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}},[s("i",{staticClass:"el-input__icon el-icon-search",attrs:{slot:"prefix"},slot:"prefix"})])],1),s("div",{staticClass:"recent-wrapper"},[s("div",{staticClass:"recent-title"},[t._v(" 最近阅读 ")]),s("div",{staticClass:"reading-recent"},[s("el-tag",{staticClass:"recent-book",class:{"no-point":""==t.readingRecent.url},attrs:{type:"warning"},on:{click:function(e){return t.toDetail(t.readingRecent.url,t.readingRecent.name,t.readingRecent.chapterIndex)}}},[t._v(" "+t._s(t.readingRecent.name)+" ")])],1)]),s("div",{staticClass:"setting-wrapper"},[s("div",{staticClass:"setting-title"},[t._v(" 基本设定 ")]),s("div",{staticClass:"setting-item"},[s("el-tag",{staticClass:"setting-connect",class:{"no-point":t.newConnect},attrs:{type:t.connectType},on:{click:t.setIP}},[t._v(" "+t._s(t.connectStatus)+" ")])],1)]),s("div",{staticClass:"bottom-icons"},[s("a",{attrs:{href:"https://github.com/zsakvo/web-yuedu",target:"_blank"}},[s("div",{staticClass:"bottom-icon"},[s("img",{attrs:{src:a("fa39"),alt:""}})])])])]),s("div",{ref:"shelfWrapper",staticClass:"shelf-wrapper"},[s("div",{staticClass:"books-wrapper"},[s("div",{staticClass:"wrapper"},t._l(t.shelf,(function(e){return s("div",{key:e.noteUrl,staticClass:"book",on:{click:function(a){return t.toDetail(e.bookUrl,e.name,e.durChapterIndex)}}},[s("div",{staticClass:"cover-img"},[s("img",{staticClass:"cover",attrs:{src:e.coverUrl||a("7e43"),alt:""}})]),s("div",{staticClass:"info",on:{click:function(a){return t.toDetail(e.bookUrl,e.name,e.durChapterIndex)}}},[s("div",{staticClass:"name"},[t._v(t._s(e.name))]),s("div",{staticClass:"sub"},[s("div",{staticClass:"author"},[t._v(" "+t._s(e.author)+" ")]),s("div",{staticClass:"dot"},[t._v("•")]),s("div",{staticClass:"size"},[t._v("共"+t._s(e.totalChapterNum)+"章")])]),s("div",{staticClass:"dur-chapter"},[t._v("已读:"+t._s(e.durChapterTitle))]),s("div",{staticClass:"last-chapter"},[t._v("最新:"+t._s(e.latestChapterTitle))])])])})),0)])])])},n=[],i=(a("7b5b"),a("82ae")),r=a.n(i),c={data:function(){return{search:"",readingRecent:{name:"尚无阅读记录",url:"",chapterIndex:0}}},mounted:function(){var t=localStorage.getItem("readingRecent");if(null!=t&&(this.readingRecent=JSON.parse(t),"undefined"==typeof this.readingRecent.chapterIndex&&(this.readingRecent.chapterIndex=0)),0==this.shelf.length){this.loading=this.$loading({target:this.$refs.shelfWrapper,lock:!0,text:"正在获取书籍信息",spinner:"el-icon-loading",background:"rgb(247,247,247)"});var e=this;r.a.get("/getBookshelf",{timeout:3e3}).then((function(t){e.loading.close(),e.$store.commit("setConnectType","success"),e.$store.commit("increaseBookNum",t.data.data.length),e.$store.commit("addBooks",t.data.data.sort((function(t,e){var a=t["durChapterTime"]||0,s=e["durChapterTime"]||0;return s-a}))),e.$store.commit("setConnectStatus","已连接 "),e.$store.commit("setNewConnect",!1)})).catch((function(t){throw e.loading.close(),e.$store.commit("setConnectType","danger"),e.$store.commit("setConnectStatus","连接失败"),e.$message.error("后端连接失败"),e.$store.commit("setNewConnect",!1),t}))}},methods:{setIP:function(){},toDetail:function(t,e,a){sessionStorage.setItem("bookUrl",t),sessionStorage.setItem("bookName",e),sessionStorage.setItem("chapterIndex",a),this.readingRecent={name:e,url:t,chapterIndex:a},localStorage.setItem("readingRecent",JSON.stringify(this.readingRecent)),this.$router.push({path:"/chapter"})}},computed:{shelf:function(){return this.$store.state.shelf},connectStatus:function(){return this.$store.state.connectStatus},connectType:function(){return this.$store.state.connectType},newConnect:function(){return this.$store.state.newConnect}}},o=c,l=(a("1e11"),a("9ca4")),d=Object(l["a"])(o,s,n,!1,null,"5f97df5a",null);e["default"]=d.exports},f088:function(t,e,a){},f820:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement;t._self._c;return t._m(0)},n=[function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"about"},[a("h1",[t._v("This is an about page")])])}],i=a("9ca4"),r={},c=Object(i["a"])(r,s,n,!1,null,null,null);e["default"]=c.exports},fa39:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAECUlEQVRYR7WXTYhcRRDHq3pY9yKrYBQ8KBsjgvHgwRhiQBTjYZm4Xe8NusawhwS/o9GLoKhgBGPAgJd1NdGIXwtZTbRf9Rqzl6gHTVyDeIkIgnEOghAM6oKHzTJd0sO8Zaa338zb7NjwmJn++Ndv+lVVVyOsoM3Ozl69sLBAiHiDc26NUuoKv9w5d14p9aeI/DI4OMgjIyN/lJXFMhOttQ8BgBaR0TLzEXEGAKzW+lCv+V0BmLmGiLtF5M5eQrFxRPxaRCaI6LOi9YUAzPwGADxxMYYjayaJ6MkoZKyTmU8AwF19Mp7LfElEW0LNZTvAzIcBYFufjedy00T0QLt2B4AxZo9S6qX/yXhT1jn3cpqme3IbSwDM/DgAvNlu3Dm3Uyl1HAA2IOJ2EdleEu5Io9H4EBHPVCqVLSISRsMuInrLazUBpqamhoaGhr4TkRsDgLVpmtbzPmPMLQBwOwD4vvzxw8P5IyJztVrtVL4my7L1iPhTx7Yj/jw/P79pfHx8vgmQZdkLiPhK+O8GBgauqVarv5f819FpxpjLlVJ/hYMi8mKSJHubAMz8KwBcF1EYI6IjqwRIlFImonGWiNZhlmVVRDxWYGTVAMx8HwB8EtMXka1orT0gIo9GJrxNRLH+FW8IMx8EgEeW5QDEgx5gTkQ2Bk7yr9b60hVb6rKAmc8BwJWBne+x4P3XiWhtPwGstV9FzpSzHuBvALgsMHaaiDp2ZbUwWZZNIuKuQOcfD7AAAJeEcaq1Xr9ao+3rmdknnscCzQse4LdWEukYazQaa2q12vl+QTDztwCwOdCr+zA8iYi3RQwREdl+ADDz9QDwIwB0OLaInPJRcEhEHoyEyAmt9d39ALDW2lg1hYjv+lfgC4WJgkTxcJIkPcuqbpC+qgKATwvm7PYAGwDgdBeRZ4notYvZCWPMDqXUe13W3to8C6y10yJyv//u6zj/2R6ziPiRiBwt6xPMrBExFZEdRcYR8WOt9bb8MNoKAJ+3Jvtwed05d4dSKtz+c4h4VGsdrRWttZMici8AXFVix+4homNLBUmWZQcQMc/9x4mommXZ84i4t11MKbV5dHR06bxvH5uZmbnZOfdN6O0RmMNE1CxulgCstdeKyBcAcFPrVTyltZ4wxiSVSuXplkhda72zh9P1rClFZFOSJHMdAP5Hq3rxR6eH+IGIvIOuqFlr94nIc10WdRzxy6riAMJnr2nn3JlcME3TppMWNWvtfhF5pmB8WX0RvZgEEEtaYUUbM2KtfUdE/FUubNHipvBmZIxZp5TaDwBprlQGIHLqzSHiPq01x4B7Xk6Z2d8TfDwPlwFozfd1f90598Hi4uKrY2NjFwrzQVkP81nNi/byAWOMv8gOp2n6fhnt/wDqJrRWLmhIrwAAAABJRU5ErkJggg=="}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/new/js/about~detail.47586100.js b/app/src/main/assets/web/new/js/about~detail.47586100.js deleted file mode 100644 index 7d1080cea..000000000 --- a/app/src/main/assets/web/new/js/about~detail.47586100.js +++ /dev/null @@ -1 +0,0 @@ -(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["about~detail"],{"04a7":function(e,t,r){"use strict";var n=r("d844");e.exports=function(e,t,r){return n.forEach(r,(function(r){e=r(e,t)})),e}},"050d":function(e,t,r){"use strict";var n=r("d844");function o(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}e.exports=function(e,t,r){if(!t)return e;var i;if(r)i=r(t);else if(n.isURLSearchParams(t))i=t.toString();else{var s=[];n.forEach(t,(function(e,t){null!==e&&"undefined"!==typeof e&&(n.isArray(e)?t+="[]":e=[e],n.forEach(e,(function(e){n.isDate(e)?e=e.toISOString():n.isObject(e)&&(e=JSON.stringify(e)),s.push(o(t)+"="+o(e))})))})),i=s.join("&")}if(i){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+i}return e}},"068e":function(e,t,r){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},"0bbf":function(e,t,r){"use strict";var n=r("d844"),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,r,i,s={};return e?(n.forEach(e.split("\n"),(function(e){if(i=e.indexOf(":"),t=n.trim(e.substr(0,i)).toLowerCase(),r=n.trim(e.substr(i+1)),t){if(s[t]&&o.indexOf(t)>=0)return;s[t]="set-cookie"===t?(s[t]?s[t]:[]).concat([r]):s[t]?s[t]+", "+r:r}})),s):s}},"11f4":function(e,t,r){"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},"155b":function(e,t,r){"use strict";var n=r("068e");function o(e){if("function"!==typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise((function(e){t=e}));var r=this;e((function(e){r.reason||(r.reason=new n(e),t(r.reason))}))}o.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},o.source=function(){var e,t=new o((function(t){e=t}));return{token:t,cancel:e}},e.exports=o},"1eb2":function(e,t,r){"use strict";var n=r("c5b9");e.exports=function(e,t,r){var o=r.config.validateStatus;!o||o(r.status)?e(r):t(n("Request failed with status code "+r.status,r.config,null,r.request,r))}},"2ed0":function(e,t,r){"use strict";(function(t){var n=r("d844"),o=r("9d72"),i={"Content-Type":"application/x-www-form-urlencoded"};function s(e,t){!n.isUndefined(e)&&n.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}function a(){var e;return("undefined"!==typeof XMLHttpRequest||"undefined"!==typeof t&&"[object process]"===Object.prototype.toString.call(t))&&(e=r("a169")),e}var u={adapter:a(),transformRequest:[function(e,t){return o(t,"Accept"),o(t,"Content-Type"),n.isFormData(e)||n.isArrayBuffer(e)||n.isBuffer(e)||n.isStream(e)||n.isFile(e)||n.isBlob(e)?e:n.isArrayBufferView(e)?e.buffer:n.isURLSearchParams(e)?(s(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):n.isObject(e)?(s(t,"application/json;charset=utf-8"),JSON.stringify(e)):e}],transformResponse:[function(e){if("string"===typeof e)try{e=JSON.parse(e)}catch(t){}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};n.forEach(["delete","get","head"],(function(e){u.headers[e]={}})),n.forEach(["post","put","patch"],(function(e){u.headers[e]=n.merge(i)})),e.exports=u}).call(this,r("eef6"))},"43d9":function(e,t,r){"use strict";var n=r("d844"),o=r("faf0"),i=r("4a67"),s=r("c9ba"),a=r("2ed0");function u(e){var t=new i(e),r=o(i.prototype.request,t);return n.extend(r,i.prototype,t),n.extend(r,t),r}var c=u(a);c.Axios=i,c.create=function(e){return u(s(c.defaults,e))},c.Cancel=r("068e"),c.CancelToken=r("155b"),c.isCancel=r("11f4"),c.all=function(e){return Promise.all(e)},c.spread=r("53f3"),e.exports=c,e.exports.default=c},"4a67":function(e,t,r){"use strict";var n=r("d844"),o=r("050d"),i=r("54b5"),s=r("c70f"),a=r("c9ba");function u(e){this.defaults=e,this.interceptors={request:new i,response:new i}}u.prototype.request=function(e){"string"===typeof e?(e=arguments[1]||{},e.url=arguments[0]):e=e||{},e=a(this.defaults,e),e.method?e.method=e.method.toLowerCase():this.defaults.method?e.method=this.defaults.method.toLowerCase():e.method="get";var t=[s,void 0],r=Promise.resolve(e);this.interceptors.request.forEach((function(e){t.unshift(e.fulfilled,e.rejected)})),this.interceptors.response.forEach((function(e){t.push(e.fulfilled,e.rejected)}));while(t.length)r=r.then(t.shift(),t.shift());return r},u.prototype.getUri=function(e){return e=a(this.defaults,e),o(e.url,e.params,e.paramsSerializer).replace(/^\?/,"")},n.forEach(["delete","get","head","options"],(function(e){u.prototype[e]=function(t,r){return this.request(n.merge(r||{},{method:e,url:t}))}})),n.forEach(["post","put","patch"],(function(e){u.prototype[e]=function(t,r,o){return this.request(n.merge(o||{},{method:e,url:t,data:r}))}})),e.exports=u},"4f37":function(e,t,r){"use strict";var n=r("ca19"),o=r("c4e8");e.exports=function(e,t){return e&&!n(t)?o(e,t):t}},"53f3":function(e,t,r){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},"54b5":function(e,t,r){"use strict";var n=r("d844");function o(){this.handlers=[]}o.prototype.use=function(e,t){return this.handlers.push({fulfilled:e,rejected:t}),this.handlers.length-1},o.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},o.prototype.forEach=function(e){n.forEach(this.handlers,(function(t){null!==t&&e(t)}))},e.exports=o},6266:function(e,t,r){(function(e){function r(e,t){for(var r=0,n=e.length-1;n>=0;n--){var o=e[n];"."===o?e.splice(n,1):".."===o?(e.splice(n,1),r++):r&&(e.splice(n,1),r--)}if(t)for(;r--;r)e.unshift("..");return e}function n(e){"string"!==typeof e&&(e+="");var t,r=0,n=-1,o=!0;for(t=e.length-1;t>=0;--t)if(47===e.charCodeAt(t)){if(!o){r=t+1;break}}else-1===n&&(o=!1,n=t+1);return-1===n?"":e.slice(r,n)}function o(e,t){if(e.filter)return e.filter(t);for(var r=[],n=0;n=-1&&!n;i--){var s=i>=0?arguments[i]:e.cwd();if("string"!==typeof s)throw new TypeError("Arguments to path.resolve must be strings");s&&(t=s+"/"+t,n="/"===s.charAt(0))}return t=r(o(t.split("/"),(function(e){return!!e})),!n).join("/"),(n?"/":"")+t||"."},t.normalize=function(e){var n=t.isAbsolute(e),s="/"===i(e,-1);return e=r(o(e.split("/"),(function(e){return!!e})),!n).join("/"),e||n||(e="."),e&&s&&(e+="/"),(n?"/":"")+e},t.isAbsolute=function(e){return"/"===e.charAt(0)},t.join=function(){var e=Array.prototype.slice.call(arguments,0);return t.normalize(o(e,(function(e,t){if("string"!==typeof e)throw new TypeError("Arguments to path.join must be strings");return e})).join("/"))},t.relative=function(e,r){function n(e){for(var t=0;t=0;r--)if(""!==e[r])break;return t>r?[]:e.slice(t,r-t+1)}e=t.resolve(e).substr(1),r=t.resolve(r).substr(1);for(var o=n(e.split("/")),i=n(r.split("/")),s=Math.min(o.length,i.length),a=s,u=0;u=1;--i)if(t=e.charCodeAt(i),47===t){if(!o){n=i;break}}else o=!1;return-1===n?r?"/":".":r&&1===n?"/":e.slice(0,n)},t.basename=function(e,t){var r=n(e);return t&&r.substr(-1*t.length)===t&&(r=r.substr(0,r.length-t.length)),r},t.extname=function(e){"string"!==typeof e&&(e+="");for(var t=-1,r=0,n=-1,o=!0,i=0,s=e.length-1;s>=0;--s){var a=e.charCodeAt(s);if(47!==a)-1===n&&(o=!1,n=s+1),46===a?-1===t?t=s:1!==i&&(i=1):-1!==t&&(i=-1);else if(!o){r=s+1;break}}return-1===t||-1===n||0===i||1===i&&t===n-1&&t===r+1?"":e.slice(t,n)};var i="b"==="ab".substr(-1)?function(e,t,r){return e.substr(t,r)}:function(e,t,r){return t<0&&(t=e.length+t),e.substr(t,r)}}).call(this,r("eef6"))},"82ae":function(e,t,r){e.exports=r("43d9")},"83fe":function(e,t,r){"use strict";var n=r("d844");e.exports=n.isStandardBrowserEnv()?function(){return{write:function(e,t,r,o,i,s){var a=[];a.push(e+"="+encodeURIComponent(t)),n.isNumber(r)&&a.push("expires="+new Date(r).toGMTString()),n.isString(o)&&a.push("path="+o),n.isString(i)&&a.push("domain="+i),!0===s&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},"9d72":function(e,t,r){"use strict";var n=r("d844");e.exports=function(e,t){n.forEach(e,(function(r,n){n!==t&&n.toUpperCase()===t.toUpperCase()&&(e[t]=r,delete e[n])}))}},a169:function(e,t,r){"use strict";var n=r("d844"),o=r("1eb2"),i=r("050d"),s=r("4f37"),a=r("0bbf"),u=r("edb4"),c=r("c5b9");e.exports=function(e){return new Promise((function(t,f){var p=e.data,l=e.headers;n.isFormData(p)&&delete l["Content-Type"];var d=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password||"";l.Authorization="Basic "+btoa(h+":"+m)}var g=s(e.baseURL,e.url);if(d.open(e.method.toUpperCase(),i(g,e.params,e.paramsSerializer),!0),d.timeout=e.timeout,d.onreadystatechange=function(){if(d&&4===d.readyState&&(0!==d.status||d.responseURL&&0===d.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in d?a(d.getAllResponseHeaders()):null,n=e.responseType&&"text"!==e.responseType?d.response:d.responseText,i={data:n,status:d.status,statusText:d.statusText,headers:r,config:e,request:d};o(t,f,i),d=null}},d.onabort=function(){d&&(f(c("Request aborted",e,"ECONNABORTED",d)),d=null)},d.onerror=function(){f(c("Network Error",e,null,d)),d=null},d.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),f(c(t,e,"ECONNABORTED",d)),d=null},n.isStandardBrowserEnv()){var y=r("83fe"),v=(e.withCredentials||u(g))&&e.xsrfCookieName?y.read(e.xsrfCookieName):void 0;v&&(l[e.xsrfHeaderName]=v)}if("setRequestHeader"in d&&n.forEach(l,(function(e,t){"undefined"===typeof p&&"content-type"===t.toLowerCase()?delete l[t]:d.setRequestHeader(t,e)})),n.isUndefined(e.withCredentials)||(d.withCredentials=!!e.withCredentials),e.responseType)try{d.responseType=e.responseType}catch(b){if("json"!==e.responseType)throw b}"function"===typeof e.onDownloadProgress&&d.addEventListener("progress",e.onDownloadProgress),"function"===typeof e.onUploadProgress&&d.upload&&d.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then((function(e){d&&(d.abort(),f(e),d=null)})),void 0===p&&(p=null),d.send(p)}))}},bd2a:function(e,t,r){"use strict";e.exports=function(e,t,r,n,o){return e.config=t,r&&(e.code=r),e.request=n,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},c4e8:function(e,t,r){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},c5b9:function(e,t,r){"use strict";var n=r("bd2a");e.exports=function(e,t,r,o,i){var s=new Error(e);return n(s,t,r,o,i)}},c70f:function(e,t,r){"use strict";var n=r("d844"),o=r("04a7"),i=r("11f4"),s=r("2ed0");function a(e){e.cancelToken&&e.cancelToken.throwIfRequested()}e.exports=function(e){a(e),e.headers=e.headers||{},e.data=o(e.data,e.headers,e.transformRequest),e.headers=n.merge(e.headers.common||{},e.headers[e.method]||{},e.headers),n.forEach(["delete","get","head","post","put","patch","common"],(function(t){delete e.headers[t]}));var t=e.adapter||s.adapter;return t(e).then((function(t){return a(e),t.data=o(t.data,t.headers,e.transformResponse),t}),(function(t){return i(t)||(a(e),t&&t.response&&(t.response.data=o(t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)}))}},c9ba:function(e,t,r){"use strict";var n=r("d844");e.exports=function(e,t){t=t||{};var r={},o=["url","method","params","data"],i=["headers","auth","proxy"],s=["baseURL","url","transformRequest","transformResponse","paramsSerializer","timeout","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","maxContentLength","validateStatus","maxRedirects","httpAgent","httpsAgent","cancelToken","socketPath"];n.forEach(o,(function(e){"undefined"!==typeof t[e]&&(r[e]=t[e])})),n.forEach(i,(function(o){n.isObject(t[o])?r[o]=n.deepMerge(e[o],t[o]):"undefined"!==typeof t[o]?r[o]=t[o]:n.isObject(e[o])?r[o]=n.deepMerge(e[o]):"undefined"!==typeof e[o]&&(r[o]=e[o])})),n.forEach(s,(function(n){"undefined"!==typeof t[n]?r[n]=t[n]:"undefined"!==typeof e[n]&&(r[n]=e[n])}));var a=o.concat(i).concat(s),u=Object.keys(t).filter((function(e){return-1===a.indexOf(e)}));return n.forEach(u,(function(n){"undefined"!==typeof t[n]?r[n]=t[n]:"undefined"!==typeof e[n]&&(r[n]=e[n])})),r}},ca19:function(e,t,r){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},d844:function(e,t,r){"use strict";var n=r("faf0"),o=Object.prototype.toString;function i(e){return"[object Array]"===o.call(e)}function s(e){return"undefined"===typeof e}function a(e){return null!==e&&!s(e)&&null!==e.constructor&&!s(e.constructor)&&"function"===typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function u(e){return"[object ArrayBuffer]"===o.call(e)}function c(e){return"undefined"!==typeof FormData&&e instanceof FormData}function f(e){var t;return t="undefined"!==typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer,t}function p(e){return"string"===typeof e}function l(e){return"number"===typeof e}function d(e){return null!==e&&"object"===typeof e}function h(e){return"[object Date]"===o.call(e)}function m(e){return"[object File]"===o.call(e)}function g(e){return"[object Blob]"===o.call(e)}function y(e){return"[object Function]"===o.call(e)}function v(e){return d(e)&&y(e.pipe)}function b(e){return"undefined"!==typeof URLSearchParams&&e instanceof URLSearchParams}function w(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function x(){return("undefined"===typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!==typeof window&&"undefined"!==typeof document)}function E(e,t){if(null!==e&&"undefined"!==typeof e)if("object"!==typeof e&&(e=[e]),i(e))for(var r=0,n=e.length;r>>32-t}function o(e,t){var n,o,r,a,u;return r=2147483648&e,a=2147483648&t,n=1073741824&e,o=1073741824&t,u=(1073741823&e)+(1073741823&t),n&o?2147483648^u^r^a:n|o?1073741824&u?3221225472^u^r^a:1073741824^u^r^a:u^r^a}function r(e,t,n){return e&t|~e&n}function a(e,t,n){return e&n|t&~n}function u(e,t,n){return e^t^n}function i(e,t,n){return t^(e|~n)}function c(e,t,a,u,i,c,l){return e=o(e,o(o(r(t,a,u),i),l)),o(n(e,c),t)}function l(e,t,r,u,i,c,l){return e=o(e,o(o(a(t,r,u),i),l)),o(n(e,c),t)}function f(e,t,r,a,i,c,l){return e=o(e,o(o(u(t,r,a),i),l)),o(n(e,c),t)}function s(e,t,r,a,u,c,l){return e=o(e,o(o(i(t,r,a),u),l)),o(n(e,c),t)}function d(e){var t,n=e.length,o=n+8,r=(o-o%64)/64,a=16*(r+1),u=Array(a-1),i=0,c=0;while(c>>29,u}function p(e){var t,n,o="",r="";for(n=0;n<=3;n++)t=e>>>8*n&255,r="0"+t.toString(16),o+=r.substr(r.length-2,2);return o}var b,h,g,m,v,y,w,C,S,P=Array(),O=7,j=12,k=17,T=22,_=5,x=9,E=14,A=20,L=4,$=11,B=16,N=23,V=6,M=10,D=15,q=21;for(P=d(t),y=1732584193,w=4023233417,C=2562383102,S=271733878,b=0;b=0&&Math.floor(t)===t&&isFinite(e)}function p(e){return o(e)&&"function"===typeof e.then&&"function"===typeof e.catch}function h(e){return null==e?"":Array.isArray(e)||c(e)&&e.toString===u?JSON.stringify(e,null,2):String(e)}function v(e){var t=parseFloat(e);return isNaN(t)?e:t}function m(e,t){for(var n=Object.create(null),r=e.split(","),o=0;o-1)return e.splice(n,1)}}var b=Object.prototype.hasOwnProperty;function _(e,t){return b.call(e,t)}function x(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}var w=/-(\w)/g,C=x((function(e){return e.replace(w,(function(e,t){return t?t.toUpperCase():""}))})),S=x((function(e){return e.charAt(0).toUpperCase()+e.slice(1)})),O=/\B([A-Z])/g,E=x((function(e){return e.replace(O,"-$1").toLowerCase()}));function k(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function $(e,t){return e.bind(t)}var j=Function.prototype.bind?$:k;function A(e,t){t=t||0;var n=e.length-t,r=new Array(n);while(n--)r[n]=e[n+t];return r}function T(e,t){for(var n in t)e[n]=t[n];return e}function M(e){for(var t={},n=0;n0,ne=Q&&Q.indexOf("edge/")>0,re=(Q&&Q.indexOf("android"),Q&&/iphone|ipad|ipod|ios/.test(Q)||"ios"===Z),oe=(Q&&/chrome\/\d+/.test(Q),Q&&/phantomjs/.test(Q),Q&&Q.match(/firefox\/(\d+)/)),ie={}.watch,ae=!1;if(Y)try{var se={};Object.defineProperty(se,"passive",{get:function(){ae=!0}}),window.addEventListener("test-passive",null,se)}catch(Ca){}var le=function(){return void 0===G&&(G=!Y&&!J&&"undefined"!==typeof e&&(e["process"]&&"server"===e["process"].env.VUE_ENV)),G},ue=Y&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function ce(e){return"function"===typeof e&&/native code/.test(e.toString())}var fe,de="undefined"!==typeof Symbol&&ce(Symbol)&&"undefined"!==typeof Reflect&&ce(Reflect.ownKeys);fe="undefined"!==typeof Set&&ce(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return!0===this.set[e]},e.prototype.add=function(e){this.set[e]=!0},e.prototype.clear=function(){this.set=Object.create(null)},e}();var pe=P,he=0,ve=function(){this.id=he++,this.subs=[]};ve.prototype.addSub=function(e){this.subs.push(e)},ve.prototype.removeSub=function(e){g(this.subs,e)},ve.prototype.depend=function(){ve.target&&ve.target.addDep(this)},ve.prototype.notify=function(){var e=this.subs.slice();for(var t=0,n=e.length;t-1)if(i&&!_(o,"default"))a=!1;else if(""===a||a===E(e)){var l=et(String,o.type);(l<0||s0&&(a=kt(a,(t||"")+"_"+n),Et(a[0])&&Et(u)&&(c[l]=we(u.text+a[0].text),a.shift()),c.push.apply(c,a)):s(a)?Et(u)?c[l]=we(u.text+a):""!==a&&c.push(we(a)):Et(a)&&Et(u)?c[l]=we(u.text+a.text):(i(e._isVList)&&o(a.tag)&&r(a.key)&&o(t)&&(a.key="__vlist"+t+"_"+n+"__"),c.push(a)));return c}function $t(e){var t=e.$options.provide;t&&(e._provided="function"===typeof t?t.call(e):t)}function jt(e){var t=At(e.$options.inject,e);t&&(je(!1),Object.keys(t).forEach((function(n){Le(e,n,t[n])})),je(!0))}function At(e,t){if(e){for(var n=Object.create(null),r=de?Reflect.ownKeys(e):Object.keys(e),o=0;o0,a=e?!!e.$stable:!i,s=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(a&&r&&r!==n&&s===r.$key&&!i&&!r.$hasNormal)return r;for(var l in o={},e)e[l]&&"$"!==l[0]&&(o[l]=Lt(t,l,e[l]))}else o={};for(var u in t)u in o||(o[u]=Rt(t,u));return e&&Object.isExtensible(e)&&(e._normalized=o),U(o,"$stable",a),U(o,"$key",s),U(o,"$hasNormal",i),o}function Lt(e,t,n){var r=function(){var e=arguments.length?n.apply(null,arguments):n({});return e=e&&"object"===typeof e&&!Array.isArray(e)?[e]:Ot(e),e&&(0===e.length||1===e.length&&e[0].isComment)?void 0:e};return n.proxy&&Object.defineProperty(e,t,{get:r,enumerable:!0,configurable:!0}),r}function Rt(e,t){return function(){return e[t]}}function Nt(e,t){var n,r,i,a,s;if(Array.isArray(e)||"string"===typeof e)for(n=new Array(e.length),r=0,i=e.length;r1?A(n):n;for(var r=A(arguments,1),o='event handler for "'+e+'"',i=0,a=n.length;idocument.createEvent("Event").timeStamp&&(Gn=function(){return Xn.now()})}function Yn(){var e,t;for(Kn=Gn(),Vn=!0,zn.sort((function(e,t){return e.id-t.id})),Un=0;UnUn&&zn[n].id>e.id)n--;zn.splice(n+1,0,e)}else zn.push(e);Wn||(Wn=!0,ht(Yn))}}var tr=0,nr=function(e,t,n,r,o){this.vm=e,o&&(e._watcher=this),e._watchers.push(this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++tr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new fe,this.newDepIds=new fe,this.expression="","function"===typeof t?this.getter=t:(this.getter=K(t),this.getter||(this.getter=P)),this.value=this.lazy?void 0:this.get()};nr.prototype.get=function(){var e;ye(this);var t=this.vm;try{e=this.getter.call(t,t)}catch(Ca){if(!this.user)throw Ca;tt(Ca,t,'getter for watcher "'+this.expression+'"')}finally{this.deep&&mt(e),ge(),this.cleanupDeps()}return e},nr.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},nr.prototype.cleanupDeps=function(){var e=this.deps.length;while(e--){var t=this.deps[e];this.newDepIds.has(t.id)||t.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},nr.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():er(this)},nr.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||l(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(Ca){tt(Ca,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},nr.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},nr.prototype.depend=function(){var e=this.deps.length;while(e--)this.deps[e].depend()},nr.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||g(this.vm._watchers,this);var e=this.deps.length;while(e--)this.deps[e].removeSub(this);this.active=!1}};var rr={enumerable:!0,configurable:!0,get:P,set:P};function or(e,t,n){rr.get=function(){return this[t][n]},rr.set=function(e){this[t][n]=e},Object.defineProperty(e,n,rr)}function ir(e){e._watchers=[];var t=e.$options;t.props&&ar(e,t.props),t.methods&&hr(e,t.methods),t.data?sr(e):Pe(e._data={},!0),t.computed&&cr(e,t.computed),t.watch&&t.watch!==ie&&vr(e,t.watch)}function ar(e,t){var n=e.$options.propsData||{},r=e._props={},o=e.$options._propKeys=[],i=!e.$parent;i||je(!1);var a=function(i){o.push(i);var a=Ye(i,t,n,e);Le(r,i,a),i in e||or(e,"_props",i)};for(var s in t)a(s);je(!0)}function sr(e){var t=e.$options.data;t=e._data="function"===typeof t?lr(t,e):t||{},c(t)||(t={});var n=Object.keys(t),r=e.$options.props,o=(e.$options.methods,n.length);while(o--){var i=n[o];0,r&&_(r,i)||V(i)||or(e,"_data",i)}Pe(t,!0)}function lr(e,t){ye();try{return e.call(t,t)}catch(Ca){return tt(Ca,t,"data()"),{}}finally{ge()}}var ur={lazy:!0};function cr(e,t){var n=e._computedWatchers=Object.create(null),r=le();for(var o in t){var i=t[o],a="function"===typeof i?i:i.get;0,r||(n[o]=new nr(e,a||P,P,ur)),o in e||fr(e,o,i)}}function fr(e,t,n){var r=!le();"function"===typeof n?(rr.get=r?dr(t):pr(n),rr.set=P):(rr.get=n.get?r&&!1!==n.cache?dr(t):pr(n.get):P,rr.set=n.set||P),Object.defineProperty(e,t,rr)}function dr(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),ve.target&&t.depend(),t.value}}function pr(e){return function(){return e.call(this,this)}}function hr(e,t){e.$options.props;for(var n in t)e[n]="function"!==typeof t[n]?P:j(t[n],e)}function vr(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var o=0;o-1)return this;var n=A(arguments,1);return n.unshift(this),"function"===typeof e.install?e.install.apply(e,n):"function"===typeof e&&e.apply(null,n),t.push(e),this}}function Or(e){e.mixin=function(e){return this.options=Ge(this.options,e),this}}function Er(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=n.cid,o=e._Ctor||(e._Ctor={});if(o[r])return o[r];var i=e.name||n.options.name;var a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=Ge(n.options,e),a["super"]=n,a.options.props&&kr(a),a.options.computed&&$r(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,z.forEach((function(e){a[e]=n[e]})),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=T({},a.options),o[r]=a,a}}function kr(e){var t=e.options.props;for(var n in t)or(e.prototype,"_props",n)}function $r(e){var t=e.options.computed;for(var n in t)fr(e.prototype,n,t[n])}function jr(e){z.forEach((function(t){e[t]=function(e,n){return n?("component"===t&&c(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"===typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}))}function Ar(e){return e&&(e.Ctor.options.name||e.tag)}function Tr(e,t){return Array.isArray(e)?e.indexOf(t)>-1:"string"===typeof e?e.split(",").indexOf(t)>-1:!!f(e)&&e.test(t)}function Mr(e,t){var n=e.cache,r=e.keys,o=e._vnode;for(var i in n){var a=n[i];if(a){var s=Ar(a.componentOptions);s&&!t(s)&&Pr(n,i,r,o)}}}function Pr(e,t,n,r){var o=e[t];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),e[t]=null,g(n,t)}br(Cr),yr(Cr),jn(Cr),Pn(Cr),gn(Cr);var Lr=[String,RegExp,Array],Rr={name:"keep-alive",abstract:!0,props:{include:Lr,exclude:Lr,max:[String,Number]},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var e in this.cache)Pr(this.cache,e,this.keys)},mounted:function(){var e=this;this.$watch("include",(function(t){Mr(e,(function(e){return Tr(t,e)}))})),this.$watch("exclude",(function(t){Mr(e,(function(e){return!Tr(t,e)}))}))},render:function(){var e=this.$slots.default,t=Cn(e),n=t&&t.componentOptions;if(n){var r=Ar(n),o=this,i=o.include,a=o.exclude;if(i&&(!r||!Tr(i,r))||a&&r&&Tr(a,r))return t;var s=this,l=s.cache,u=s.keys,c=null==t.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):t.key;l[c]?(t.componentInstance=l[c].componentInstance,g(u,c),u.push(c)):(l[c]=t,u.push(c),this.max&&u.length>parseInt(this.max)&&Pr(l,u[0],u,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}},Nr={KeepAlive:Rr};function Ir(e){var t={get:function(){return D}};Object.defineProperty(e,"config",t),e.util={warn:pe,extend:T,mergeOptions:Ge,defineReactive:Le},e.set=Re,e.delete=Ne,e.nextTick=ht,e.observable=function(e){return Pe(e),e},e.options=Object.create(null),z.forEach((function(t){e.options[t+"s"]=Object.create(null)})),e.options._base=e,T(e.options.components,Nr),Sr(e),Or(e),Er(e),jr(e)}Ir(Cr),Object.defineProperty(Cr.prototype,"$isServer",{get:le}),Object.defineProperty(Cr.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Cr,"FunctionalRenderContext",{value:Jt}),Cr.version="2.6.11";var Fr=m("style,class"),Hr=m("input,textarea,option,select,progress"),zr=function(e,t,n){return"value"===n&&Hr(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Br=m("contenteditable,draggable,spellcheck"),Dr=m("events,caret,typing,plaintext-only"),Wr=function(e,t){return Gr(t)||"false"===t?"false":"contenteditable"===e&&Dr(t)?t:"true"},Vr=m("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),Ur="http://www.w3.org/1999/xlink",qr=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Kr=function(e){return qr(e)?e.slice(6,e.length):""},Gr=function(e){return null==e||!1===e};function Xr(e){var t=e.data,n=e,r=e;while(o(r.componentInstance))r=r.componentInstance._vnode,r&&r.data&&(t=Yr(r.data,t));while(o(n=n.parent))n&&n.data&&(t=Yr(t,n.data));return Jr(t.staticClass,t.class)}function Yr(e,t){return{staticClass:Zr(e.staticClass,t.staticClass),class:o(e.class)?[e.class,t.class]:t.class}}function Jr(e,t){return o(e)||o(t)?Zr(e,Qr(t)):""}function Zr(e,t){return e?t?e+" "+t:e:t||""}function Qr(e){return Array.isArray(e)?eo(e):l(e)?to(e):"string"===typeof e?e:""}function eo(e){for(var t,n="",r=0,i=e.length;r-1?so[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:so[e]=/HTMLUnknownElement/.test(t.toString())}var uo=m("text,number,password,search,email,tel,url");function co(e){if("string"===typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}function fo(e,t){var n=document.createElement(e);return"select"!==e||t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n}function po(e,t){return document.createElementNS(no[e],t)}function ho(e){return document.createTextNode(e)}function vo(e){return document.createComment(e)}function mo(e,t,n){e.insertBefore(t,n)}function yo(e,t){e.removeChild(t)}function go(e,t){e.appendChild(t)}function bo(e){return e.parentNode}function _o(e){return e.nextSibling}function xo(e){return e.tagName}function wo(e,t){e.textContent=t}function Co(e,t){e.setAttribute(t,"")}var So=Object.freeze({createElement:fo,createElementNS:po,createTextNode:ho,createComment:vo,insertBefore:mo,removeChild:yo,appendChild:go,parentNode:bo,nextSibling:_o,tagName:xo,setTextContent:wo,setStyleScope:Co}),Oo={create:function(e,t){Eo(t)},update:function(e,t){e.data.ref!==t.data.ref&&(Eo(e,!0),Eo(t))},destroy:function(e){Eo(e,!0)}};function Eo(e,t){var n=e.data.ref;if(o(n)){var r=e.context,i=e.componentInstance||e.elm,a=r.$refs;t?Array.isArray(a[n])?g(a[n],i):a[n]===i&&(a[n]=void 0):e.data.refInFor?Array.isArray(a[n])?a[n].indexOf(i)<0&&a[n].push(i):a[n]=[i]:a[n]=i}}var ko=new be("",{},[]),$o=["create","activate","update","remove","destroy"];function jo(e,t){return e.key===t.key&&(e.tag===t.tag&&e.isComment===t.isComment&&o(e.data)===o(t.data)&&Ao(e,t)||i(e.isAsyncPlaceholder)&&e.asyncFactory===t.asyncFactory&&r(t.asyncFactory.error))}function Ao(e,t){if("input"!==e.tag)return!0;var n,r=o(n=e.data)&&o(n=n.attrs)&&n.type,i=o(n=t.data)&&o(n=n.attrs)&&n.type;return r===i||uo(r)&&uo(i)}function To(e,t,n){var r,i,a={};for(r=t;r<=n;++r)i=e[r].key,o(i)&&(a[i]=r);return a}function Mo(e){var t,n,a={},l=e.modules,u=e.nodeOps;for(t=0;t<$o.length;++t)for(a[$o[t]]=[],n=0;nv?(f=r(n[g+1])?null:n[g+1].elm,C(e,f,n,h,g,i)):h>g&&O(t,d,v)}function $(e,t,n,r){for(var i=n;i-1?Wo(e,t,n):Vr(t)?Gr(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Br(t)?e.setAttribute(t,Wr(t,n)):qr(t)?Gr(n)?e.removeAttributeNS(Ur,Kr(t)):e.setAttributeNS(Ur,t,n):Wo(e,t,n)}function Wo(e,t,n){if(Gr(n))e.removeAttribute(t);else{if(ee&&!te&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){var r=function(t){t.stopImmediatePropagation(),e.removeEventListener("input",r)};e.addEventListener("input",r),e.__ieph=!0}e.setAttribute(t,n)}}var Vo={create:Bo,update:Bo};function Uo(e,t){var n=t.elm,i=t.data,a=e.data;if(!(r(i.staticClass)&&r(i.class)&&(r(a)||r(a.staticClass)&&r(a.class)))){var s=Xr(t),l=n._transitionClasses;o(l)&&(s=Zr(s,Qr(l))),s!==n._prevClass&&(n.setAttribute("class",s),n._prevClass=s)}}var qo,Ko={create:Uo,update:Uo},Go="__r",Xo="__c";function Yo(e){if(o(e[Go])){var t=ee?"change":"input";e[t]=[].concat(e[Go],e[t]||[]),delete e[Go]}o(e[Xo])&&(e.change=[].concat(e[Xo],e.change||[]),delete e[Xo])}function Jo(e,t,n){var r=qo;return function o(){var i=t.apply(null,arguments);null!==i&&ei(e,o,n,r)}}var Zo=at&&!(oe&&Number(oe[1])<=53);function Qo(e,t,n,r){if(Zo){var o=Kn,i=t;t=i._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=o||e.timeStamp<=0||e.target.ownerDocument!==document)return i.apply(this,arguments)}}qo.addEventListener(e,t,ae?{capture:n,passive:r}:n)}function ei(e,t,n,r){(r||qo).removeEventListener(e,t._wrapper||t,n)}function ti(e,t){if(!r(e.data.on)||!r(t.data.on)){var n=t.data.on||{},o=e.data.on||{};qo=t.elm,Yo(n),_t(n,o,Qo,ei,Jo,t.context),qo=void 0}}var ni,ri={create:ti,update:ti};function oi(e,t){if(!r(e.data.domProps)||!r(t.data.domProps)){var n,i,a=t.elm,s=e.data.domProps||{},l=t.data.domProps||{};for(n in o(l.__ob__)&&(l=t.data.domProps=T({},l)),s)n in l||(a[n]="");for(n in l){if(i=l[n],"textContent"===n||"innerHTML"===n){if(t.children&&(t.children.length=0),i===s[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n&&"PROGRESS"!==a.tagName){a._value=i;var u=r(i)?"":String(i);ii(a,u)&&(a.value=u)}else if("innerHTML"===n&&oo(a.tagName)&&r(a.innerHTML)){ni=ni||document.createElement("div"),ni.innerHTML=""+i+"";var c=ni.firstChild;while(a.firstChild)a.removeChild(a.firstChild);while(c.firstChild)a.appendChild(c.firstChild)}else if(i!==s[n])try{a[n]=i}catch(Ca){}}}}function ii(e,t){return!e.composing&&("OPTION"===e.tagName||ai(e,t)||si(e,t))}function ai(e,t){var n=!0;try{n=document.activeElement!==e}catch(Ca){}return n&&e.value!==t}function si(e,t){var n=e.value,r=e._vModifiers;if(o(r)){if(r.number)return v(n)!==v(t);if(r.trim)return n.trim()!==t.trim()}return n!==t}var li={create:oi,update:oi},ui=x((function(e){var t={},n=/;(?![^(]*\))/g,r=/:(.+)/;return e.split(n).forEach((function(e){if(e){var n=e.split(r);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}));function ci(e){var t=fi(e.style);return e.staticStyle?T(e.staticStyle,t):t}function fi(e){return Array.isArray(e)?M(e):"string"===typeof e?ui(e):e}function di(e,t){var n,r={};if(t){var o=e;while(o.componentInstance)o=o.componentInstance._vnode,o&&o.data&&(n=ci(o.data))&&T(r,n)}(n=ci(e.data))&&T(r,n);var i=e;while(i=i.parent)i.data&&(n=ci(i.data))&&T(r,n);return r}var pi,hi=/^--/,vi=/\s*!important$/,mi=function(e,t,n){if(hi.test(t))e.style.setProperty(t,n);else if(vi.test(n))e.style.setProperty(E(t),n.replace(vi,""),"important");else{var r=gi(t);if(Array.isArray(n))for(var o=0,i=n.length;o-1?t.split(xi).forEach((function(t){return e.classList.add(t)})):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Ci(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(xi).forEach((function(t){return e.classList.remove(t)})):e.classList.remove(t),e.classList.length||e.removeAttribute("class");else{var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";while(n.indexOf(r)>=0)n=n.replace(r," ");n=n.trim(),n?e.setAttribute("class",n):e.removeAttribute("class")}}function Si(e){if(e){if("object"===typeof e){var t={};return!1!==e.css&&T(t,Oi(e.name||"v")),T(t,e),t}return"string"===typeof e?Oi(e):void 0}}var Oi=x((function(e){return{enterClass:e+"-enter",enterToClass:e+"-enter-to",enterActiveClass:e+"-enter-active",leaveClass:e+"-leave",leaveToClass:e+"-leave-to",leaveActiveClass:e+"-leave-active"}})),Ei=Y&&!te,ki="transition",$i="animation",ji="transition",Ai="transitionend",Ti="animation",Mi="animationend";Ei&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(ji="WebkitTransition",Ai="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Ti="WebkitAnimation",Mi="webkitAnimationEnd"));var Pi=Y?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(e){return e()};function Li(e){Pi((function(){Pi(e)}))}function Ri(e,t){var n=e._transitionClasses||(e._transitionClasses=[]);n.indexOf(t)<0&&(n.push(t),wi(e,t))}function Ni(e,t){e._transitionClasses&&g(e._transitionClasses,t),Ci(e,t)}function Ii(e,t,n){var r=Hi(e,t),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s=o===ki?Ai:Mi,l=0,u=function(){e.removeEventListener(s,c),n()},c=function(t){t.target===e&&++l>=a&&u()};setTimeout((function(){l0&&(n=ki,c=a,f=i.length):t===$i?u>0&&(n=$i,c=u,f=l.length):(c=Math.max(a,u),n=c>0?a>u?ki:$i:null,f=n?n===ki?i.length:l.length:0);var d=n===ki&&Fi.test(r[ji+"Property"]);return{type:n,timeout:c,propCount:f,hasTransform:d}}function zi(e,t){while(e.length1}function qi(e,t){!0!==t.data.show&&Di(t)}var Ki=Y?{create:qi,activate:qi,remove:function(e,t){!0!==e.data.show?Wi(e,t):t()}}:{},Gi=[Vo,Ko,ri,li,_i,Ki],Xi=Gi.concat(zo),Yi=Mo({nodeOps:So,modules:Xi});te&&document.addEventListener("selectionchange",(function(){var e=document.activeElement;e&&e.vmodel&&oa(e,"input")}));var Ji={inserted:function(e,t,n,r){"select"===n.tag?(r.elm&&!r.elm._vOptions?xt(n,"postpatch",(function(){Ji.componentUpdated(e,t,n)})):Zi(e,t,n.context),e._vOptions=[].map.call(e.options,ta)):("textarea"===n.tag||uo(e.type))&&(e._vModifiers=t.modifiers,t.modifiers.lazy||(e.addEventListener("compositionstart",na),e.addEventListener("compositionend",ra),e.addEventListener("change",ra),te&&(e.vmodel=!0)))},componentUpdated:function(e,t,n){if("select"===n.tag){Zi(e,t,n.context);var r=e._vOptions,o=e._vOptions=[].map.call(e.options,ta);if(o.some((function(e,t){return!N(e,r[t])}))){var i=e.multiple?t.value.some((function(e){return ea(e,o)})):t.value!==t.oldValue&&ea(t.value,o);i&&oa(e,"change")}}}};function Zi(e,t,n){Qi(e,t,n),(ee||ne)&&setTimeout((function(){Qi(e,t,n)}),0)}function Qi(e,t,n){var r=t.value,o=e.multiple;if(!o||Array.isArray(r)){for(var i,a,s=0,l=e.options.length;s-1,a.selected!==i&&(a.selected=i);else if(N(ta(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));o||(e.selectedIndex=-1)}}function ea(e,t){return t.every((function(t){return!N(t,e)}))}function ta(e){return"_value"in e?e._value:e.value}function na(e){e.target.composing=!0}function ra(e){e.target.composing&&(e.target.composing=!1,oa(e.target,"input"))}function oa(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function ia(e){return!e.componentInstance||e.data&&e.data.transition?e:ia(e.componentInstance._vnode)}var aa={bind:function(e,t,n){var r=t.value;n=ia(n);var o=n.data&&n.data.transition,i=e.__vOriginalDisplay="none"===e.style.display?"":e.style.display;r&&o?(n.data.show=!0,Di(n,(function(){e.style.display=i}))):e.style.display=r?i:"none"},update:function(e,t,n){var r=t.value,o=t.oldValue;if(!r!==!o){n=ia(n);var i=n.data&&n.data.transition;i?(n.data.show=!0,r?Di(n,(function(){e.style.display=e.__vOriginalDisplay})):Wi(n,(function(){e.style.display="none"}))):e.style.display=r?e.__vOriginalDisplay:"none"}},unbind:function(e,t,n,r,o){o||(e.style.display=e.__vOriginalDisplay)}},sa={model:Ji,show:aa},la={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function ua(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?ua(Cn(t.children)):e}function ca(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var o=n._parentListeners;for(var i in o)t[C(i)]=o[i];return t}function fa(e,t){if(/\d-keep-alive$/.test(t.tag))return e("keep-alive",{props:t.componentOptions.propsData})}function da(e){while(e=e.parent)if(e.data.transition)return!0}function pa(e,t){return t.key===e.key&&t.tag===e.tag}var ha=function(e){return e.tag||wn(e)},va=function(e){return"show"===e.name},ma={name:"transition",props:la,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(ha),n.length)){0;var r=this.mode;0;var o=n[0];if(da(this.$vnode))return o;var i=ua(o);if(!i)return o;if(this._leaving)return fa(e,o);var a="__transition-"+this._uid+"-";i.key=null==i.key?i.isComment?a+"comment":a+i.tag:s(i.key)?0===String(i.key).indexOf(a)?i.key:a+i.key:i.key;var l=(i.data||(i.data={})).transition=ca(this),u=this._vnode,c=ua(u);if(i.data.directives&&i.data.directives.some(va)&&(i.data.show=!0),c&&c.data&&!pa(i,c)&&!wn(c)&&(!c.componentInstance||!c.componentInstance._vnode.isComment)){var f=c.data.transition=T({},l);if("out-in"===r)return this._leaving=!0,xt(f,"afterLeave",(function(){t._leaving=!1,t.$forceUpdate()})),fa(e,o);if("in-out"===r){if(wn(i))return u;var d,p=function(){d()};xt(l,"afterEnter",p),xt(l,"enterCancelled",p),xt(f,"delayLeave",(function(e){d=e}))}}return o}}},ya=T({tag:String,moveClass:String},la);delete ya.mode;var ga={props:ya,beforeMount:function(){var e=this,t=this._update;this._update=function(n,r){var o=Tn(e);e.__patch__(e._vnode,e.kept,!1,!0),e._vnode=e.kept,o(),t.call(e,n,r)}},render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=ca(this),s=0;s0){var r=t[t.length-1];if(r.id===e){if(r.modalClass){var o=r.modalClass.trim().split(/\s+/);o.forEach((function(e){return(0,i.removeClass)(n,e)}))}t.pop(),t.length>0&&(n.style.zIndex=t[t.length-1].zIndex)}else for(var a=t.length-1;a>=0;a--)if(t[a].id===e){t.splice(a,1);break}}0===t.length&&(this.modalFade&&(0,i.addClass)(n,"v-modal-leave"),setTimeout((function(){0===t.length&&(n.parentNode&&n.parentNode.removeChild(n),n.style.display="none",d.modalDom=void 0),(0,i.removeClass)(n,"v-modal-leave")}),200))}};Object.defineProperty(d,"zIndex",{configurable:!0,get:function(){return l||(u=u||(o.default.prototype.$ELEMENT||{}).zIndex||2e3,l=!0),u},set:function(e){u=e}});var p=function(){if(!o.default.prototype.$isServer&&d.modalStack.length>0){var e=d.modalStack[d.modalStack.length-1];if(!e)return;var t=d.getInstance(e.id);return t}};o.default.prototype.$isServer||window.addEventListener("keydown",(function(e){if(27===e.keyCode){var t=p();t&&t.closeOnPressEscape&&(t.handleClose?t.handleClose():t.handleAction?t.handleAction("cancel"):t.close())}})),t.default=d},"0655":function(e,t,n){"use strict";n.r(t),function(e){var n=function(){if("undefined"!==typeof Map)return Map;function e(e,t){var n=-1;return e.some((function(e,r){return e[0]===t&&(n=r,!0)})),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(t){var n=e(this.__entries__,t),r=this.__entries__[n];return r&&r[1]},t.prototype.set=function(t,n){var r=e(this.__entries__,t);~r?this.__entries__[r][1]=n:this.__entries__.push([t,n])},t.prototype.delete=function(t){var n=this.__entries__,r=e(n,t);~r&&n.splice(r,1)},t.prototype.has=function(t){return!!~e(this.__entries__,t)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(e,t){void 0===t&&(t=null);for(var n=0,r=this.__entries__;n0},e.prototype.connect_=function(){r&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),c?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){r&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t,r=u.some((function(e){return!!~n.indexOf(e)}));r&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),d=function(e,t){for(var n=0,r=Object.keys(t);n0},e}(),$="undefined"!==typeof WeakMap?new WeakMap:new n,j=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=f.getInstance(),r=new k(t,n,this);$.set(this,r)}return e}();["observe","unobserve","disconnect"].forEach((function(e){j.prototype[e]=function(){var t;return(t=$.get(this))[e].apply(t,arguments)}}));var A=function(){return"undefined"!==typeof o.ResizeObserver?o.ResizeObserver:j}();t["default"]=A}.call(this,n("9edd"))},"06cb":function(e,t,n){"use strict";t.__esModule=!0;var r=n("0261"),o=a(r),i=n("c865");function a(e){return e&&e.__esModule?e:{default:e}}var s=[],l="@@clickoutsideContext",u=void 0,c=0;function f(e,t,n){return function(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};!(n&&n.context&&r.target&&o.target)||e.contains(r.target)||e.contains(o.target)||e===r.target||n.context.popperElm&&(n.context.popperElm.contains(r.target)||n.context.popperElm.contains(o.target))||(t.expression&&e[l].methodName&&n.context[e[l].methodName]?n.context[e[l].methodName]():e[l].bindingFn&&e[l].bindingFn())}}!o.default.prototype.$isServer&&(0,i.on)(document,"mousedown",(function(e){return u=e})),!o.default.prototype.$isServer&&(0,i.on)(document,"mouseup",(function(e){s.forEach((function(t){return t[l].documentHandler(e,u)}))})),t.default={bind:function(e,t,n){s.push(e);var r=c++;e[l]={id:r,documentHandler:f(e,t,n),methodName:t.expression,bindingFn:t.value}},update:function(e,t,n){e[l].documentHandler=f(e,t,n),e[l].methodName=t.expression,e[l].bindingFn=t.value},unbind:function(e){for(var t=s.length,n=0;nh;h++)if(m=c?b(r(g=e[h])[0],g[1]):b(e[h]),m&&m instanceof u)return m;return new u(!1)}d=p.call(e)}y=d.next;while(!(g=y.call(d)).done)if(m=l(d,b,g.value,c),"object"==typeof m&&m&&m instanceof u)return m;return new u(!1)};c.stop=function(e){return new u(!0,e)}},2697:function(e,t,n){"use strict";t.__esModule=!0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n("ea16"),i=a(o);function a(e){return e&&e.__esModule?e:{default:e}}var s,l=l||{};l.Dialog=function(e,t,n){var o=this;if(this.dialogNode=e,null===this.dialogNode||"dialog"!==this.dialogNode.getAttribute("role"))throw new Error("Dialog() requires a DOM element with ARIA role of dialog.");"string"===typeof t?this.focusAfterClosed=document.getElementById(t):"object"===("undefined"===typeof t?"undefined":r(t))?this.focusAfterClosed=t:this.focusAfterClosed=null,"string"===typeof n?this.focusFirst=document.getElementById(n):"object"===("undefined"===typeof n?"undefined":r(n))?this.focusFirst=n:this.focusFirst=null,this.focusFirst?this.focusFirst.focus():i.default.focusFirstDescendant(this.dialogNode),this.lastFocus=document.activeElement,s=function(e){o.trapFocus(e)},this.addListeners()},l.Dialog.prototype.addListeners=function(){document.addEventListener("focus",s,!0)},l.Dialog.prototype.removeListeners=function(){document.removeEventListener("focus",s,!0)},l.Dialog.prototype.closeDialog=function(){var e=this;this.removeListeners(),this.focusAfterClosed&&setTimeout((function(){e.focusAfterClosed.focus()}))},l.Dialog.prototype.trapFocus=function(e){i.default.IgnoreUtilFocusChanges||(this.dialogNode.contains(e.target)?this.lastFocus=e.target:(i.default.focusFirstDescendant(this.dialogNode),this.lastFocus===document.activeElement&&i.default.focusLastDescendant(this.dialogNode),this.lastFocus=document.activeElement))},t.default=l.Dialog},"26fe":function(e,t,n){},2732:function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},"27b5":function(e,t,n){var r=n("d910").f,o=n("faa8"),i=n("90fb"),a=i("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,a)&&r(e,a,{configurable:!0,value:t})}},2895:function(e,t,n){"use strict";var r,o=n("3f5d"); -/** - * Checks if an event is supported in the current execution environment. - * - * NOTE: This will not work correctly for non-generic events such as `change`, - * `reset`, `load`, `error`, and `select`. - * - * Borrows from Modernizr. - * - * @param {string} eventNameSuffix Event name, e.g. "click". - * @param {?boolean} capture Check if the capture phase is supported. - * @return {boolean} True if the event is supported. - * @internal - * @license Modernizr 3.0.0pre (Custom Build) | MIT - */ -function i(e,t){if(!o.canUseDOM||t&&!("addEventListener"in document))return!1;var n="on"+e,i=n in document;if(!i){var a=document.createElement("div");a.setAttribute(n,"return;"),i="function"===typeof a[n]}return!i&&r&&"wheel"===e&&(i=document.implementation.hasFeature("Events.wheel","3.0")),i}o.canUseDOM&&(r=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("","")),e.exports=i},2984:function(e,t,n){},"2a91":function(e,t,n){var r=n("47ae"),o=n("2118"),i=n("90fb"),a=i("toStringTag"),s="Arguments"==o(function(){return arguments}()),l=function(e,t){try{return e[t]}catch(n){}};e.exports=r?o:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=l(t=Object(e),a))?n:s?o(t):"Object"==(r=o(t))&&"function"==typeof t.callee?"Arguments":r}},"2abc":function(e,t,n){"use strict";var r,o,i,a=n("908e"),s=n("0fc1"),l=n("faa8"),u=n("90fb"),c=n("9b9d"),f=u("iterator"),d=!1,p=function(){return this};[].keys&&(i=[].keys(),"next"in i?(o=a(a(i)),o!==Object.prototype&&(r=o)):d=!0),void 0==r&&(r={}),c||l(r,f)||s(r,f,p),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:d}},"2ae1":function(e,t){var n,r,o,i,a,s,l,u,c,f,d,p,h,v,m,y=!1;function g(){if(!y){y=!0;var e=navigator.userAgent,t=/(?:MSIE.(\d+\.\d+))|(?:(?:Firefox|GranParadiso|Iceweasel).(\d+\.\d+))|(?:Opera(?:.+Version.|.)(\d+\.\d+))|(?:AppleWebKit.(\d+(?:\.\d+)?))|(?:Trident\/\d+\.\d+.*rv:(\d+\.\d+))/.exec(e),g=/(Mac OS X)|(Windows)|(Linux)/.exec(e);if(p=/\b(iPhone|iP[ao]d)/.exec(e),h=/\b(iP[ao]d)/.exec(e),f=/Android/i.exec(e),v=/FBAN\/\w+;/i.exec(e),m=/Mobile/i.exec(e),d=!!/Win64/.exec(e),t){n=t[1]?parseFloat(t[1]):t[5]?parseFloat(t[5]):NaN,n&&document&&document.documentMode&&(n=document.documentMode);var b=/(?:Trident\/(\d+.\d+))/.exec(e);s=b?parseFloat(b[1])+4:n,r=t[2]?parseFloat(t[2]):NaN,o=t[3]?parseFloat(t[3]):NaN,i=t[4]?parseFloat(t[4]):NaN,i?(t=/(?:Chrome\/(\d+\.\d+))/.exec(e),a=t&&t[1]?parseFloat(t[1]):NaN):a=NaN}else n=r=o=a=i=NaN;if(g){if(g[1]){var _=/(?:Mac OS X (\d+(?:[._]\d+)?))/.exec(e);l=!_||parseFloat(_[1].replace("_","."))}else l=!1;u=!!g[2],c=!!g[3]}else l=u=c=!1}}var b={ie:function(){return g()||n},ieCompatibilityMode:function(){return g()||s>n},ie64:function(){return b.ie()&&d},firefox:function(){return g()||r},opera:function(){return g()||o},webkit:function(){return g()||i},safari:function(){return b.webkit()},chrome:function(){return g()||a},windows:function(){return g()||u},osx:function(){return g()||l},linux:function(){return g()||c},iphone:function(){return g()||p},mobile:function(){return g()||p||h||f||m},nativeApp:function(){return g()||v},android:function(){return g()||f},ipad:function(){return g()||h}};e.exports=b},"2c83":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=130)}({130:function(e,t,n){"use strict";n.r(t);var r=n(3),o={default:{order:""},selection:{width:48,minWidth:48,realWidth:48,order:"",className:"el-table-column--selection"},expand:{width:48,minWidth:48,realWidth:48,order:""},index:{width:48,minWidth:48,realWidth:48,order:""}},i={selection:{renderHeader:function(e,t){var n=t.store;return e("el-checkbox",{attrs:{disabled:n.states.data&&0===n.states.data.length,indeterminate:n.states.selection.length>0&&!this.isAllSelected,value:this.isAllSelected},nativeOn:{click:this.toggleAllSelection}})},renderCell:function(e,t){var n=t.row,r=t.column,o=t.store,i=t.$index;return e("el-checkbox",{nativeOn:{click:function(e){return e.stopPropagation()}},attrs:{value:o.isSelected(n),disabled:!!r.selectable&&!r.selectable.call(null,n,i)},on:{input:function(){o.commit("rowSelectedChanged",n)}}})},sortable:!1,resizable:!1},index:{renderHeader:function(e,t){var n=t.column;return n.label||"#"},renderCell:function(e,t){var n=t.$index,r=t.column,o=n+1,i=r.index;return"number"===typeof i?o=n+i:"function"===typeof i&&(o=i(n)),e("div",[o])},sortable:!1},expand:{renderHeader:function(e,t){var n=t.column;return n.label||""},renderCell:function(e,t){var n=t.row,r=t.store,o=["el-table__expand-icon"];r.states.expandRows.indexOf(n)>-1&&o.push("el-table__expand-icon--expanded");var i=function(e){e.stopPropagation(),r.toggleRowExpansion(n)};return e("div",{class:o,on:{click:i}},[e("i",{class:"el-icon el-icon-arrow-right"})])},sortable:!1,resizable:!1,className:"el-table__expand-column"}};function a(e,t){var n=t.row,o=t.column,i=t.$index,a=o.property,s=a&&Object(r["getPropByPath"])(n,a).v;return o&&o.formatter?o.formatter(n,o,s,i):s}function s(e,t){var n=t.row,r=t.treeNode,o=t.store;if(!r)return null;var i=[],a=function(e){e.stopPropagation(),o.loadOrToggle(n)};if(r.indent&&i.push(e("span",{class:"el-table__indent",style:{"padding-left":r.indent+"px"}})),"boolean"!==typeof r.expanded||r.noLazyChildren)i.push(e("span",{class:"el-table__placeholder"}));else{var s=["el-table__expand-icon",r.expanded?"el-table__expand-icon--expanded":""],l=["el-icon-arrow-right"];r.loading&&(l=["el-icon-loading"]),i.push(e("div",{class:s,on:{click:a}},[e("i",{class:l})]))}return i}var l=n(8),u=n(18),c=n.n(u),f=Object.assign||function(e){for(var t=1;t-1}))}}},data:function(){return{isSubColumn:!1,columns:[]}},computed:{owner:function(){var e=this.$parent;while(e&&!e.tableId)e=e.$parent;return e},columnOrTableParent:function(){var e=this.$parent;while(e&&!e.tableId&&!e.columnId)e=e.$parent;return e},realWidth:function(){return Object(l["l"])(this.width)},realMinWidth:function(){return Object(l["k"])(this.minWidth)},realAlign:function(){return this.align?"is-"+this.align:null},realHeaderAlign:function(){return this.headerAlign?"is-"+this.headerAlign:this.realAlign}},methods:{getPropsData:function(){for(var e=this,t=arguments.length,n=Array(t),r=0;rt.key[n])return 1}return 0};return e.map((function(e,t){return{value:e,index:t,key:s?s(e,t):null}})).sort((function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*n})).map((function(e){return e.value}))},l=function(e,t){var n=null;return e.columns.forEach((function(e){e.id===t&&(n=e)})),n},u=function(e,t){for(var n=null,r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",o=function(e){return!(Array.isArray(e)&&e.length)};function i(e,a,s){t(e,a,s),a.forEach((function(e){if(e[r])t(e,null,s+1);else{var a=e[n];o(a)||i(e,a,s+1)}}))}e.forEach((function(e){if(e[r])t(e,null,0);else{var a=e[n];o(a)||i(e,a,0)}}))}}})},"2f19":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=74)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},2:function(e,t){e.exports=n("c865")},3:function(e,t){e.exports=n("df57")},5:function(e,t){e.exports=n("5397")},7:function(e,t){e.exports=n("0261")},74:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("span",[n("transition",{attrs:{name:e.transition},on:{"after-enter":e.handleAfterEnter,"after-leave":e.handleAfterLeave}},[n("div",{directives:[{name:"show",rawName:"v-show",value:!e.disabled&&e.showPopper,expression:"!disabled && showPopper"}],ref:"popper",staticClass:"el-popover el-popper",class:[e.popperClass,e.content&&"el-popover--plain"],style:{width:e.width+"px"},attrs:{role:"tooltip",id:e.tooltipId,"aria-hidden":e.disabled||!e.showPopper?"true":"false"}},[e.title?n("div",{staticClass:"el-popover__title",domProps:{textContent:e._s(e.title)}}):e._e(),e._t("default",[e._v(e._s(e.content))])],2)]),e._t("reference")],2)},o=[];r._withStripped=!0;var i=n(5),a=n.n(i),s=n(2),l=n(3),u={name:"ElPopover",mixins:[a.a],props:{trigger:{type:String,default:"click",validator:function(e){return["click","focus","hover","manual"].indexOf(e)>-1}},openDelay:{type:Number,default:0},closeDelay:{type:Number,default:200},title:String,disabled:Boolean,content:String,reference:{},popperClass:String,width:{},visibleArrow:{default:!0},arrowOffset:{type:Number,default:0},transition:{type:String,default:"fade-in-linear"},tabindex:{type:Number,default:0}},computed:{tooltipId:function(){return"el-popover-"+Object(l["generateId"])()}},watch:{showPopper:function(e){this.disabled||(e?this.$emit("show"):this.$emit("hide"))}},mounted:function(){var e=this,t=this.referenceElm=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$slots.reference&&this.$slots.reference[0]&&(t=this.referenceElm=this.$slots.reference[0].elm),t&&(Object(s["addClass"])(t,"el-popover__reference"),t.setAttribute("aria-describedby",this.tooltipId),t.setAttribute("tabindex",this.tabindex),n.setAttribute("tabindex",0),"click"!==this.trigger&&(Object(s["on"])(t,"focusin",(function(){e.handleFocus();var n=t.__vue__;n&&"function"===typeof n.focus&&n.focus()})),Object(s["on"])(n,"focusin",this.handleFocus),Object(s["on"])(t,"focusout",this.handleBlur),Object(s["on"])(n,"focusout",this.handleBlur)),Object(s["on"])(t,"keydown",this.handleKeydown),Object(s["on"])(t,"click",this.handleClick)),"click"===this.trigger?(Object(s["on"])(t,"click",this.doToggle),Object(s["on"])(document,"click",this.handleDocumentClick)):"hover"===this.trigger?(Object(s["on"])(t,"mouseenter",this.handleMouseEnter),Object(s["on"])(n,"mouseenter",this.handleMouseEnter),Object(s["on"])(t,"mouseleave",this.handleMouseLeave),Object(s["on"])(n,"mouseleave",this.handleMouseLeave)):"focus"===this.trigger&&(this.tabindex<0&&console.warn("[Element Warn][Popover]a negative taindex means that the element cannot be focused by tab key"),t.querySelector("input, textarea")?(Object(s["on"])(t,"focusin",this.doShow),Object(s["on"])(t,"focusout",this.doClose)):(Object(s["on"])(t,"mousedown",this.doShow),Object(s["on"])(t,"mouseup",this.doClose)))},beforeDestroy:function(){this.cleanup()},deactivated:function(){this.cleanup()},methods:{doToggle:function(){this.showPopper=!this.showPopper},doShow:function(){this.showPopper=!0},doClose:function(){this.showPopper=!1},handleFocus:function(){Object(s["addClass"])(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!0)},handleClick:function(){Object(s["removeClass"])(this.referenceElm,"focusing")},handleBlur:function(){Object(s["removeClass"])(this.referenceElm,"focusing"),"click"!==this.trigger&&"focus"!==this.trigger||(this.showPopper=!1)},handleMouseEnter:function(){var e=this;clearTimeout(this._timer),this.openDelay?this._timer=setTimeout((function(){e.showPopper=!0}),this.openDelay):this.showPopper=!0},handleKeydown:function(e){27===e.keyCode&&"manual"!==this.trigger&&this.doClose()},handleMouseLeave:function(){var e=this;clearTimeout(this._timer),this.closeDelay?this._timer=setTimeout((function(){e.showPopper=!1}),this.closeDelay):this.showPopper=!1},handleDocumentClick:function(e){var t=this.reference||this.$refs.reference,n=this.popper||this.$refs.popper;!t&&this.$slots.reference&&this.$slots.reference[0]&&(t=this.referenceElm=this.$slots.reference[0].elm),this.$el&&t&&!this.$el.contains(e.target)&&!t.contains(e.target)&&n&&!n.contains(e.target)&&(this.showPopper=!1)},handleAfterEnter:function(){this.$emit("after-enter")},handleAfterLeave:function(){this.$emit("after-leave"),this.doDestroy()},cleanup:function(){(this.openDelay||this.closeDelay)&&clearTimeout(this._timer)}},destroyed:function(){var e=this.reference;Object(s["off"])(e,"click",this.doToggle),Object(s["off"])(e,"mouseup",this.doClose),Object(s["off"])(e,"mousedown",this.doShow),Object(s["off"])(e,"focusin",this.doShow),Object(s["off"])(e,"focusout",this.doClose),Object(s["off"])(e,"mousedown",this.doShow),Object(s["off"])(e,"mouseup",this.doClose),Object(s["off"])(e,"mouseleave",this.handleMouseLeave),Object(s["off"])(e,"mouseenter",this.handleMouseEnter),Object(s["off"])(document,"click",this.handleDocumentClick)}},c=u,f=n(0),d=Object(f["a"])(c,r,o,!1,null,null,null);d.options.__file="packages/popover/src/main.vue";var p=d.exports,h=function(e,t,n){var r=t.expression?t.value:t.arg,o=n.context.$refs[r];o&&(Array.isArray(o)?o[0].$refs.reference=e:o.$refs.reference=e)},v={bind:function(e,t,n){h(e,t,n)},inserted:function(e,t,n){h(e,t,n)}},m=n(7),y=n.n(m);y.a.directive("popover",v),p.install=function(e){e.directive("popover",v),e.component(p.name,p)},p.directive=v;t["default"]=p}})},"31d0":function(e,t,n){},3242:function(e,t,n){"use strict";function r(e,t,n){this.$children.forEach((function(o){var i=o.$options.componentName;i===e?o.$emit.apply(o,[t].concat(n)):r.apply(o,[e,t].concat([n]))}))}t.__esModule=!0,t.default={methods:{dispatch:function(e,t,n){var r=this.$parent||this.$root,o=r.$options.componentName;while(r&&(!o||o!==e))r=r.$parent,r&&(o=r.$options.componentName);r&&r.$emit.apply(r,[t].concat(n))},broadcast:function(e,t,n){r.call(this,e,t,n)}}}},"32a0":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=83)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},4:function(e,t){e.exports=n("3242")},83:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("label",{staticClass:"el-checkbox",class:[e.border&&e.checkboxSize?"el-checkbox--"+e.checkboxSize:"",{"is-disabled":e.isDisabled},{"is-bordered":e.border},{"is-checked":e.isChecked}],attrs:{id:e.id}},[n("span",{staticClass:"el-checkbox__input",class:{"is-disabled":e.isDisabled,"is-checked":e.isChecked,"is-indeterminate":e.indeterminate,"is-focus":e.focus},attrs:{tabindex:!!e.indeterminate&&0,role:!!e.indeterminate&&"checkbox","aria-checked":!!e.indeterminate&&"mixed"}},[n("span",{staticClass:"el-checkbox__inner"}),e.trueLabel||e.falseLabel?n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",name:e.name,disabled:e.isDisabled,"true-value":e.trueLabel,"false-value":e.falseLabel},domProps:{checked:Array.isArray(e.model)?e._i(e.model,null)>-1:e._q(e.model,e.trueLabel)},on:{change:[function(t){var n=e.model,r=t.target,o=r.checked?e.trueLabel:e.falseLabel;if(Array.isArray(n)){var i=null,a=e._i(n,i);r.checked?a<0&&(e.model=n.concat([i])):a>-1&&(e.model=n.slice(0,a).concat(n.slice(a+1)))}else e.model=o},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}}):n("input",{directives:[{name:"model",rawName:"v-model",value:e.model,expression:"model"}],staticClass:"el-checkbox__original",attrs:{type:"checkbox","aria-hidden":e.indeterminate?"true":"false",disabled:e.isDisabled,name:e.name},domProps:{value:e.label,checked:Array.isArray(e.model)?e._i(e.model,e.label)>-1:e.model},on:{change:[function(t){var n=e.model,r=t.target,o=!!r.checked;if(Array.isArray(n)){var i=e.label,a=e._i(n,i);r.checked?a<0&&(e.model=n.concat([i])):a>-1&&(e.model=n.slice(0,a).concat(n.slice(a+1)))}else e.model=o},e.handleChange],focus:function(t){e.focus=!0},blur:function(t){e.focus=!1}}})]),e.$slots.default||e.label?n("span",{staticClass:"el-checkbox__label"},[e._t("default"),e.$slots.default?e._e():[e._v(e._s(e.label))]],2):e._e()])},o=[];r._withStripped=!0;var i=n(4),a=n.n(i),s={name:"ElCheckbox",mixins:[a.a],inject:{elForm:{default:""},elFormItem:{default:""}},componentName:"ElCheckbox",data:function(){return{selfModel:!1,focus:!1,isLimitExceeded:!1}},computed:{model:{get:function(){return this.isGroup?this.store:void 0!==this.value?this.value:this.selfModel},set:function(e){this.isGroup?(this.isLimitExceeded=!1,void 0!==this._checkboxGroup.min&&e.lengththis._checkboxGroup.max&&(this.isLimitExceeded=!0),!1===this.isLimitExceeded&&this.dispatch("ElCheckboxGroup","input",[e])):(this.$emit("input",e),this.selfModel=e)}},isChecked:function(){return"[object Boolean]"==={}.toString.call(this.model)?this.model:Array.isArray(this.model)?this.model.indexOf(this.label)>-1:null!==this.model&&void 0!==this.model?this.model===this.trueLabel:void 0},isGroup:function(){var e=this.$parent;while(e){if("ElCheckboxGroup"===e.$options.componentName)return this._checkboxGroup=e,!0;e=e.$parent}return!1},store:function(){return this._checkboxGroup?this._checkboxGroup.value:this.value},isLimitDisabled:function(){var e=this._checkboxGroup,t=e.max,n=e.min;return!(!t&&!n)&&this.model.length>=t&&!this.isChecked||this.model.length<=n&&this.isChecked},isDisabled:function(){return this.isGroup?this._checkboxGroup.disabled||this.disabled||(this.elForm||{}).disabled||this.isLimitDisabled:this.disabled||(this.elForm||{}).disabled},_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},checkboxSize:function(){var e=this.size||this._elFormItemSize||(this.$ELEMENT||{}).size;return this.isGroup&&this._checkboxGroup.checkboxGroupSize||e}},props:{value:{},label:{},indeterminate:Boolean,disabled:Boolean,checked:Boolean,name:String,trueLabel:[String,Number],falseLabel:[String,Number],id:String,controls:String,border:Boolean,size:String},methods:{addToStore:function(){Array.isArray(this.model)&&-1===this.model.indexOf(this.label)?this.model.push(this.label):this.model=this.trueLabel||!0},handleChange:function(e){var t=this;if(!this.isLimitExceeded){var n=void 0;n=e.target.checked?void 0===this.trueLabel||this.trueLabel:void 0!==this.falseLabel&&this.falseLabel,this.$emit("change",n,e),this.$nextTick((function(){t.isGroup&&t.dispatch("ElCheckboxGroup","change",[t._checkboxGroup.value])}))}}},created:function(){this.checked&&this.addToStore()},mounted:function(){this.indeterminate&&this.$el.setAttribute("aria-controls",this.controls)},watch:{value:function(e){this.dispatch("ElFormItem","el.form.change",e)}}},l=s,u=n(0),c=Object(u["a"])(l,r,o,!1,null,null,null);c.options.__file="packages/checkbox/src/checkbox.vue";var f=c.exports;f.install=function(e){e.component(f.name,f)};t["default"]=f}})},3553:function(e,t,n){var r=n("2732");e.exports=function(e){return Object(r(e))}},"36d4":function(e,t,n){"use strict";t.__esModule=!0;var r=n("1ab3");t.default={methods:{t:function(){for(var e=arguments.length,t=Array(e),n=0;n0?r:n)(e)}},"3f00":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=97)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},97:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("button",{staticClass:"el-button",class:[e.type?"el-button--"+e.type:"",e.buttonSize?"el-button--"+e.buttonSize:"",{"is-disabled":e.buttonDisabled,"is-loading":e.loading,"is-plain":e.plain,"is-round":e.round,"is-circle":e.circle}],attrs:{disabled:e.buttonDisabled||e.loading,autofocus:e.autofocus,type:e.nativeType},on:{click:e.handleClick}},[e.loading?n("i",{staticClass:"el-icon-loading"}):e._e(),e.icon&&!e.loading?n("i",{class:e.icon}):e._e(),e.$slots.default?n("span",[e._t("default")],2):e._e()])},o=[];r._withStripped=!0;var i={name:"ElButton",inject:{elForm:{default:""},elFormItem:{default:""}},props:{type:{type:String,default:"default"},size:String,icon:{type:String,default:""},nativeType:{type:String,default:"button"},loading:Boolean,disabled:Boolean,plain:Boolean,autofocus:Boolean,round:Boolean,circle:Boolean},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},buttonSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},buttonDisabled:function(){return this.disabled||(this.elForm||{}).disabled}},methods:{handleClick:function(e){this.$emit("click",e)}}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/button/src/button.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},"3f11":function(e,t,n){"use strict"; -/*! - * vue-router v3.3.4 - * (c) 2020 Evan You - * @license MIT - */function r(e,t){0}function o(e){return Object.prototype.toString.call(e).indexOf("Error")>-1}function i(e,t){return o(e)&&e._isRouter&&(null==t||e.type===t)}function a(e,t){for(var n in t)e[n]=t[n];return e}var s={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render:function(e,t){var n=t.props,r=t.children,o=t.parent,i=t.data;i.routerView=!0;var s=o.$createElement,u=n.name,c=o.$route,f=o._routerViewCache||(o._routerViewCache={}),d=0,p=!1;while(o&&o._routerRoot!==o){var h=o.$vnode?o.$vnode.data:{};h.routerView&&d++,h.keepAlive&&o._directInactive&&o._inactive&&(p=!0),o=o.$parent}if(i.routerViewDepth=d,p){var v=f[u],m=v&&v.component;return m?(v.configProps&&l(m,i,v.route,v.configProps),s(m,i,r)):s()}var y=c.matched[d],g=y&&y.components[u];if(!y||!g)return f[u]=null,s();f[u]={component:g},i.registerRouteInstance=function(e,t){var n=y.instances[u];(t&&n!==e||!t&&n===e)&&(y.instances[u]=t)},(i.hook||(i.hook={})).prepatch=function(e,t){y.instances[u]=t.componentInstance},i.hook.init=function(e){e.data.keepAlive&&e.componentInstance&&e.componentInstance!==y.instances[u]&&(y.instances[u]=e.componentInstance)};var b=y.props&&y.props[u];return b&&(a(f[u],{route:c,configProps:b}),l(g,i,c,b)),s(g,i,r)}};function l(e,t,n,r){var o=t.props=u(n,r);if(o){o=t.props=a({},o);var i=t.attrs=t.attrs||{};for(var s in o)e.props&&s in e.props||(i[s]=o[s],delete o[s])}}function u(e,t){switch(typeof t){case"undefined":return;case"object":return t;case"function":return t(e);case"boolean":return t?e.params:void 0;default:0}}var c=/[!'()*]/g,f=function(e){return"%"+e.charCodeAt(0).toString(16)},d=/%2C/g,p=function(e){return encodeURIComponent(e).replace(c,f).replace(d,",")},h=decodeURIComponent;function v(e,t,n){void 0===t&&(t={});var r,o=n||m;try{r=o(e||"")}catch(a){r={}}for(var i in t)r[i]=t[i];return r}function m(e){var t={};return e=e.trim().replace(/^(\?|#|&)/,""),e?(e.split("&").forEach((function(e){var n=e.replace(/\+/g," ").split("="),r=h(n.shift()),o=n.length>0?h(n.join("=")):null;void 0===t[r]?t[r]=o:Array.isArray(t[r])?t[r].push(o):t[r]=[t[r],o]})),t):t}function y(e){var t=e?Object.keys(e).map((function(t){var n=e[t];if(void 0===n)return"";if(null===n)return p(t);if(Array.isArray(n)){var r=[];return n.forEach((function(e){void 0!==e&&(null===e?r.push(p(t)):r.push(p(t)+"="+p(e)))})),r.join("&")}return p(t)+"="+p(n)})).filter((function(e){return e.length>0})).join("&"):null;return t?"?"+t:""}var g=/\/?$/;function b(e,t,n,r){var o=r&&r.options.stringifyQuery,i=t.query||{};try{i=_(i)}catch(s){}var a={name:t.name||e&&e.name,meta:e&&e.meta||{},path:t.path||"/",hash:t.hash||"",query:i,params:t.params||{},fullPath:C(t,o),matched:e?w(e):[]};return n&&(a.redirectedFrom=C(n,o)),Object.freeze(a)}function _(e){if(Array.isArray(e))return e.map(_);if(e&&"object"===typeof e){var t={};for(var n in e)t[n]=_(e[n]);return t}return e}var x=b(null,{path:"/"});function w(e){var t=[];while(e)t.unshift(e),e=e.parent;return t}function C(e,t){var n=e.path,r=e.query;void 0===r&&(r={});var o=e.hash;void 0===o&&(o="");var i=t||y;return(n||"/")+i(r)+o}function S(e,t){return t===x?e===t:!!t&&(e.path&&t.path?e.path.replace(g,"")===t.path.replace(g,"")&&e.hash===t.hash&&O(e.query,t.query):!(!e.name||!t.name)&&(e.name===t.name&&e.hash===t.hash&&O(e.query,t.query)&&O(e.params,t.params)))}function O(e,t){if(void 0===e&&(e={}),void 0===t&&(t={}),!e||!t)return e===t;var n=Object.keys(e),r=Object.keys(t);return n.length===r.length&&n.every((function(n){var r=e[n],o=t[n];return"object"===typeof r&&"object"===typeof o?O(r,o):String(r)===String(o)}))}function E(e,t){return 0===e.path.replace(g,"/").indexOf(t.path.replace(g,"/"))&&(!t.hash||e.hash===t.hash)&&k(e.query,t.query)}function k(e,t){for(var n in t)if(!(n in e))return!1;return!0}function $(e,t,n){var r=e.charAt(0);if("/"===r)return e;if("?"===r||"#"===r)return t+e;var o=t.split("/");n&&o[o.length-1]||o.pop();for(var i=e.replace(/^\//,"").split("/"),a=0;a=0&&(t=e.slice(r),e=e.slice(0,r));var o=e.indexOf("?");return o>=0&&(n=e.slice(o+1),e=e.slice(0,o)),{path:e,query:n,hash:t}}function A(e){return e.replace(/\/\//g,"/")}var T=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)},M=J,P=F,L=H,R=D,N=Y,I=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function F(e,t){var n,r=[],o=0,i=0,a="",s=t&&t.delimiter||"/";while(null!=(n=I.exec(e))){var l=n[0],u=n[1],c=n.index;if(a+=e.slice(i,c),i=c+l.length,u)a+=u[1];else{var f=e[i],d=n[2],p=n[3],h=n[4],v=n[5],m=n[6],y=n[7];a&&(r.push(a),a="");var g=null!=d&&null!=f&&f!==d,b="+"===m||"*"===m,_="?"===m||"*"===m,x=n[2]||s,w=h||v;r.push({name:p||o++,prefix:d||"",delimiter:x,optional:_,repeat:b,partial:g,asterisk:!!y,pattern:w?V(w):y?".*":"[^"+W(x)+"]+?"})}}return i1||!w.length)return 0===w.length?e():e("span",{},w)}if("a"===this.tag)x.on=_,x.attrs={href:l,"aria-current":y};else{var C=se(this.$slots.default);if(C){C.isStatic=!1;var O=C.data=a({},C.data);for(var k in O.on=O.on||{},O.on){var $=O.on[k];k in _&&(O.on[k]=Array.isArray($)?$:[$])}for(var j in _)j in O.on?O.on[j].push(_[j]):O.on[j]=g;var A=C.data.attrs=a({},C.data.attrs);A.href=l,A["aria-current"]=y}else x.on=_}return e(this.tag,x,this.$slots.default)}};function ae(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&(void 0===e.button||0===e.button)){if(e.currentTarget&&e.currentTarget.getAttribute){var t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function se(e){if(e)for(var t,n=0;n-1&&(s.params[d]=n.params[d]);return s.path=Q(u.path,s.params,'named route "'+l+'"'),c(u,s,a)}if(s.path){s.params={};for(var p=0;p=e.length?n():e[o]?t(e[o],(function(){r(o+1)})):r(o+1)};r(0)}function He(e){return function(t,n,r){var i=!1,a=0,s=null;ze(e,(function(e,t,n,l){if("function"===typeof e&&void 0===e.cid){i=!0,a++;var u,c=Ve((function(t){We(t)&&(t=t.default),e.resolved="function"===typeof t?t:te.extend(t),n.components[l]=t,a--,a<=0&&r()})),f=Ve((function(e){var t="Failed to resolve async component "+l+": "+e;s||(s=o(e)?e:new Error(t),r(s))}));try{u=e(c,f)}catch(p){f(p)}if(u)if("function"===typeof u.then)u.then(c,f);else{var d=u.component;d&&"function"===typeof d.then&&d.then(c,f)}}})),i||r()}}function ze(e,t){return Be(e.map((function(e){return Object.keys(e.components).map((function(n){return t(e.components[n],e.instances[n],e,n)}))})))}function Be(e){return Array.prototype.concat.apply([],e)}var De="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag;function We(e){return e.__esModule||De&&"Module"===e[Symbol.toStringTag]}function Ve(e){var t=!1;return function(){var n=[],r=arguments.length;while(r--)n[r]=arguments[r];if(!t)return t=!0,e.apply(this,n)}}var Ue={redirected:1,aborted:2,cancelled:3,duplicated:4};function qe(e,t){return Ye(e,t,Ue.redirected,'Redirected when going from "'+e.fullPath+'" to "'+Ze(t)+'" via a navigation guard.')}function Ke(e,t){return Ye(e,t,Ue.duplicated,'Avoided redundant navigation to current location: "'+e.fullPath+'".')}function Ge(e,t){return Ye(e,t,Ue.cancelled,'Navigation cancelled from "'+e.fullPath+'" to "'+t.fullPath+'" with a new navigation.')}function Xe(e,t){return Ye(e,t,Ue.aborted,'Navigation aborted from "'+e.fullPath+'" to "'+t.fullPath+'" via a navigation guard.')}function Ye(e,t,n,r){var o=new Error(r);return o._isRouter=!0,o.from=e,o.to=t,o.type=n,o}var Je=["params","query","hash"];function Ze(e){if("string"===typeof e)return e;if("path"in e)return e.path;var t={};return Je.forEach((function(n){n in e&&(t[n]=e[n])})),JSON.stringify(t,null,2)}var Qe=function(e,t){this.router=e,this.base=et(t),this.current=x,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function et(e){if(!e)if(ue){var t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^https?:\/\/[^\/]+/,"")}else e="/";return"/"!==e.charAt(0)&&(e="/"+e),e.replace(/\/$/,"")}function tt(e,t){var n,r=Math.max(e.length,t.length);for(n=0;n0)){var t=this.router,n=t.options.scrollBehavior,r=Re&&n;r&&this.listeners.push(Ce());var o=function(){var n=e.current,o=ft(e.base);e.current===x&&o===e._startLocation||e.transitionTo(o,(function(e){r&&Se(t,e,n,!0)}))};window.addEventListener("popstate",o),this.listeners.push((function(){window.removeEventListener("popstate",o)}))}},t.prototype.go=function(e){window.history.go(e)},t.prototype.push=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){Ne(A(r.base+e.fullPath)),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){Ie(A(r.base+e.fullPath)),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.ensureURL=function(e){if(ft(this.base)!==this.current.fullPath){var t=A(this.base+this.current.fullPath);e?Ne(t):Ie(t)}},t.prototype.getCurrentLocation=function(){return ft(this.base)},t}(Qe);function ft(e){var t=decodeURI(window.location.pathname);return e&&0===t.toLowerCase().indexOf(e.toLowerCase())&&(t=t.slice(e.length)),(t||"/")+window.location.search+window.location.hash}var dt=function(e){function t(t,n,r){e.call(this,t,n),r&&pt(this.base)||ht()}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.setupListeners=function(){var e=this;if(!(this.listeners.length>0)){var t=this.router,n=t.options.scrollBehavior,r=Re&&n;r&&this.listeners.push(Ce());var o=function(){var t=e.current;ht()&&e.transitionTo(vt(),(function(n){r&&Se(e.router,n,t,!0),Re||gt(n.fullPath)}))},i=Re?"popstate":"hashchange";window.addEventListener(i,o),this.listeners.push((function(){window.removeEventListener(i,o)}))}},t.prototype.push=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){yt(e.fullPath),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this,o=this,i=o.current;this.transitionTo(e,(function(e){gt(e.fullPath),Se(r.router,e,i,!1),t&&t(e)}),n)},t.prototype.go=function(e){window.history.go(e)},t.prototype.ensureURL=function(e){var t=this.current.fullPath;vt()!==t&&(e?yt(t):gt(t))},t.prototype.getCurrentLocation=function(){return vt()},t}(Qe);function pt(e){var t=ft(e);if(!/^\/#/.test(t))return window.location.replace(A(e+"/#"+t)),!0}function ht(){var e=vt();return"/"===e.charAt(0)||(gt("/"+e),!1)}function vt(){var e=window.location.href,t=e.indexOf("#");if(t<0)return"";e=e.slice(t+1);var n=e.indexOf("?");if(n<0){var r=e.indexOf("#");e=r>-1?decodeURI(e.slice(0,r))+e.slice(r):decodeURI(e)}else e=decodeURI(e.slice(0,n))+e.slice(n);return e}function mt(e){var t=window.location.href,n=t.indexOf("#"),r=n>=0?t.slice(0,n):t;return r+"#"+e}function yt(e){Re?Ne(mt(e)):window.location.hash=e}function gt(e){Re?Ie(mt(e)):window.location.replace(mt(e))}var bt=function(e){function t(t,n){e.call(this,t,n),this.stack=[],this.index=-1}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.push=function(e,t,n){var r=this;this.transitionTo(e,(function(e){r.stack=r.stack.slice(0,r.index+1).concat(e),r.index++,t&&t(e)}),n)},t.prototype.replace=function(e,t,n){var r=this;this.transitionTo(e,(function(e){r.stack=r.stack.slice(0,r.index).concat(e),t&&t(e)}),n)},t.prototype.go=function(e){var t=this,n=this.index+e;if(!(n<0||n>=this.stack.length)){var r=this.stack[n];this.confirmTransition(r,(function(){t.index=n,t.updateRoute(r)}),(function(e){i(e,Ue.duplicated)&&(t.index=n)}))}},t.prototype.getCurrentLocation=function(){var e=this.stack[this.stack.length-1];return e?e.fullPath:"/"},t.prototype.ensureURL=function(){},t}(Qe),_t=function(e){void 0===e&&(e={}),this.app=null,this.apps=[],this.options=e,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=he(e.routes||[],this);var t=e.mode||"hash";switch(this.fallback="history"===t&&!Re&&!1!==e.fallback,this.fallback&&(t="hash"),ue||(t="abstract"),this.mode=t,t){case"history":this.history=new ct(this,e.base);break;case"hash":this.history=new dt(this,e.base,this.fallback);break;case"abstract":this.history=new bt(this,e.base);break;default:0}},xt={currentRoute:{configurable:!0}};function wt(e,t){return e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function Ct(e,t,n){var r="hash"===n?"#"+t:t;return e?A(e+"/"+r):r}_t.prototype.match=function(e,t,n){return this.matcher.match(e,t,n)},xt.currentRoute.get=function(){return this.history&&this.history.current},_t.prototype.init=function(e){var t=this;if(this.apps.push(e),e.$once("hook:destroyed",(function(){var n=t.apps.indexOf(e);n>-1&&t.apps.splice(n,1),t.app===e&&(t.app=t.apps[0]||null),t.app||t.history.teardownListeners()})),!this.app){this.app=e;var n=this.history;if(n instanceof ct||n instanceof dt){var r=function(){n.setupListeners()};n.transitionTo(n.getCurrentLocation(),r,r)}n.listen((function(e){t.apps.forEach((function(t){t._route=e}))}))}},_t.prototype.beforeEach=function(e){return wt(this.beforeHooks,e)},_t.prototype.beforeResolve=function(e){return wt(this.resolveHooks,e)},_t.prototype.afterEach=function(e){return wt(this.afterHooks,e)},_t.prototype.onReady=function(e,t){this.history.onReady(e,t)},_t.prototype.onError=function(e){this.history.onError(e)},_t.prototype.push=function(e,t,n){var r=this;if(!t&&!n&&"undefined"!==typeof Promise)return new Promise((function(t,n){r.history.push(e,t,n)}));this.history.push(e,t,n)},_t.prototype.replace=function(e,t,n){var r=this;if(!t&&!n&&"undefined"!==typeof Promise)return new Promise((function(t,n){r.history.replace(e,t,n)}));this.history.replace(e,t,n)},_t.prototype.go=function(e){this.history.go(e)},_t.prototype.back=function(){this.go(-1)},_t.prototype.forward=function(){this.go(1)},_t.prototype.getMatchedComponents=function(e){var t=e?e.matched?e:this.resolve(e).route:this.currentRoute;return t?[].concat.apply([],t.matched.map((function(e){return Object.keys(e.components).map((function(t){return e.components[t]}))}))):[]},_t.prototype.resolve=function(e,t,n){t=t||this.history.current;var r=ee(e,t,n,this),o=this.match(r,t),i=o.redirectedFrom||o.fullPath,a=this.history.base,s=Ct(a,i,this.mode);return{location:r,route:o,href:s,normalizedTo:r,resolved:o}},_t.prototype.addRoutes=function(e){this.matcher.addRoutes(e),this.history.current!==x&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(_t.prototype,xt),_t.install=le,_t.version="3.3.4",ue&&window.Vue&&window.Vue.use(_t),t["a"]=_t},"3f5d":function(e,t,n){"use strict";var r=!("undefined"===typeof window||!window.document||!window.document.createElement),o={canUseDOM:r,canUseWorkers:"undefined"!==typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=o},"403f":function(e,t,n){"use strict";var r=n("6d7a"),o=n("d910"),i=n("90fb"),a=n("1e2c"),s=i("species");e.exports=function(e){var t=r(e),n=o.f;a&&t&&!t[s]&&n(t,s,{configurable:!0,get:function(){return this}})}},"423c":function(e,t,n){},"45af":function(e,t,n){var r=n("da10"),o=n("d88d"),i=n("e1d6"),a=function(e){return function(t,n,a){var s,l=r(t),u=o(l.length),c=i(a,u);if(e&&n!=n){while(u>c)if(s=l[c++],s!=s)return!0}else for(;u>c;c++)if((e||c in l)&&l[c]===n)return e||c||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},"47ae":function(e,t,n){var r=n("90fb"),o=r("toStringTag"),i={};i[o]="z",e.exports="[object z]"===String(i)},"4a42":function(e,t,n){"use strict";t.__esModule=!0,t.default={el:{colorpicker:{confirm:"确定",clear:"清空"},datepicker:{now:"此刻",today:"今天",cancel:"取消",clear:"清空",confirm:"确定",selectDate:"选择日期",selectTime:"选择时间",startDate:"开始日期",startTime:"开始时间",endDate:"结束日期",endTime:"结束时间",prevYear:"前一年",nextYear:"后一年",prevMonth:"上个月",nextMonth:"下个月",year:"年",month1:"1 月",month2:"2 月",month3:"3 月",month4:"4 月",month5:"5 月",month6:"6 月",month7:"7 月",month8:"8 月",month9:"9 月",month10:"10 月",month11:"11 月",month12:"12 月",weeks:{sun:"日",mon:"一",tue:"二",wed:"三",thu:"四",fri:"五",sat:"六"},months:{jan:"一月",feb:"二月",mar:"三月",apr:"四月",may:"五月",jun:"六月",jul:"七月",aug:"八月",sep:"九月",oct:"十月",nov:"十一月",dec:"十二月"}},select:{loading:"加载中",noMatch:"无匹配数据",noData:"无数据",placeholder:"请选择"},cascader:{noMatch:"无匹配数据",loading:"加载中",placeholder:"请选择",noData:"暂无数据"},pagination:{goto:"前往",pagesize:"条/页",total:"共 {total} 条",pageClassifier:"页"},messagebox:{title:"提示",confirm:"确定",cancel:"取消",error:"输入的数据不合法!"},upload:{deleteTip:"按 delete 键可删除",delete:"删除",preview:"查看图片",continue:"继续上传"},table:{emptyText:"暂无数据",confirmFilter:"筛选",resetFilter:"重置",clearFilter:"全部",sumText:"合计"},tree:{emptyText:"暂无数据"},transfer:{noMatch:"无匹配数据",noData:"无数据",titles:["列表 1","列表 2"],filterPlaceholder:"请输入搜索内容",noCheckedFormat:"共 {total} 项",hasCheckedFormat:"已选 {checked}/{total} 项"},image:{error:"加载失败"},pageHeader:{title:"返回"},popconfirm:{confirmButtonText:"确定",cancelButtonText:"取消"}}}},"4fda":function(e,t,n){var r=n("6d7a");e.exports=r("navigator","userAgent")||""},"50fb":function(e,t,n){var r=n("857c"),o=n("d1fd");e.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,n={};try{e=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set,e.call(n,[]),t=n instanceof Array}catch(i){}return function(n,i){return r(n),o(i),t?e.call(n,i):n.__proto__=i,n}}():void 0)},"52f9":function(e,t,n){"use strict";var r=n("47ae"),o=n("2a91");e.exports=r?{}.toString:function(){return"[object "+o(this)+"]"}},5397:function(e,t,n){"use strict";t.__esModule=!0;var r=n("0261"),o=a(r),i=n("72e8");function a(e){return e&&e.__esModule?e:{default:e}}var s=o.default.prototype.$isServer?function(){}:n("f062"),l=function(e){return e.stopPropagation()};t.default={props:{transformOrigin:{type:[Boolean,String],default:!0},placement:{type:String,default:"bottom"},boundariesPadding:{type:Number,default:5},reference:{},popper:{},offset:{default:0},value:Boolean,visibleArrow:Boolean,arrowOffset:{type:Number,default:35},appendToBody:{type:Boolean,default:!0},popperOptions:{type:Object,default:function(){return{gpuAcceleration:!1}}}},data:function(){return{showPopper:!1,currentPlacement:""}},watch:{value:{immediate:!0,handler:function(e){this.showPopper=e,this.$emit("input",e)}},showPopper:function(e){this.disabled||(e?this.updatePopper():this.destroyPopper(),this.$emit("input",e))}},methods:{createPopper:function(){var e=this;if(!this.$isServer&&(this.currentPlacement=this.currentPlacement||this.placement,/^(top|bottom|left|right)(-start|-end)?$/g.test(this.currentPlacement))){var t=this.popperOptions,n=this.popperElm=this.popperElm||this.popper||this.$refs.popper,r=this.referenceElm=this.referenceElm||this.reference||this.$refs.reference;!r&&this.$slots.reference&&this.$slots.reference[0]&&(r=this.referenceElm=this.$slots.reference[0].elm),n&&r&&(this.visibleArrow&&this.appendArrow(n),this.appendToBody&&document.body.appendChild(this.popperElm),this.popperJS&&this.popperJS.destroy&&this.popperJS.destroy(),t.placement=this.currentPlacement,t.offset=this.offset,t.arrowOffset=this.arrowOffset,this.popperJS=new s(r,n,t),this.popperJS.onCreate((function(t){e.$emit("created",e),e.resetTransformOrigin(),e.$nextTick(e.updatePopper)})),"function"===typeof t.onUpdate&&this.popperJS.onUpdate(t.onUpdate),this.popperJS._popper.style.zIndex=i.PopupManager.nextZIndex(),this.popperElm.addEventListener("click",l))}},updatePopper:function(){var e=this.popperJS;e?(e.update(),e._popper&&(e._popper.style.zIndex=i.PopupManager.nextZIndex())):this.createPopper()},doDestroy:function(e){!this.popperJS||this.showPopper&&!e||(this.popperJS.destroy(),this.popperJS=null)},destroyPopper:function(){this.popperJS&&this.resetTransformOrigin()},resetTransformOrigin:function(){if(this.transformOrigin){var e={top:"bottom",bottom:"top",left:"right",right:"left"},t=this.popperJS._popper.getAttribute("x-placement").split("-")[0],n=e[t];this.popperJS._popper.style.transformOrigin="string"===typeof this.transformOrigin?this.transformOrigin:["top","bottom"].indexOf(t)>-1?"center "+n:n+" center"}},appendArrow:function(e){var t=void 0;if(!this.appended){for(var n in this.appended=!0,e.attributes)if(/^_v-/.test(e.attributes[n].name)){t=e.attributes[n].name;break}var r=document.createElement("div");t&&r.setAttribute(t,""),r.setAttribute("x-arrow",""),r.className="popper__arrow",e.appendChild(r)}}},beforeDestroy:function(){this.doDestroy(!0),this.popperElm&&this.popperElm.parentNode===document.body&&(this.popperElm.removeEventListener("click",l),document.body.removeChild(this.popperElm))},deactivated:function(){this.$options.beforeDestroy[0].call(this)}}},"546a":function(e,t,n){e.exports=n("76ab")},"5baf":function(e,t,n){"use strict";var r=function(e){return o(e)&&!i(e)};function o(e){return!!e&&"object"===typeof e}function i(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||l(e)}var a="function"===typeof Symbol&&Symbol.for,s=a?Symbol.for("react.element"):60103;function l(e){return e.$$typeof===s}function u(e){return Array.isArray(e)?[]:{}}function c(e,t){var n=t&&!0===t.clone;return n&&r(e)?p(u(e),e,t):e}function f(e,t,n){var o=e.slice();return t.forEach((function(t,i){"undefined"===typeof o[i]?o[i]=c(t,n):r(t)?o[i]=p(e[i],t,n):-1===e.indexOf(t)&&o.push(c(t,n))})),o}function d(e,t,n){var o={};return r(e)&&Object.keys(e).forEach((function(t){o[t]=c(e[t],n)})),Object.keys(t).forEach((function(i){r(t[i])&&e[i]?o[i]=p(e[i],t[i],n):o[i]=c(t[i],n)})),o}function p(e,t,n){var r=Array.isArray(t),o=Array.isArray(e),i=n||{arrayMerge:f},a=r===o;if(a){if(r){var s=i.arrayMerge||f;return s(e,t,n)}return d(e,t,n)}return c(t,n)}p.all=function(e,t){if(!Array.isArray(e)||e.length<2)throw new Error("first argument should be an array with at least two elements");return e.reduce((function(e,n){return p(e,n,t)}))};var h=p;e.exports=h},"604f":function(e,t,n){var r=n("d890"),o=n("1025"),i=r.WeakMap;e.exports="function"===typeof i&&/native code/.test(o(i))},"60bf":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=56)}([function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},,function(e,t){e.exports=n("c865")},function(e,t){e.exports=n("df57")},,function(e,t){e.exports=n("5397")},function(e,t){e.exports=n("36d4")},function(e,t){e.exports=n("0261")},function(e,t,n){"use strict";n.d(t,"b",(function(){return i})),n.d(t,"i",(function(){return s})),n.d(t,"d",(function(){return l})),n.d(t,"e",(function(){return u})),n.d(t,"c",(function(){return c})),n.d(t,"g",(function(){return f})),n.d(t,"f",(function(){return d})),n.d(t,"h",(function(){return h})),n.d(t,"l",(function(){return v})),n.d(t,"k",(function(){return m})),n.d(t,"j",(function(){return y})),n.d(t,"a",(function(){return g})),n.d(t,"m",(function(){return b})),n.d(t,"n",(function(){return _}));var r=n(3),o="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i=function(e){var t=e.target;while(t&&"HTML"!==t.tagName.toUpperCase()){if("TD"===t.tagName.toUpperCase())return t;t=t.parentNode}return null},a=function(e){return null!==e&&"object"===("undefined"===typeof e?"undefined":o(e))},s=function(e,t,n,o,i){if(!t&&!o&&(!i||Array.isArray(i)&&!i.length))return e;n="string"===typeof n?"descending"===n?-1:1:n&&n<0?-1:1;var s=o?null:function(n,o){return i?(Array.isArray(i)||(i=[i]),i.map((function(t){return"string"===typeof t?Object(r["getValueByPath"])(n,t):t(n,o,e)}))):("$key"!==t&&a(n)&&"$value"in n&&(n=n.$value),[a(n)?Object(r["getValueByPath"])(n,t):n])},l=function(e,t){if(o)return o(e.value,t.value);for(var n=0,r=e.key.length;nt.key[n])return 1}return 0};return e.map((function(e,t){return{value:e,index:t,key:s?s(e,t):null}})).sort((function(e,t){var r=l(e,t);return r||(r=e.index-t.index),r*n})).map((function(e){return e.value}))},l=function(e,t){var n=null;return e.columns.forEach((function(e){e.id===t&&(n=e)})),n},u=function(e,t){for(var n=null,r=0;r2&&void 0!==arguments[2]?arguments[2]:"children",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"hasChildren",o=function(e){return!(Array.isArray(e)&&e.length)};function i(e,a,s){t(e,a,s),a.forEach((function(e){if(e[r])t(e,null,s+1);else{var a=e[n];o(a)||i(e,a,s+1)}}))}e.forEach((function(e){if(e[r])t(e,null,0);else{var a=e[n];o(a)||i(e,a,0)}}))}},function(e,t){e.exports=n("eb40")},,function(e,t){e.exports=n("a4f6")},function(e,t){e.exports=n("06cb")},,function(e,t){e.exports=n("3ab7")},function(e,t){e.exports=n("72e8")},function(e,t){e.exports=n("e1fd")},function(e,t){e.exports=n("ca47")},function(e,t){e.exports=n("32a0")},,,,,,,,,,,function(e,t){e.exports=n("c944")},,,,,,,,,,function(e,t){e.exports=n("e857")},function(e,t){e.exports=n("3a6a")},,,function(e,t){e.exports=n("63ec")},,,function(e,t){e.exports=n("546a")},,,,,,,,,,function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-table",class:[{"el-table--fit":e.fit,"el-table--striped":e.stripe,"el-table--border":e.border||e.isGroup,"el-table--hidden":e.isHidden,"el-table--group":e.isGroup,"el-table--fluid-height":e.maxHeight,"el-table--scrollable-x":e.layout.scrollX,"el-table--scrollable-y":e.layout.scrollY,"el-table--enable-row-hover":!e.store.states.isComplex,"el-table--enable-row-transition":0!==(e.store.states.data||[]).length&&(e.store.states.data||[]).length<100},e.tableSize?"el-table--"+e.tableSize:""],on:{mouseleave:function(t){e.handleMouseLeave(t)}}},[n("div",{ref:"hiddenColumns",staticClass:"hidden-columns"},[e._t("default")],2),e.showHeader?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"headerWrapper",staticClass:"el-table__header-wrapper"},[n("table-header",{ref:"tableHeader",style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"default-sort":e.defaultSort}})],1):e._e(),n("div",{ref:"bodyWrapper",staticClass:"el-table__body-wrapper",class:[e.layout.scrollX?"is-scrolling-"+e.scrollPosition:"is-scrolling-none"],style:[e.bodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{context:e.context,store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.data&&0!==e.data.length?e._e():n("div",{ref:"emptyBlock",staticClass:"el-table__empty-block",style:e.emptyBlockStyle},[n("span",{staticClass:"el-table__empty-text"},[e._t("empty",[e._v(e._s(e.emptyText||e.t("el.table.emptyText")))])],2)]),e.$slots.append?n("div",{ref:"appendWrapper",staticClass:"el-table__append-wrapper"},[e._t("append")],2):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"},{name:"mousewheel",rawName:"v-mousewheel",value:e.handleHeaderFooterMousewheel,expression:"handleHeaderFooterMousewheel"}],ref:"footerWrapper",staticClass:"el-table__footer-wrapper"},[n("table-footer",{style:{width:e.layout.bodyWidth?e.layout.bodyWidth+"px":""},attrs:{store:e.store,border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,"default-sort":e.defaultSort}})],1):e._e(),e.fixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"fixedWrapper",staticClass:"el-table__fixed",style:[{width:e.layout.fixedWidth?e.layout.fixedWidth+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"fixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"fixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"fixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"left",store:e.store,stripe:e.stripe,highlight:e.highlightCurrentRow,"row-class-name":e.rowClassName,"row-style":e.rowStyle}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"fixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"left",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{directives:[{name:"mousewheel",rawName:"v-mousewheel",value:e.handleFixedMousewheel,expression:"handleFixedMousewheel"}],ref:"rightFixedWrapper",staticClass:"el-table__fixed-right",style:[{width:e.layout.rightFixedWidth?e.layout.rightFixedWidth+"px":"",right:e.layout.scrollY?(e.border?e.layout.gutterWidth:e.layout.gutterWidth||0)+"px":""},e.fixedHeight]},[e.showHeader?n("div",{ref:"rightFixedHeaderWrapper",staticClass:"el-table__fixed-header-wrapper"},[n("table-header",{ref:"rightFixedTableHeader",style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,store:e.store}})],1):e._e(),n("div",{ref:"rightFixedBodyWrapper",staticClass:"el-table__fixed-body-wrapper",style:[{top:e.layout.headerHeight+"px"},e.fixedBodyHeight]},[n("table-body",{style:{width:e.bodyWidth},attrs:{fixed:"right",store:e.store,stripe:e.stripe,"row-class-name":e.rowClassName,"row-style":e.rowStyle,highlight:e.highlightCurrentRow}}),e.$slots.append?n("div",{staticClass:"el-table__append-gutter",style:{height:e.layout.appendHeight+"px"}}):e._e()],1),e.showSummary?n("div",{directives:[{name:"show",rawName:"v-show",value:e.data&&e.data.length>0,expression:"data && data.length > 0"}],ref:"rightFixedFooterWrapper",staticClass:"el-table__fixed-footer-wrapper"},[n("table-footer",{style:{width:e.bodyWidth},attrs:{fixed:"right",border:e.border,"sum-text":e.sumText||e.t("el.table.sumText"),"summary-method":e.summaryMethod,store:e.store}})],1):e._e()]):e._e(),e.rightFixedColumns.length>0?n("div",{ref:"rightFixedPatch",staticClass:"el-table__fixed-right-patch",style:{width:e.layout.scrollY?e.layout.gutterWidth+"px":"0",height:e.layout.headerHeight+"px"}}):e._e(),n("div",{directives:[{name:"show",rawName:"v-show",value:e.resizeProxyVisible,expression:"resizeProxyVisible"}],ref:"resizeProxy",staticClass:"el-table__column-resize-proxy"})])},o=[];r._withStripped=!0;var i=n(18),a=n.n(i),s=n(43),l=n(16),u=n(46),c=n.n(u),f="undefined"!==typeof navigator&&navigator.userAgent.toLowerCase().indexOf("firefox")>-1,d=function(e,t){e&&e.addEventListener&&e.addEventListener(f?"DOMMouseScroll":"mousewheel",(function(e){var n=c()(e);t&&t.apply(this,[e,n])}))},p={bind:function(e,t){d(e,t.value)}},h=n(6),v=n.n(h),m=n(11),y=n.n(m),g=n(7),b=n.n(g),_=n(9),x=n.n(_),w=n(8),C={data:function(){return{states:{defaultExpandAll:!1,expandRows:[]}}},methods:{updateExpandRows:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,r=e.rowKey,o=e.defaultExpandAll,i=e.expandRows;if(o)this.states.expandRows=n.slice();else if(r){var a=Object(w["f"])(i,r);this.states.expandRows=n.reduce((function(e,t){var n=Object(w["g"])(t,r),o=a[n];return o&&e.push(t),e}),[])}else this.states.expandRows=[]},toggleRowExpansion:function(e,t){var n=Object(w["m"])(this.states.expandRows,e,t);n&&(this.table.$emit("expand-change",e,this.states.expandRows.slice()),this.scheduleLayout())},setExpandRowKeys:function(e){this.assertRowKey();var t=this.states,n=t.data,r=t.rowKey,o=Object(w["f"])(n,r);this.states.expandRows=e.reduce((function(e,t){var n=o[t];return n&&e.push(n.row),e}),[])},isRowExpanded:function(e){var t=this.states,n=t.expandRows,r=void 0===n?[]:n,o=t.rowKey;if(o){var i=Object(w["f"])(r,o);return!!i[Object(w["g"])(e,o)]}return-1!==r.indexOf(e)}}},S=n(3),O={data:function(){return{states:{_currentRowKey:null,currentRow:null}}},methods:{setCurrentRowKey:function(e){this.assertRowKey(),this.states._currentRowKey=e,this.setCurrentRowByKey(e)},restoreCurrentRowKey:function(){this.states._currentRowKey=null},setCurrentRowByKey:function(e){var t=this.states,n=t.data,r=void 0===n?[]:n,o=t.rowKey,i=null;o&&(i=Object(S["arrayFind"])(r,(function(t){return Object(w["g"])(t,o)===e}))),t.currentRow=i},updateCurrentRow:function(e){var t=this.states,n=this.table,r=t.currentRow;if(e&&e!==r)return t.currentRow=e,void n.$emit("current-change",e,r);!e&&r&&(t.currentRow=null,n.$emit("current-change",null,r))},updateCurrentRowData:function(){var e=this.states,t=this.table,n=e.rowKey,r=e._currentRowKey,o=e.data||[],i=e.currentRow;if(-1===o.indexOf(i)&&i){if(n){var a=Object(w["g"])(i,n);this.setCurrentRowByKey(a)}else e.currentRow=null;null===e.currentRow&&t.$emit("current-change",null,i)}else r&&(this.setCurrentRowByKey(r),this.restoreCurrentRowKey())}}},E=Object.assign||function(e){for(var t=1;t0&&t[0]&&"selection"===t[0].type&&!t[0].fixed&&(t[0].fixed=!0,e.fixedColumns.unshift(t[0]));var n=t.filter((function(e){return!e.fixed}));e.originColumns=[].concat(e.fixedColumns).concat(n).concat(e.rightFixedColumns);var r=j(n),o=j(e.fixedColumns),i=j(e.rightFixedColumns);e.leafColumnsLength=r.length,e.fixedLeafColumnsLength=o.length,e.rightFixedLeafColumnsLength=i.length,e.columns=[].concat(o).concat(r).concat(i),e.isComplex=e.fixedColumns.length>0||e.rightFixedColumns.length>0},scheduleLayout:function(e){e&&this.updateColumns(),this.table.debouncedUpdateLayout()},isSelected:function(e){var t=this.states.selection,n=void 0===t?[]:t;return n.indexOf(e)>-1},clearSelection:function(){var e=this.states;e.isAllSelected=!1;var t=e.selection;t.length&&(e.selection=[],this.table.$emit("selection-change",[]))},cleanSelection:function(){var e=this.states,t=e.data,n=e.rowKey,r=e.selection,o=void 0;if(n){o=[];var i=Object(w["f"])(r,n),a=Object(w["f"])(t,n);for(var s in i)i.hasOwnProperty(s)&&!a[s]&&o.push(i[s].row)}else o=r.filter((function(e){return-1===t.indexOf(e)}));if(o.length){var l=r.filter((function(e){return-1===o.indexOf(e)}));e.selection=l,this.table.$emit("selection-change",l.slice())}},toggleRowSelection:function(e,t){var n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],r=Object(w["m"])(this.states.selection,e,t);if(r){var o=(this.states.selection||[]).slice();n&&this.table.$emit("select",o,e),this.table.$emit("selection-change",o)}},_toggleAllSelection:function(){var e=this.states,t=e.data,n=void 0===t?[]:t,r=e.selection,o=e.selectOnIndeterminate?!e.isAllSelected:!(e.isAllSelected||r.length);e.isAllSelected=o;var i=!1;n.forEach((function(t,n){e.selectable?e.selectable.call(null,t,n)&&Object(w["m"])(r,t,o)&&(i=!0):Object(w["m"])(r,t,o)&&(i=!0)})),i&&this.table.$emit("selection-change",r?r.slice():[]),this.table.$emit("select-all",r)},updateSelectionByRowKey:function(){var e=this.states,t=e.selection,n=e.rowKey,r=e.data,o=Object(w["f"])(t,n);r.forEach((function(e){var r=Object(w["g"])(e,n),i=o[r];i&&(t[i.index]=e)}))},updateAllSelected:function(){var e=this.states,t=e.selection,n=e.rowKey,r=e.selectable,o=e.data||[];if(0!==o.length){var i=void 0;n&&(i=Object(w["f"])(t,n));for(var a=function(e){return i?!!i[Object(w["g"])(e,n)]:-1!==t.indexOf(e)},s=!0,l=0,u=0,c=o.length;u1?n-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:{};if(!e)throw new Error("Table is required.");var n=new T;return n.table=e,n.toggleAllSelection=P()(10,n._toggleAllSelection),Object.keys(t).forEach((function(e){n.states[e]=t[e]})),n}function R(e){var t={};return Object.keys(e).forEach((function(n){var r=e[n],o=void 0;"string"===typeof r?o=function(){return this.store.states[r]}:"function"===typeof r?o=function(){return r.call(this,this.store.states)}:console.error("invalid value type"),o&&(t[n]=o)})),t}var N=n(39),I=n.n(N);function F(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var H=function(){function e(t){for(var n in F(this,e),this.observers=[],this.table=null,this.store=null,this.columns=null,this.fit=!0,this.showHeader=!0,this.height=null,this.scrollX=!1,this.scrollY=!1,this.bodyWidth=null,this.fixedWidth=null,this.rightFixedWidth=null,this.tableHeight=null,this.headerHeight=44,this.appendHeight=0,this.footerHeight=44,this.viewportHeight=null,this.bodyHeight=null,this.fixedBodyHeight=null,this.gutterWidth=I()(),t)t.hasOwnProperty(n)&&(this[n]=t[n]);if(!this.table)throw new Error("table is required for Table Layout");if(!this.store)throw new Error("store is required for Table Layout")}return e.prototype.updateScrollY=function(){var e=this.height;if(null===e)return!1;var t=this.table.bodyWrapper;if(this.table.$el&&t){var n=t.querySelector(".el-table__body"),r=this.scrollY,o=n.offsetHeight>this.bodyHeight;return this.scrollY=o,r!==o}return!1},e.prototype.setHeight=function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"height";if(!b.a.prototype.$isServer){var r=this.table.$el;if(e=Object(w["j"])(e),this.height=e,!r&&(e||0===e))return b.a.nextTick((function(){return t.setHeight(e,n)}));"number"===typeof e?(r.style[n]=e+"px",this.updateElsHeight()):"string"===typeof e&&(r.style[n]=e,this.updateElsHeight())}},e.prototype.setMaxHeight=function(e){this.setHeight(e,"max-height")},e.prototype.getFlattenColumns=function(){var e=[],t=this.table.columns;return t.forEach((function(t){t.isColumnGroup?e.push.apply(e,t.columns):e.push(t)})),e},e.prototype.updateElsHeight=function(){var e=this;if(!this.table.$ready)return b.a.nextTick((function(){return e.updateElsHeight()}));var t=this.table.$refs,n=t.headerWrapper,r=t.appendWrapper,o=t.footerWrapper;if(this.appendHeight=r?r.offsetHeight:0,!this.showHeader||n){var i=n?n.querySelector(".el-table__header tr"):null,a=this.headerDisplayNone(i),s=this.headerHeight=this.showHeader?n.offsetHeight:0;if(this.showHeader&&!a&&n.offsetWidth>0&&(this.table.columns||[]).length>0&&s<2)return b.a.nextTick((function(){return e.updateElsHeight()}));var l=this.tableHeight=this.table.$el.clientHeight,u=this.footerHeight=o?o.offsetHeight:0;null!==this.height&&(this.bodyHeight=l-s-u+(o?1:0)),this.fixedBodyHeight=this.scrollX?this.bodyHeight-this.gutterWidth:this.bodyHeight;var c=!(this.store.states.data&&this.store.states.data.length);this.viewportHeight=this.scrollX?l-(c?0:this.gutterWidth):l,this.updateScrollY(),this.notifyObservers("scrollable")}},e.prototype.headerDisplayNone=function(e){if(!e)return!0;var t=e;while("DIV"!==t.tagName){if("none"===getComputedStyle(t).display)return!0;t=t.parentElement}return!1},e.prototype.updateColumnsWidth=function(){if(!b.a.prototype.$isServer){var e=this.fit,t=this.table.$el.clientWidth,n=0,r=this.getFlattenColumns(),o=r.filter((function(e){return"number"!==typeof e.width}));if(r.forEach((function(e){"number"===typeof e.width&&e.realWidth&&(e.realWidth=null)})),o.length>0&&e){r.forEach((function(e){n+=e.width||e.minWidth||80}));var i=this.scrollY?this.gutterWidth:0;if(n<=t-i){this.scrollX=!1;var a=t-i-n;if(1===o.length)o[0].realWidth=(o[0].minWidth||80)+a;else{var s=o.reduce((function(e,t){return e+(t.minWidth||80)}),0),l=a/s,u=0;o.forEach((function(e,t){if(0!==t){var n=Math.floor((e.minWidth||80)*l);u+=n,e.realWidth=(e.minWidth||80)+n}})),o[0].realWidth=(o[0].minWidth||80)+a-u}}else this.scrollX=!0,o.forEach((function(e){e.realWidth=e.minWidth}));this.bodyWidth=Math.max(n,t),this.table.resizeState.width=this.bodyWidth}else r.forEach((function(e){e.width||e.minWidth?e.realWidth=e.width||e.minWidth:e.realWidth=80,n+=e.realWidth})),this.scrollX=n>t,this.bodyWidth=n;var c=this.store.states.fixedColumns;if(c.length>0){var f=0;c.forEach((function(e){f+=e.realWidth||e.width})),this.fixedWidth=f}var d=this.store.states.rightFixedColumns;if(d.length>0){var p=0;d.forEach((function(e){p+=e.realWidth||e.width})),this.rightFixedWidth=p}this.notifyObservers("columns")}},e.prototype.addObserver=function(e){this.observers.push(e)},e.prototype.removeObserver=function(e){var t=this.observers.indexOf(e);-1!==t&&this.observers.splice(t,1)},e.prototype.notifyObservers=function(e){var t=this,n=this.observers;n.forEach((function(n){switch(e){case"columns":n.onColumnsChange(t);break;case"scrollable":n.onScrollableChange(t);break;default:throw new Error("Table Layout don't have event "+e+".")}}))},e}(),z=H,B=n(2),D=n(29),W=n.n(D),V={created:function(){this.tableLayout.addObserver(this)},destroyed:function(){this.tableLayout.removeObserver(this)},computed:{tableLayout:function(){var e=this.layout;if(!e&&this.table&&(e=this.table.layout),!e)throw new Error("Can not find table layout.");return e}},mounted:function(){this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout)},updated:function(){this.__updated__||(this.onColumnsChange(this.tableLayout),this.onScrollableChange(this.tableLayout),this.__updated__=!0)},methods:{onColumnsChange:function(e){var t=this.$el.querySelectorAll("colgroup > col");if(t.length){var n=e.getFlattenColumns(),r={};n.forEach((function(e){r[e.id]=e}));for(var o=0,i=t.length;o col[name=gutter]"),n=0,r=t.length;n=this.leftFixedLeafCount:"right"===this.fixed?e=this.columnsCount-this.rightFixedLeafCount},getSpan:function(e,t,n,r){var o=1,i=1,a=this.table.spanMethod;if("function"===typeof a){var s=a({row:e,column:t,rowIndex:n,columnIndex:r});Array.isArray(s)?(o=s[0],i=s[1]):"object"===("undefined"===typeof s?"undefined":U(s))&&(o=s.rowspan,i=s.colspan)}return{rowspan:o,colspan:i}},getRowStyle:function(e,t){var n=this.table.rowStyle;return"function"===typeof n?n.call(null,{row:e,rowIndex:t}):n||null},getRowClass:function(e,t){var n=["el-table__row"];this.table.highlightCurrentRow&&e===this.store.states.currentRow&&n.push("current-row"),this.stripe&&t%2===1&&n.push("el-table__row--striped");var r=this.table.rowClassName;return"string"===typeof r?n.push(r):"function"===typeof r&&n.push(r.call(null,{row:e,rowIndex:t})),this.store.states.expandRows.indexOf(e)>-1&&n.push("expanded"),n},getCellStyle:function(e,t,n,r){var o=this.table.cellStyle;return"function"===typeof o?o.call(null,{rowIndex:e,columnIndex:t,row:n,column:r}):o},getCellClass:function(e,t,n,r){var o=[r.id,r.align,r.className];this.isColumnHidden(t)&&o.push("is-hidden");var i=this.table.cellClassName;return"string"===typeof i?o.push(i):"function"===typeof i&&o.push(i.call(null,{rowIndex:e,columnIndex:t,row:n,column:r})),o.join(" ")},getColspanRealWidth:function(e,t,n){if(t<1)return e[n].realWidth;var r=e.map((function(e){var t=e.realWidth;return t})).slice(n,n+t);return r.reduce((function(e,t){return e+t}),-1)},handleCellMouseEnter:function(e,t){var n=this.table,r=Object(w["b"])(e);if(r){var o=Object(w["c"])(n,r),i=n.hoverState={cell:r,column:o,row:t};n.$emit("cell-mouse-enter",i.row,i.column,i.cell,e)}var a=e.target.querySelector(".cell");if(Object(B["hasClass"])(a,"el-tooltip")&&a.childNodes.length){var s=document.createRange();s.setStart(a,0),s.setEnd(a,a.childNodes.length);var l=s.getBoundingClientRect().width,u=(parseInt(Object(B["getStyle"])(a,"paddingLeft"),10)||0)+(parseInt(Object(B["getStyle"])(a,"paddingRight"),10)||0);if((l+u>a.offsetWidth||a.scrollWidth>a.offsetWidth)&&this.$refs.tooltip){var c=this.$refs.tooltip;this.tooltipContent=r.innerText||r.textContent,c.referenceElm=r,c.$refs.popper&&(c.$refs.popper.style.display="none"),c.doDestroy(),c.setExpectedState(!0),this.activateTooltip(c)}}},handleCellMouseLeave:function(e){var t=this.$refs.tooltip;t&&(t.setExpectedState(!1),t.handleClosePopper());var n=Object(w["b"])(e);if(n){var r=this.table.hoverState||{};this.table.$emit("cell-mouse-leave",r.row,r.column,r.cell,e)}},handleMouseEnter:P()(30,(function(e){this.store.commit("setHoverRow",e)})),handleMouseLeave:P()(30,(function(){this.store.commit("setHoverRow",null)})),handleContextMenu:function(e,t){this.handleEvent(e,t,"contextmenu")},handleDoubleClick:function(e,t){this.handleEvent(e,t,"dblclick")},handleClick:function(e,t){this.store.commit("setCurrentRow",t),this.handleEvent(e,t,"click")},handleEvent:function(e,t,n){var r=this.table,o=Object(w["b"])(e),i=void 0;o&&(i=Object(w["c"])(r,o),i&&r.$emit("cell-"+n,t,i,o,e)),r.$emit("row-"+n,t,i,e)},rowRender:function(e,t,n){var r=this,o=this.$createElement,i=this.treeIndent,a=this.columns,s=this.firstDefaultColumnIndex,l=a.map((function(e,t){return r.isColumnHidden(t)})),u=this.getRowClass(e,t),c=!0;n&&(u.push("el-table__row--level-"+n.level),c=n.display);var f=c?null:{display:"none"};return o("tr",{style:[f,this.getRowStyle(e,t)],class:u,key:this.getKeyOfRow(e,t),on:{dblclick:function(t){return r.handleDoubleClick(t,e)},click:function(t){return r.handleClick(t,e)},contextmenu:function(t){return r.handleContextMenu(t,e)},mouseenter:function(e){return r.handleMouseEnter(t)},mouseleave:this.handleMouseLeave}},[a.map((function(u,c){var f=r.getSpan(e,u,t,c),d=f.rowspan,p=f.colspan;if(!d||!p)return null;var h=q({},u);h.realWidth=r.getColspanRealWidth(a,p,c);var v={store:r.store,_self:r.context||r.table.$vnode.context,column:h,row:e,$index:t};return c===s&&n&&(v.treeNode={indent:n.level*i,level:n.level},"boolean"===typeof n.expanded&&(v.treeNode.expanded=n.expanded,"loading"in n&&(v.treeNode.loading=n.loading),"noLazyChildren"in n&&(v.treeNode.noLazyChildren=n.noLazyChildren))),o("td",{style:r.getCellStyle(t,c,e,u),class:r.getCellClass(t,c,e,u),attrs:{rowspan:d,colspan:p},on:{mouseenter:function(t){return r.handleCellMouseEnter(t,e)},mouseleave:r.handleCellMouseLeave}},[u.renderCell.call(r._renderProxy,r.$createElement,v,l[c])])}))])},wrappedRowRender:function(e,t){var n=this,r=this.$createElement,o=this.store,i=o.isRowExpanded,a=o.assertRowKey,s=o.states,l=s.treeData,u=s.lazyTreeNodeMap,c=s.childrenColumnName,f=s.rowKey;if(this.hasExpandColumn&&i(e)){var d=this.table.renderExpanded,p=this.rowRender(e,t);return d?[[p,r("tr",{key:"expanded-row__"+p.key},[r("td",{attrs:{colspan:this.columnsCount},class:"el-table__expanded-cell"},[d(this.$createElement,{row:e,$index:t,store:this.store})])])]]:(console.error("[Element Error]renderExpanded is required."),p)}if(Object.keys(l).length){a();var h=Object(w["g"])(e,f),v=l[h],m=null;v&&(m={expanded:v.expanded,level:v.level,display:!0},"boolean"===typeof v.lazy&&("boolean"===typeof v.loaded&&v.loaded&&(m.noLazyChildren=!(v.children&&v.children.length)),m.loading=v.loading));var y=[this.rowRender(e,t,m)];if(v){var g=0,b=function e(r,o){r&&r.length&&o&&r.forEach((function(r){var i={display:o.display&&o.expanded,level:o.level+1},a=Object(w["g"])(r,f);if(void 0===a||null===a)throw new Error("for nested data item, row-key is required.");if(v=q({},l[a]),v&&(i.expanded=v.expanded,v.level=v.level||i.level,v.display=!(!v.expanded||!i.display),"boolean"===typeof v.lazy&&("boolean"===typeof v.loaded&&v.loaded&&(i.noLazyChildren=!(v.children&&v.children.length)),i.loading=v.loading)),g++,y.push(n.rowRender(r,t+g,i)),v){var s=u[a]||r[c];e(s,v)}}))};v.display=!0;var _=u[h]||e[c];b(_,v)}return y}return this.rowRender(e,t)}}},G=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"el-zoom-in-top"}},[e.multiple?n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("div",{staticClass:"el-table-filter__content"},[n("el-scrollbar",{attrs:{"wrap-class":"el-table-filter__wrap"}},[n("el-checkbox-group",{staticClass:"el-table-filter__checkbox-group",model:{value:e.filteredValue,callback:function(t){e.filteredValue=t},expression:"filteredValue"}},e._l(e.filters,(function(t){return n("el-checkbox",{key:t.value,attrs:{label:t.value}},[e._v(e._s(t.text))])})),1)],1)],1),n("div",{staticClass:"el-table-filter__bottom"},[n("button",{class:{"is-disabled":0===e.filteredValue.length},attrs:{disabled:0===e.filteredValue.length},on:{click:e.handleConfirm}},[e._v(e._s(e.t("el.table.confirmFilter")))]),n("button",{on:{click:e.handleReset}},[e._v(e._s(e.t("el.table.resetFilter")))])])]):n("div",{directives:[{name:"clickoutside",rawName:"v-clickoutside",value:e.handleOutsideClick,expression:"handleOutsideClick"},{name:"show",rawName:"v-show",value:e.showPopper,expression:"showPopper"}],staticClass:"el-table-filter"},[n("ul",{staticClass:"el-table-filter__list"},[n("li",{staticClass:"el-table-filter__list-item",class:{"is-active":void 0===e.filterValue||null===e.filterValue},on:{click:function(t){e.handleSelect(null)}}},[e._v(e._s(e.t("el.table.clearFilter")))]),e._l(e.filters,(function(t){return n("li",{key:t.value,staticClass:"el-table-filter__list-item",class:{"is-active":e.isActive(t)},attrs:{label:t.value},on:{click:function(n){e.handleSelect(t.value)}}},[e._v(e._s(t.text))])}))],2)])])},X=[];G._withStripped=!0;var Y=n(5),J=n.n(Y),Z=n(15),Q=n(12),ee=n.n(Q),te=[];!b.a.prototype.$isServer&&document.addEventListener("click",(function(e){te.forEach((function(t){var n=e.target;t&&t.$el&&(n===t.$el||t.$el.contains(n)||t.handleOutsideClick&&t.handleOutsideClick(e))}))}));var ne={open:function(e){e&&te.push(e)},close:function(e){var t=te.indexOf(e);-1!==t&&te.splice(e,1)}},re=n(40),oe=n.n(re),ie=n(14),ae=n.n(ie),se={name:"ElTableFilterPanel",mixins:[J.a,v.a],directives:{Clickoutside:ee.a},components:{ElCheckbox:a.a,ElCheckboxGroup:oe.a,ElScrollbar:ae.a},props:{placement:{type:String,default:"bottom-end"}},methods:{isActive:function(e){return e.value===this.filterValue},handleOutsideClick:function(){var e=this;setTimeout((function(){e.showPopper=!1}),16)},handleConfirm:function(){this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleReset:function(){this.filteredValue=[],this.confirmFilter(this.filteredValue),this.handleOutsideClick()},handleSelect:function(e){this.filterValue=e,"undefined"!==typeof e&&null!==e?this.confirmFilter(this.filteredValue):this.confirmFilter([]),this.handleOutsideClick()},confirmFilter:function(e){this.table.store.commit("filterChange",{column:this.column,values:e}),this.table.store.updateAllSelected()}},data:function(){return{table:null,cell:null,column:null}},computed:{filters:function(){return this.column&&this.column.filters},filterValue:{get:function(){return(this.column.filteredValue||[])[0]},set:function(e){this.filteredValue&&("undefined"!==typeof e&&null!==e?this.filteredValue.splice(0,1,e):this.filteredValue.splice(0,1))}},filteredValue:{get:function(){return this.column&&this.column.filteredValue||[]},set:function(e){this.column&&(this.column.filteredValue=e)}},multiple:function(){return!this.column||this.column.filterMultiple}},mounted:function(){var e=this;this.popperElm=this.$el,this.referenceElm=this.cell,this.table.bodyWrapper.addEventListener("scroll",(function(){e.updatePopper()})),this.$watch("showPopper",(function(t){e.column&&(e.column.filterOpened=t),t?ne.open(e):ne.close(e)}))},watch:{showPopper:function(e){!0===e&&parseInt(this.popperJS._popper.style.zIndex,10)1;return o&&(this.$parent.isGroup=!0),e("table",{class:"el-table__header",attrs:{cellspacing:"0",cellpadding:"0",border:"0"}},[e("colgroup",[this.columns.map((function(t){return e("col",{attrs:{name:t.id},key:t.id})})),this.hasGutter?e("col",{attrs:{name:"gutter"}}):""]),e("thead",{class:[{"is-group":o,"has-gutter":this.hasGutter}]},[this._l(r,(function(n,r){return e("tr",{style:t.getHeaderRowStyle(r),class:t.getHeaderRowClass(r)},[n.map((function(o,i){return e("th",{attrs:{colspan:o.colSpan,rowspan:o.rowSpan},on:{mousemove:function(e){return t.handleMouseMove(e,o)},mouseout:t.handleMouseOut,mousedown:function(e){return t.handleMouseDown(e,o)},click:function(e){return t.handleHeaderClick(e,o)},contextmenu:function(e){return t.handleHeaderContextMenu(e,o)}},style:t.getHeaderCellStyle(r,i,n,o),class:t.getHeaderCellClass(r,i,n,o),key:o.id},[e("div",{class:["cell",o.filteredValue&&o.filteredValue.length>0?"highlight":"",o.labelClassName]},[o.renderHeader?o.renderHeader.call(t._renderProxy,e,{column:o,$index:i,store:t.store,_self:t.$parent.$vnode.context}):o.label,o.sortable?e("span",{class:"caret-wrapper",on:{click:function(e){return t.handleSortClick(e,o)}}},[e("i",{class:"sort-caret ascending",on:{click:function(e){return t.handleSortClick(e,o,"ascending")}}}),e("i",{class:"sort-caret descending",on:{click:function(e){return t.handleSortClick(e,o,"descending")}}})]):"",o.filterable?e("span",{class:"el-table__column-filter-trigger",on:{click:function(e){return t.handleFilterClick(e,o)}}},[e("i",{class:["el-icon-arrow-down",o.filterOpened?"el-icon-arrow-up":""]})]):""])])})),t.hasGutter?e("th",{class:"gutter"}):""])}))])])},props:{fixed:String,store:{required:!0},border:Boolean,defaultSort:{type:Object,default:function(){return{prop:"",order:""}}}},components:{ElCheckbox:a.a},computed:de({table:function(){return this.$parent},hasGutter:function(){return!this.fixed&&this.tableLayout.gutterWidth}},R({columns:"columns",isAllSelected:"isAllSelected",leftFixedLeafCount:"fixedLeafColumnsLength",rightFixedLeafCount:"rightFixedLeafColumnsLength",columnsCount:function(e){return e.columns.length},leftFixedCount:function(e){return e.fixedColumns.length},rightFixedCount:function(e){return e.rightFixedColumns.length}})),created:function(){this.filterPanels={}},mounted:function(){var e=this;this.$nextTick((function(){var t=e.defaultSort,n=t.prop,r=t.order,o=!0;e.store.commit("sort",{prop:n,order:r,init:o})}))},beforeDestroy:function(){var e=this.filterPanels;for(var t in e)e.hasOwnProperty(t)&&e[t]&&e[t].$destroy(!0)},methods:{isCellHidden:function(e,t){for(var n=0,r=0;r=this.leftFixedLeafCount:"right"===this.fixed?n=this.columnsCount-this.rightFixedLeafCount},getHeaderRowStyle:function(e){var t=this.table.headerRowStyle;return"function"===typeof t?t.call(null,{rowIndex:e}):t},getHeaderRowClass:function(e){var t=[],n=this.table.headerRowClassName;return"string"===typeof n?t.push(n):"function"===typeof n&&t.push(n.call(null,{rowIndex:e})),t.join(" ")},getHeaderCellStyle:function(e,t,n,r){var o=this.table.headerCellStyle;return"function"===typeof o?o.call(null,{rowIndex:e,columnIndex:t,row:n,column:r}):o},getHeaderCellClass:function(e,t,n,r){var o=[r.id,r.order,r.headerAlign,r.className,r.labelClassName];0===e&&this.isCellHidden(t,n)&&o.push("is-hidden"),r.children||o.push("is-leaf"),r.sortable&&o.push("is-sortable");var i=this.table.headerCellClassName;return"string"===typeof i?o.push(i):"function"===typeof i&&o.push(i.call(null,{rowIndex:e,columnIndex:t,row:n,column:r})),o.join(" ")},toggleAllSelection:function(e){e.stopPropagation(),this.store.commit("toggleAllSelection")},handleFilterClick:function(e,t){e.stopPropagation();var n=e.target,r="TH"===n.tagName?n:n.parentNode;if(!Object(B["hasClass"])(r,"noclick")){r=r.querySelector(".el-table__column-filter-trigger")||r;var o=this.$parent,i=this.filterPanels[t.id];i&&t.filterOpened?i.showPopper=!1:(i||(i=new b.a(fe),this.filterPanels[t.id]=i,t.filterPlacement&&(i.placement=t.filterPlacement),i.table=o,i.cell=r,i.column=t,!this.$isServer&&i.$mount(document.createElement("div"))),setTimeout((function(){i.showPopper=!0}),16))}},handleHeaderClick:function(e,t){!t.filters&&t.sortable?this.handleSortClick(e,t):t.filterable&&!t.sortable&&this.handleFilterClick(e,t),this.$parent.$emit("header-click",t,e)},handleHeaderContextMenu:function(e,t){this.$parent.$emit("header-contextmenu",t,e)},handleMouseDown:function(e,t){var n=this;if(!this.$isServer&&!(t.children&&t.children.length>0)&&this.draggingColumn&&this.border){this.dragging=!0,this.$parent.resizeProxyVisible=!0;var r=this.$parent,o=r.$el,i=o.getBoundingClientRect().left,a=this.$el.querySelector("th."+t.id),s=a.getBoundingClientRect(),l=s.left-i+30;Object(B["addClass"])(a,"noclick"),this.dragState={startMouseLeft:e.clientX,startLeft:s.right-i,startColumnLeft:s.left-i,tableLeft:i};var u=r.$refs.resizeProxy;u.style.left=this.dragState.startLeft+"px",document.onselectstart=function(){return!1},document.ondragstart=function(){return!1};var c=function(e){var t=e.clientX-n.dragState.startMouseLeft,r=n.dragState.startLeft+t;u.style.left=Math.max(l,r)+"px"},f=function o(){if(n.dragging){var i=n.dragState,s=i.startColumnLeft,l=i.startLeft,f=parseInt(u.style.left,10),d=f-s;t.width=t.realWidth=d,r.$emit("header-dragend",t.width,l-s,t,e),n.store.scheduleLayout(),document.body.style.cursor="",n.dragging=!1,n.draggingColumn=null,n.dragState={},r.resizeProxyVisible=!1}document.removeEventListener("mousemove",c),document.removeEventListener("mouseup",o),document.onselectstart=null,document.ondragstart=null,setTimeout((function(){Object(B["removeClass"])(a,"noclick")}),0)};document.addEventListener("mousemove",c),document.addEventListener("mouseup",f)}},handleMouseMove:function(e,t){if(!(t.children&&t.children.length>0)){var n=e.target;while(n&&"TH"!==n.tagName)n=n.parentNode;if(t&&t.resizable&&!this.dragging&&this.border){var r=n.getBoundingClientRect(),o=document.body.style;r.width>12&&r.right-e.pageX<8?(o.cursor="col-resize",Object(B["hasClass"])(n,"is-sortable")&&(n.style.cursor="col-resize"),this.draggingColumn=t):this.dragging||(o.cursor="",Object(B["hasClass"])(n,"is-sortable")&&(n.style.cursor="pointer"),this.draggingColumn=null)}}},handleMouseOut:function(){this.$isServer||(document.body.style.cursor="")},toggleOrder:function(e){var t=e.order,n=e.sortOrders;if(""===t)return n[0];var r=n.indexOf(t||null);return n[r>n.length-2?0:r+1]},handleSortClick:function(e,t,n){e.stopPropagation();var r=t.order===n?null:n||this.toggleOrder(t),o=e.target;while(o&&"TH"!==o.tagName)o=o.parentNode;if(o&&"TH"===o.tagName&&Object(B["hasClass"])(o,"noclick"))Object(B["removeClass"])(o,"noclick");else if(t.sortable){var i=this.store.states,a=i.sortProp,s=void 0,l=i.sortingColumn;(l!==t||l===t&&null===l.order)&&(l&&(l.order=null),i.sortingColumn=t,a=t.property),s=t.order=r||null,i.sortProp=a,i.sortOrder=s,this.store.commit("changeSortCondition")}}},data:function(){return{draggingColumn:null,dragging:!1,dragState:{}}}},me=Object.assign||function(e){for(var t=1;t=this.leftFixedLeafCount;if("right"===this.fixed){for(var r=0,o=0;o=this.columnsCount-this.rightFixedCount)},getRowClasses:function(e,t){var n=[e.id,e.align,e.labelClassName];return e.className&&n.push(e.className),this.isCellHidden(t,this.columns,e)&&n.push("is-hidden"),e.children||n.push("is-leaf"),n}}},ge=Object.assign||function(e){for(var t=1;t0){var r=n.scrollTop;t.pixelY<0&&0!==r&&e.preventDefault(),t.pixelY>0&&n.scrollHeight-n.clientHeight>r&&e.preventDefault(),n.scrollTop+=Math.ceil(t.pixelY/5)}else n.scrollLeft+=Math.ceil(t.pixelX/5)},handleHeaderFooterMousewheel:function(e,t){var n=t.pixelX,r=t.pixelY;Math.abs(n)>=Math.abs(r)&&(this.bodyWrapper.scrollLeft+=t.pixelX/5)},syncPostion:Object(s["throttle"])(20,(function(){var e=this.bodyWrapper,t=e.scrollLeft,n=e.scrollTop,r=e.offsetWidth,o=e.scrollWidth,i=this.$refs,a=i.headerWrapper,s=i.footerWrapper,l=i.fixedBodyWrapper,u=i.rightFixedBodyWrapper;a&&(a.scrollLeft=t),s&&(s.scrollLeft=t),l&&(l.scrollTop=n),u&&(u.scrollTop=n);var c=o-r-1;this.scrollPosition=t>=c?"right":0===t?"left":"middle"})),bindEvents:function(){this.bodyWrapper.addEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(l["addResizeListener"])(this.$el,this.resizeListener)},unbindEvents:function(){this.bodyWrapper.removeEventListener("scroll",this.syncPostion,{passive:!0}),this.fit&&Object(l["removeResizeListener"])(this.$el,this.resizeListener)},resizeListener:function(){if(this.$ready){var e=!1,t=this.$el,n=this.resizeState,r=n.width,o=n.height,i=t.offsetWidth;r!==i&&(e=!0);var a=t.offsetHeight;(this.height||this.shouldUpdateHeight)&&o!==a&&(e=!0),e&&(this.resizeState.width=i,this.resizeState.height=a,this.doLayout())}},doLayout:function(){this.shouldUpdateHeight&&this.layout.updateElsHeight(),this.layout.updateColumnsWidth()},sort:function(e,t){this.store.commit("sort",{prop:e,order:t})},toggleAllSelection:function(){this.store.commit("toggleAllSelection")}},computed:ge({tableSize:function(){return this.size||(this.$ELEMENT||{}).size},bodyWrapper:function(){return this.$refs.bodyWrapper},shouldUpdateHeight:function(){return this.height||this.maxHeight||this.fixedColumns.length>0||this.rightFixedColumns.length>0},bodyWidth:function(){var e=this.layout,t=e.bodyWidth,n=e.scrollY,r=e.gutterWidth;return t?t-(n?r:0)+"px":""},bodyHeight:function(){var e=this.layout,t=e.headerHeight,n=void 0===t?0:t,r=e.bodyHeight,o=e.footerHeight,i=void 0===o?0:o;if(this.height)return{height:r?r+"px":""};if(this.maxHeight){var a=Object(w["j"])(this.maxHeight);if("number"===typeof a)return{"max-height":a-i-(this.showHeader?n:0)+"px"}}return{}},fixedBodyHeight:function(){if(this.height)return{height:this.layout.fixedBodyHeight?this.layout.fixedBodyHeight+"px":""};if(this.maxHeight){var e=Object(w["j"])(this.maxHeight);if("number"===typeof e)return e=this.layout.scrollX?e-this.layout.gutterWidth:e,this.showHeader&&(e-=this.layout.headerHeight),e-=this.layout.footerHeight,{"max-height":e+"px"}}return{}},fixedHeight:function(){return this.maxHeight?this.showSummary?{bottom:0}:{bottom:this.layout.scrollX&&this.data.length?this.layout.gutterWidth+"px":""}:this.showSummary?{height:this.layout.tableHeight?this.layout.tableHeight+"px":""}:{height:this.layout.viewportHeight?this.layout.viewportHeight+"px":""}},emptyBlockStyle:function(){if(this.data&&this.data.length)return null;var e="100%";return this.layout.appendHeight&&(e="calc(100% - "+this.layout.appendHeight+"px)"),{width:this.bodyWidth,height:e}}},R({selection:"selection",columns:"columns",tableData:"data",fixedColumns:"fixedColumns",rightFixedColumns:"rightFixedColumns"})),watch:{height:{immediate:!0,handler:function(e){this.layout.setHeight(e)}},maxHeight:{immediate:!0,handler:function(e){this.layout.setMaxHeight(e)}},currentRowKey:{immediate:!0,handler:function(e){this.rowKey&&this.store.setCurrentRowKey(e)}},data:{immediate:!0,handler:function(e){this.store.commit("setData",e)}},expandRowKeys:{immediate:!0,handler:function(e){e&&this.store.setExpandRowKeysAdapter(e)}}},created:function(){var e=this;this.tableId="el-table_"+be++,this.debouncedUpdateLayout=Object(s["debounce"])(50,(function(){return e.doLayout()}))},mounted:function(){var e=this;this.bindEvents(),this.store.updateColumns(),this.doLayout(),this.resizeState={width:this.$el.offsetWidth,height:this.$el.offsetHeight},this.store.states.columns.forEach((function(t){t.filteredValue&&t.filteredValue.length&&e.store.commit("filterChange",{column:t,values:t.filteredValue,silent:!0})})),this.$ready=!0},destroyed:function(){this.unbindEvents()},data:function(){var e=this.treeProps,t=e.hasChildren,n=void 0===t?"hasChildren":t,r=e.children,o=void 0===r?"children":r;this.store=L(this,{rowKey:this.rowKey,defaultExpandAll:this.defaultExpandAll,selectOnIndeterminate:this.selectOnIndeterminate,indent:this.indent,lazy:this.lazy,lazyColumnIdentifier:n,childrenColumnName:o});var i=new z({store:this.store,table:this,fit:this.fit,showHeader:this.showHeader});return{layout:i,isHidden:!1,renderExpanded:null,resizeProxyVisible:!1,resizeState:{width:null,height:null},isGroup:!1,scrollPosition:"left"}}},xe=_e,we=Object(ue["a"])(xe,r,o,!1,null,null,null);we.options.__file="packages/table/src/table.vue";var Ce=we.exports;Ce.install=function(e){e.component(Ce.name,Ce)};t["default"]=Ce}])},"60f8":function(e,t){e.exports=function(e,t,n,r){var o,i=0;function a(){var a=this,s=Number(new Date)-i,l=arguments;function u(){i=Number(new Date),n.apply(a,l)}function c(){o=void 0}r&&!o&&u(),o&&clearTimeout(o),void 0===r&&s>e?u():!0!==t&&(o=setTimeout(r?c:u,void 0===r?e-s:e))}return"boolean"!==typeof t&&(r=n,n=t,t=void 0),a}},6389:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=77)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},10:function(e,t){e.exports=n("77bb")},13:function(e,t){e.exports=n("3f00")},15:function(e,t){e.exports=n("72e8")},19:function(e,t){e.exports=n("1ab3")},2:function(e,t){e.exports=n("c865")},23:function(e,t){e.exports=n("d508")},47:function(e,t){e.exports=n("2697")},6:function(e,t){e.exports=n("36d4")},7:function(e,t){e.exports=n("0261")},77:function(e,t,n){"use strict";n.r(t);var r=n(7),o=n.n(r),i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"msgbox-fade"}},[n("div",{directives:[{name:"show",rawName:"v-show",value:e.visible,expression:"visible"}],staticClass:"el-message-box__wrapper",attrs:{tabindex:"-1",role:"dialog","aria-modal":"true","aria-label":e.title||"dialog"},on:{click:function(t){return t.target!==t.currentTarget?null:e.handleWrapperClick(t)}}},[n("div",{staticClass:"el-message-box",class:[e.customClass,e.center&&"el-message-box--center"]},[null!==e.title?n("div",{staticClass:"el-message-box__header"},[n("div",{staticClass:"el-message-box__title"},[e.icon&&e.center?n("div",{class:["el-message-box__status",e.icon]}):e._e(),n("span",[e._v(e._s(e.title))])]),e.showClose?n("button",{staticClass:"el-message-box__headerbtn",attrs:{type:"button","aria-label":"Close"},on:{click:function(t){e.handleAction(e.distinguishCancelAndClose?"close":"cancel")},keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction(e.distinguishCancelAndClose?"close":"cancel")}}},[n("i",{staticClass:"el-message-box__close el-icon-close"})]):e._e()]):e._e(),n("div",{staticClass:"el-message-box__content"},[n("div",{staticClass:"el-message-box__container"},[e.icon&&!e.center&&""!==e.message?n("div",{class:["el-message-box__status",e.icon]}):e._e(),""!==e.message?n("div",{staticClass:"el-message-box__message"},[e._t("default",[e.dangerouslyUseHTMLString?n("p",{domProps:{innerHTML:e._s(e.message)}}):n("p",[e._v(e._s(e.message))])])],2):e._e()]),n("div",{directives:[{name:"show",rawName:"v-show",value:e.showInput,expression:"showInput"}],staticClass:"el-message-box__input"},[n("el-input",{ref:"input",attrs:{type:e.inputType,placeholder:e.inputPlaceholder},nativeOn:{keydown:function(t){return!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleInputEnter(t)}},model:{value:e.inputValue,callback:function(t){e.inputValue=t},expression:"inputValue"}}),n("div",{staticClass:"el-message-box__errormsg",style:{visibility:e.editorErrorMessage?"visible":"hidden"}},[e._v(e._s(e.editorErrorMessage))])],1)]),n("div",{staticClass:"el-message-box__btns"},[e.showCancelButton?n("el-button",{class:[e.cancelButtonClasses],attrs:{loading:e.cancelButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("cancel")}},nativeOn:{click:function(t){e.handleAction("cancel")}}},[e._v("\n "+e._s(e.cancelButtonText||e.t("el.messagebox.cancel"))+"\n ")]):e._e(),n("el-button",{directives:[{name:"show",rawName:"v-show",value:e.showConfirmButton,expression:"showConfirmButton"}],ref:"confirm",class:[e.confirmButtonClasses],attrs:{loading:e.confirmButtonLoading,round:e.roundButton,size:"small"},on:{keydown:function(t){if(!("button"in t)&&e._k(t.keyCode,"enter",13,t.key,"Enter"))return null;e.handleAction("confirm")}},nativeOn:{click:function(t){e.handleAction("confirm")}}},[e._v("\n "+e._s(e.confirmButtonText||e.t("el.messagebox.confirm"))+"\n ")])],1)])])])},a=[];i._withStripped=!0;var s=n(15),l=n.n(s),u=n(6),c=n.n(u),f=n(10),d=n.n(f),p=n(13),h=n.n(p),v=n(2),m=n(19),y=n(47),g=n.n(y),b=void 0,_={success:"success",info:"info",warning:"warning",error:"error"},x={mixins:[l.a,c.a],props:{modal:{default:!0},lockScroll:{default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{default:!0},closeOnPressEscape:{default:!0},closeOnHashChange:{default:!0},center:{default:!1,type:Boolean},roundButton:{default:!1,type:Boolean}},components:{ElInput:d.a,ElButton:h.a},computed:{icon:function(){var e=this.type,t=this.iconClass;return t||(e&&_[e]?"el-icon-"+_[e]:"")},confirmButtonClasses:function(){return"el-button--primary "+this.confirmButtonClass},cancelButtonClasses:function(){return""+this.cancelButtonClass}},methods:{getSafeClose:function(){var e=this,t=this.uid;return function(){e.$nextTick((function(){t===e.uid&&e.doClose()}))}},doClose:function(){var e=this;this.visible&&(this.visible=!1,this._closing=!0,this.onClose&&this.onClose(),b.closeDialog(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose(),setTimeout((function(){e.action&&e.callback(e.action,e)})))},handleWrapperClick:function(){this.closeOnClickModal&&this.handleAction(this.distinguishCancelAndClose?"close":"cancel")},handleInputEnter:function(){if("textarea"!==this.inputType)return this.handleAction("confirm")},handleAction:function(e){("prompt"!==this.$type||"confirm"!==e||this.validate())&&(this.action=e,"function"===typeof this.beforeClose?(this.close=this.getSafeClose(),this.beforeClose(e,this,this.close)):this.doClose())},validate:function(){if("prompt"===this.$type){var e=this.inputPattern;if(e&&!e.test(this.inputValue||""))return this.editorErrorMessage=this.inputErrorMessage||Object(m["t"])("el.messagebox.error"),Object(v["addClass"])(this.getInputElement(),"invalid"),!1;var t=this.inputValidator;if("function"===typeof t){var n=t(this.inputValue);if(!1===n)return this.editorErrorMessage=this.inputErrorMessage||Object(m["t"])("el.messagebox.error"),Object(v["addClass"])(this.getInputElement(),"invalid"),!1;if("string"===typeof n)return this.editorErrorMessage=n,Object(v["addClass"])(this.getInputElement(),"invalid"),!1}}return this.editorErrorMessage="",Object(v["removeClass"])(this.getInputElement(),"invalid"),!0},getFirstFocus:function(){var e=this.$el.querySelector(".el-message-box__btns .el-button"),t=this.$el.querySelector(".el-message-box__btns .el-message-box__title");return e||t},getInputElement:function(){var e=this.$refs.input.$refs;return e.input||e.textarea},handleClose:function(){this.handleAction("close")}},watch:{inputValue:{immediate:!0,handler:function(e){var t=this;this.$nextTick((function(n){"prompt"===t.$type&&null!==e&&t.validate()}))}},visible:function(e){var t=this;e&&(this.uid++,"alert"!==this.$type&&"confirm"!==this.$type||this.$nextTick((function(){t.$refs.confirm.$el.focus()})),this.focusAfterClosed=document.activeElement,b=new g.a(this.$el,this.focusAfterClosed,this.getFirstFocus())),"prompt"===this.$type&&(e?setTimeout((function(){t.$refs.input&&t.$refs.input.$el&&t.getInputElement().focus()}),500):(this.editorErrorMessage="",Object(v["removeClass"])(this.getInputElement(),"invalid")))}},mounted:function(){var e=this;this.$nextTick((function(){e.closeOnHashChange&&window.addEventListener("hashchange",e.close)}))},beforeDestroy:function(){this.closeOnHashChange&&window.removeEventListener("hashchange",this.close),setTimeout((function(){b.closeDialog()}))},data:function(){return{uid:1,title:void 0,message:"",type:"",iconClass:"",customClass:"",showInput:!1,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,action:"",confirmButtonText:"",cancelButtonText:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonClass:"",confirmButtonDisabled:!1,cancelButtonClass:"",editorErrorMessage:null,callback:null,dangerouslyUseHTMLString:!1,focusAfterClosed:null,isOnComposition:!1,distinguishCancelAndClose:!1}}},w=x,C=n(0),S=Object(C["a"])(w,i,a,!1,null,null,null);S.options.__file="packages/message-box/src/main.vue";var O=S.exports,E=n(9),k=n.n(E),$=n(23),j="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},A={title:null,message:"",type:"",iconClass:"",showInput:!1,showClose:!0,modalFade:!0,lockScroll:!0,closeOnClickModal:!0,closeOnPressEscape:!0,closeOnHashChange:!0,inputValue:null,inputPlaceholder:"",inputType:"text",inputPattern:null,inputValidator:null,inputErrorMessage:"",showConfirmButton:!0,showCancelButton:!1,confirmButtonPosition:"right",confirmButtonHighlight:!1,cancelButtonHighlight:!1,confirmButtonText:"",cancelButtonText:"",confirmButtonClass:"",cancelButtonClass:"",customClass:"",beforeClose:null,dangerouslyUseHTMLString:!1,center:!1,roundButton:!1,distinguishCancelAndClose:!1},T=o.a.extend(O),M=void 0,P=void 0,L=[],R=function(e){if(M){var t=M.callback;"function"===typeof t&&(P.showInput?t(P.inputValue,e):t(e)),M.resolve&&("confirm"===e?P.showInput?M.resolve({value:P.inputValue,action:e}):M.resolve(e):!M.reject||"cancel"!==e&&"close"!==e||M.reject(e))}},N=function(){P=new T({el:document.createElement("div")}),P.callback=R},I=function e(){if(P||N(),P.action="",(!P.visible||P.closeTimer)&&L.length>0){M=L.shift();var t=M.options;for(var n in t)t.hasOwnProperty(n)&&(P[n]=t[n]);void 0===t.callback&&(P.callback=R);var r=P.callback;P.callback=function(t,n){r(t,n),e()},Object($["isVNode"])(P.message)?(P.$slots.default=[P.message],P.message=null):delete P.$slots.default,["modal","showClose","closeOnClickModal","closeOnPressEscape","closeOnHashChange"].forEach((function(e){void 0===P[e]&&(P[e]=!0)})),document.body.appendChild(P.$el),o.a.nextTick((function(){P.visible=!0}))}},F=function e(t,n){if(!o.a.prototype.$isServer){if("string"===typeof t||Object($["isVNode"])(t)?(t={message:t},"string"===typeof arguments[1]&&(t.title=arguments[1])):t.callback&&!n&&(n=t.callback),"undefined"!==typeof Promise)return new Promise((function(r,o){L.push({options:k()({},A,e.defaults,t),callback:n,resolve:r,reject:o}),I()}));L.push({options:k()({},A,e.defaults,t),callback:n}),I()}};F.setDefaults=function(e){F.defaults=e},F.alert=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,$type:"alert",closeOnPressEscape:!1,closeOnClickModal:!1},n))},F.confirm=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,$type:"confirm",showCancelButton:!0},n))},F.prompt=function(e,t,n){return"object"===("undefined"===typeof t?"undefined":j(t))?(n=t,t=""):void 0===t&&(t=""),F(k()({title:t,message:e,showCancelButton:!0,showInput:!0,$type:"prompt"},n))},F.close=function(){P.doClose(),P.visible=!1,L=[],M=null};var H=F;t["default"]=H},9:function(e,t){e.exports=n("eb40")}})},"63ec":function(e,t,n){var r=n("60f8"),o=n("ca47");e.exports={throttle:r,debounce:o}},"692f":function(e,t,n){var r=n("efe2"),o=n("2118"),i="".split;e.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==o(e)?i.call(e,""):Object(e)}:Object},"69c5":function(e,t,n){var r=n("857c");e.exports=function(e,t,n,o){try{return o?t(r(n)[0],n[1]):t(n)}catch(a){var i=e["return"];throw void 0!==i&&r(i.call(e)),a}}},"69fa":function(e,t,n){"use strict";var r=n("2abc").IteratorPrototype,o=n("6d60"),i=n("38b9"),a=n("27b5"),s=n("9806"),l=function(){return this};e.exports=function(e,t,n){var u=t+" Iterator";return e.prototype=o(r,{next:i(1,n)}),a(e,u,!1,!0),s[u]=l,e}},"6a7c":function(e,t,n){"use strict";t.__esModule=!0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.default=function(e){function t(e){for(var t=arguments.length,n=Array(t>1?t-1:0),a=1;a",d="<",p="prototype",h="script",v=c("IE_PROTO"),m=function(){},y=function(e){return d+h+f+e+d+"/"+h+f},g=function(e){e.write(y("")),e.close();var t=e.parentWindow.Object;return e=null,t},b=function(){var e,t=u("iframe"),n="java"+h+":";return t.style.display="none",l.appendChild(t),t.src=String(n),e=t.contentWindow.document,e.open(),e.write(y("document.F=Object")),e.close(),e.F},_=function(){try{r=document.domain&&new ActiveXObject("htmlfile")}catch(t){}_=r?g(r):b();var e=a.length;while(e--)delete _[p][a[e]];return _()};s[v]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(m[p]=o(e),n=new m,m[p]=null,n[v]=e):n=_(),void 0===t?n:i(n,t)}},"6d7a":function(e,t,n){var r=n("1b99"),o=n("d890"),i=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?i(r[e])||i(o[e]):r[e]&&r[e][t]||o[e]&&o[e][t]}},"6eb9":function(e,t,n){},"6fdf":function(e,t,n){var r=n("6d7a");e.exports=r("document","documentElement")},"72e8":function(e,t,n){"use strict";t.__esModule=!0,t.PopupManager=void 0;var r=n("0261"),o=d(r),i=n("eb40"),a=d(i),s=n("0540"),l=d(s),u=n("e857"),c=d(u),f=n("c865");function d(e){return e&&e.__esModule?e:{default:e}}var p=1,h=void 0;t.default={props:{visible:{type:Boolean,default:!1},openDelay:{},closeDelay:{},zIndex:{},modal:{type:Boolean,default:!1},modalFade:{type:Boolean,default:!0},modalClass:{},modalAppendToBody:{type:Boolean,default:!1},lockScroll:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!1},closeOnClickModal:{type:Boolean,default:!1}},beforeMount:function(){this._popupId="popup-"+p++,l.default.register(this._popupId,this)},beforeDestroy:function(){l.default.deregister(this._popupId),l.default.closeModal(this._popupId),this.restoreBodyStyle()},data:function(){return{opened:!1,bodyPaddingRight:null,computedBodyPaddingRight:0,withoutHiddenClass:!0,rendered:!1}},watch:{visible:function(e){var t=this;if(e){if(this._opening)return;this.rendered?this.open():(this.rendered=!0,o.default.nextTick((function(){t.open()})))}else this.close()}},methods:{open:function(e){var t=this;this.rendered||(this.rendered=!0);var n=(0,a.default)({},this.$props||this,e);this._closeTimer&&(clearTimeout(this._closeTimer),this._closeTimer=null),clearTimeout(this._openTimer);var r=Number(n.openDelay);r>0?this._openTimer=setTimeout((function(){t._openTimer=null,t.doOpen(n)}),r):this.doOpen(n)},doOpen:function(e){if(!this.$isServer&&(!this.willOpen||this.willOpen())&&!this.opened){this._opening=!0;var t=this.$el,n=e.modal,r=e.zIndex;if(r&&(l.default.zIndex=r),n&&(this._closing&&(l.default.closeModal(this._popupId),this._closing=!1),l.default.openModal(this._popupId,l.default.nextZIndex(),this.modalAppendToBody?void 0:t,e.modalClass,e.modalFade),e.lockScroll)){this.withoutHiddenClass=!(0,f.hasClass)(document.body,"el-popup-parent--hidden"),this.withoutHiddenClass&&(this.bodyPaddingRight=document.body.style.paddingRight,this.computedBodyPaddingRight=parseInt((0,f.getStyle)(document.body,"paddingRight"),10)),h=(0,c.default)();var o=document.documentElement.clientHeight0&&(o||"scroll"===i)&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.computedBodyPaddingRight+h+"px"),(0,f.addClass)(document.body,"el-popup-parent--hidden")}"static"===getComputedStyle(t).position&&(t.style.position="absolute"),t.style.zIndex=l.default.nextZIndex(),this.opened=!0,this.onOpen&&this.onOpen(),this.doAfterOpen()}},doAfterOpen:function(){this._opening=!1},close:function(){var e=this;if(!this.willClose||this.willClose()){null!==this._openTimer&&(clearTimeout(this._openTimer),this._openTimer=null),clearTimeout(this._closeTimer);var t=Number(this.closeDelay);t>0?this._closeTimer=setTimeout((function(){e._closeTimer=null,e.doClose()}),t):this.doClose()}},doClose:function(){this._closing=!0,this.onClose&&this.onClose(),this.lockScroll&&setTimeout(this.restoreBodyStyle,200),this.opened=!1,this.doAfterClose()},doAfterClose:function(){l.default.closeModal(this._popupId),this._closing=!1},restoreBodyStyle:function(){this.modal&&this.withoutHiddenClass&&(document.body.style.paddingRight=this.bodyPaddingRight,(0,f.removeClass)(document.body,"el-popup-parent--hidden")),this.withoutHiddenClass=!0}}},t.PopupManager=l.default},"74cb":function(e,t,n){var r=n("c54b");e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},"769c":function(e,t,n){var r=n("4fda");e.exports=/(iphone|ipod|ipad).*applewebkit/i.test(r)},"76ab":function(e,t,n){"use strict";var r=n("2ae1"),o=n("2895"),i=10,a=40,s=800;function l(e){var t=0,n=0,r=0,o=0;return"detail"in e&&(n=e.detail),"wheelDelta"in e&&(n=-e.wheelDelta/120),"wheelDeltaY"in e&&(n=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=n,n=0),r=t*i,o=n*i,"deltaY"in e&&(o=e.deltaY),"deltaX"in e&&(r=e.deltaX),(r||o)&&e.deltaMode&&(1==e.deltaMode?(r*=a,o*=a):(r*=s,o*=s)),r&&!t&&(t=r<1?-1:1),o&&!n&&(n=o<1?-1:1),{spinX:t,spinY:n,pixelX:r,pixelY:o}}l.getEventType=function(){return r.firefox()?"DOMMouseScroll":o("wheel")?"wheel":"mousewheel"},e.exports=l},7763:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=124)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},124:function(e,t,n){"use strict";n.r(t);var r,o,i={name:"ElTag",props:{text:String,closable:Boolean,type:String,hit:Boolean,disableTransitions:Boolean,color:String,size:String,effect:{type:String,default:"light",validator:function(e){return-1!==["dark","light","plain"].indexOf(e)}}},methods:{handleClose:function(e){e.stopPropagation(),this.$emit("close",e)},handleClick:function(e){this.$emit("click",e)}},computed:{tagSize:function(){return this.size||(this.$ELEMENT||{}).size}},render:function(e){var t=this.type,n=this.tagSize,r=this.hit,o=this.effect,i=["el-tag",t?"el-tag--"+t:"",n?"el-tag--"+n:"",o?"el-tag--"+o:"",r&&"is-hit"],a=e("span",{class:i,style:{backgroundColor:this.color},on:{click:this.handleClick}},[this.$slots.default,this.closable&&e("i",{class:"el-tag__close el-icon-close",on:{click:this.handleClose}})]);return this.disableTransitions?a:e("transition",{attrs:{name:"el-zoom-in-center"}},[a])}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/tag/src/tag.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},"77bb":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=76)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},11:function(e,t){e.exports=n("a4f6")},21:function(e,t){e.exports=n("bd85")},4:function(e,t){e.exports=n("3242")},76:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{class:["textarea"===e.type?"el-textarea":"el-input",e.inputSize?"el-input--"+e.inputSize:"",{"is-disabled":e.inputDisabled,"is-exceed":e.inputExceed,"el-input-group":e.$slots.prepend||e.$slots.append,"el-input-group--append":e.$slots.append,"el-input-group--prepend":e.$slots.prepend,"el-input--prefix":e.$slots.prefix||e.prefixIcon,"el-input--suffix":e.$slots.suffix||e.suffixIcon||e.clearable||e.showPassword}],on:{mouseenter:function(t){e.hovering=!0},mouseleave:function(t){e.hovering=!1}}},["textarea"!==e.type?[e.$slots.prepend?n("div",{staticClass:"el-input-group__prepend"},[e._t("prepend")],2):e._e(),"textarea"!==e.type?n("input",e._b({ref:"input",staticClass:"el-input__inner",attrs:{tabindex:e.tabindex,type:e.showPassword?e.passwordVisible?"text":"password":e.type,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"input",e.$attrs,!1)):e._e(),e.$slots.prefix||e.prefixIcon?n("span",{staticClass:"el-input__prefix"},[e._t("prefix"),e.prefixIcon?n("i",{staticClass:"el-input__icon",class:e.prefixIcon}):e._e()],2):e._e(),e.getSuffixVisible()?n("span",{staticClass:"el-input__suffix"},[n("span",{staticClass:"el-input__suffix-inner"},[e.showClear&&e.showPwdVisible&&e.isWordLimitVisible?e._e():[e._t("suffix"),e.suffixIcon?n("i",{staticClass:"el-input__icon",class:e.suffixIcon}):e._e()],e.showClear?n("i",{staticClass:"el-input__icon el-icon-circle-close el-input__clear",on:{mousedown:function(e){e.preventDefault()},click:e.clear}}):e._e(),e.showPwdVisible?n("i",{staticClass:"el-input__icon el-icon-view el-input__clear",on:{click:e.handlePasswordVisible}}):e._e(),e.isWordLimitVisible?n("span",{staticClass:"el-input__count"},[n("span",{staticClass:"el-input__count-inner"},[e._v("\n "+e._s(e.textLength)+"/"+e._s(e.upperLimit)+"\n ")])]):e._e()],2),e.validateState?n("i",{staticClass:"el-input__icon",class:["el-input__validateIcon",e.validateIcon]}):e._e()]):e._e(),e.$slots.append?n("div",{staticClass:"el-input-group__append"},[e._t("append")],2):e._e()]:n("textarea",e._b({ref:"textarea",staticClass:"el-textarea__inner",style:e.textareaStyle,attrs:{tabindex:e.tabindex,disabled:e.inputDisabled,readonly:e.readonly,autocomplete:e.autoComplete||e.autocomplete,"aria-label":e.label},on:{compositionstart:e.handleCompositionStart,compositionupdate:e.handleCompositionUpdate,compositionend:e.handleCompositionEnd,input:e.handleInput,focus:e.handleFocus,blur:e.handleBlur,change:e.handleChange}},"textarea",e.$attrs,!1)),e.isWordLimitVisible&&"textarea"===e.type?n("span",{staticClass:"el-input__count"},[e._v(e._s(e.textLength)+"/"+e._s(e.upperLimit))]):e._e()],2)},o=[];r._withStripped=!0;var i=n(4),a=n.n(i),s=n(11),l=n.n(s),u=void 0,c="\n height:0 !important;\n visibility:hidden !important;\n overflow:hidden !important;\n position:absolute !important;\n z-index:-1000 !important;\n top:0 !important;\n right:0 !important\n",f=["letter-spacing","line-height","padding-top","padding-bottom","font-family","font-weight","font-size","text-rendering","text-transform","width","text-indent","padding-left","padding-right","border-width","box-sizing"];function d(e){var t=window.getComputedStyle(e),n=t.getPropertyValue("box-sizing"),r=parseFloat(t.getPropertyValue("padding-bottom"))+parseFloat(t.getPropertyValue("padding-top")),o=parseFloat(t.getPropertyValue("border-bottom-width"))+parseFloat(t.getPropertyValue("border-top-width")),i=f.map((function(e){return e+":"+t.getPropertyValue(e)})).join(";");return{contextStyle:i,paddingSize:r,borderSize:o,boxSizing:n}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;u||(u=document.createElement("textarea"),document.body.appendChild(u));var r=d(e),o=r.paddingSize,i=r.borderSize,a=r.boxSizing,s=r.contextStyle;u.setAttribute("style",s+";"+c),u.value=e.value||e.placeholder||"";var l=u.scrollHeight,f={};"border-box"===a?l+=i:"content-box"===a&&(l-=o),u.value="";var p=u.scrollHeight-o;if(null!==t){var h=p*t;"border-box"===a&&(h=h+o+i),l=Math.max(h,l),f.minHeight=h+"px"}if(null!==n){var v=p*n;"border-box"===a&&(v=v+o+i),l=Math.min(v,l)}return f.height=l+"px",u.parentNode&&u.parentNode.removeChild(u),u=null,f}var h=n(9),v=n.n(h),m=n(21),y={name:"ElInput",componentName:"ElInput",mixins:[a.a,l.a],inheritAttrs:!1,inject:{elForm:{default:""},elFormItem:{default:""}},data:function(){return{textareaCalcStyle:{},hovering:!1,focused:!1,isComposing:!1,passwordVisible:!1}},props:{value:[String,Number],size:String,resize:String,form:String,disabled:Boolean,readonly:Boolean,type:{type:String,default:"text"},autosize:{type:[Boolean,Object],default:!1},autocomplete:{type:String,default:"off"},autoComplete:{type:String,validator:function(e){return!0}},validateEvent:{type:Boolean,default:!0},suffixIcon:String,prefixIcon:String,label:String,clearable:{type:Boolean,default:!1},showPassword:{type:Boolean,default:!1},showWordLimit:{type:Boolean,default:!1},tabindex:String},computed:{_elFormItemSize:function(){return(this.elFormItem||{}).elFormItemSize},validateState:function(){return this.elFormItem?this.elFormItem.validateState:""},needStatusIcon:function(){return!!this.elForm&&this.elForm.statusIcon},validateIcon:function(){return{validating:"el-icon-loading",success:"el-icon-circle-check",error:"el-icon-circle-close"}[this.validateState]},textareaStyle:function(){return v()({},this.textareaCalcStyle,{resize:this.resize})},inputSize:function(){return this.size||this._elFormItemSize||(this.$ELEMENT||{}).size},inputDisabled:function(){return this.disabled||(this.elForm||{}).disabled},nativeInputValue:function(){return null===this.value||void 0===this.value?"":String(this.value)},showClear:function(){return this.clearable&&!this.inputDisabled&&!this.readonly&&this.nativeInputValue&&(this.focused||this.hovering)},showPwdVisible:function(){return this.showPassword&&!this.inputDisabled&&!this.readonly&&(!!this.nativeInputValue||this.focused)},isWordLimitVisible:function(){return this.showWordLimit&&this.$attrs.maxlength&&("text"===this.type||"textarea"===this.type)&&!this.inputDisabled&&!this.readonly&&!this.showPassword},upperLimit:function(){return this.$attrs.maxlength},textLength:function(){return"number"===typeof this.value?String(this.value).length:(this.value||"").length},inputExceed:function(){return this.isWordLimitVisible&&this.textLength>this.upperLimit}},watch:{value:function(e){this.$nextTick(this.resizeTextarea),this.validateEvent&&this.dispatch("ElFormItem","el.form.change",[e])},nativeInputValue:function(){this.setNativeInputValue()},type:function(){var e=this;this.$nextTick((function(){e.setNativeInputValue(),e.resizeTextarea(),e.updateIconOffset()}))}},methods:{focus:function(){this.getInput().focus()},blur:function(){this.getInput().blur()},getMigratingConfig:function(){return{props:{icon:"icon is removed, use suffix-icon / prefix-icon instead.","on-icon-click":"on-icon-click is removed."},events:{click:"click is removed."}}},handleBlur:function(e){this.focused=!1,this.$emit("blur",e),this.validateEvent&&this.dispatch("ElFormItem","el.form.blur",[this.value])},select:function(){this.getInput().select()},resizeTextarea:function(){if(!this.$isServer){var e=this.autosize,t=this.type;if("textarea"===t)if(e){var n=e.minRows,r=e.maxRows;this.textareaCalcStyle=p(this.$refs.textarea,n,r)}else this.textareaCalcStyle={minHeight:p(this.$refs.textarea).minHeight}}},setNativeInputValue:function(){var e=this.getInput();e&&e.value!==this.nativeInputValue&&(e.value=this.nativeInputValue)},handleFocus:function(e){this.focused=!0,this.$emit("focus",e)},handleCompositionStart:function(){this.isComposing=!0},handleCompositionUpdate:function(e){var t=e.target.value,n=t[t.length-1]||"";this.isComposing=!Object(m["isKorean"])(n)},handleCompositionEnd:function(e){this.isComposing&&(this.isComposing=!1,this.handleInput(e))},handleInput:function(e){this.isComposing||e.target.value!==this.nativeInputValue&&(this.$emit("input",e.target.value),this.$nextTick(this.setNativeInputValue))},handleChange:function(e){this.$emit("change",e.target.value)},calcIconOffset:function(e){var t=[].slice.call(this.$el.querySelectorAll(".el-input__"+e)||[]);if(t.length){for(var n=null,r=0;r0&&(this.timer=setTimeout((function(){e.closed||e.close()}),this.duration))},keydown:function(e){27===e.keyCode&&(this.closed||this.close())}},mounted:function(){this.startTimer(),document.addEventListener("keydown",this.keydown)},beforeDestroy:function(){document.removeEventListener("keydown",this.keydown)}},u=l,c=n(0),f=Object(c["a"])(u,i,a,!1,null,null,null);f.options.__file="packages/message/src/main.vue";var d=f.exports,p=n(15),h=n(23),v=o.a.extend(d),m=void 0,y=[],g=1,b=function e(t){if(!o.a.prototype.$isServer){t=t||{},"string"===typeof t&&(t={message:t});var n=t.onClose,r="message_"+g++;t.onClose=function(){e.close(r,n)},m=new v({data:t}),m.id=r,Object(h["isVNode"])(m.message)&&(m.$slots.default=[m.message],m.message=null),m.$mount(),document.body.appendChild(m.$el);var i=t.offset||20;return y.forEach((function(e){i+=e.$el.offsetHeight+16})),m.verticalOffset=i,m.visible=!0,m.$el.style.zIndex=p["PopupManager"].nextZIndex(),y.push(m),m}};["success","warning","info","error"].forEach((function(e){b[e]=function(t){return"string"===typeof t&&(t={message:t}),t.type=e,b(t)}})),b.close=function(e,t){for(var n=y.length,r=-1,o=void 0,i=0;iy.length-1))for(var a=r;a=0;e--)y[e].close()};var _=b;t["default"]=_}})},"7db2":function(e,t,n){var r=n("6d28"),o=n("7e8b"),i=r("keys");e.exports=function(e){return i[e]||(i[e]=o(e))}},"7e05":function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:300,r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!e||!t)throw new Error("instance & callback is required");var o=!1,i=function(){o||(o=!0,t&&t.apply(null,arguments))};r?e.$once("after-leave",i):e.$on("after-leave",i),setTimeout((function(){i()}),n+100)}},"7e8b":function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol("+String(void 0===e?"":e)+")_"+(++n+r).toString(36)}},"848f":function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=122)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},122:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("span",{staticClass:"el-breadcrumb__item"},[n("span",{ref:"link",class:["el-breadcrumb__inner",e.to?"is-link":""],attrs:{role:"link"}},[e._t("default")],2),e.separatorClass?n("i",{staticClass:"el-breadcrumb__separator",class:e.separatorClass}):n("span",{staticClass:"el-breadcrumb__separator",attrs:{role:"presentation"}},[e._v(e._s(e.separator))])])},o=[];r._withStripped=!0;var i={name:"ElBreadcrumbItem",props:{to:{},replace:Boolean},data:function(){return{separator:"",separatorClass:""}},inject:["elBreadcrumb"],mounted:function(){var e=this;this.separator=this.elBreadcrumb.separator,this.separatorClass=this.elBreadcrumb.separatorClass;var t=this.$refs.link;t.setAttribute("role","link"),t.addEventListener("click",(function(t){var n=e.to,r=e.$router;n&&r&&(e.replace?r.replace(n):r.push(n))}))}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/breadcrumb/src/breadcrumb-item.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})},"857c":function(e,t,n){var r=n("a719");e.exports=function(e){if(!r(e))throw TypeError(String(e)+" is not an object");return e}},"857d":function(e,t,n){"use strict";function r(e){return"[object String]"===Object.prototype.toString.call(e)}function o(e){return"[object Object]"===Object.prototype.toString.call(e)}function i(e){return e&&e.nodeType===Node.ELEMENT_NODE}t.__esModule=!0,t.isString=r,t.isObject=o,t.isHtmlElement=i;t.isFunction=function(e){var t={};return e&&"[object Function]"===t.toString.call(e)},t.isUndefined=function(e){return void 0===e},t.isDefined=function(e){return void 0!==e&&null!==e}},"8d44":function(e,t,n){var r=n("6d7a"),o=n("b338"),i=n("0a60"),a=n("857c");e.exports=r("Reflect","ownKeys")||function(e){var t=o.f(a(e)),n=i.f;return n?t.concat(n(e)):t}},"908e":function(e,t,n){var r=n("faa8"),o=n("3553"),i=n("7db2"),a=n("eec6"),s=i("IE_PROTO"),l=Object.prototype;e.exports=a?Object.getPrototypeOf:function(e){return e=o(e),r(e,s)?e[s]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?l:null}},"90fb":function(e,t,n){var r=n("d890"),o=n("6d28"),i=n("faa8"),a=n("7e8b"),s=n("c54b"),l=n("74cb"),u=o("wks"),c=r.Symbol,f=l?c:c&&c.withoutSetter||a;e.exports=function(e){return i(u,e)||(s&&i(c,e)?u[e]=c[e]:u[e]=f("Symbol."+e)),u[e]}},"91e2":function(e,t,n){var r=n("d890");e.exports=r.Promise},9622:function(e,t,n){var r=n("d890");e.exports=function(e,t){var n=r.console;n&&n.error&&(1===arguments.length?n.error(e):n.error(e,t))}},9806:function(e,t){e.exports={}},"98a9":function(e,t,n){var r=n("90fb"),o=n("9806"),i=r("iterator"),a=Array.prototype;e.exports=function(e){return void 0!==e&&(o.Array===e||a[i]===e)}},"99ab":function(e,t,n){var r=n("1944");e.exports=function(e,t,n){for(var o in t)r(e,o,t[o],n);return e}},"99ad":function(e,t,n){"use strict";var r=n("857c");e.exports=function(){var e=r(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},"99ee":function(e,t,n){"use strict";var r=n("1c8b"),o=n("69fa"),i=n("908e"),a=n("50fb"),s=n("27b5"),l=n("0fc1"),u=n("1944"),c=n("90fb"),f=n("9b9d"),d=n("9806"),p=n("2abc"),h=p.IteratorPrototype,v=p.BUGGY_SAFARI_ITERATORS,m=c("iterator"),y="keys",g="values",b="entries",_=function(){return this};e.exports=function(e,t,n,c,p,x,w){o(n,t,c);var C,S,O,E=function(e){if(e===p&&T)return T;if(!v&&e in j)return j[e];switch(e){case y:return function(){return new n(this,e)};case g:return function(){return new n(this,e)};case b:return function(){return new n(this,e)}}return function(){return new n(this)}},k=t+" Iterator",$=!1,j=e.prototype,A=j[m]||j["@@iterator"]||p&&j[p],T=!v&&A||E(p),M="Array"==t&&j.entries||A;if(M&&(C=i(M.call(new e)),h!==Object.prototype&&C.next&&(f||i(C)===h||(a?a(C,h):"function"!=typeof C[m]&&l(C,m,_)),s(C,k,!0,!0),f&&(d[k]=_))),p==g&&A&&A.name!==g&&($=!0,T=function(){return A.call(this)}),f&&!w||j[m]===T||l(j,m,T),d[t]=T,p)if(S={values:E(g),keys:x?T:E(y),entries:E(b)},w)for(O in S)(v||$||!(O in j))&&u(j,O,S[O]);else r({target:t,proto:!0,forced:v||$},S);return S}},"9b9d":function(e,t){e.exports=!1},"9ca4":function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},"9edd":function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}e.exports=n},"9f3a":function(e,t,n){"use strict";(function(e){ -/*! - * vuex v3.5.1 - * (c) 2020 Evan You - * @license MIT - */ -function n(e){var t=Number(e.version.split(".")[0]);if(t>=2)e.mixin({beforeCreate:r});else{var n=e.prototype._init;e.prototype._init=function(e){void 0===e&&(e={}),e.init=e.init?[r].concat(e.init):r,n.call(this,e)}}function r(){var e=this.$options;e.store?this.$store="function"===typeof e.store?e.store():e.store:e.parent&&e.parent.$store&&(this.$store=e.parent.$store)}}var r="undefined"!==typeof window?window:"undefined"!==typeof e?e:{},o=r.__VUE_DEVTOOLS_GLOBAL_HOOK__;function i(e){o&&(e._devtoolHook=o,o.emit("vuex:init",e),o.on("vuex:travel-to-state",(function(t){e.replaceState(t)})),e.subscribe((function(e,t){o.emit("vuex:mutation",e,t)}),{prepend:!0}),e.subscribeAction((function(e,t){o.emit("vuex:action",e,t)}),{prepend:!0}))}function a(e,t){return e.filter(t)[0]}function s(e,t){if(void 0===t&&(t=[]),null===e||"object"!==typeof e)return e;var n=a(t,(function(t){return t.original===e}));if(n)return n.copy;var r=Array.isArray(e)?[]:{};return t.push({original:e,copy:r}),Object.keys(e).forEach((function(n){r[n]=s(e[n],t)})),r}function l(e,t){Object.keys(e).forEach((function(n){return t(e[n],n)}))}function u(e){return null!==e&&"object"===typeof e}function c(e){return e&&"function"===typeof e.then}function f(e,t){return function(){return e(t)}}var d=function(e,t){this.runtime=t,this._children=Object.create(null),this._rawModule=e;var n=e.state;this.state=("function"===typeof n?n():n)||{}},p={namespaced:{configurable:!0}};p.namespaced.get=function(){return!!this._rawModule.namespaced},d.prototype.addChild=function(e,t){this._children[e]=t},d.prototype.removeChild=function(e){delete this._children[e]},d.prototype.getChild=function(e){return this._children[e]},d.prototype.hasChild=function(e){return e in this._children},d.prototype.update=function(e){this._rawModule.namespaced=e.namespaced,e.actions&&(this._rawModule.actions=e.actions),e.mutations&&(this._rawModule.mutations=e.mutations),e.getters&&(this._rawModule.getters=e.getters)},d.prototype.forEachChild=function(e){l(this._children,e)},d.prototype.forEachGetter=function(e){this._rawModule.getters&&l(this._rawModule.getters,e)},d.prototype.forEachAction=function(e){this._rawModule.actions&&l(this._rawModule.actions,e)},d.prototype.forEachMutation=function(e){this._rawModule.mutations&&l(this._rawModule.mutations,e)},Object.defineProperties(d.prototype,p);var h=function(e){this.register([],e,!1)};function v(e,t,n){if(t.update(n),n.modules)for(var r in n.modules){if(!t.getChild(r))return void 0;v(e.concat(r),t.getChild(r),n.modules[r])}}h.prototype.get=function(e){return e.reduce((function(e,t){return e.getChild(t)}),this.root)},h.prototype.getNamespace=function(e){var t=this.root;return e.reduce((function(e,n){return t=t.getChild(n),e+(t.namespaced?n+"/":"")}),"")},h.prototype.update=function(e){v([],this.root,e)},h.prototype.register=function(e,t,n){var r=this;void 0===n&&(n=!0);var o=new d(t,n);if(0===e.length)this.root=o;else{var i=this.get(e.slice(0,-1));i.addChild(e[e.length-1],o)}t.modules&&l(t.modules,(function(t,o){r.register(e.concat(o),t,n)}))},h.prototype.unregister=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1],r=t.getChild(n);r&&r.runtime&&t.removeChild(n)},h.prototype.isRegistered=function(e){var t=this.get(e.slice(0,-1)),n=e[e.length-1];return t.hasChild(n)};var m;var y=function(e){var t=this;void 0===e&&(e={}),!m&&"undefined"!==typeof window&&window.Vue&&T(window.Vue);var n=e.plugins;void 0===n&&(n=[]);var r=e.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new h(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new m,this._makeLocalGettersCache=Object.create(null);var o=this,a=this,s=a.dispatch,l=a.commit;this.dispatch=function(e,t){return s.call(o,e,t)},this.commit=function(e,t,n){return l.call(o,e,t,n)},this.strict=r;var u=this._modules.root.state;w(this,u,[],this._modules.root),x(this,u),n.forEach((function(e){return e(t)}));var c=void 0!==e.devtools?e.devtools:m.config.devtools;c&&i(this)},g={state:{configurable:!0}};function b(e,t,n){return t.indexOf(e)<0&&(n&&n.prepend?t.unshift(e):t.push(e)),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function _(e,t){e._actions=Object.create(null),e._mutations=Object.create(null),e._wrappedGetters=Object.create(null),e._modulesNamespaceMap=Object.create(null);var n=e.state;w(e,n,[],e._modules.root,!0),x(e,n,t)}function x(e,t,n){var r=e._vm;e.getters={},e._makeLocalGettersCache=Object.create(null);var o=e._wrappedGetters,i={};l(o,(function(t,n){i[n]=f(t,e),Object.defineProperty(e.getters,n,{get:function(){return e._vm[n]},enumerable:!0})}));var a=m.config.silent;m.config.silent=!0,e._vm=new m({data:{$$state:t},computed:i}),m.config.silent=a,e.strict&&$(e),r&&(n&&e._withCommit((function(){r._data.$$state=null})),m.nextTick((function(){return r.$destroy()})))}function w(e,t,n,r,o){var i=!n.length,a=e._modules.getNamespace(n);if(r.namespaced&&(e._modulesNamespaceMap[a],e._modulesNamespaceMap[a]=r),!i&&!o){var s=j(t,n.slice(0,-1)),l=n[n.length-1];e._withCommit((function(){m.set(s,l,r.state)}))}var u=r.context=C(e,a,n);r.forEachMutation((function(t,n){var r=a+n;O(e,r,t,u)})),r.forEachAction((function(t,n){var r=t.root?n:a+n,o=t.handler||t;E(e,r,o,u)})),r.forEachGetter((function(t,n){var r=a+n;k(e,r,t,u)})),r.forEachChild((function(r,i){w(e,t,n.concat(i),r,o)}))}function C(e,t,n){var r=""===t,o={dispatch:r?e.dispatch:function(n,r,o){var i=A(n,r,o),a=i.payload,s=i.options,l=i.type;return s&&s.root||(l=t+l),e.dispatch(l,a)},commit:r?e.commit:function(n,r,o){var i=A(n,r,o),a=i.payload,s=i.options,l=i.type;s&&s.root||(l=t+l),e.commit(l,a,s)}};return Object.defineProperties(o,{getters:{get:r?function(){return e.getters}:function(){return S(e,t)}},state:{get:function(){return j(e.state,n)}}}),o}function S(e,t){if(!e._makeLocalGettersCache[t]){var n={},r=t.length;Object.keys(e.getters).forEach((function(o){if(o.slice(0,r)===t){var i=o.slice(r);Object.defineProperty(n,i,{get:function(){return e.getters[o]},enumerable:!0})}})),e._makeLocalGettersCache[t]=n}return e._makeLocalGettersCache[t]}function O(e,t,n,r){var o=e._mutations[t]||(e._mutations[t]=[]);o.push((function(t){n.call(e,r.state,t)}))}function E(e,t,n,r){var o=e._actions[t]||(e._actions[t]=[]);o.push((function(t){var o=n.call(e,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:e.getters,rootState:e.state},t);return c(o)||(o=Promise.resolve(o)),e._devtoolHook?o.catch((function(t){throw e._devtoolHook.emit("vuex:error",t),t})):o}))}function k(e,t,n,r){e._wrappedGetters[t]||(e._wrappedGetters[t]=function(e){return n(r.state,r.getters,e.state,e.getters)})}function $(e){e._vm.$watch((function(){return this._data.$$state}),(function(){0}),{deep:!0,sync:!0})}function j(e,t){return t.reduce((function(e,t){return e[t]}),e)}function A(e,t,n){return u(e)&&e.type&&(n=t,t=e,e=e.type),{type:e,payload:t,options:n}}function T(e){m&&e===m||(m=e,n(m))}g.state.get=function(){return this._vm._data.$$state},g.state.set=function(e){0},y.prototype.commit=function(e,t,n){var r=this,o=A(e,t,n),i=o.type,a=o.payload,s=(o.options,{type:i,payload:a}),l=this._mutations[i];l&&(this._withCommit((function(){l.forEach((function(e){e(a)}))})),this._subscribers.slice().forEach((function(e){return e(s,r.state)})))},y.prototype.dispatch=function(e,t){var n=this,r=A(e,t),o=r.type,i=r.payload,a={type:o,payload:i},s=this._actions[o];if(s){try{this._actionSubscribers.slice().filter((function(e){return e.before})).forEach((function(e){return e.before(a,n.state)}))}catch(u){0}var l=s.length>1?Promise.all(s.map((function(e){return e(i)}))):s[0](i);return new Promise((function(e,t){l.then((function(t){try{n._actionSubscribers.filter((function(e){return e.after})).forEach((function(e){return e.after(a,n.state)}))}catch(u){0}e(t)}),(function(e){try{n._actionSubscribers.filter((function(e){return e.error})).forEach((function(t){return t.error(a,n.state,e)}))}catch(u){0}t(e)}))}))}},y.prototype.subscribe=function(e,t){return b(e,this._subscribers,t)},y.prototype.subscribeAction=function(e,t){var n="function"===typeof e?{before:e}:e;return b(n,this._actionSubscribers,t)},y.prototype.watch=function(e,t,n){var r=this;return this._watcherVM.$watch((function(){return e(r.state,r.getters)}),t,n)},y.prototype.replaceState=function(e){var t=this;this._withCommit((function(){t._vm._data.$$state=e}))},y.prototype.registerModule=function(e,t,n){void 0===n&&(n={}),"string"===typeof e&&(e=[e]),this._modules.register(e,t),w(this,this.state,e,this._modules.get(e),n.preserveState),x(this,this.state)},y.prototype.unregisterModule=function(e){var t=this;"string"===typeof e&&(e=[e]),this._modules.unregister(e),this._withCommit((function(){var n=j(t.state,e.slice(0,-1));m.delete(n,e[e.length-1])})),_(this)},y.prototype.hasModule=function(e){return"string"===typeof e&&(e=[e]),this._modules.isRegistered(e)},y.prototype.hotUpdate=function(e){this._modules.update(e),_(this,!0)},y.prototype._withCommit=function(e){var t=this._committing;this._committing=!0,e(),this._committing=t},Object.defineProperties(y.prototype,g);var M=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=this.$store.state,n=this.$store.getters;if(e){var r=z(this.$store,"mapState",e);if(!r)return;t=r.context.state,n=r.context.getters}return"function"===typeof o?o.call(this,t,n):t[o]},n[r].vuex=!0})),n})),P=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=[],n=arguments.length;while(n--)t[n]=arguments[n];var r=this.$store.commit;if(e){var i=z(this.$store,"mapMutations",e);if(!i)return;r=i.context.commit}return"function"===typeof o?o.apply(this,[r].concat(t)):r.apply(this.$store,[o].concat(t))}})),n})),L=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;o=e+o,n[r]=function(){if(!e||z(this.$store,"mapGetters",e))return this.$store.getters[o]},n[r].vuex=!0})),n})),R=H((function(e,t){var n={};return I(t).forEach((function(t){var r=t.key,o=t.val;n[r]=function(){var t=[],n=arguments.length;while(n--)t[n]=arguments[n];var r=this.$store.dispatch;if(e){var i=z(this.$store,"mapActions",e);if(!i)return;r=i.context.dispatch}return"function"===typeof o?o.apply(this,[r].concat(t)):r.apply(this.$store,[o].concat(t))}})),n})),N=function(e){return{mapState:M.bind(null,e),mapGetters:L.bind(null,e),mapMutations:P.bind(null,e),mapActions:R.bind(null,e)}};function I(e){return F(e)?Array.isArray(e)?e.map((function(e){return{key:e,val:e}})):Object.keys(e).map((function(t){return{key:t,val:e[t]}})):[]}function F(e){return Array.isArray(e)||u(e)}function H(e){return function(t,n){return"string"!==typeof t?(n=t,t=""):"/"!==t.charAt(t.length-1)&&(t+="/"),e(t,n)}}function z(e,t,n){var r=e._modulesNamespaceMap[n];return r}function B(e){void 0===e&&(e={});var t=e.collapsed;void 0===t&&(t=!0);var n=e.filter;void 0===n&&(n=function(e,t,n){return!0});var r=e.transformer;void 0===r&&(r=function(e){return e});var o=e.mutationTransformer;void 0===o&&(o=function(e){return e});var i=e.actionFilter;void 0===i&&(i=function(e,t){return!0});var a=e.actionTransformer;void 0===a&&(a=function(e){return e});var l=e.logMutations;void 0===l&&(l=!0);var u=e.logActions;void 0===u&&(u=!0);var c=e.logger;return void 0===c&&(c=console),function(e){var f=s(e.state);"undefined"!==typeof c&&(l&&e.subscribe((function(e,i){var a=s(i);if(n(e,f,a)){var l=V(),u=o(e),d="mutation "+e.type+l;D(c,d,t),c.log("%c prev state","color: #9E9E9E; font-weight: bold",r(f)),c.log("%c mutation","color: #03A9F4; font-weight: bold",u),c.log("%c next state","color: #4CAF50; font-weight: bold",r(a)),W(c)}f=a})),u&&e.subscribeAction((function(e,n){if(i(e,n)){var r=V(),o=a(e),s="action "+e.type+r;D(c,s,t),c.log("%c action","color: #03A9F4; font-weight: bold",o),W(c)}})))}}function D(e,t,n){var r=n?e.groupCollapsed:e.group;try{r.call(e,t)}catch(o){e.log(t)}}function W(e){try{e.groupEnd()}catch(t){e.log("—— log end ——")}}function V(){var e=new Date;return" @ "+q(e.getHours(),2)+":"+q(e.getMinutes(),2)+":"+q(e.getSeconds(),2)+"."+q(e.getMilliseconds(),3)}function U(e,t){return new Array(t+1).join(e)}function q(e,t){return U("0",t-e.toString().length)+e}var K={Store:y,install:T,version:"3.5.1",mapState:M,mapMutations:P,mapGetters:L,mapActions:R,createNamespacedHelpers:N,createLogger:B};t["a"]=K}).call(this,n("9edd"))},"9f67":function(e,t,n){var r=n("a719");e.exports=function(e,t){if(!r(e))return e;var n,o;if(t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;if("function"==typeof(n=e.valueOf)&&!r(o=n.call(e)))return o;if(!t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;throw TypeError("Can't convert object to primitive value")}},"9fe5":function(e,t,n){var r=n("857c"),o=n("a719"),i=n("d0c2");e.exports=function(e,t){if(r(e),o(t)&&t.constructor===e)return t;var n=i.f(e),a=n.resolve;return a(t),n.promise}},a133:function(e,t,n){"use strict";var r=n("da10"),o=n("258f"),i=n("9806"),a=n("b702"),s=n("99ee"),l="Array Iterator",u=a.set,c=a.getterFor(l);e.exports=s(Array,"Array",(function(e,t){u(this,{type:l,target:r(e),index:0,kind:t})}),(function(){var e=c(this),t=e.target,n=e.kind,r=e.index++;return!t||r>=t.length?(e.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:r,done:!1}:"values"==n?{value:t[r],done:!1}:{value:[r,t[r]],done:!1}}),"values"),i.Arguments=i.Array,o("keys"),o("values"),o("entries")},a4f6:function(e,t,n){"use strict";t.__esModule=!0;n("df57");t.default={mounted:function(){},methods:{getMigratingConfig:function(){return{props:{},events:{}}}}}},a719:function(e,t){e.exports=function(e){return"object"===typeof e?null!==e:"function"===typeof e}},aa6b:function(e,t,n){var r=n("1e2c"),o=n("ef71"),i=n("38b9"),a=n("da10"),s=n("9f67"),l=n("faa8"),u=n("2039"),c=Object.getOwnPropertyDescriptor;t.f=r?c:function(e,t){if(e=a(e),t=s(t,!0),u)try{return c(e,t)}catch(n){}if(l(e,t))return i(!o.f.call(e,t),e[t])}},ae25:function(e,t,n){var r=n("d890"),o=n("a719"),i=r.document,a=o(i)&&o(i.createElement);e.exports=function(e){return a?i.createElement(e):{}}},b11c:function(e,t,n){},b338:function(e,t,n){var r=n("ead4"),o=n("18f6"),i=o.concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,i)}},b60f:function(e,t,n){var r=n("2a91"),o=n("9806"),i=n("90fb"),a=i("iterator");e.exports=function(e){if(void 0!=e)return e[a]||e["@@iterator"]||o[r(e)]}},b702:function(e,t,n){var r,o,i,a=n("604f"),s=n("d890"),l=n("a719"),u=n("0fc1"),c=n("faa8"),f=n("7db2"),d=n("d5a8"),p=s.WeakMap,h=function(e){return i(e)?o(e):r(e,{})},v=function(e){return function(t){var n;if(!l(t)||(n=o(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}};if(a){var m=new p,y=m.get,g=m.has,b=m.set;r=function(e,t){return b.call(m,e,t),t},o=function(e){return y.call(m,e)||{}},i=function(e){return g.call(m,e)}}else{var _=f("state");d[_]=!0,r=function(e,t){return u(e,_,t),t},o=function(e){return c(e,_)?e[_]:{}},i=function(e){return c(e,_)}}e.exports={set:r,get:o,has:i,enforce:h,getterFor:v}},b764:function(e,t,n){},b824:function(e,t){e.exports=function(e){try{return{error:!1,value:e()}}catch(t){return{error:!0,value:t}}}},bd84:function(e,t,n){var r,o,i,a=n("d890"),s=n("efe2"),l=n("2118"),u=n("e349"),c=n("6fdf"),f=n("ae25"),d=n("769c"),p=a.location,h=a.setImmediate,v=a.clearImmediate,m=a.process,y=a.MessageChannel,g=a.Dispatch,b=0,_={},x="onreadystatechange",w=function(e){if(_.hasOwnProperty(e)){var t=_[e];delete _[e],t()}},C=function(e){return function(){w(e)}},S=function(e){w(e.data)},O=function(e){a.postMessage(e+"",p.protocol+"//"+p.host)};h&&v||(h=function(e){var t=[],n=1;while(arguments.length>n)t.push(arguments[n++]);return _[++b]=function(){("function"==typeof e?e:Function(e)).apply(void 0,t)},r(b),b},v=function(e){delete _[e]},"process"==l(m)?r=function(e){m.nextTick(C(e))}:g&&g.now?r=function(e){g.now(C(e))}:y&&!d?(o=new y,i=o.port2,o.port1.onmessage=S,r=u(i.postMessage,i,1)):!a.addEventListener||"function"!=typeof postMessage||a.importScripts||s(O)||"file:"===p.protocol?r=x in f("script")?function(e){c.appendChild(f("script"))[x]=function(){c.removeChild(this),w(e)}}:function(e){setTimeout(C(e),0)}:(r=O,a.addEventListener("message",S,!1))),e.exports={set:h,clear:v}},bd85:function(e,t,n){"use strict";function r(e){return void 0!==e&&null!==e}function o(e){var t=/([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi;return t.test(e)}t.__esModule=!0,t.isDef=r,t.isKorean=o},c107:function(e,t,n){},c4e4:function(e,t){e.exports=function(e,t,n){if(!(e instanceof t))throw TypeError("Incorrect "+(n?n+" ":"")+"invocation");return e}},c54b:function(e,t,n){var r=n("efe2");e.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},c69d:function(e,t,n){var r=n("faa8"),o=n("8d44"),i=n("aa6b"),a=n("d910");e.exports=function(e,t){for(var n=o(t),s=a.f,l=i.f,u=0;u-1}function m(e,t){if(e){for(var n=e.className,r=(t||"").split(" "),o=0,i=r.length;or.top&&n.right>r.left&&n.left0&&(this.timeoutPending=setTimeout((function(){e.showPopper=!1}),this.hideAfter)))},handleClosePopper:function(){this.enterable&&this.expectedState||this.manual||(clearTimeout(this.timeout),this.timeoutPending&&clearTimeout(this.timeoutPending),this.showPopper=!1,this.disabled&&this.doDestroy())},setExpectedState:function(e){!1===e&&clearTimeout(this.timeoutPending),this.expectedState=e},getFirstElement:function(){var e=this.$slots.default;if(!Array.isArray(e))return null;for(var t=null,n=0;n0?o(r(e),9007199254740991):0}},d890:function(e,t,n){(function(t){var n=function(e){return e&&e.Math==Math&&e};e.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof t&&t)||Function("return this")()}).call(this,n("9edd"))},d8fc:function(e,t,n){"use strict";var r=n("1e2c"),o=n("efe2"),i=n("cbab"),a=n("0a60"),s=n("ef71"),l=n("3553"),u=n("692f"),c=Object.assign,f=Object.defineProperty;e.exports=!c||o((function(){if(r&&1!==c({b:1},c(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach((function(e){t[e]=e})),7!=c({},e)[n]||i(c({},t)).join("")!=o}))?function(e,t){var n=l(e),o=arguments.length,c=1,f=a.f,d=s.f;while(o>c){var p,h=u(arguments[c++]),v=f?i(h).concat(f(h)):i(h),m=v.length,y=0;while(m>y)p=v[y++],r&&!d.call(h,p)||(n[p]=h[p])}return n}:c},d910:function(e,t,n){var r=n("1e2c"),o=n("2039"),i=n("857c"),a=n("9f67"),s=Object.defineProperty;t.f=r?s:function(e,t,n){if(i(e),t=a(t,!0),i(n),o)try{return s(e,t,n)}catch(r){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},da10:function(e,t,n){var r=n("692f"),o=n("2732");e.exports=function(e){return r(o(e))}},dbe8:function(e,t,n){var r=n("1e2c"),o=n("d910"),i=n("857c"),a=n("cbab");e.exports=r?Object.defineProperties:function(e,t){i(e);var n,r=a(t),s=r.length,l=0;while(s>l)o.f(e,n=r[l++],t[n]);return e}},df57:function(e,t,n){"use strict";t.__esModule=!0,t.isEmpty=t.isEqual=t.arrayEquals=t.looseEqual=t.capitalize=t.kebabCase=t.autoprefixer=t.isFirefox=t.isEdge=t.isIE=t.coerceTruthyValueToArray=t.arrayFind=t.arrayFindIndex=t.escapeRegexpString=t.valueEquals=t.generateId=t.getValueByPath=void 0;var r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t.noop=u,t.hasOwn=c,t.toObject=d,t.getPropByPath=p,t.rafThrottle=g,t.objToArray=b;var o=n("0261"),i=s(o),a=n("857d");function s(e){return e&&e.__esModule?e:{default:e}}var l=Object.prototype.hasOwnProperty;function u(){}function c(e,t){return l.call(e,t)}function f(e,t){for(var n in t)e[n]=t[n];return e}function d(e){for(var t={},n=0;n0&&void 0!==arguments[0]?arguments[0]:"";return String(e).replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")};var h=t.arrayFindIndex=function(e,t){for(var n=0;n!==e.length;++n)if(t(e[n]))return n;return-1},v=(t.arrayFind=function(e,t){var n=h(e,t);return-1!==n?e[n]:void 0},t.coerceTruthyValueToArray=function(e){return Array.isArray(e)?e:e?[e]:[]},t.isIE=function(){return!i.default.prototype.$isServer&&!isNaN(Number(document.documentMode))},t.isEdge=function(){return!i.default.prototype.$isServer&&navigator.userAgent.indexOf("Edge")>-1},t.isFirefox=function(){return!i.default.prototype.$isServer&&!!window.navigator.userAgent.match(/firefox/i)},t.autoprefixer=function(e){if("object"!==("undefined"===typeof e?"undefined":r(e)))return e;var t=["transform","transition","animation"],n=["ms-","webkit-"];return t.forEach((function(t){var r=e[t];t&&r&&n.forEach((function(n){e[n+t]=r}))})),e},t.kebabCase=function(e){var t=/([^-])([A-Z])/g;return e.replace(t,"$1-$2").replace(t,"$1-$2").toLowerCase()},t.capitalize=function(e){return(0,a.isString)(e)?e.charAt(0).toUpperCase()+e.slice(1):e},t.looseEqual=function(e,t){var n=(0,a.isObject)(e),r=(0,a.isObject)(t);return n&&r?JSON.stringify(e)===JSON.stringify(t):!n&&!r&&String(e)===String(t)}),m=t.arrayEquals=function(e,t){if(e=e||[],t=t||[],e.length!==t.length)return!1;for(var n=0;n=t.length)break;o=t[r++]}else{if(r=t.next(),r.done)break;o=r.value}var i=o,a=i.target.__resizeListeners__||[];a.length&&a.forEach((function(e){e()}))}};t.addResizeListener=function(e,t){a||(e.__resizeListeners__||(e.__resizeListeners__=[],e.__ro__=new o.default(s),e.__ro__.observe(e)),e.__resizeListeners__.push(t))},t.removeResizeListener=function(e,t){e&&e.__resizeListeners__&&(e.__resizeListeners__.splice(e.__resizeListeners__.indexOf(t),1),e.__resizeListeners__.length||e.__ro__.disconnect())}},e349:function(e,t,n){var r=n("0c3c");e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},e4db:function(e,t,n){var r=n("d890"),o=n("0fc1");e.exports=function(e,t){try{o(r,e,t)}catch(n){r[e]=t}return t}},e6e5:function(e,t,n){},e857:function(e,t,n){"use strict";t.__esModule=!0,t.default=function(){if(o.default.prototype.$isServer)return 0;if(void 0!==a)return a;var e=document.createElement("div");e.className="el-scrollbar__wrap",e.style.visibility="hidden",e.style.width="100px",e.style.position="absolute",e.style.top="-9999px",document.body.appendChild(e);var t=e.offsetWidth;e.style.overflow="scroll";var n=document.createElement("div");n.style.width="100%",e.appendChild(n);var r=n.offsetWidth;return e.parentNode.removeChild(e),a=t-r,a};var r=n("0261"),o=i(r);function i(e){return e&&e.__esModule?e:{default:e}}var a=void 0},e8bd:function(e,t,n){},e8d6:function(e,t,n){var r=n("efe2"),o=/#|\.prototype\./,i=function(e,t){var n=s[a(e)];return n==u||n!=l&&("function"==typeof t?r(t):!!t)},a=i.normalize=function(e){return String(e).replace(o,".").toLowerCase()},s=i.data={},l=i.NATIVE="N",u=i.POLYFILL="P";e.exports=i},ea16:function(e,t,n){"use strict";t.__esModule=!0;var r=r||{};r.Utils=r.Utils||{},r.Utils.focusFirstDescendant=function(e){for(var t=0;t=0;t--){var n=e.childNodes[t];if(r.Utils.attemptFocus(n)||r.Utils.focusLastDescendant(n))return!0}return!1},r.Utils.attemptFocus=function(e){if(!r.Utils.isFocusable(e))return!1;r.Utils.IgnoreUtilFocusChanges=!0;try{e.focus()}catch(t){}return r.Utils.IgnoreUtilFocusChanges=!1,document.activeElement===e},r.Utils.isFocusable=function(e){if(e.tabIndex>0||0===e.tabIndex&&null!==e.getAttribute("tabIndex"))return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&"ignore"!==e.rel;case"INPUT":return"hidden"!==e.type&&"file"!==e.type;case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},r.Utils.triggerEvent=function(e,t){var n=void 0;n=/^mouse|click/.test(t)?"MouseEvents":/^key/.test(t)?"KeyboardEvent":"HTMLEvents";for(var r=document.createEvent(n),o=arguments.length,i=Array(o>2?o-2:0),a=2;al)r(s,n=t[l++])&&(~i(u,n)||u.push(n));return u}},eb40:function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e){for(var t=1,n=arguments.length;t=51&&/native code/.test(H))return!1;var t=H.resolve(1),n=function(e){e((function(){}),(function(){}))},r=t.constructor={};return r[L]=n,!(t.then((function(){}))instanceof n)})),ne=te||!w((function(e){H.all(e)["catch"]((function(){}))})),re=function(e){var t;return!(!m(e)||"function"!=typeof(t=e.then))&&t},oe=function(e,t,n){if(!t.notified){t.notified=!0;var r=t.reactions;O((function(){var o=t.value,i=t.state==J,a=0;while(r.length>a){var s,l,u,c=r[a++],f=i?c.ok:c.fail,d=c.resolve,p=c.reject,h=c.domain;try{f?(i||(t.rejection===ee&&le(e,t),t.rejection=Q),!0===f?s=o:(h&&h.enter(),s=f(o),h&&(h.exit(),u=!0)),s===c.promise?p(z("Promise-chain cycle")):(l=re(s))?l.call(s,d,p):d(s)):p(o)}catch(v){h&&!u&&h.exit(),p(v)}}t.reactions=[],t.notified=!1,n&&!t.rejection&&ae(e,t)}))}},ie=function(e,t,n){var r,o;K?(r=B.createEvent("Event"),r.promise=t,r.reason=n,r.initEvent(e,!1,!0),u.dispatchEvent(r)):r={promise:t,reason:n},(o=u["on"+e])?o(r):e===G&&k("Unhandled promise rejection",n)},ae=function(e,t){S.call(u,(function(){var n,r=t.value,o=se(t);if(o&&(n=j((function(){q?D.emit("unhandledRejection",r,e):ie(G,e,r)})),t.rejection=q||se(t)?ee:Q,n.error))throw n.value}))},se=function(e){return e.rejection!==Q&&!e.parent},le=function(e,t){S.call(u,(function(){q?D.emit("rejectionHandled",e):ie(X,e,t.value)}))},ue=function(e,t,n,r){return function(o){e(t,n,o,r)}},ce=function(e,t,n,r){t.done||(t.done=!0,r&&(t=r),t.value=n,t.state=Z,oe(e,t,!0))},fe=function(e,t,n,r){if(!t.done){t.done=!0,r&&(t=r);try{if(e===n)throw z("Promise can't be resolved itself");var o=re(n);o?O((function(){var r={done:!1};try{o.call(n,ue(fe,e,r,t),ue(ce,e,r,t))}catch(i){ce(e,r,i,t)}})):(t.value=n,t.state=J,oe(e,t,!1))}catch(i){ce(e,{done:!1},i,t)}}};te&&(H=function(e){g(this,H,R),y(e),r.call(this);var t=N(this);try{e(ue(fe,this,t),ue(ce,this,t))}catch(n){ce(this,t,n)}},r=function(e){I(this,{type:R,done:!1,notified:!1,parent:!1,reactions:[],rejection:!1,state:Y,value:void 0})},r.prototype=p(H.prototype,{then:function(e,t){var n=F(this),r=V(C(this,H));return r.ok="function"!=typeof e||e,r.fail="function"==typeof t&&t,r.domain=q?D.domain:void 0,n.parent=!0,n.reactions.push(r),n.state!=Y&&oe(this,n,!1),r.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new r,t=N(e);this.promise=e,this.resolve=ue(fe,e,t),this.reject=ue(ce,e,t)},$.f=V=function(e){return e===H||e===i?new o(e):U(e)},l||"function"!=typeof f||(a=f.prototype.then,d(f.prototype,"then",(function(e,t){var n=this;return new H((function(e,t){a.call(n,e,t)})).then(e,t)}),{unsafe:!0}),"function"==typeof W&&s({global:!0,enumerable:!0,forced:!0},{fetch:function(e){return E(H,W.apply(u,arguments))}}))),s({global:!0,wrap:!0,forced:te},{Promise:H}),h(H,R,!1,!0),v(R),i=c(R),s({target:R,stat:!0,forced:te},{reject:function(e){var t=V(this);return t.reject.call(void 0,e),t.promise}}),s({target:R,stat:!0,forced:l||te},{resolve:function(e){return E(l&&this===i?H:this,e)}}),s({target:R,stat:!0,forced:ne},{all:function(e){var t=this,n=V(t),r=n.resolve,o=n.reject,i=j((function(){var n=y(t.resolve),i=[],a=0,s=1;x(e,(function(e){var l=a++,u=!1;i.push(void 0),s++,n.call(t,e).then((function(e){u||(u=!0,i[l]=e,--s||r(i))}),o)})),--s||r(i)}));return i.error&&o(i.value),n.promise},race:function(e){var t=this,n=V(t),r=n.reject,o=j((function(){var o=y(t.resolve);x(e,(function(e){o.call(t,e).then(n.resolve,r)}))}));return o.error&&r(o.value),n.promise}})},ee61:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=68)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},15:function(e,t){e.exports=n("72e8")},2:function(e,t){e.exports=n("c865")},41:function(e,t){e.exports=n("7e05")},68:function(e,t,n){"use strict";n.r(t);var r=n(7),o=n.n(r),i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"el-loading-fade"},on:{"after-leave":e.handleAfterLeave}},[n("div",{directives:[{name:"show",rawName:"v-show",value:e.visible,expression:"visible"}],staticClass:"el-loading-mask",class:[e.customClass,{"is-fullscreen":e.fullscreen}],style:{backgroundColor:e.background||""}},[n("div",{staticClass:"el-loading-spinner"},[e.spinner?n("i",{class:e.spinner}):n("svg",{staticClass:"circular",attrs:{viewBox:"25 25 50 50"}},[n("circle",{staticClass:"path",attrs:{cx:"50",cy:"50",r:"20",fill:"none"}})]),e.text?n("p",{staticClass:"el-loading-text"},[e._v(e._s(e.text))]):e._e()])])])},a=[];i._withStripped=!0;var s={data:function(){return{text:null,spinner:null,background:null,fullscreen:!0,visible:!1,customClass:""}},methods:{handleAfterLeave:function(){this.$emit("after-leave")},setText:function(e){this.text=e}}},l=s,u=n(0),c=Object(u["a"])(l,i,a,!1,null,null,null);c.options.__file="packages/loading/src/loading.vue";var f=c.exports,d=n(2),p=n(15),h=n(41),v=n.n(h),m=o.a.extend(f),y={install:function(e){if(!e.prototype.$isServer){var t=function(t,r){r.value?e.nextTick((function(){r.modifiers.fullscreen?(t.originalPosition=Object(d["getStyle"])(document.body,"position"),t.originalOverflow=Object(d["getStyle"])(document.body,"overflow"),t.maskStyle.zIndex=p["PopupManager"].nextZIndex(),Object(d["addClass"])(t.mask,"is-fullscreen"),n(document.body,t,r)):(Object(d["removeClass"])(t.mask,"is-fullscreen"),r.modifiers.body?(t.originalPosition=Object(d["getStyle"])(document.body,"position"),["top","left"].forEach((function(e){var n="top"===e?"scrollTop":"scrollLeft";t.maskStyle[e]=t.getBoundingClientRect()[e]+document.body[n]+document.documentElement[n]-parseInt(Object(d["getStyle"])(document.body,"margin-"+e),10)+"px"})),["height","width"].forEach((function(e){t.maskStyle[e]=t.getBoundingClientRect()[e]+"px"})),n(document.body,t,r)):(t.originalPosition=Object(d["getStyle"])(t,"position"),n(t,t,r)))})):(v()(t.instance,(function(e){if(t.instance.hiding){t.domVisible=!1;var n=r.modifiers.fullscreen||r.modifiers.body?document.body:t;Object(d["removeClass"])(n,"el-loading-parent--relative"),Object(d["removeClass"])(n,"el-loading-parent--hidden"),t.instance.hiding=!1}}),300,!0),t.instance.visible=!1,t.instance.hiding=!0)},n=function(t,n,r){n.domVisible||"none"===Object(d["getStyle"])(n,"display")||"hidden"===Object(d["getStyle"])(n,"visibility")?n.domVisible&&!0===n.instance.hiding&&(n.instance.visible=!0,n.instance.hiding=!1):(Object.keys(n.maskStyle).forEach((function(e){n.mask.style[e]=n.maskStyle[e]})),"absolute"!==n.originalPosition&&"fixed"!==n.originalPosition&&Object(d["addClass"])(t,"el-loading-parent--relative"),r.modifiers.fullscreen&&r.modifiers.lock&&Object(d["addClass"])(t,"el-loading-parent--hidden"),n.domVisible=!0,t.appendChild(n.mask),e.nextTick((function(){n.instance.hiding?n.instance.$emit("after-leave"):n.instance.visible=!0})),n.domInserted=!0)};e.directive("loading",{bind:function(e,n,r){var o=e.getAttribute("element-loading-text"),i=e.getAttribute("element-loading-spinner"),a=e.getAttribute("element-loading-background"),s=e.getAttribute("element-loading-custom-class"),l=r.context,u=new m({el:document.createElement("div"),data:{text:l&&l[o]||o,spinner:l&&l[i]||i,background:l&&l[a]||a,customClass:l&&l[s]||s,fullscreen:!!n.modifiers.fullscreen}});e.instance=u,e.mask=u.$el,e.maskStyle={},n.value&&t(e,n)},update:function(e,n){e.instance.setText(e.getAttribute("element-loading-text")),n.oldValue!==n.value&&t(e,n)},unbind:function(e,n){e.domInserted&&(e.mask&&e.mask.parentNode&&e.mask.parentNode.removeChild(e.mask),t(e,{value:!1,modifiers:n.modifiers})),e.instance&&e.instance.$destroy()}})}}},g=y,b=n(9),_=n.n(b),x=o.a.extend(f),w={text:null,fullscreen:!0,body:!1,lock:!1,customClass:""},C=void 0;x.prototype.originalPosition="",x.prototype.originalOverflow="",x.prototype.close=function(){var e=this;this.fullscreen&&(C=void 0),v()(this,(function(t){var n=e.fullscreen||e.body?document.body:e.target;Object(d["removeClass"])(n,"el-loading-parent--relative"),Object(d["removeClass"])(n,"el-loading-parent--hidden"),e.$el&&e.$el.parentNode&&e.$el.parentNode.removeChild(e.$el),e.$destroy()}),300),this.visible=!1};var S=function(e,t,n){var r={};e.fullscreen?(n.originalPosition=Object(d["getStyle"])(document.body,"position"),n.originalOverflow=Object(d["getStyle"])(document.body,"overflow"),r.zIndex=p["PopupManager"].nextZIndex()):e.body?(n.originalPosition=Object(d["getStyle"])(document.body,"position"),["top","left"].forEach((function(t){var n="top"===t?"scrollTop":"scrollLeft";r[t]=e.target.getBoundingClientRect()[t]+document.body[n]+document.documentElement[n]+"px"})),["height","width"].forEach((function(t){r[t]=e.target.getBoundingClientRect()[t]+"px"}))):n.originalPosition=Object(d["getStyle"])(t,"position"),Object.keys(r).forEach((function(e){n.$el.style[e]=r[e]}))},O=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!o.a.prototype.$isServer){if(e=_()({},w,e),"string"===typeof e.target&&(e.target=document.querySelector(e.target)),e.target=e.target||document.body,e.target!==document.body?e.fullscreen=!1:e.body=!0,e.fullscreen&&C)return C;var t=e.body?document.body:e.target,n=new x({el:document.createElement("div"),data:e});return S(e,t,n),"absolute"!==n.originalPosition&&"fixed"!==n.originalPosition&&Object(d["addClass"])(t,"el-loading-parent--relative"),e.fullscreen&&e.lock&&Object(d["addClass"])(t,"el-loading-parent--hidden"),t.appendChild(n.$el),o.a.nextTick((function(){n.visible=!0})),e.fullscreen&&(C=n),n}},E=O;t["default"]={install:function(e){e.use(g),e.prototype.$loading=E},directive:g,service:E}},7:function(e,t){e.exports=n("0261")},9:function(e,t){e.exports=n("eb40")}})},eec6:function(e,t,n){var r=n("efe2");e.exports=!r((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},ef4c:function(e,t,n){var r=n("857c"),o=n("0c3c"),i=n("90fb"),a=i("species");e.exports=function(e,t){var n,i=r(e).constructor;return void 0===i||void 0==(n=r(i)[a])?t:o(n)}},ef71:function(e,t,n){"use strict";var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);t.f=i?function(e){var t=o(this,e);return!!t&&t.enumerable}:r},efe2:function(e,t){e.exports=function(e){try{return!!e()}catch(t){return!0}}},f062:function(e,t,n){"use strict";var r,o;"function"===typeof Symbol&&Symbol.iterator;(function(i,a){r=a,o="function"===typeof r?r.call(t,n,t,e):r,void 0===o||(e.exports=o)})(0,(function(){var e=window,t={placement:"bottom",gpuAcceleration:!0,offset:0,boundariesElement:"viewport",boundariesPadding:5,preventOverflowOrder:["left","right","top","bottom"],flipBehavior:"flip",arrowElement:"[x-arrow]",arrowOffset:0,modifiers:["shift","offset","preventOverflow","keepTogether","arrow","flip","applyStyle"],modifiersIgnored:[],forceAbsolute:!1};function n(e,n,r){this._reference=e.jquery?e[0]:e,this.state={};var o="undefined"===typeof n||null===n,i=n&&"[object Object]"===Object.prototype.toString.call(n);return this._popper=o||i?this.parse(i?n:{}):n.jquery?n[0]:n,this._options=Object.assign({},t,r),this._options.modifiers=this._options.modifiers.map(function(e){if(-1===this._options.modifiersIgnored.indexOf(e))return"applyStyle"===e&&this._popper.setAttribute("x-placement",this._options.placement),this.modifiers[e]||e}.bind(this)),this.state.position=this._getPosition(this._popper,this._reference),f(this._popper,{position:this.state.position,top:0}),this.update(),this._setupEventListeners(),this}function r(t){var n=t.style.display,r=t.style.visibility;t.style.display="block",t.style.visibility="hidden";t.offsetWidth;var o=e.getComputedStyle(t),i=parseFloat(o.marginTop)+parseFloat(o.marginBottom),a=parseFloat(o.marginLeft)+parseFloat(o.marginRight),s={width:t.offsetWidth+a,height:t.offsetHeight+i};return t.style.display=n,t.style.visibility=r,s}function o(e){var t={left:"right",right:"left",bottom:"top",top:"bottom"};return e.replace(/left|right|bottom|top/g,(function(e){return t[e]}))}function i(e){var t=Object.assign({},e);return t.right=t.left+t.width,t.bottom=t.top+t.height,t}function a(e,t){var n,r=0;for(n in e){if(e[n]===t)return r;r++}return null}function s(t,n){var r=e.getComputedStyle(t,null);return r[n]}function l(t){var n=t.offsetParent;return n!==e.document.body&&n?n:e.document.documentElement}function u(t){var n=t.parentNode;return n?n===e.document?e.document.body.scrollTop||e.document.body.scrollLeft?e.document.body:e.document.documentElement:-1!==["scroll","auto"].indexOf(s(n,"overflow"))||-1!==["scroll","auto"].indexOf(s(n,"overflow-x"))||-1!==["scroll","auto"].indexOf(s(n,"overflow-y"))?n:u(t.parentNode):t}function c(t){return t!==e.document.body&&("fixed"===s(t,"position")||(t.parentNode?c(t.parentNode):t))}function f(e,t){function n(e){return""!==e&&!isNaN(parseFloat(e))&&isFinite(e)}Object.keys(t).forEach((function(r){var o="";-1!==["width","height","top","right","bottom","left"].indexOf(r)&&n(t[r])&&(o="px"),e.style[r]=t[r]+o}))}function d(e){var t={};return e&&"[object Function]"===t.toString.call(e)}function p(e){var t={width:e.offsetWidth,height:e.offsetHeight,left:e.offsetLeft,top:e.offsetTop};return t.right=t.left+t.width,t.bottom=t.top+t.height,t}function h(e){var t=e.getBoundingClientRect(),n=-1!=navigator.userAgent.indexOf("MSIE"),r=n&&"HTML"===e.tagName?-e.scrollTop:t.top;return{left:t.left,top:r,right:t.right,bottom:t.bottom,width:t.right-t.left,height:t.bottom-r}}function v(e,t,n){var r=h(e),o=h(t);if(n){var i=u(t);o.top+=i.scrollTop,o.bottom+=i.scrollTop,o.left+=i.scrollLeft,o.right+=i.scrollLeft}var a={top:r.top-o.top,left:r.left-o.left,bottom:r.top-o.top+r.height,right:r.left-o.left+r.width,width:r.width,height:r.height};return a}function m(t){for(var n=["","ms","webkit","moz","o"],r=0;r1&&console.warn("WARNING: the given `parent` query("+t.parent+") matched more than one element, the first one will be used"),0===a.length)throw"ERROR: the given `parent` doesn't exists!";a=a[0]}return a.length>1&&a instanceof Element===!1&&(console.warn("WARNING: you have passed as parent a list of elements, the first one will be used"),a=a[0]),a.appendChild(o),o;function s(e,t){t.forEach((function(t){e.classList.add(t)}))}function l(e,t){t.forEach((function(t){e.setAttribute(t.split(":")[0],t.split(":")[1]||"")}))}},n.prototype._getPosition=function(e,t){var n=l(t);if(this._options.forceAbsolute)return"absolute";var r=c(t,n);return r?"fixed":"absolute"},n.prototype._getOffsets=function(e,t,n){n=n.split("-")[0];var o={};o.position=this.state.position;var i="fixed"===o.position,a=v(t,l(e),i),s=r(e);return-1!==["right","left"].indexOf(n)?(o.top=a.top+a.height/2-s.height/2,o.left="left"===n?a.left-s.width:a.right):(o.left=a.left+a.width/2-s.width/2,o.top="top"===n?a.top-s.height:a.bottom),o.width=s.width,o.height=s.height,{popper:o,reference:a}},n.prototype._setupEventListeners=function(){if(this.state.updateBound=this.update.bind(this),e.addEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement){var t=u(this._reference);t!==e.document.body&&t!==e.document.documentElement||(t=e),t.addEventListener("scroll",this.state.updateBound),this.state.scrollTarget=t}},n.prototype._removeEventListeners=function(){e.removeEventListener("resize",this.state.updateBound),"window"!==this._options.boundariesElement&&this.state.scrollTarget&&(this.state.scrollTarget.removeEventListener("scroll",this.state.updateBound),this.state.scrollTarget=null),this.state.updateBound=null},n.prototype._getBoundaries=function(t,n,r){var o,i,a={};if("window"===r){var s=e.document.body,c=e.document.documentElement;i=Math.max(s.scrollHeight,s.offsetHeight,c.clientHeight,c.scrollHeight,c.offsetHeight),o=Math.max(s.scrollWidth,s.offsetWidth,c.clientWidth,c.scrollWidth,c.offsetWidth),a={top:0,right:o,bottom:i,left:0}}else if("viewport"===r){var f=l(this._popper),d=u(this._popper),h=p(f),v=function(e){return e==document.body?Math.max(document.documentElement.scrollTop,document.body.scrollTop):e.scrollTop},m=function(e){return e==document.body?Math.max(document.documentElement.scrollLeft,document.body.scrollLeft):e.scrollLeft},y="fixed"===t.offsets.popper.position?0:v(d),g="fixed"===t.offsets.popper.position?0:m(d);a={top:0-(h.top-y),right:e.document.documentElement.clientWidth-(h.left-g),bottom:e.document.documentElement.clientHeight-(h.top-y),left:0-(h.left-g)}}else a=l(this._popper)===r?{top:0,left:0,right:r.clientWidth,bottom:r.clientHeight}:p(r);return a.left+=n,a.right-=n,a.top=a.top+n,a.bottom=a.bottom-n,a},n.prototype.runModifiers=function(e,t,n){var r=t.slice();return void 0!==n&&(r=this._options.modifiers.slice(0,a(this._options.modifiers,n))),r.forEach(function(t){d(t)&&(e=t.call(this,e))}.bind(this)),e},n.prototype.isModifierRequired=function(e,t){var n=a(this._options.modifiers,e);return!!this._options.modifiers.slice(0,n).filter((function(e){return e===t})).length},n.prototype.modifiers={},n.prototype.modifiers.applyStyle=function(e){var t,n={position:e.offsets.popper.position},r=Math.round(e.offsets.popper.left),o=Math.round(e.offsets.popper.top);return this._options.gpuAcceleration&&(t=m("transform"))?(n[t]="translate3d("+r+"px, "+o+"px, 0)",n.top=0,n.left=0):(n.left=r,n.top=o),Object.assign(n,e.styles),f(this._popper,n),this._popper.setAttribute("x-placement",e.placement),this.isModifierRequired(this.modifiers.applyStyle,this.modifiers.arrow)&&e.offsets.arrow&&f(e.arrowElement,e.offsets.arrow),e},n.prototype.modifiers.shift=function(e){var t=e.placement,n=t.split("-")[0],r=t.split("-")[1];if(r){var o=e.offsets.reference,a=i(e.offsets.popper),s={y:{start:{top:o.top},end:{top:o.top+o.height-a.height}},x:{start:{left:o.left},end:{left:o.left+o.width-a.width}}},l=-1!==["bottom","top"].indexOf(n)?"x":"y";e.offsets.popper=Object.assign(a,s[l][r])}return e},n.prototype.modifiers.preventOverflow=function(e){var t=this._options.preventOverflowOrder,n=i(e.offsets.popper),r={left:function(){var t=n.left;return n.lefte.boundaries.right&&(t=Math.min(n.left,e.boundaries.right-n.width)),{left:t}},top:function(){var t=n.top;return n.tope.boundaries.bottom&&(t=Math.min(n.top,e.boundaries.bottom-n.height)),{top:t}}};return t.forEach((function(t){e.offsets.popper=Object.assign(n,r[t]())})),e},n.prototype.modifiers.keepTogether=function(e){var t=i(e.offsets.popper),n=e.offsets.reference,r=Math.floor;return t.rightr(n.right)&&(e.offsets.popper.left=r(n.right)),t.bottomr(n.bottom)&&(e.offsets.popper.top=r(n.bottom)),e},n.prototype.modifiers.flip=function(e){if(!this.isModifierRequired(this.modifiers.flip,this.modifiers.preventOverflow))return console.warn("WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!"),e;if(e.flipped&&e.placement===e._originalPlacement)return e;var t=e.placement.split("-")[0],n=o(t),r=e.placement.split("-")[1]||"",a=[];return a="flip"===this._options.flipBehavior?[t,n]:this._options.flipBehavior,a.forEach(function(s,l){if(t===s&&a.length!==l+1){t=e.placement.split("-")[0],n=o(t);var u=i(e.offsets.popper),c=-1!==["right","bottom"].indexOf(t);(c&&Math.floor(e.offsets.reference[t])>Math.floor(u[n])||!c&&Math.floor(e.offsets.reference[t])s[p]&&(e.offsets.popper[f]+=l[f]+h-s[p]);var v=l[f]+(n||l[c]/2-h/2),m=v-s[f];return m=Math.max(Math.min(s[c]-h-8,m),8),o[f]=m,o[d]="",e.offsets.arrow=o,e.arrowElement=t,e},Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:function(e){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var t=Object(e),n=1;n=74)&&(r=a.match(/Chrome\/(\d+)/),r&&(o=r[1]))),e.exports=o&&+o},faa8:function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},fce0:function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=110)}({0:function(e,t,n){"use strict";function r(e,t,n,r,o,i,a,s){var l,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(l=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,l):[l]}return{exports:e,options:u}}n.d(t,"a",(function(){return r}))},110:function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"el-breadcrumb",attrs:{"aria-label":"Breadcrumb",role:"navigation"}},[e._t("default")],2)},o=[];r._withStripped=!0;var i={name:"ElBreadcrumb",props:{separator:{type:String,default:"/"},separatorClass:{type:String,default:""}},provide:function(){return{elBreadcrumb:this}},mounted:function(){var e=this.$el.querySelectorAll(".el-breadcrumb__item");e.length&&e[e.length-1].setAttribute("aria-current","page")}},a=i,s=n(0),l=Object(s["a"])(a,r,o,!1,null,null,null);l.options.__file="packages/breadcrumb/src/breadcrumb.vue";var u=l.exports;u.install=function(e){e.component(u.name,u)};t["default"]=u}})}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/new/js/detail.ff471d08.js b/app/src/main/assets/web/new/js/detail.ff471d08.js deleted file mode 100644 index 63845b64e..000000000 --- a/app/src/main/assets/web/new/js/detail.ff471d08.js +++ /dev/null @@ -1 +0,0 @@ -(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["detail"],{"05b3":function(t,e,n){},"064b":function(t,e){t.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},"0827":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg=="},"0a51":function(t,e,n){"use strict";var i=n("1c8b"),o=n("1e2c"),r=n("d890"),a=n("faa8"),s=n("a719"),c=n("d910").f,l=n("c69d"),u=r.Symbol;if(o&&"function"==typeof u&&(!("description"in u.prototype)||void 0!==u().description)){var A={},f=function(){var t=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),e=this instanceof f?new u(t):void 0===t?u():u(t);return""===t&&(A[e]=!0),e};l(f,u);var g=f.prototype=u.prototype;g.constructor=f;var d=g.toString,h="Symbol(test)"==String(u("test")),p=/^Symbol\((.*)\)[^)]+$/;c(g,"description",{configurable:!0,get:function(){var t=s(this)?this.valueOf():this,e=d.call(t);if(a(A,t))return"";var n=h?e.slice(7,-1):e.replace(p,"$1");return""===n?void 0:n}}),i({global:!0,forced:!0},{Symbol:f})}},"0d7a":function(t,e,n){"use strict";var i=n("b2a2"),o=n("8a1c"),r=n("857c"),a=n("2732"),s=n("ef4c"),c=n("38eb"),l=n("d88d"),u=n("59da"),A=n("5139"),f=n("efe2"),g=[].push,d=Math.min,h=4294967295,p=!f((function(){return!RegExp(h,"y")}));i("split",2,(function(t,e,n){var i;return i="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var i=String(a(this)),r=void 0===n?h:n>>>0;if(0===r)return[];if(void 0===t)return[i];if(!o(t))return e.call(i,t,r);var s,c,l,u=[],f=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),d=0,p=new RegExp(t.source,f+"g");while(s=A.call(p,i)){if(c=p.lastIndex,c>d&&(u.push(i.slice(d,s.index)),s.length>1&&s.index=r))break;p.lastIndex===s.index&&p.lastIndex++}return d===i.length?!l&&p.test("")||u.push(""):u.push(i.slice(d)),u.length>r?u.slice(0,r):u}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var o=a(this),r=void 0==e?void 0:e[t];return void 0!==r?r.call(e,o,n):i.call(String(o),e,n)},function(t,o){var a=n(i,t,this,o,i!==e);if(a.done)return a.value;var A=r(t),f=String(this),g=s(A,RegExp),m=A.unicode,v=(A.ignoreCase?"i":"")+(A.multiline?"m":"")+(A.unicode?"u":"")+(p?"y":"g"),b=new g(p?A:"^(?:"+A.source+")",v),C=void 0===o?h:o>>>0;if(0===C)return[];if(0===f.length)return null===u(b,f)?[f]:[];var y=0,S=0,x=[];while(S=51||!i((function(){var e=[],n=e.constructor={};return n[a]=function(){return{foo:1}},1!==e[t](Boolean).foo}))}},"22ef":function(t,e,n){"use strict";var i=n("efe2");function o(t,e){return RegExp(t,e)}e.UNSUPPORTED_Y=i((function(){var t=o("a","y");return t.lastIndex=2,null!=t.exec("abcd")})),e.BROKEN_CARET=i((function(){var t=o("^r","gy");return t.lastIndex=2,null!=t.exec("str")}))},"2eeb":function(t,e,n){"use strict";var i=n("1c8b"),o=n("5dfd").map,r=n("1ea7"),a=n("ff9c"),s=r("map"),c=a("map");i({target:"Array",proto:!0,forced:!s||!c},{map:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}})},"356c":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXPz8/R0dHT09PU1NToNyAhAAACdElEQVQozw3NP0xTQQDH8d9d7sFrG+QeKVgQ4aoFCwFkYERyLY//0UB8GNGg1WAC0RBGJrzW4mCXQmpgvCYOwEAYiulSpYtza2KiW7s5FgNJFSV2/CzfL7RwpoJ20iadmgA8owOyaxmusKE44scBeb4vIv00dqYgmf6jzWcr7W6INbDQeZbQL9ytXeYgtFfzmW1Fek5msxJlwhyt6qDDxOLQzpVPompYrMPnEnhvLm7M5BxY5nowAj3zkydAkpC0FIG6g7AK+Ub25ybyNWVYwtpseP2rfrQwiGRpfqrnMuPeuvr2dA0p2YsHF2XghkrXKtZ8tLBjR7S2qIaYbKmyLd/QP+EogLjqqwNw5Lq1pDlMLkM5+gNoSvdq+Pxmz9/61EFq6GYM6GqaGvlN95zy3gsmEWI8K3k8OP9OmRLEPO6DP3Wv3g42COinJTZ33dcIvs4ESp6opMTjDs6mcYTEbFeUifuxh989yZrIx4lkpuixxz0nHLCekKbE17suKhYkMGhoYhTZtVBvg4bfq/1L1Im0AGMVpBFwumM0zwyuKiCMi5dqR4Flx47AGyF2xTbxqUdTwCH94BT3DozpLV5WuAL/N8rGtHKjotBOOuOtCJ9E21uqsyBoLOzaXbHPrK5PQBP+fBfeidvJAeMIAmzVt5IkJJ9DBWaZDAepYUhlQqHt0h72SJ3j8TZHom64f516xx9T5evgMPgwG82jZdJaJIDyWp6LAjOCclVyzNA3iTKzIULlBQEPaTXlPHok5gISclmyaWZlqY2aTHdRHpJOwTdDEQ3ZfKtbpclcNhyVClagmY+fIfyKukntPqBgnx5QvZHk/D/MK8JMClrSigAAAABJRU5ErkJggg=="},"38eb":function(t,e,n){"use strict";var i=n("f62c").charAt;t.exports=function(t,e,n){return e+(n?i(t,e).length:1)}},4039:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX0/PTx+fH2/vbz+/P4//htSO9OAAAC5UlEQVQ4yyWT0QGjMAxDZTsDWKQDmJQBYrgBUsr+M517x0+LRWw9CyA+pC1YzndrMgHaNXVKQ+di13Of1qbur48nWhuRjj8i6ON8e7pNm7zyag/DBTfS9Z4Hup1fUuXMKY4HEE8QOHCByXkIkl7lDT239RtL9quO4JItmmhOAHXg45QuYKrQFLyGJcRvaTw6kQqZy6mkR6JAPFH/XqsQjEDRmUOA+MNLHGyMUT7AHApoAhjgjIJmCxy6XHdf648AWCdGe57IUDazCeTImQOY4/z+eVYVX2IjOw9RydeAeJwl79iGi4HpgQgHEchWraUZLtayu8scq0lHHHUKMY3Ml8hB7CS1jOckDLG9ccgNeX3124phOcjL9fPnWJhTXpLHeG9DRmHnTxHEaHakS2J51lwAJcUraNbuU7q4gMTDQj3Eripc/x+qFM5VEKAB1roQfAkX5/PxqnS2QpOrxfK1Zft0/omV5T+xCSBUAIbEIwUQgvAfxFE1O8dnk233+1UZiqJ1mAbsue6Yt8tF+yOrxC/YrUhzC4qPlE3EbR5hGKhhHdlrg7J9WunV7L7BcYQwAeE59u2tnN1c6gfVYrQiLSZ9OxZdWDXQq0+r0Pbarh3UqGCwauVvbiXuDsNxCtLDdW9rTF8oQYN4EoXXdfmwNguQP26n/tRjDeo+F2W7PjWtfSr6Bn/z+cXOLp4NnMV4RytvSW4B68m+XN9XfZTFGhO/S+cHTuTqZDC21ccA0N7QsePALaDQC3D1f94U9CWo+aq6BjB3v0rxIimBM12296M3aKPHjXLQE9KQKH4By8RHraJ3AgVto2r4xdFqlaPaiAHLl1ZF4P2pI6cYc+K8UZdcmxy7lqGc1IoPxLmIFuIeEZ6j2sQT88muEg1zwrEDTIX5U/ZmcsqfgVlBumiBLF4sAyhf9BFlXOPKLZ4H0iFb3VoHrGhtHTldKrOvP2/reu2zfV8CXMPqzRdlgd0a5eI7WwB/AYcgavcqxXWEAAAAAElFTkSuQmCC"},4350:function(t,e,n){var i=n("90fb");e.f=i},4400:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAElBMVEUQERMODxESFBYWGBkaHB0eICLm6ozJAAACkUlEQVQ4yyWTUdLbMAiEASfvoOkBkBy/O5keIE0v8E/uf5h+68qZWALELgu2MG9PP9qyvCzTVhrrsPGOCjvTfXQZvtp/W3Gy6LCITqs4q/DZ+KYl76zKzHVYpY2wNY27nqN1sbLGcrLH3/ENH4oWlGctsDu8AO+HzTLlsYdh8MzP1m6YDMz0ACfcimvakBj+mwO/+5Uta5teOD379sxK1fUxmUhv8MU3jUT5gs26PMephFznkLcpQZ6/dPL9C/GWHcCxDN6oZhD5xBm5qoYBPA+PFE/H1tXDWcWl8TW7rS+4dUzAVy0BIrvC4/HcqW2TkG1HO8q9dC23INAg7NA4AFRFkDTM2lfELPyFzi1VddcpX2z0KjHBUDmdLNJ6dDps4ytrX+FPsZwE31wSL+6OWfHOAJ3+Y0Rk/MiKfmWNPg7oVP/U3Ck9FoCkC2gBpALOiqbMNTkOe8P4FWkTD2Y9Q3+5VmV0uLUJBl68U5uAK2Kl6QDXvLxbwweOL2sixW78uU8p0ysfc7cWrF1j6B1sPJ4WgclYSnJN1bzozrhEcFHmRzBkbJWqqdG+EYJXRFmT5jnLXPUNF6WBdoFbTxYsmDXVLU/WA7MExNc93sJS5hIXDeLxzMScHzdhKvEkibr6cQXYPrmtmTA7JcInISrTzRDvShTdka0uVGrsJAAR6tSn1sKziZtfKVjAxPrJsYgZO0bye+vKTZ/DgoAoLGNO6jYHimZYTL/3pLJHawquJukjBpfz8WOGVSVIWx9ywUfS5iENutidRM4NzkAmxgUSQ68xgNOU+ZLalr4TS2V+D2xqukZig+Z9DilR7Nouzwp1cp/3E5q6Rdlf08obKvAM4qZ6pMr+w3PmQALSSBfjyZn5DwrNRVbywBQiAAAAAElFTkSuQmCC"},"477e":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAACVBMVEX28ef48+n69esoK7jYAAAB4UlEQVQozw2OsW4bQQxEhwLXkDrysGdEqRRgVShfQQq8wOr2jD0jSpXCLvwXbtKfADlFqgSwC/9ljqweZgYzQFnb/QGepYhA9jzmTc1WaSEtQpbFgjWATI00ZZtIckXx8q2Oe5yEByBy+RHOTcM+VVTadULsvxvRC/q8WTwgcWGD+Mnaqa0oy2gw2pKFzK+PzEsus5hP9AHojKslVynLlioVTBEN8cjDNnZoR1uMGTiZAAN47HxMtEkGUE9b8HWzkqNX5Lpk0yVziAJOs46rK1pG/xNuXLjz95fSDoJE5IqG23MAYPtWoeWPvfVtIV/Ng9oH3W0gGMPIOqd4MK4QZ55dV61gOb8Zxp7I9qayaGxp6Q91cmC0ZRdBwEQVHWzSAanlZwVWc9yljeTCeaHjBVvlPSLeyeBUT2rPdJegQI103jVS3uYkyIx1il6mslMDedZuOkwzolsagvPuQAfp7cYg7k9V1NOxfq64PNSvMdwONV4VYEmqlbpZy5OAakRKkjPnL4CBv5/OZRgoWHBmNbxB0LgB1I4vXFj93UoF2/0TPEsWwV9EhbIiTPqYoTHYoMn3enTDjmrFeDTIzaL1bUC/PBIMuF+vSSYSaxoVt90EO3Gu1zrMuMRGUk7Ffv3L+A931Gsb/yBoIgAAAABJRU5ErkJggg=="},5139:function(t,e,n){"use strict";var i=n("99ad"),o=n("22ef"),r=RegExp.prototype.exec,a=String.prototype.replace,s=r,c=function(){var t=/a/,e=/b*/g;return r.call(t,"a"),r.call(e,"a"),0!==t.lastIndex||0!==e.lastIndex}(),l=o.UNSUPPORTED_Y||o.BROKEN_CARET,u=void 0!==/()??/.exec("")[1],A=c||u||l;A&&(s=function(t){var e,n,o,s,A=this,f=l&&A.sticky,g=i.call(A),d=A.source,h=0,p=t;return f&&(g=g.replace("y",""),-1===g.indexOf("g")&&(g+="g"),p=String(t).slice(A.lastIndex),A.lastIndex>0&&(!A.multiline||A.multiline&&"\n"!==t[A.lastIndex-1])&&(d="(?: "+d+")",p=" "+p,h++),n=new RegExp("^(?:"+d+")",g)),u&&(n=new RegExp("^"+d+"$(?!\\s)",g)),c&&(e=A.lastIndex),o=r.call(f?n:A,p),f?o?(o.input=o.input.slice(h),o[0]=o[0].slice(h),o.index=A.lastIndex,A.lastIndex+=o[0].length):A.lastIndex=0:c&&o&&(A.lastIndex=A.global?o.index+o[0].length:e),u&&o&&o.length>1&&a.call(o[0],n,(function(){for(s=1;s1&&void 0!==arguments[1]?arguments[1]:{};switch(l=f.duration||1e3,o=f.offset||0,d=f.callback,r=f.easing||u,a=f.a11y||!1,s(f.container)){case"object":t=f.container;break;case"string":t=document.querySelector(f.container);break;default:t=window}switch(n=h(),s(A)){case"number":e=void 0,a=!1,i=n+A;break;case"object":e=A,i=p(e);break;case"string":e=document.querySelector(A),i=p(e);break}switch(c=i-n+o,s(f.duration)){case"number":l=f.duration;break;case"function":l=f.duration(c);break}requestAnimationFrame(v)}return C},f=A(),g=f,d=n("7286"),h=n.n(d),p=n("477e"),m=n.n(p),v=n("e160"),b=n.n(v),C=n("df5e"),y=n.n(C),S=n("ec0f"),x=n.n(S),B=n("b671"),I=n.n(B),E=n("5629"),k=n.n(E),w=n("d0e3"),O=n.n(w),U=n("4039"),D=n.n(U),Q=n("1e75"),V=n.n(Q),R=n("1632"),P=n.n(R),M=n("7abd"),F=n.n(M),L=n("356c"),N=n.n(L),T=n("b165"),K=n.n(T),H=n("cf68"),J=n.n(H),z=n("4400"),W=n.n(z),G=n("802e"),q=n.n(G),Y=n("0827"),Z=n.n(Y),j={themes:[{body:"#ede7da url("+h.a+") repeat",content:"#ede7da url("+m.a+") repeat",popup:"#ede7da url("+b.a+") repeat"},{body:"#ede7da url("+y.a+") repeat",content:"#ede7da url("+x.a+") repeat",popup:"#ede7da url("+I.a+") repeat"},{body:"#ede7da url("+k.a+") repeat",content:"#ede7da url("+O.a+") repeat",popup:"#ede7da url("+D.a+") repeat"},{body:"#ede7da url("+V.a+") repeat",content:"#ede7da url("+P.a+") repeat",popup:"#ede7da url("+F.a+") repeat"},{body:"#ebcece repeat",content:"#f5e4e4 repeat",popup:"#faeceb repeat"},{body:"#ede7da url("+N.a+") repeat",content:"#ede7da url("+K.a+") repeat",popup:"#ede7da url("+J.a+") repeat"},{body:"#ede7da url("+W.a+") repeat",content:"#ede7da url("+q.a+") repeat",popup:"#ede7da url("+Z.a+") repeat"}],fonts:[{fontFamily:"Microsoft YaHei, PingFangSC-Regular, HelveticaNeue-Light, Helvetica Neue Light, sans-serif"},{fontFamily:"PingFangSC-Regular, -apple-system, Simsun"},{fontFamily:"Kaiti"}]},X=j,$=(n("05b3"),{name:"PopCata",data:function(){return{isNight:6==this.$store.state.config.theme,index:this.$store.state.readingBook.index}},computed:{catalog:function(){return this.$store.state.readingBook.catalog},popCataVisible:function(){return this.$store.state.popCataVisible},theme:function(){return this.$store.state.config.theme},popupTheme:function(){return{background:X.themes[this.theme].popup}}},mounted:function(){},watch:{theme:function(t){this.isNight=6==t},popCataVisible:function(){this.$nextTick((function(){var t=this.$store.state.readingBook.index,e=this.$refs.cataData;g(this.$refs.cata[t],{container:e,duration:0})}))}},methods:{isSelected:function(t){return t==this.$store.state.readingBook.index},gotoChapter:function(t){this.index=this.catalog.indexOf(t),this.$store.commit("setPopCataVisible",!1),this.$store.commit("setContentLoading",!0),this.$emit("getContent",this.index)}}}),_=$,tt=(n("7f9c"),n("9ca4")),et=Object(tt["a"])(_,r,a,!1,null,"8c647fa4",null),nt=et.exports,it=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"settings-wrapper",class:{night:t.isNight,day:!t.isNight},style:t.popupTheme},[n("div",{staticClass:"settings-title"},[t._v("设置")]),n("div",{staticClass:"setting-list"},[n("ul",[n("li",{staticClass:"theme-list"},[n("i",[t._v("阅读主题")]),t._l(t.themeColors,(function(e,i){return n("span",{key:i,ref:"themes",refInFor:!0,staticClass:"theme-item",class:{selected:t.selectedTheme==i},style:e,on:{click:function(e){return t.setTheme(i)}}},[i<6?n("em",{staticClass:"iconfont"},[t._v("")]):n("em",{staticClass:"moon-icon"},[t._v(t._s(t.moonIcon))])])}))],2),n("li",{staticClass:"font-list"},[n("i",[t._v("正文字体")]),t._l(t.fonts,(function(e,i){return n("span",{key:i,staticClass:"font-item",class:{selected:t.selectedFont==i},on:{click:function(e){return t.setFont(i)}}},[t._v(t._s(e))])}))],2),n("li",{staticClass:"font-size"},[n("i",[t._v("字体大小")]),n("div",{staticClass:"resize"},[n("span",{staticClass:"less",on:{click:t.lessFontSize}},[n("em",{staticClass:"iconfont"},[t._v("")])]),n("b"),t._v(" "),n("span",{staticClass:"lang"},[t._v(t._s(t.fontSize))]),n("b"),n("span",{staticClass:"more",on:{click:t.moreFontSize}},[n("em",{staticClass:"iconfont"},[t._v("")])])])]),n("li",{staticClass:"read-width"},[n("i",[t._v("页面宽度")]),n("div",{staticClass:"resize"},[n("span",{staticClass:"less",on:{click:t.lessReadWidth}},[n("em",{staticClass:"iconfont"},[t._v("")])]),n("b"),t._v(" "),n("span",{staticClass:"lang"},[t._v(t._s(t.readWidth))]),n("b"),n("span",{staticClass:"more",on:{click:t.moreReadWidth}},[n("em",{staticClass:"iconfont"},[t._v("")])])])])])])])},ot=[],rt=(n("82da"),{name:"ReadSettings",data:function(){return{theme:0,isNight:6==this.$store.state.config.theme,moonIcon:"",themeColors:[{background:"rgba(250, 245, 235, 0.8)"},{background:"rgba(245, 234, 204, 0.8)"},{background:"rgba(230, 242, 230, 0.8)"},{background:"rgba(228, 241, 245, 0.8)"},{background:"rgba(245, 228, 228, 0.8)"},{background:"rgba(224, 224, 224, 0.8)"},{background:"rgba(0, 0, 0, 0.5)"}],moonIconStyle:{display:"inline",color:"rgba(255,255,255,0.2)"},fonts:["雅黑","宋体","楷书"]}},mounted:function(){var t=this.$store.state.config;this.theme=t.theme,6==this.theme?this.moonIcon="":this.moonIcon=""},computed:{config:function(){return this.$store.state.config},popupTheme:function(){return{background:X.themes[this.config.theme].popup}},selectedTheme:function(){return this.$store.state.config.theme},selectedFont:function(){return this.$store.state.config.font},fontSize:function(){return this.$store.state.config.fontSize},readWidth:function(){return this.$store.state.config.readWidth}},methods:{setTheme:function(t){6==t?(this.isNight=!0,this.moonIcon="",this.moonIconStyle.color="#ed4259"):(this.isNight=!1,this.moonIcon="",this.moonIconStyle.color="rgba(255,255,255,0.2)");var e=this.config;e.theme=t,this.$store.commit("setConfig",e)},setFont:function(t){var e=this.config;e.font=t,this.$store.commit("setConfig",e)},moreFontSize:function(){var t=this.config;t.fontSize<48&&(t.fontSize+=2),this.$store.commit("setConfig",t)},lessFontSize:function(){var t=this.config;t.fontSize>12&&(t.fontSize-=2),this.$store.commit("setConfig",t)},moreReadWidth:function(){var t=this.config;t.readWidth<960&&(t.readWidth+=160),this.$store.commit("setConfig",t)},lessReadWidth:function(){var t=this.config;t.readWidth>640&&(t.readWidth-=160),this.$store.commit("setConfig",t)}}}),at=rt,st=(n("8157"),Object(tt["a"])(at,it,ot,!1,null,"3d7e2fe5",null)),ct=st.exports,lt=(n("2eeb"),{name:"pcontent",data:function(){return{}},props:["carray"],render:function(){var t=arguments[0],e=this.fontFamily,n=this.fontSize,i=e;return i.fontSize=n,this.show?t("div",[this.carray.map((function(e){return t("p",{style:i},[e])}))]):t("div")},computed:{show:function(){return this.$store.state.showContent},fontFamily:function(){return X.fonts[this.$store.state.config.font]},fontSize:function(){return this.$store.state.config.fontSize+"px"}},watch:{fontSize:function(){var t=this;t.$store.commit("setShowContent",!1),this.$nextTick((function(){t.$store.commit("setShowContent",!0)}))}}}),ut=lt,At=(n("a9ea"),Object(tt["a"])(ut,c,l,!1,null,"6ee085ae",null)),ft=At.exports,gt=n("82ae"),dt=n.n(gt),ht={components:{PopCata:nt,Pcontent:ft,ReadSettings:ct},created:function(){var t=JSON.parse(localStorage.getItem("config"));null!=t&&this.$store.commit("setConfig",t)},beforeCreate:function(){var t=JSON.parse(localStorage.getItem("config"));null!=t&&this.$store.commit("setConfig",t)},mounted:function(){var t=this;this.loading=this.$loading({target:this.$refs.content,lock:!0,text:"正在获取内容",spinner:"el-icon-loading",background:"rgba(0,0,0,0)"});var e=this,n=sessionStorage.getItem("bookUrl"),i=sessionStorage.getItem("bookName"),o=sessionStorage.getItem("chapterIndex")||0,r=JSON.parse(localStorage.getItem(n));null==r&&(r={bookName:i,bookUrl:n,index:o},localStorage.setItem(n,JSON.stringify(r))),this.getCatalog(n).then((function(n){var i=n.data.data;r.catalog=i,e.$store.commit("setReadingBook",r);var o=e.$store.state.readingBook.index||0;t.getContent(o),window.addEventListener("keyup",(function(t){switch(t.key){case"ArrowLeft":t.stopPropagation(),t.preventDefault(),e.toLastChapter();break;case"ArrowRight":t.stopPropagation(),t.preventDefault(),e.toNextChapter();break;case"ArrowUp":t.stopPropagation(),t.preventDefault(),0===document.documentElement.scrollTop?e.$message.warning("已到达页面顶部"):g(0-document.documentElement.clientHeight+100);break;case"ArrowDown":t.stopPropagation(),t.preventDefault(),document.documentElement.clientHeight+document.documentElement.scrollTop===document.documentElement.scrollHeight?e.$message.warning("已到达页面底部"):g(document.documentElement.clientHeight-100);break}}))}),(function(t){throw e.loading.close(),e.$message.error("获取书籍目录失败"),t}))},watch:{chapterName:function(t){this.title=t},content:function(){this.$store.commit("setContentLoading",!1)},theme:function(t){this.isNight=6==t},bodyColor:function(t){this.bodyTheme.background=t},chapterColor:function(t){this.chapterTheme.background=t},readWidth:function(t){this.chapterTheme.width=t;var e=-((parseInt(t)+130)/2+68)+"px",n=-((parseInt(t)+130)/2+52)+"px";this.leftBarTheme.marginLeft=e,this.rightBarTheme.marginRight=n},popupColor:function(t){this.leftBarTheme.background=t,this.rightBarTheme.background=t},readSettingsVisible:function(t){if(!t){var e=JSON.stringify(this.$store.state.config);localStorage.setItem("config",e)}}},data:function(){return{title:"",content:[],noPoint:!0,isNight:6==this.$store.state.config.theme,bodyTheme:{background:X.themes[this.$store.state.config.theme].body},chapterTheme:{background:X.themes[this.$store.state.config.theme].content,width:this.$store.state.config.readWidth-130+"px"},leftBarTheme:{background:X.themes[this.$store.state.config.theme].popup,marginLeft:-(this.$store.state.config.readWidth/2+68)+"px"},rightBarTheme:{background:X.themes[this.$store.state.config.theme].popup,marginRight:-(this.$store.state.config.readWidth/2+52)+"px"}}},computed:{catalog:function(){return this.$store.state.catalog},windowHeight:function(){return window.innerHeight},contentHeight:function(){return this.$refs.content.offsetHeight},popCataVisible:{get:function(){return this.$store.state.popCataVisible},set:function(t){this.$store.commit("setPopCataVisible",t)}},readSettingsVisible:{get:function(){return this.$store.state.readSettingsVisible},set:function(t){this.$store.commit("setReadSettingsVisible",t)}},config:function(){return this.$store.state.config},theme:function(){return this.config.theme},bodyColor:function(){return X.themes[this.config.theme].body},chapterColor:function(){return X.themes[this.config.theme].content},popupColor:function(){return X.themes[this.config.theme].popup},readWidth:function(){return this.$store.state.config.readWidth-130+"px"},cataWidth:function(){return this.$store.state.config.readWidth-33},show:function(){return this.$store.state.showContent}},methods:{getCatalog:function(t){return dt.a.get("/getChapterList?url="+encodeURIComponent(t))},getContent:function(t){var e=this;this.$store.commit("setShowContent",!1),this.loading.visible||(this.loading=this.$loading({target:this.$refs.content,lock:!0,text:"正在获取内容",spinner:"el-icon-loading",background:"rgba(0,0,0,0)"}));var n=sessionStorage.getItem("bookUrl"),i=JSON.parse(localStorage.getItem(n));i.index=t,localStorage.setItem(n,JSON.stringify(i)),this.$store.state.readingBook.index=t;var o=this.$store.state.readingBook.catalog[t].title,r=this.$store.state.readingBook.catalog[t].index;this.title=o,g(this.$refs.top,{duration:0});var a=this;dt.a.get("/getBookContent?url="+encodeURIComponent(n)+"&index="+r).then((function(t){var n=t.data.data,i=n.split("\n\n"),o="";o=i.length>1?i[1].split("\n"):i[0].split("\n"),a.content=o,e.$store.commit("setContentLoading",!0),a.loading.close(),a.noPoint=!1,a.$store.commit("setShowContent",!0)}),(function(t){throw a.$message.error("获取章节内容失败"),a.content="  获取章节内容失败!",t}))},toTop:function(){g(this.$refs.top)},toBottom:function(){g(this.$refs.bottom)},toNextChapter:function(){this.$store.commit("setContentLoading",!0);var t=this.$store.state.readingBook.index;t++,"undefined"!==typeof this.$store.state.readingBook.catalog[t]?(this.$message.info("下一章"),this.getContent(t)):this.$message.error("本章是最后一章")},toLastChapter:function(){this.$store.commit("setContentLoading",!0);var t=this.$store.state.readingBook.index;t--,"undefined"!==typeof this.$store.state.readingBook.catalog[t]?(this.$message.info("上一章"),this.getContent(t)):this.$message.error("本章是第一章")},toShelf:function(){this.$router.push("/")}}},pt=ht,mt=(n("f40b"),Object(tt["a"])(pt,i,o,!1,null,"3d823984",null));e["default"]=mt.exports},5629:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXN383Q4tDP4c/R5NEInCCXAAACVElEQVQozw3Hv2sTYRwH4M/79pJ7bZL2bXqtERJ97zjUpbZDhg6pfC8qibi8hLR0EaJ0EFxaCSWDxjfpj1zrYBcRBKE6SAfBJWsx9i8IQfdQxDlKtA6t2OnhQfN3lbG7ytYRywF8rVoPCNO0X2sQOKDpAnSDK2VwkHgmh5yLGT8qASt+2KofnNt2Xg1gf1UF8AoM6052cRMNaloLZb7RKQGrKKji2OefsZF+VqIvos5ZLVIZCX61JcwUdk56wASVkgQvzPfvmT2twTSwyYaC/Pl/UhAHorFhBgZtL6XdAZRp1tkPwC1NLa9CWs5prLhI85NBQsLdXvjDymG3/EbYfQhVNYqc3TtktQhWLY3ko0QsdMbSEp+64v0NfxyqLbIGdh6M2xHHlLBGqKTyQo4E/nebBgBfe1GpdeywYXc8CT7D3cKXuMXkBy4xN6o5OuKamYp3DVI6uccO9lxgd2CAlJgI2BGgaAgIJV/TYwKqu3WFccjbMuA+bVkWgS2bfnlRbD1Eb1sDyWMmjKYIBgGAWbqKRicfvzBkBIz3V5AKnguWdglQEysQsSuVzOg6ALy1pitA5ykGCsc857BRYcgCSZyFOdvoOigSGoPc5Ta73mgxshIcQE5sHMHd9D7yqITw7JO+GHVMxjhzYLcKPSEgmz3fU+BRy3iYNtiXLaBssCW8KguReqkQOTb3MStV0Ugt4U1eIs1RZWRII6Ww8xeNNItyGGQI4ZMlpg/3lQtkl2JFnBp1imRyFe0kK2Id3PCslMgiQNMS77gvFeDhG3cSkYvheeg/e7ClIh5oh+IAAAAASUVORK5CYII="},"588a":function(t,e,n){},"59da":function(t,e,n){var i=n("2118"),o=n("5139");t.exports=function(t,e){var n=t.exec;if("function"===typeof n){var r=n.call(t,e);if("object"!==typeof r)throw TypeError("RegExp exec method returned something other than an Object or null");return r}if("RegExp"!==i(t))throw TypeError("RegExp#exec called on incompatible receiver");return o.call(t,e)}},"5dfd":function(t,e,n){var i=n("e349"),o=n("692f"),r=n("3553"),a=n("d88d"),s=n("1ca1"),c=[].push,l=function(t){var e=1==t,n=2==t,l=3==t,u=4==t,A=6==t,f=5==t||A;return function(g,d,h,p){for(var m,v,b=r(g),C=o(b),y=i(d,h,3),S=a(C.length),x=0,B=p||s,I=e?B(g,S):n?B(g,0):void 0;S>x;x++)if((f||x in C)&&(m=C[x],v=y(m,x,b),t))if(e)I[x]=v;else if(v)switch(t){case 3:return!0;case 5:return m;case 6:return x;case 2:c.call(I,m)}else if(u)return!1;return A?-1:l||u?u:I}};t.exports={forEach:l(0),map:l(1),filter:l(2),some:l(3),every:l(4),find:l(5),findIndex:l(6)}},"61b6":function(t,e,n){},"6d51":function(t,e,n){var i=n("1b99"),o=n("faa8"),r=n("4350"),a=n("d910").f;t.exports=function(t){var e=i.Symbol||(i.Symbol={});o(e,t)||a(e,t,{value:r.f(t)})}},7286:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXr5djn4dTp49bt59rT6LKxAAACnElEQVQozw3NUUwScRzA8d8R6MF8YMIx8uk47hDSJbj14IPzOGc7jPLvwTGg5uAYDbe2tt56cLtznvEnS6yDqCcEaWi91DvrbLJZz7b1aFtz1aO+2OZWvn+/+4CHeB6BMYaqBLfjPNRY6RFT2JJYby+uAk4WUTrtlmJ4hgPYb2q1XGDQjaK8pgJHvqNaAX+KyuIkDXpgQinb46nOulnn4b5laUHTxLfseeArAoNOeJlOIjdoal0n1FA7tKFv5roK+YaHOqP3P0XyKHPHY+MhTRe5uCZnKhtJKw2eSrSoBDPLtpZuNcFNJcFyiCMxOaaHIfXz1e8HQbWLySrBQ4x0x1qlhnHlnz2HQEC6TNb0gTHXa7IKhcaHqkE015hk9whA0YeWiLIXf7Fa2CZo3DjqjB4tTuF8jIcbfcEx5z/w4sXpQhXW+ju0cqh7icTFmRMaG+v6CIvTjcSpHcH8JEsF3EPh3fRthYdVLLgI2fWXm85/pGFE4l046s70L+yKCcirGFR+jbpy3kMmiCGHrSezVONsn1RBixncyk2PcVWk7DlgxHo8iZwDyq5uAUD854dZhdIFYzKoQig2haUKi1lVufz2RZUZPZ41n/hrOQB6h0Hhg8I367FNoEHgeM/KY7szSeQwD8q2WE3HM35ZLl0K1MJiOtHIkBclRQUwZnyOWcNsRQQgVLj1PSqkjF9DsoOSaSg3iinKzvfmgsNFFfpP/2T3GLGvL4fHEfwIX1sVvXcPqLztehWGcfn9nI2U9nTfCgJPe/jFPLZwgVEzimBgAm0VIyK2tt1cE/AzQdLK+SxLSQ4aDCZnnId94OG2S1XwvnTbNk/ZnhyRCQT+sZM6z9g6LXL1BOBe+zJySiFkHAINCtnQokbCJ/apCv0foqPiZVfhpywAAAAASUVORK5CYII="},"74e7":function(t,e,n){var i=n("2118");t.exports=Array.isArray||function(t){return"Array"==i(t)}},"7abd":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXm9PXq+Pno9vfs+vttWKBGAAACPElEQVQozw3RQWrbQACF4TfCMjPqZgIj4RRaxsZKE0PuMBZ2cLKaCI9RDAXFmJJknUWWI1O1UlamOMHJSjGkuFn3AD2Cr9CepDrAg+/xIxK4QwIqHHQkUhQ/WuphInVIFBojl8QXc012Tgq4RTtVHWVLZVFh1tEoI91uiN4joCqde8Ukn/zGM1B2W4ari2PtTwyw55Ld+Wways54qhGPyS6FzbIT3lIY8WwWdCq56Yolx6KmSKzoqrsCB5heAp4TGNQWJ1Pc6XlE5jQD5OlIX9I47A9uiUQcPQxcury/ToyxWJG/za6ki88crxKPocKS59Sl3EtBG7C89fCGflpfqoSzCeC4crioJA7F0V5+8MaSIk4qSCdwzpogmbqzEirVpGiS2dOVJvUuuqFEmhHao06KEpq+8lvHI14NJk3Qrmi9vBuRLwAz0qZB4hsDXQFXgtnlpDX3C6ug9BquSw/CYtwAzuTz5vuQNdr/YibhR68378ehZH30FSpjh71LpQkrsj+Q062h5WwZ5wlRoD6uQJy1DqvSYuCUapMBqT5YA4ZFw4KlWapxoUGlKWrx0eDQvmigu4WMYt97ruru98fYL8/0lG6CTOFcFWBhFK5gKw19h2JN808nh7xhkU6sWKLXdtkqBL6h+lULK5k19wFB/FldnGYf3LDeuf6IC2/MzJOSOP0qPxLqzaGIqtBcFIItrstkazONOkrc1D1czjuwEGESB4JJnjgSMN7PXAu7fZQpl1C236C+9mM4Af8P98Ch4R2TRl8AAAAASUVORK5CYII="},"7f9c":function(t,e,n){"use strict";var i=n("61b6"),o=n.n(i);o.a},"802e":function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg=="},8157:function(t,e,n){"use strict";var i=n("588a"),o=n.n(i);o.a},"82da":function(t,e,n){},"8a1c":function(t,e,n){var i=n("a719"),o=n("2118"),r=n("90fb"),a=r("match");t.exports=function(t){var e;return i(t)&&(void 0!==(e=t[a])?!!e:"RegExp"==o(t))}},"96db":function(t,e,n){"use strict";var i=n("f62c").charAt,o=n("b702"),r=n("99ee"),a="String Iterator",s=o.set,c=o.getterFor(a);r(String,"String",(function(t){s(this,{type:a,string:String(t),index:0})}),(function(){var t,e=c(this),n=e.string,o=e.index;return o>=n.length?{value:void 0,done:!0}:(t=i(n,o),e.index+=t.length,{value:t,done:!1})}))},"9b11":function(t,e,n){var i=n("6d51");i("iterator")},a9ea:function(t,e,n){"use strict";var i=n("f1ce"),o=n.n(i);o.a},af86:function(t,e,n){var i=n("d890"),o=n("064b"),r=n("a133"),a=n("0fc1"),s=n("90fb"),c=s("iterator"),l=s("toStringTag"),u=r.values;for(var A in o){var f=i[A],g=f&&f.prototype;if(g){if(g[c]!==u)try{a(g,c,u)}catch(h){g[c]=u}if(g[l]||a(g,l,A),o[A])for(var d in r)if(g[d]!==r[d])try{a(g,d,r[d])}catch(h){g[d]=r[d]}}}},b165:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXe3t7a2trc3Nzg4OCXP9lCAAACoklEQVQozwXBzU/TYBwA4N+QEr4CNbSFwcFuowSqMRvEAwShHWAYNsu7dS0dLnGUSWT4kZB4lGzE4VtcwgIDJqcOWLJxcv4BOoQZuCPxSNSD4WSWLJGL8XmAIiyo2RgJ4A1pxQQlOxRAszLTdnPu2oQGb05RC5slJld7ZAIfo4O44Bn1ud59F0BcjnYOa17Jhwc6EdiKettncsXjT1f8KUBZUW41pK0Jc1Az4dEV3rkkPBtDSZ83Blyt0kSf2PRjzIykoBwINisPbPPtljdVE9iAXRfUPkXLVIgYrCccp5g687NdZbcJ+xa5VE/HhTtT23IKsN5jj/pcUd0dTZNAqCVw72n4gOwnTOC0vvHfaauT8d9zAoRRfPpISZRVyUiw8ELzOG1b2DZpFzkSrHLhq52twDEdyZHwvp2j4uv/bjvOf23/AcEtTuJbY5Cp4YcAer1IGkUzOo2rn8LQOKjFJw3NTw24nprQXY5aF4wxcqcSdbFQ00H4xFl8Drx4X4CikvAM1tuR8bKIBCBoLnKN10KJG4zKAsc7c9WEB9gnCi6BhVjqoco6t20ILAJuVctvaEZK732cRHDRmGfuihOam0o2CHByUZ/epCcVlRs2wmCnMqsd6aSim3ibBJtm1LGyXW3Bb7tJCPlFtUG+SvPdeEUAB60lNdo+VQbLcwRNVtT68FsLcr1+NotgNihlpExS1V2SFgNbeC8bEhgm8sM17wSi6Us2gxVWJU/5GKBpandvfyYbU1yHCLpCgWGbbPXn40rehEsUXKIJr9DMKgICfjc4bl1YfvUhE/YIECGRqjCxSM9hrybAIkND5OeWfFZsXkxB+qDzb7pUQ3EfQ3Ml6EChEt3D+iS01VqC7EQ/Z/DuPQcz4yChoFQJce2Qr+NNAv0HxofmpXGqgHkAAAAASUVORK5CYII="},b2a2:function(t,e,n){"use strict";n("e35a");var i=n("1944"),o=n("efe2"),r=n("90fb"),a=n("5139"),s=n("0fc1"),c=r("species"),l=!o((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")})),u=function(){return"$0"==="a".replace(/./,"$0")}(),A=r("replace"),f=function(){return!!/./[A]&&""===/./[A]("a","$0")}(),g=!o((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));t.exports=function(t,e,n,A){var d=r(t),h=!o((function(){var e={};return e[d]=function(){return 7},7!=""[t](e)})),p=h&&!o((function(){var e=!1,n=/a/;return"split"===t&&(n={},n.constructor={},n.constructor[c]=function(){return n},n.flags="",n[d]=/./[d]),n.exec=function(){return e=!0,null},n[d](""),!e}));if(!h||!p||"replace"===t&&(!l||!u||f)||"split"===t&&!g){var m=/./[d],v=n(d,""[t],(function(t,e,n,i,o){return e.exec===a?h&&!o?{done:!0,value:m.call(e,n,i)}:{done:!0,value:t.call(n,e,i)}:{done:!1}}),{REPLACE_KEEPS_$0:u,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:f}),b=v[0],C=v[1];i(String.prototype,t,b),i(RegExp.prototype,d,2==e?function(t,e){return C.call(t,this,e)}:function(t){return C.call(t,this)})}A&&s(RegExp.prototype[d],"sham",!0)}},b671:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX48dr48Nf58tv379X17NJtIBxUAAACFUlEQVQ4y1XRUZakMAgF0Af2AiDWApDZgHZqAV1nZv9rGh7Rj7Y8McUFEg1wvcMESMNVD/neU8Xcaz7nYYkYlYO6Ti82PBI4BvIEg1aj3wKwRvIMgZsUy5LdhCawPFh1sZs4SrlyN9fQKpv8s5dgZ2eLyqqJiu+WkCmUEybXkm3INS01WAiv0PapJ0CZc0SJQUzcWnZYbOOY20iFD8Bk+/j2A3wNxH7GdShFYS5ff237kXh9I9zSkQmIAhOsOSVfJ6DIXTMDaPnzkRJ92S1BQQmXl5LdirgRLLDdcYqcGPwe3QN4xCBiGNbrqq9wpW1XCecChwaQdVOsRDpPCpeoolPdxeXp3WNB9PHVzWBHlygy4NJCCrFHREv6bDt0VGwJZASkpONmm1UseGeFKAQexgaAkrfYWl3AGxWOLL2AIMBNbCXpktmS3k3vHeYjGCPBa43wJTurO3ZFVpQSJdAZGLoHTyk1upkjxMEaIxum3iIARcCa5kSkFAW5fi1mUlL9eyOsaanFmOMruwvEdE3ZYzsRSzo5ewRLXyVPPEvknt8ij4DvCg2O7xOgBCUprEzV4z1WekSpUgI8DT2mrnSOXKRfQavwuKA1F+tFnMKdJSUpMA7wQAifWRkMgjUKKZE4lBl6MCM4B1pq1P4uIjDE6Pq6rL0FnW1nIFmta5vrSvq/Ch4tpqG/ZNyyWa5jZPktq81eYv8Bt5s4iFITOp4AAAAASUVORK5CYII="},b891:function(t,e,n){},c051:function(t,e,n){var i=n("da10"),o=n("b338").f,r={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[],s=function(t){try{return o(t)}catch(e){return a.slice()}};t.exports.f=function(t){return a&&"[object Window]"==r.call(t)?s(t):o(i(t))}},c27e:function(t,e,n){"use strict";var i=n("1ce1"),o=n.n(i);o.a},c84b:function(t,e,n){"use strict";n.r(e);var i=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"detail-wrapper"},[n("div",{staticClass:"detail"},[n("div",{staticClass:"bar"},[n("el-breadcrumb",{attrs:{separator:"/"}},[n("el-breadcrumb-item",{staticClass:"index",attrs:{to:{path:"/"}}},[t._v("书架")]),n("el-breadcrumb-item",{staticClass:"sub"},[t._v("目录")])],1)],1),n("el-divider"),n("div",{staticClass:"catalog"},t._l(this.$store.state.catalog,(function(e){return n("div",{key:e.index,staticClass:"note",on:{click:function(n){return t.toChapter(e.url,e.title,e.index)}}},[t._v(" "+t._s(e.title)+" ")])})),0)],1)])},o=[],r=n("82ae"),a=n.n(r),s={data:function(){return{key:"value"}},mounted:function(){var t=this;a.a.get("/getChapterList?url="+encodeURIComponent(sessionStorage.getItem("bookUrl"))).then((function(e){t.$store.commit("setCatalog",e.data.data),sessionStorage.setItem("catalog",JSON.stringify(e.data.data))}),(function(t){throw t}))},methods:{toChapter:function(t,e,n){sessionStorage.setItem("chapterID",n),this.$router.push({path:"/chapter"})}}},c=s,l=(n("c27e"),n("9ca4")),u=Object(l["a"])(c,i,o,!1,null,"6d476c76",null);e["default"]=u.exports},cf68:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEXm5ubo6Ojp6enr6+vt7e1FnZagAAACrklEQVQ4yx1SixUbMQgT3AKAFwDcAfzpBN1/qMrJS5w7bCQhC6IGSUGYQJd6Ox9ZPXi1AGJBavhUTT0JjYPGAab9WcDYIxsmlnxkayX8mhxCmKHA75az5cfRbWybEExiu08xDSgGym0mwuf3j4SvHeQxDJJzh2zp4iOlrD8iOb4SXyC1wiOLRTcnrje+nGamFeXVKWkmzbFIPChkmJ6Fg7mBpV8n+JGOVCd4jv1thThkjeQGNeafpeV3rsEWLfyWc8tC9jOv6FQ8rRzHOOVB+jCYEUAJpDvh8xHNFm/Tm5p5lw94Pp3NhtKEfQsGvnXhowdZE73hPwxKvjDd4i4PCdd0fe3W5fO8ktAsUAacLgstpUw60JCiPLg2XpkgiqPIYYXJd9ksGIT3q+LlevypzItvO+s0F1dBzVr2QDMUkYmuyGcrIS44mVJ7JVKwQXjYuBYp0Uetecbswzsikzu3gUR8bJC/C8Gd/NAzI/xdUGOYQQHDZ8X2d5XuzGRUiXAi9si5CRgoiToRZPtzLJkd0FUHRHZwJf0BHT1sE7gcnh0jmKKlSSF4/GBirGk5+K9NKlGDCfc9JtPhg78JdabH0YQRKNZnJ8tFnPfXHJb4xum1TTCeEmyEdbyEJLjznMLHuFD2Y9NEkSleIBs7SiCbblhgctVi9ch++kDYnn1C9DA5TvdPsToXM55wI6k+8eKT1blwPTqWb5CFJ+7dTBmab+KHy+xwNtItXhZNSpHD2fxnynrxG3ZBKRe8KBpXk11AnadlccEhr9w1nBBvBylNkv7A8eqpGBCDqhitmWQXBjjdS6idr/QjXWLDeMzMbVDoJuM8zN7WenMZWXgZ2vX3F01J3jHZbwk1LRP+DWEvDJtOUoh/AIaBUz5VpWyhuyx4QtgL/NmgC6kM/JvNe+R/C/5aL7BKIbYAAAAASUVORK5CYII="},d0e3:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXh7eHl8eXj7+Pn8+eTbH1KAAACPElEQVQozxWPQWrbQABF/0xn3JFKQRTZOIuUsbCCbOgdRoYEOauxkYPcTRyTlPQWIxEltrsRwQ6hK9nEQek6F+gNTE/Q3qLLusv34cN7SH3mFicdYW4gNIhJWXPBRVXzjcFD0IqeU4o4PRbAIVjyico0vJpIifqPfL80QN9DAQY5ucRHE/hpHxBldXe9GilaHKcKMlj6pho2zXgkNdBl0oJ8kiF1DSiJF1ZHBJkQr0Dbux/5I42Zp4cFahJDFGeW6/QjBwmFY/Q7vZ2SnoOdW2parv/Cnm81+m0xrEfiVXQ3W4nOXIqVYi3l6AAQBwMFkViVBANMto4enXHPNTkHBB0oVj4r5vHzCWayrgBvxtygDlDB2CNDjd80ZInY69aKVYZcfJ8DW+fWuc+syEODALx+ojqoafHsthTI+ZW27PGpIeo/cR6YKcbqIuIFhHmBrzAovzIOOJk1ucvcDzrMRYGVBH2yvcAOf0KiKwfRovBI3tm/kW1eemtfNWwIIXE2mJNhvoszfmMBfRCv0OPwd2321uDW3nx2q/BDxFVeoN1g7a6Im8yRnoawa8kbdXnU0cHeTMxKfZGlJgvLb3sKsxgglQnDdAfvj9LUnqWRDo0GiUmPwyU7TAsD7wHeIW3Nfy1qVGKoE9NgJCdYCAexNRob9yCn4DAQmXtQuUtera6bEmTTXhZy6h856xi4mnEl6BI9mfISkLbtJyZIMJIAUd5ZOBEu88KRAk71yxfItj/hpIB0Errv4gO1os4/UICf+o3kkqwAAAAASUVORK5CYII="},d7e1:function(t,e,n){"use strict";var i=n("efe2");t.exports=function(t,e){var n=[][t];return!!n&&i((function(){n.call(null,e||function(){throw 1},1)}))}},df5e:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAATlBMVEXdzaHh0KPgz6LdzKDezqLczJ7ezZ/fz6Dcy5zi0aXdzZ3fz6Tfz57h0KDg0aLcyZrg0KXi0qPfzZ3j06bh0qbdyJbfzJrhz5/cxpLZwo0vDconAAAFn0lEQVRIxxyPW5LjMAwDAT5FybLl2JnM3P+i6+wXWVC1GoQGaD0h4XM3Q5o4T0HgABHBi6pZ4CDXXcUOFd6VhqC3Kch4EI8w9oMXwvU6m5LOOvcxKMOhuu8i5+5cMjcgb0t4F2uvOoeI3/MlT4IqsbtM9UG2AGSXUOsxzPevnXzK1CSHytZLvx7VdQmUcJsJCxJh2nmHW12Qod1qPjt8pih47uQ9aGpoNWF+yElCt60oH7vdIU/MnlRPSBLC/VwqxcKR8PFqnADN9ih5ufqnTlG9KwCofvs7kKYqOPHTNMQ93j9qNImFw9vjHPZ0F1m8hUUVB/Q/TrRYDMXr9++APMFARAt6sPh6wVAXzxUGhZsFUwCNfPZ8/72TAHebAhvuOuT3gO1Vn5d9Jd5sBRkg0p2seL9B7ulkjFJFIt9HPpLzdSzzMP3UcodAfMqC6pBuET2heHK1itZf1GZ1bi0BwOSxiCS8f/JBHMPMM4XCu3Mt1uz9lJbDJRqsKDZuikzkvskQEz6hanfDfO494azY5JpqPqOF1RhxD9XYEdaNxiqWqakKgmPfmrsta8KAiwF4HBxGVUJAgeSqQaiRRZJ7D2jedhw5t1CIAKxag0CBA60BpoBE6DcUi8O5AuM4pLfN0kHLmeu2B4e6HofqbgxsTWUw3PAODqa1oDtyzgXBlusi1KFdclMPE8O3jvLJ8RNi5/RxDQVzVmXA233XQ4KummunfxvLOZo+iH37964YjP06995CTdu9hsvErqJNzmf4wTrZ5DL7+qW9EoLnadrx67b8dUtrJnBXaT1N1uvPaYRKpWkq52xNsMN7vv4Sdryt/f4MhQoMCKnvVxikai1CQ6ZsnwJDc8+3Y/z8HcfvYQNq66pnAu1Hwa+3KNSwbNu8h3nDPqTl9fl7tx8fBhFfdS0o0F3JUKEZtZG9b/LZEM95lzaR30OnWPzroMxyZYdBIMoMnpN0J+m7/40+/P4soFSUjgzE7yY5zrMJuoZv0CmpVguYx1pprfb5HOviRVhHUVi/352shxCYrYBZxGtVaxiAz/MsaGSIsB7R1t4zJXH//n7RTTQQwxqcGEqEvklFHUgiO2GvJV+jAIPR+N29usWDoiSOVrN3XuqT1egQJAAU9EwslVJC8u0rGcy+WPqktJhjfMpatIG6CDAb0v5H34MGKqiVRue7GGLZ9Otxtt4JIrAhxBDwDuqI9JavcO0A7GlqFt219tH/bln9jBXzaKWAEqJV0CBxs5TwM8EvUPHaa8S86vN303MVWOsl3goDBHPWSoQ9c0kQmCKljfsKNH1+ofEOHW8a9a7glZGS8fPieL/SRSs0LAhI4FDTnXs1QYtubv2+IXPZpHB4bhivRexBkYKsSrYXNjvMUbVXpVJ+N6haV72c1k2zrnv5IYBMJBYTSZx0KTkoM3vY93rU/qs7zHplc/3d2ACadhFWByrn9LUk2IWb5JywvawTQc3F0iz+lgsBmInAIemBJtft2plKIlAFOgcroigrG2XlDsAzywQECNyaI8yr2ogoh7D4qJOYmZBzQgoZAM1PAcB8sDrr1uE5CDMR+nWSSVUGUCHAs8Vd21HOE0FzNj37pX0sLp9p3K8k++xxpkmzDxK64rmTSJnDUuIgTeslui6lg92jonZXI4jqNiUuzN4IagcKMjCniMGCODoo8T4tGDprn2hRww+NrnYiCwokd9iiWrkmbRfXYGLAoZrjO1lVQKExjUy5fIkgJURmz2uGFdASwwlWx5gDVTMK7hP6ISRVsFbYNmqtZL9MQtio285PaekyzDhZmtdexCYB0SZcTmBdhvdbmAEonk8hwcHQuZN1kVqrhyKoHHsnQhQAjF7SG533Da2S4LGjx1LoZqp7XeKQLDUBmYmydG0NQHpMeR5lRIRQc1PQ2ASMQflF4YBDMt0/GFlEHeRwCcEAAAAASUVORK5CYII="},e160:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEX6+fP8+/X+/ff///kbczPAAAACeElEQVQozxXHQUgUUQAG4P8936yzs6VvZNZmN9QxVxiF9OLBoOjtOC6rQq6ygXjI2fCQBdXBg4egtzFGdqkoI+zgBFbqkm3hQSxhFYLotOcubeKhOnVYoqQy+m4f5g5TvpX0xHLbLY9j8SMhJp+Jk4LfAUS2kVRIjILmnwGBTX42PhCVlDJQkIiy2nWAvaJ1h+oFIpJ0hMSYVbyyrgDWshcMpMyL1brPDQKWmduO+KTJ6XeXAMK9Yc3FpD7atyNwg6kt5XgFpLPhjUTFSYVn2abDiugGShwD8JTVRJVo/2ecuKtRb/qc4BK+9TboFfokog4T2Fn6Oqdnsjk90NMS76Rji6E0NmwkPBAZ4Xbkw8KoDAkAbEhkc78e9omxxgxg6qa5HvMv+UZbCV0qmHnSHKl5TxeA2XTCGWekR581mwC5crBH81PznASqB9va3TbkYAjJPLfg5uBfXaJgIgIBv9eessRIhxe7PA7kj6uUMeMaQ/OEQOYRaaHlqH2Gxwsl6E/pwVY5FH7uCypBZPKvDQyVziYBrAkMURe2MOOOxG/eQpp5PF+bFzUV5HtPj9GeiVSNZDELleifYTp9NAjsoiXg4cW+4ZORkdSMB/B74aAdjhsVakhgkugsbmqcDSLEoWp8zRjrux3tli6Q5uM3E+maT99Wy0RiP7tboiuRZle2c6CYeL2kcUc1KvPtQKucogMadKVTQOJYCeyCYlhQQ/Q7Etfd/vBygy9iqy+LyHeF46saCYvW6ingsbA9RBWtdi8GgUXW+oQx9/wP6bAAX1TWeV+CbShZDlQ9xT6SoSxZmKRAkmXb60kzEzkRF+Ccb94BGspGJoN/UzmyR4wjXHAAAAAASUVORK5CYII="},e35a:function(t,e,n){"use strict";var i=n("1c8b"),o=n("5139");i({target:"RegExp",proto:!0,forced:/./.exec!==o},{exec:o})},ec0f:function(t,e){t.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAALVBMVEXx58b168ny6Mjz6sn06sf27Mvw5sTz6cbw5cLy58T37svv47/168v37s7t4Ltrv0//AAAEjUlEQVQ4yw2Ty2sTURxGf3dmOqmPxb0zmaStCnfmZpL6gpmbxIpUSMZGrSJkxsZiVZimNVaqMklrUnWTRq2KIDFWWx+IFrIRFxXEB4KIgqu6EBdu7M6FIPg32PW3+DhwDmBaYrK56KP4HGIsvg/uvOV0wK+qgBMlO9BujuH4DSJlOseqV5a/BEF97gt0ChyIPqBhXI9BtqtIB8vJB/LdCQ3OVjaLNX0g7+OmoI4e7nkemAqX6o8vg0yyQAyQS7IfgvFbI+6QyI3R4KELxw7kwM2ooQfyQigYnwY5MZbMlHI1DvnQVCoVcrt+R+bO7vPDif3ybNajwqAAe443dpfDsPt379VMWZzGRuqM79mQF+DUz9nt74bQ8J/O80MtVR51U02JKKmTCvTzLVf+vuxP/aHnPo9+2bW+zVsJ0Y630/CrfzX+b+UL+7O68Rczv+7lrMh5etfKXvhc2rk6KforxuoO2xB2tcxKfeXHt18rHOiHI/0RRjW/YGRDkHiwo3nzqL60o58C/bgRuaj7vk+QOwOhpnFNdjuWpKMCGP8Yapu9Ty5FTHKQLGSEFikjd9ADwP9ciaNNjc5qMH6w50AF/LKOsOYqsOG9GjKgc7ZXolqntm6fysJ6Ma6ll2CiqmOgE6O7x1wXExklbeqMYcwsmJmOoigt8SBg2WfilDSsAZJcBxDcrqtBXzFQJqZNHfscyIhoZlygAtyYAceah+elrFbI+46gEHDGiW878Kj7JpWyfhg6iyRMymV1MKBSeVpfgLHIohyTojI6sRyK1VpcqzVZeEBLOnA9unhGKUXPJDYtV9Dxuz4iA5xSkSWhCJdAiJR9PHlvfvbntbrR14FDqUNRAYDJmSnv3oKxuz5+7fiblgVJyYLTbgUM05P7LESkoXvyWNfb0aUU6FZizgQIa25VqKQZqFrk6v6BsqqIHlQmkQ9KrBhkC20/DrFsAFEEYLjM+lj2wYHXCwnNvZQR42XJ2iVK+UBXnI+OBE6oXpUUHiQ1yg0MhA03iwGbnOdQYc1CMiPIPQrCQJFH4L4BMFktAtKd9PN5gnU2Gra4KuK+V+mjtBRpAGIqDVe4wnSnajiFGO5d7smvhVQEMEYwqshrENIEaY7YeblJYtsb3QhAHWZCEKK67swwPMKw0If1Ta+6DgHmlgPzcUTSbi3rrv1Y64/BYEMPQ5SDHUOR022B4QRF6xLUPAaPX/V4IDI5N2BMwx4LqO1uO4j6uW7NvM7lATqGAxY/ZHVgoGZbu7SvkNR75x6qGSB23FdouENVwN7sCbewTdsXGrrnQ5ZZKOCOFtMTIzxlPu6eYmtL+nMFmoK7OeXajn86r9sqWbfmvHC4IagE5qfCPGZvLSq5F55hHIxJFa4/vRxHBlz0og4TojU1l/MOHJX17lybdF0mQhFO44JYUNt3UA473IXw/iPfDWtKG5oFSXIF5iU/VnyDSjxxeDk3jAXRyVyGTNB9FxH9qcFDNJpVbt2y9LytUXkK7Py6+z1RezHQqnoY8XcLimmd8dCnBhQCuaGpJCq3SoIlmYvLz8UkWhJw7T8k+Db/DYEKwgAAAABJRU5ErkJggg=="},ecb4:function(t,e,n){"use strict";var i=n("1c8b"),o=n("45af").indexOf,r=n("d7e1"),a=n("ff9c"),s=[].indexOf,c=!!s&&1/[1].indexOf(1,-0)<0,l=r("indexOf"),u=a("indexOf",{ACCESSORS:!0,1:0});i({target:"Array",proto:!0,forced:c||!l||!u},{indexOf:function(t){return c?s.apply(this,arguments)||0:o(this,t,arguments.length>1?arguments[1]:void 0)}})},f1ce:function(t,e,n){},f3dd:function(t,e,n){"use strict";var i=n("1c8b"),o=n("d890"),r=n("6d7a"),a=n("9b9d"),s=n("1e2c"),c=n("c54b"),l=n("74cb"),u=n("efe2"),A=n("faa8"),f=n("74e7"),g=n("a719"),d=n("857c"),h=n("3553"),p=n("da10"),m=n("9f67"),v=n("38b9"),b=n("6d60"),C=n("cbab"),y=n("b338"),S=n("c051"),x=n("0a60"),B=n("aa6b"),I=n("d910"),E=n("ef71"),k=n("0fc1"),w=n("1944"),O=n("6d28"),U=n("7db2"),D=n("d5a8"),Q=n("7e8b"),V=n("90fb"),R=n("4350"),P=n("6d51"),M=n("27b5"),F=n("b702"),L=n("5dfd").forEach,N=U("hidden"),T="Symbol",K="prototype",H=V("toPrimitive"),J=F.set,z=F.getterFor(T),W=Object[K],G=o.Symbol,q=r("JSON","stringify"),Y=B.f,Z=I.f,j=S.f,X=E.f,$=O("symbols"),_=O("op-symbols"),tt=O("string-to-symbol-registry"),et=O("symbol-to-string-registry"),nt=O("wks"),it=o.QObject,ot=!it||!it[K]||!it[K].findChild,rt=s&&u((function(){return 7!=b(Z({},"a",{get:function(){return Z(this,"a",{value:7}).a}})).a}))?function(t,e,n){var i=Y(W,e);i&&delete W[e],Z(t,e,n),i&&t!==W&&Z(W,e,i)}:Z,at=function(t,e){var n=$[t]=b(G[K]);return J(n,{type:T,tag:t,description:e}),s||(n.description=e),n},st=l?function(t){return"symbol"==typeof t}:function(t){return Object(t)instanceof G},ct=function(t,e,n){t===W&&ct(_,e,n),d(t);var i=m(e,!0);return d(n),A($,i)?(n.enumerable?(A(t,N)&&t[N][i]&&(t[N][i]=!1),n=b(n,{enumerable:v(0,!1)})):(A(t,N)||Z(t,N,v(1,{})),t[N][i]=!0),rt(t,i,n)):Z(t,i,n)},lt=function(t,e){d(t);var n=p(e),i=C(n).concat(dt(n));return L(i,(function(e){s&&!At.call(n,e)||ct(t,e,n[e])})),t},ut=function(t,e){return void 0===e?b(t):lt(b(t),e)},At=function(t){var e=m(t,!0),n=X.call(this,e);return!(this===W&&A($,e)&&!A(_,e))&&(!(n||!A(this,e)||!A($,e)||A(this,N)&&this[N][e])||n)},ft=function(t,e){var n=p(t),i=m(e,!0);if(n!==W||!A($,i)||A(_,i)){var o=Y(n,i);return!o||!A($,i)||A(n,N)&&n[N][i]||(o.enumerable=!0),o}},gt=function(t){var e=j(p(t)),n=[];return L(e,(function(t){A($,t)||A(D,t)||n.push(t)})),n},dt=function(t){var e=t===W,n=j(e?_:p(t)),i=[];return L(n,(function(t){!A($,t)||e&&!A(W,t)||i.push($[t])})),i};if(c||(G=function(){if(this instanceof G)throw TypeError("Symbol is not a constructor");var t=arguments.length&&void 0!==arguments[0]?String(arguments[0]):void 0,e=Q(t),n=function(t){this===W&&n.call(_,t),A(this,N)&&A(this[N],e)&&(this[N][e]=!1),rt(this,e,v(1,t))};return s&&ot&&rt(W,e,{configurable:!0,set:n}),at(e,t)},w(G[K],"toString",(function(){return z(this).tag})),w(G,"withoutSetter",(function(t){return at(Q(t),t)})),E.f=At,I.f=ct,B.f=ft,y.f=S.f=gt,x.f=dt,R.f=function(t){return at(V(t),t)},s&&(Z(G[K],"description",{configurable:!0,get:function(){return z(this).description}}),a||w(W,"propertyIsEnumerable",At,{unsafe:!0}))),i({global:!0,wrap:!0,forced:!c,sham:!c},{Symbol:G}),L(C(nt),(function(t){P(t)})),i({target:T,stat:!0,forced:!c},{for:function(t){var e=String(t);if(A(tt,e))return tt[e];var n=G(e);return tt[e]=n,et[n]=e,n},keyFor:function(t){if(!st(t))throw TypeError(t+" is not a symbol");if(A(et,t))return et[t]},useSetter:function(){ot=!0},useSimple:function(){ot=!1}}),i({target:"Object",stat:!0,forced:!c,sham:!s},{create:ut,defineProperty:ct,defineProperties:lt,getOwnPropertyDescriptor:ft}),i({target:"Object",stat:!0,forced:!c},{getOwnPropertyNames:gt,getOwnPropertySymbols:dt}),i({target:"Object",stat:!0,forced:u((function(){x.f(1)}))},{getOwnPropertySymbols:function(t){return x.f(h(t))}}),q){var ht=!c||u((function(){var t=G();return"[null]"!=q([t])||"{}"!=q({a:t})||"{}"!=q(Object(t))}));i({target:"JSON",stat:!0,forced:ht},{stringify:function(t,e,n){var i,o=[t],r=1;while(arguments.length>r)o.push(arguments[r++]);if(i=e,(g(e)||void 0!==t)&&!st(t))return f(e)||(e=function(t,e){if("function"==typeof i&&(e=i.call(this,t,e)),!st(e))return e}),o[1]=e,q.apply(null,o)}})}G[K][H]||k(G[K],H,G[K].valueOf),M(G,T),D[N]=!0},f40b:function(t,e,n){"use strict";var i=n("b891"),o=n.n(i);o.a},f62c:function(t,e,n){var i=n("3da3"),o=n("2732"),r=function(t){return function(e,n){var r,a,s=String(o(e)),c=i(n),l=s.length;return c<0||c>=l?t?"":void 0:(r=s.charCodeAt(c),r<55296||r>56319||c+1===l||(a=s.charCodeAt(c+1))<56320||a>57343?t?s.charAt(c):r:t?s.slice(c,c+2):a-56320+(r-55296<<10)+65536)}};t.exports={codeAt:r(!1),charAt:r(!0)}},ff9c:function(t,e,n){var i=n("1e2c"),o=n("efe2"),r=n("faa8"),a=Object.defineProperty,s={},c=function(t){throw t};t.exports=function(t,e){if(r(s,t))return s[t];e||(e={});var n=[][t],l=!!r(e,"ACCESSORS")&&e.ACCESSORS,u=r(e,0)?e[0]:c,A=r(e,1)?e[1]:void 0;return s[t]=!!n&&!o((function(){if(l&&!i)return!0;var t={length:-1};l?a(t,1,{enumerable:!0,get:c}):t[1]=1,n.call(t,u,A)}))}}}]); \ No newline at end of file diff --git a/app/src/main/assets/web/new/precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js b/app/src/main/assets/web/new/precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js deleted file mode 100644 index 459edb5e1..000000000 --- a/app/src/main/assets/web/new/precache-manifest.9ae0b839acd886dbe2adc2f9d92aeabf.js +++ /dev/null @@ -1,74 +0,0 @@ -self.__precacheManifest = (self.__precacheManifest || []).concat([ - { - "revision": "80a8de284bb3fa9a4a9f", - "url": "css/about.8c965d87.css" - }, - { - "revision": "9da3e990110565bfa57c", - "url": "css/app.e1c0d2e4.css" - }, - { - "revision": "c900d6091039998c94b9", - "url": "css/chunk-vendors.ad4ff18f.css" - }, - { - "revision": "607ffe83acdcd9c9180e", - "url": "css/detail.fb767a87.css" - }, - { - "revision": "535877f50039c0cb49a6196a5b7517cd", - "url": "fonts/element-icons.535877f5.woff" - }, - { - "revision": "732389ded34cb9c52dd88271f1345af9", - "url": "fonts/element-icons.732389de.ttf" - }, - { - "revision": "f9a3fb0e145017e166dd4d91d9280cc4", - "url": "fonts/iconfont.f9a3fb0e.woff" - }, - { - "revision": "f39ecc1a1d2a1eff3aca8aadd818bb61", - "url": "fonts/popfont.f39ecc1a.ttf" - }, - { - "revision": "6c094b6d4ae9404dbed273c41b06fae8", - "url": "fonts/shelffont.6c094b6d.ttf" - }, - { - "revision": "b5c48bc1e1fe73212a31be704875b71f", - "url": "img/noCover.b5c48bc1.jpeg" - }, - { - "revision": "1006935c8b91408961a7012a08445ffd", - "url": "index.html" - }, - { - "revision": "80a8de284bb3fa9a4a9f", - "url": "js/about.a0534951.js" - }, - { - "revision": "8e5e793e10c338503af6", - "url": "js/about~detail.47586100.js" - }, - { - "revision": "9da3e990110565bfa57c", - "url": "js/app.a7aae935.js" - }, - { - "revision": "c900d6091039998c94b9", - "url": "js/chunk-vendors.c98251cd.js" - }, - { - "revision": "607ffe83acdcd9c9180e", - "url": "js/detail.ff471d08.js" - }, - { - "revision": "b46d04eb43bc31ca0f9f95121646440d", - "url": "manifest.json" - }, - { - "revision": "735ab4f94fbcd57074377afca324c813", - "url": "robots.txt" - } -]); \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/css/stylewf.css b/app/src/main/assets/web/uploadBook/css/stylewf.css new file mode 100644 index 000000000..ff8d10457 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/css/stylewf.css @@ -0,0 +1,49 @@ +@charset "utf-8"; +/* CSS Document */ +body, div, p, ul, h1, h2, h3, h4, h5,span , img,dl,dt,dd{ margin: 0; padding: 0; } +html, body { height: 100%;} +body { font-family: Helvetica,'微软雅黑';background:#03a9f4 + /*url(../i/bg.jpg)*/; font-size: 14px; color: #313131; } +h1, h2, h3, h4, h5 {margin:0;padding:0;} +em { font-style: normal; } +ul, ol, li { list-style: none } +img { border: 0 none } +a { text-decoration: none; color: #858585; } +.fl{float:left;} +.fr{float:right;} +/*layut css*/ +.wrap{width:860px; margin:0 auto; overflow:hidden;zoom:1;} +.right{width:527px; float:right; background:#eeeeed; overflow:hidden;} +.title{height:50px; background:#8bc34a; line-height:3.0em;color:#fff;font-size:16px;padding-left:25px;} +.jdt{ background:#bebebe; height:3px; position:relative;} +.jdt p{ position:absolute;left:0;top:0; height:3px; } +.jdt p.orange{background:#8bc34a;} +.jdt p.gray{background:#999;} + +.first{background:#8bc34a;} +.right dl{ overflow:hidden;zoom:1;} +.grybg{background:#eaeae9;} +.first dt,.first dd{ background:url(../i/wifi_d_bg.png) no-repeat right top;_background:none;_filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src="wifi_d_bg.png")} +.right dt,.first dt{width:255px;padding-left:25px;} +.right dd,.first dd{width:110px; text-align:center;} +.right dt,.right dd{color:#555;float:left; line-height:2.8em;font-size:16px;} +.right dd.orange{color:#8bc34a; cursor:pointer} +.right dd.gray{color:#aaa;} + +.scroll{height:900px; overflow-y:scroll;padding-bottom:10px;position:relative;z-index:9999;} +.left{margin-right:527px;} +.logo{height:112px; background:url(../i/wifi_logo_3.png) no-repeat left 24px;margin-bottom:36px;} +.wf_box{height:204px;margin-bottom:46px; overflow:hidden;} +.wf_box h3{padding:124px 0 0 0;color:#fff;width:200px; height:76px; margin-left:55px; text-align:center;} +.wf_wifi{ background:url(../i/wifi_img.png) no-repeat 47px top;} +.wf_active,.wf_normal{background:url(../i/wifi_tz0.png) no-repeat;} +.wf_active{background-position:56px -210px;} +.wf_normal{background-position:56px top;} +.wf_btn{width:203px;height:100px;padding-left:37px;margin-bottom:16px;} +.wf_left{width:6px; height:100px; background:url(../i/wifi_left.png) no-repeat left top; float:left;} +.wf_right{width:6px; height:100px; background:url(../i/wifi_right.png) no-repeat left top; float:left;} +.wf_center{width:228px; float:left; background:#8bc34a; text-align:center;color:#fff;height:100px;} +.wf_center h3{font-size:24px; line-height:1.6em;margin-top:22px;} +.wf_center p{font-size:14px; line-height:1.6em;} +.wf_gs{color:#fff; line-height:1.6em;padding-left:37px;} +.right dd.wf_bgn{ background:none;} \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/i/wifi_logo_3.png b/app/src/main/assets/web/uploadBook/i/wifi_logo_3.png new file mode 100644 index 000000000..618c4873a Binary files /dev/null and b/app/src/main/assets/web/uploadBook/i/wifi_logo_3.png differ diff --git a/app/src/main/assets/web/uploadBook/i/wifi_logo_3.svg b/app/src/main/assets/web/uploadBook/i/wifi_logo_3.svg new file mode 100644 index 000000000..7361683b0 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/i/wifi_logo_3.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/assets/web/uploadBook/i/wifi_tz0.png b/app/src/main/assets/web/uploadBook/i/wifi_tz0.png new file mode 100644 index 000000000..85aa8a7e4 Binary files /dev/null and b/app/src/main/assets/web/uploadBook/i/wifi_tz0.png differ diff --git a/app/src/main/assets/web/uploadBook/index.html b/app/src/main/assets/web/uploadBook/index.html new file mode 100644 index 000000000..a3e99428f --- /dev/null +++ b/app/src/main/assets/web/uploadBook/index.html @@ -0,0 +1,43 @@ + + + + + + + + + +
+
+

+
+
+
+
+
+ +
+
    +
    +
    +
    + +
    +

    +
    +
    +
    +
    +

    +
    +
    + + + + + + + + + diff --git a/app/src/main/assets/web/uploadBook/js/common.js b/app/src/main/assets/web/uploadBook/js/common.js new file mode 100644 index 000000000..b7735b331 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/common.js @@ -0,0 +1,256 @@ +/** + * 公共函数 + */ + +//全局的配置文件 +var config = { + fileTypes: "txt|epub|umd", //允许上传的文件格式 "txt|epub" // |doc|docx|wps|xls|xlsx|et|ppt|pptx|dps + //url : "http://"+location.host+"?action=addBook",//"http://localhost/t/post.php",// + url: "../addLocalBook", + fileLimitSize: 500 * 1024 * 1024 + +} + +var file = { + "inQueue": [], //已经在队列里面的文件,包括 HTML5上传和 Flash上传的 + "clientHaveFiles": [] // 客户端已经存在的文件列表 +} + +/** + * HTML5 和 flash 公用,所有文件对象集合 + * @var array + */ +var filesUpload = []; // + +$.ajax({ + url: "http://" + location.host + '?action=getBooksList&t=' + (+new Date()),//"http://localhost/t/t.php",// + async: false,//同步获取数据 + dataType: "json", + success: function (data) { + + try { + var dataLen = data.length; + if (dataLen > 0) { + for (var i = 0; i < dataLen; i++) { + filesUpload.push(data[i]); + } + } + } catch (e) { } + + } +}) + +//统计文件大小 +function countFileSize(fileSize) { + var KB = 1024; + var MB = 1024 * 1024; + if (KB >= fileSize) { + return fileSize + "B"; + } else if (MB >= fileSize) { + return (fileSize / KB).toFixed(2) + "KB"; + } else { + return (fileSize / MB).toFixed(2) + "MB"; + } +} + +//如果文件太长进行截取 +function substr_string(name) { + var maxLen = 15; + var len = name.length; + if (len < 17) return name; + + var lastIndex = name.lastIndexOf("."); + var suffix = name.substr(lastIndex); + var pre = name.substr(0, lastIndex); + var preLen = pre.length; + var preStart = preLen - 10; + //前面10个 + 后面5个 + var fileName = pre.substr(0, 10) + "...." + pre.substr(preStart > 4 ? -4 : -preStart, 4) + suffix; + return fileName +} + + +function checkFile(file) { + + if (file.size > config.fileLimitSize) { + return jsonLang.t11; + } + + if (!file.name || !file.name.toLowerCase().match('(' + config.fileTypes + ')$')) { + return jsonLang.t12; + } + + var len = filesUpload.length; + for (var i = 0; i < len; i++) { + if (filesUpload[i].name == file.name) { + return jsonLang.t13; + } + } + return null; +} + +/** + * 添加文件时,回调的函数 + * @param object file 文件对象 + * @param int type 0 是swf 上传的,1 是html5上传的 + */ +function fileQueued(file, type) { + var size = 0, fid = file.id, name = ""; + type = type || 0; + + if (file != undefined) { + //计算文件大小 单位MB + size = countFileSize(file.size); + name = substr_string(file.name) + //创建要插入的元素 + // "

    "+name+"

    "+ + // "
    "+ + // "
    "+ + // "
    "+size+"
    "; + + var HTML = '
  • ' + + '
    ' + + '
    ' + name + '
    ' + + '
    ' + size + '
    ' + + '
    ' + + '0% ' + jsonLang.t9 + '
    ' + + '
    ' + + '

    ' + + '
  • '; + + + jQuery("#tableStyle").append(HTML); + //保存falsh_id,为上传做准备 + //global_flash_id.push(file.id); + //更改背景颜色 + changeTrBackGroundColor() + } +} + +function changeTrBackGroundColor() { + var getTr = document.getElementById("tableStyle").getElementsByTagName("dl"); + trNum = getTr.length; + for (var i = 0; i < trNum; i++) { + if (i % 2 == 0) { + getTr[i].style.backgroundColor = "#f3f3f3"; + } + } +} + + + +//上传时返回的状态 +function uploadProgress(file, bytesLoaded, bytesTotal) { + if (!$("#progress_bar_p_" + file.id).hasClass("orange")) { + $("#progress_bar_p_" + file.id).addClass("orange"); + } + $("#progress_bar_p_" + file.id).css("width", (bytesLoaded / bytesTotal) * 100 + "%"); + $("#progress_bar_span_" + file.id).html(parseInt((bytesLoaded / bytesTotal) * 100) + "%"); + +} + + +//上传成功 +function uploadSuccess(file, serverData, res) { + var id = "handle_button_" + file.id; + $("#" + id).replaceWith("
    " + jsonLang.t10 + "
    ") +} + + +//取消上传 +function userCancelUpload(file_id, type) { + + if (type == 0) { + SWFFuns.cancelUpload(file_id); + } else { + HTML5Funs.cancelUpload(file_id); + } + + $("#handle_button_" + file_id).html(jsonLang.t14).removeClass("orange").addClass("gray"); + //如果已经上传一部分了 + if ($("#progress_bar_p_" + file_id).hasClass("orange")) { + $("#progress_bar_p_" + file_id).removeClass("orange"); + $("#progress_bar_p_" + file_id).addClass("gray"); + } +} + + +/** + * 通过文件名称 从全局的文件列表中获取文件对象 + * + */ +function getFileFomeFilesUpload(filename) { + var len = filesUpload.length; + for (var i = 0; i < len; i++) { + if (filesUpload[i].name == filename) { + return filesUpload[i]; + } + } + + return null; +} + + +/** + * 往全局的 上传列表添加一个数据 + */ +function addFileToFilesUpload(file) { + + if (typeof file == "string") { + filesUpload.push({ "name": file }) + return true; + } else if (typeof file == "object") { + filesUpload.push(file); + return true; + } + + return false; +} + +/** + * 往全局的 上传列表添加一个数据 + */ +function updateFileToFilesUpload(file) { + + var len = filesUpload.length; + for (var i = 0; i < len; i++) { + if (filesUpload[i].name == file.name) { + filesUpload[i] = file; + return true; + } + } + + return false; +} + +/** + * 查找在数组中的位置 + */ +function findObjectKey(object, fid) { + var len = object.length; + for (var i = 0; i < len; i++) { + if (object[i].id == fid) { + return i; + } + } + return -1; +} + +/** + * 从全局的文件集合中移除文件,一般上传失败时使用 + * @param array files 文件对象集合 [{},{},{}] + * @param int fid 要删除的文件id + * @return 删除后的数组, 其实数组是引用类型可以不返回 + */ +function removeFileFromFilesUpload(files, fid) { + + var filesUploadKey = -1; + + filesUploadKey = findObjectKey(files, fid); + //从全局文件中移除 + if (filesUploadKey > -1) + files.splice(filesUploadKey, 1); + + return files; +} \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/html5_fun.js b/app/src/main/assets/web/uploadBook/js/html5_fun.js new file mode 100644 index 000000000..53b8c3314 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/html5_fun.js @@ -0,0 +1,244 @@ +/** + * 处理拖拽上传 + */ +var isDragOver = false;//拖拽触发点 +var fileNumber = -1; //上传文件编号 +var fileNumberPex = "zyFileUpload_"; //编号前缀 +var currUploadfile = {}; //当前上传的文件对象 + +var uploadQueue = [];//上传队列集合 +var isUploading = false;//是否正在上传 + +var XHR +try { + XHR = new XMLHttpRequest(); +} catch (e) { } + +(function (isSupportFileUpload) { + + //不支持拖拽上传,或者 不支持FormData ,显示WiFi表示 + if (!isSupportFileUpload) { + $("#drag_area").addClass("wf_wifi"); + $("#drag_area h3").html(""); + return; + //更换样式 + } else { + $("#drag_area").addClass("wf_normal"); + } + + + addEvent(); + + /** + * 添加事件 + */ + function addEvent() { + var dropArea = $('#drag_area h3')[0]; + dropArea.addEventListener('dragover', handleDragOver, false); + dropArea.addEventListener('dragleave', handleDragLeave, false); + dropArea.addEventListener('drop', handleDrop, false); + } + + /** + * 松开拖拽文件的处理,进行上传 + */ + function handleDrop(evt) { + + evt.stopPropagation(); + evt.preventDefault(); + $('#drag_area').addClass('wf_normal').removeClass('wf_active'); + + console.log("Drop"); + isDragOver = false; + + var file = {}; + var errorMsgs = []; + var len = evt.dataTransfer.files.length; + + for (var i = 0; i < len; i++) { + fileNumber++; + file = evt.dataTransfer.files[i]; + file.id = fileNumberPex + fileNumber; + + //检测文件 + msg = checkFile(file) + //文件可以通过 + if (!msg) { + //添加全局 + filesUpload.push(file); + //添加上传队列 + uploadQueue.push(file); + //在页面进行展示 + fileQueued(file, 1); + } else { + errorMsgs.push(msg) + } + } + + if (errorMsgs.length > 0) { + //只选择做一个进行上传 + if (len == 1) { + alert(errorMsgs[0]); + + } else { + a1 = len; + a2 = len - errorMsgs.length; + + var replaceArr = new Array(a1, a2); + alert(stringReplace(jsonLang.t7, replaceArr)); + } + } + + //拿出第一个,进行上传 + if (!isUploading && uploadQueue.length > 0) uploadFiles(uploadQueue.shift()); + } + + + function handleDragOver(evt) { + + evt.stopPropagation(); + evt.preventDefault(); + //防止多次DOM操作 + if (!isDragOver) { + console.log("Over"); + $('#drag_area').addClass('wf_active').removeClass('wf_normal'); + isDragOver = true; + } + + + } + + function handleDragLeave(evt) { + + console.log("DragLeave"); + evt.stopPropagation(); + evt.preventDefault(); + isDragOver = false; + $('#drag_area').addClass('wf_normal').removeClass('wf_active'); + } + + + + function uploadFiles(file) { + //正在上传 + isUploading = true; + //设置上传的数据 + var fd = new FormData(); + fd.append("Filename", file.name); + fd.append("Filedata", file); + fd.append("Upload", "Submit Query"); + //设置当前的上传对象 + currUploadfile = file; + + if (XHR.readyState > 0) { + XHR = new XMLHttpRequest(); + } + + XHR.upload.addEventListener("progress", progress, false); + XHR.upload.addEventListener("load", requestLoad, false); + XHR.upload.addEventListener("error", error, false); + XHR.upload.addEventListener("abort", abort, false); + XHR.upload.addEventListener("loadend", loadend, false); + XHR.upload.addEventListener("loadstart", loadstart, false); + XHR.open("POST", config.url); + XHR.send(fd); + XHR.onreadystatechange = function () { + + //只要上传完成不管成功失败 + if (XHR.readyState == 4) { + console.log("onreadystatechange ", XHR.status, +new Date()); + + if (XHR.status == 200) { + uploadSuccess(currUploadfile, {}, XHR.status) + } else { + uploadError() + } + + //进行下一个上传 + nextUpload() + } + }; + + } + + //请求完成,无论失败或成功 + function loadend(evt) { + console.log("loadend", +new Date(), evt); + } + //请求开始 + function loadstart(evt) { + console.log("loadstart", evt); + } + + //在请求发送或接收数据期间,在服务器指定的时间间隔触发。 + function progress(evt) { + uploadProgress(currUploadfile, evt.loaded || evt.position, evt.total) + } + + //在请求被取消时触发,例如,在调用 abort() 方法时。 + function abort(evt) { + console.log("abort", evt); + } + + //在请求失败时触发。 + function error(evt) { + //终止ajax请求 + XHR.abort(); + uploadError() + nextUpload(); + } + + //在请求成功完成时触发。 + function requestLoad(evt) { + console.log("requestLoad", +new Date(), evt); + } + + //进行下一个上传 + function nextUpload() { + isUploading = false; + if (uploadQueue.length > 0) { + uploadFiles(uploadQueue.shift()); + } else { + //米有正在上传的了 + currUploadfile = {} + } + } + + //上传出错误了,比如断网, + function uploadError() { + //移除全局变量中的,上传出错的 + removeFileFromFilesUpload(filesUpload, currUploadfile.id) + var file = currUploadfile; + $("#handle_button_" + file.id).replaceWith("
    " + jsonLang.t8 + "
    ") + } + + + //对外部注册的函数 + var HTML5Funs = { + /** + * 取消上传 + * @param string fid 文件的Id + */ + cancelUpload: function (fid) { + + var filesUploadKey = -1; + var uploadQueueKey = -1; + + + //从全局中删除文件 + removeFileFromFilesUpload(filesUpload, fid) + + //如果是正在上传的,AJAX取消 + if (currUploadfile.id == fid) { + XHR.abort(); + } else { + //从上传队列中移除 + removeFileFromFilesUpload(uploadQueue, fid) + } + } + } + + window.HTML5Funs = HTML5Funs; + + +})("FormData" in window && "ondrop" in document.body) \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js b/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js new file mode 100644 index 000000000..7c2430802 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js @@ -0,0 +1,154 @@ +/*! + * jQuery JavaScript Library v1.4.2 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Sat Feb 13 22:33:48 2010 -0500 + */ +(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, +Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& +(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, +a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== +"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, +function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
    a"; +var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, +parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= +false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= +s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, +applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; +else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, +a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== +w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, +cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, +function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); +k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), +C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= +e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& +f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; +if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", +e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, +"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, +d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); +t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| +g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= +h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& +q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

    ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: +function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= +{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, +""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); +return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", +""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= +c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? +c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= +function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= +Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, +"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= +a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= +a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== +"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
    ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, +serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), +function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, +global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& +e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? +"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== +false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= +false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", +c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| +d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); +g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== +1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== +"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; +if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== +"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| +c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; +this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= +this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, +e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
    "; +a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); +c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, +d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- +f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": +"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in +e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/app/src/main/assets/web/uploadBook/js/langSwich.js b/app/src/main/assets/web/uploadBook/js/langSwich.js new file mode 100644 index 000000000..b3b5a749a --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/langSwich.js @@ -0,0 +1,89 @@ +/** + * wifi传书功能,供客户端调用 + * 切换语言环境 addby zhongweikang@zhangyue.com 2015/09/23 + */ + +var language;//当前语言 +var jsonEn;//英文 +var jsonZh;//中文 + +/** + * 语言包 + */ +jsonEn = { + t0: "WiFi Upload", t1: "Documents", t2: "Name", t3: "Size", t4: "Operation", + t5: "Supported Format:TXT、EPUB、UMD", t6: "Drag here and Drop to upload", + t7: "You choosed [txt] files ,we only can upload [txt] of them。\nPlease choose TXT、EPUB、UMD files, file name cannot be repeated.", + t8: "Failed", t9: "Cancel", t10: "Done", t11: "One of books is over 500MB", t12: "Bad file mode", + t13: "File already exists", t14: "Canceled" +}; +jsonZh = { + t0: "传书", t1: "文档", t2: "名字", t3: "大小", t4: "操作", + t5: "支持类型:TXT、EPUB、UMD", t6: "拖至此处上传", + t7: "你选择了[txt]个文件,我们只能上传其中的[txt]。\n请选择 TXT、EPUB、UMD 类型的文件,文件的名字不能重复。", + t8: "失败", t9: "取消", t10: "完成", t11: "上传文件不能大于500MB", t12: "无效的文件格式", + t13: "文件已存在", t14: "已取消" +}; + +if (navigator.language) { + language = navigator.language; +} +else { + language = navigator.browserLanguage; +} +language = language.toLowerCase(language); + +/** + * 切换语言 + */ +function langSwich(s) { + switch (s) { + case 'en-us': + _strReplace(jsonEn); + break; + case 'zh-cn': + _strReplace(jsonZh); + break; + default: + _strReplace(jsonEn); + break; + } +} +function _strReplace(d) { + window.jsonLang = d;//全局变量在其他js文件中会遇到,勿删。 + for (var i in jsonLang) { + $('[data-js=' + i + ']').html(d[i]) + } +} + +/** + * 特殊字符串替换,例:语言包中的 t7 + * content 替换前的文本 + * replace 待插入的文本 (支持变量或数组) + */ +var isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} +function stringReplace(content, replace) { + + var str = content; + if (!str) { return null; } + + if (isArray(replace)) { + strs = str.split("[txt]"); + count = strs.length - 1; + + var string = ''; + for (var i = 0; i < count; i++) { + string = string + strs[i] + replace[i]; + } + string = string + strs[count]; + } else { + string = str.replace(replace, "[txt]"); + } + return string; +} + + +//执行 +langSwich(language); diff --git a/app/src/main/assets/web/uploadBook/js/swf_fun.js b/app/src/main/assets/web/uploadBook/js/swf_fun.js new file mode 100644 index 000000000..f57c16209 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/swf_fun.js @@ -0,0 +1,183 @@ +/** + * swf 上传 + */ +var swfu;//swfupload 对象 +var swfSelectCount = 0;// 当前选中的文件数量 + +window.onload = function () { + var settings = { + upload_url: config.url, + /*post_params: {"PHPSESSID" : ""},*/ + file_size_limit: config.fileLimitSize + " B", + file_types: "*." + config.fileTypes.split("|").join(";*."), + + file_types_description: "All Files", + file_upload_limit: 1000, //配置上传个数 + file_queue_limit: 0, + custom_settings: { + progressTarget: "fsUploadProgress", + cancelButtonId: "btnCancel" + }, + debug: 0, + + button_cursor: SWFUpload.CURSOR.HAND, + button_image_url: "i/wifi_btn_b.png", + button_width: "240", + button_height: "100", + button_float: "right", + button_placeholder_id: "spanButtonPlaceHolder", + button_text: '', + + assume_success_timeout: 30, + file_queued_handler: swfFileQueued, + file_queue_error_handler: fileQueueError, + file_dialog_complete_handler: fileDialogComplete, + upload_start_handler: uploadStart, + upload_progress_handler: uploadProgress, + upload_error_handler: uploadError, + upload_success_handler: uploadSuccess, + upload_complete_handler: uploadComplete, + queue_complete_handler: queueComplete + }; + swfu = new SWFUpload(settings); +}; + + + +//上传完成 +function uploadComplete(file, server, response) { + //继续下一个文件的上传 + this.startUpload(); +} + +//完成队列里的上传 +function queueComplete(numFilesUploaded) { + +} + +function userStartUpload(file_id) { + swfu.startUpload(file_id); +} + + + + + +function fileQueueError(file, errorCode, message) { + switch (errorCode) { + case -100: + //alert("Over 100 books"); + case -110: + //alert("One of books is over 500MB"); + break; + case -120: + //alert("One of books is 0KB"); + break; + } +} +//入列完毕 +function fileDialogComplete(numFilesSelected, numFilesQueued) { + if (numFilesSelected > 0) { + this.startUpload() + } +} +//开始上传 +function uploadStart(file) { + return true; +} + +//上传出错 +function uploadError(file, errorCode, message) { + switch (errorCode) { + case SWFUpload.UPLOAD_ERROR.HTTP_ERROR: + errorMessage = "Error"; + break; + case SWFUpload.UPLOAD_ERROR.UPLOAD_FAILED: + errorMessage = "Failed"; + break; + case SWFUpload.UPLOAD_ERROR.IO_ERROR: + errorMessage = "Please open wifi upload page"; + break; + case SWFUpload.UPLOAD_ERROR.SECURITY_ERROR: + errorMessage = "Security error"; + break; + case SWFUpload.UPLOAD_ERROR.UPLOAD_LIMIT_EXCEEDED: + errorMessage = "Security error"; + break; + case SWFUpload.UPLOAD_ERROR.FILE_VALIDATION_FAILED: + errorMessage = "Unable to verify. Skip "; + break; + default: + errorMessage = "Unhandled error"; + break; + } + + //从上传队列中移除 + removeFileFromFilesUpload(filesUpload, file.id) + + errorMessage = jsonLang.t8; + + $("#handle_button_" + file.id).replaceWith("
    " + errorMessage + "
    ") +} + + +var tmp = 0; +var errorFile = 0; +var errorMsgs = []; + +function swfFileQueued(file) { + + + //本次上传选中的文件个数 + if (swfSelectCount == 0) swfSelectCount = this.getStats().files_queued + //检测文件 + msg = checkFile(file) + + + //文件可以通过 + if (!msg) { + //添加全局的队列 + filesUpload.push(file); + //在页面进行展示 + fileQueued(file, 0) + } else { + //从上传队列移除,验证失败的文件 + this.cancelUpload(file.id, false) + errorMsgs.push(msg) + } + + + + //队列选择完毕,初始化所有的数据 + if (++tmp == swfSelectCount) { + + if (errorMsgs.length > 0) { + //只选择做一个进行上传 + if (swfSelectCount == 1) { + alert(errorMsgs[0]); + } else { + a1 = swfSelectCount; + a2 = swfSelectCount - errorMsgs.length; + + var replaceArr = new Array(a1, a2); + alert(stringReplace(jsonLang.t7, replaceArr)); + } + } + + tmp = 0; + errorFile = 0; + swfSelectCount = 0; + errorMsgs = [] + } + + +} + + + +var SWFFuns = { + cancelUpload: function (fid) { + swfu.cancelUpload(fid, false); + } + +} \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/swfupload.js b/app/src/main/assets/web/uploadBook/js/swfupload.js new file mode 100644 index 000000000..776d3b0a4 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/swfupload.js @@ -0,0 +1,1146 @@ +/** + * SWFUpload: http://www.swfupload.org, http://swfupload.googlecode.com + * + * mmSWFUpload 1.0: Flash upload dialog - http://profandesign.se/swfupload/, http://www.vinterwebb.se/ + * + * SWFUpload is (c) 2006-2007 Lars Huring, Olov Nilz�n and Mammon Media and is released under the MIT License: + * http://www.opensource.org/licenses/mit-license.php + * + * SWFUpload 2 is (c) 2007-2008 Jake Roberts and is released under the MIT License: + * http://www.opensource.org/licenses/mit-license.php + * + * SWFObject v2.2 + * is released under the MIT License + */ + + + +/* ******************* */ +/* Constructor & Init */ +/* ******************* */ +var SWFUpload; +var swfobject; + +if (SWFUpload == undefined) { + SWFUpload = function (settings) { + this.initSWFUpload(settings); + }; +} + +SWFUpload.prototype.initSWFUpload = function (userSettings) { + try { + this.customSettings = {}; // A container where developers can place their own settings associated with this instance. + this.settings = {}; + this.eventQueue = []; + this.movieName = "SWFUpload_" + SWFUpload.movieCount++; + this.movieElement = null; + + + // Setup global control tracking + SWFUpload.instances[this.movieName] = this; + + // Load the settings. Load the Flash movie. + this.initSettings(userSettings); + this.loadSupport(); + if (this.swfuploadPreload()) { + this.loadFlash(); + } + + this.displayDebugInfo(); + } catch (ex) { + delete SWFUpload.instances[this.movieName]; + throw ex; + } +}; + +/* *************** */ +/* Static Members */ +/* *************** */ +SWFUpload.instances = {}; +SWFUpload.movieCount = 0; +SWFUpload.version = "2.5.0 2010-01-15 Beta 2"; +SWFUpload.QUEUE_ERROR = { + QUEUE_LIMIT_EXCEEDED : -100, + FILE_EXCEEDS_SIZE_LIMIT : -110, + ZERO_BYTE_FILE : -120, + INVALID_FILETYPE : -130 +}; +SWFUpload.UPLOAD_ERROR = { + HTTP_ERROR : -200, + MISSING_UPLOAD_URL : -210, + IO_ERROR : -220, + SECURITY_ERROR : -230, + UPLOAD_LIMIT_EXCEEDED : -240, + UPLOAD_FAILED : -250, + SPECIFIED_FILE_ID_NOT_FOUND : -260, + FILE_VALIDATION_FAILED : -270, + FILE_CANCELLED : -280, + UPLOAD_STOPPED : -290, + RESIZE : -300 +}; +SWFUpload.FILE_STATUS = { + QUEUED : -1, + IN_PROGRESS : -2, + ERROR : -3, + COMPLETE : -4, + CANCELLED : -5 +}; +SWFUpload.UPLOAD_TYPE = { + NORMAL : -1, + RESIZED : -2 +}; + +SWFUpload.BUTTON_ACTION = { + SELECT_FILE : -100, + SELECT_FILES : -110, + START_UPLOAD : -120, + JAVASCRIPT : -130, // DEPRECATED + NONE : -130 +}; +SWFUpload.CURSOR = { + ARROW : -1, + HAND : -2 +}; +SWFUpload.WINDOW_MODE = { + WINDOW : "window", + TRANSPARENT : "transparent", + OPAQUE : "opaque" +}; + +SWFUpload.RESIZE_ENCODING = { + JPEG : -1, + PNG : -2 +}; + +// Private: takes a URL, determines if it is relative and converts to an absolute URL +// using the current site. Only processes the URL if it can, otherwise returns the URL untouched +SWFUpload.completeURL = function (url) { + try { + var path = "", indexSlash = -1; + if (typeof(url) !== "string" || url.match(/^https?:\/\//i) || url.match(/^\//) || url === "") { + return url; + } + + indexSlash = window.location.pathname.lastIndexOf("/"); + if (indexSlash <= 0) { + path = "/"; + } else { + path = window.location.pathname.substr(0, indexSlash) + "/"; + } + + return path + url; + } catch (ex) { + return url; + } +}; + +// Public: assign a new function to onload to use swfobject's domLoad functionality +SWFUpload.onload = function () {}; + + +/* ******************** */ +/* Instance Members */ +/* ******************** */ + +// Private: initSettings ensures that all the +// settings are set, getting a default value if one was not assigned. +SWFUpload.prototype.initSettings = function (userSettings) { + this.ensureDefault = function (settingName, defaultValue) { + var setting = userSettings[settingName]; + if (setting != undefined) { + this.settings[settingName] = setting; + } else { + this.settings[settingName] = defaultValue; + } + }; + + // Upload backend settings + this.ensureDefault("upload_url", ""); + this.ensureDefault("preserve_relative_urls", false); + this.ensureDefault("file_post_name", "Filedata"); + this.ensureDefault("post_params", {}); + this.ensureDefault("use_query_string", false); + this.ensureDefault("requeue_on_error", false); + this.ensureDefault("http_success", []); + this.ensureDefault("assume_success_timeout", 0); + + // File Settings + this.ensureDefault("file_types", "*.*"); + this.ensureDefault("file_types_description", "All Files"); + this.ensureDefault("file_size_limit", 0); // Default zero means "unlimited" + this.ensureDefault("file_upload_limit", 0); + this.ensureDefault("file_queue_limit", 0); + + // Flash Settings + this.ensureDefault("flash_url", "swfupload.swf"); + this.ensureDefault("flash9_url", "swfupload_fp9.swf"); + this.ensureDefault("prevent_swf_caching", true); + + // Button Settings + this.ensureDefault("button_image_url", ""); + this.ensureDefault("button_width", 1); + this.ensureDefault("button_height", 1); + this.ensureDefault("button_text", ""); + this.ensureDefault("button_text_style", "color: #000000; font-size: 16pt;"); + this.ensureDefault("button_text_top_padding", 0); + this.ensureDefault("button_text_left_padding", 0); + this.ensureDefault("button_action", SWFUpload.BUTTON_ACTION.SELECT_FILES); + this.ensureDefault("button_disabled", false); + this.ensureDefault("button_placeholder_id", ""); + this.ensureDefault("button_placeholder", null); + this.ensureDefault("button_cursor", SWFUpload.CURSOR.ARROW); + this.ensureDefault("button_window_mode", SWFUpload.WINDOW_MODE.TRANSPARENT); + + // Debug Settings + this.ensureDefault("debug", false); + this.settings.debug_enabled = this.settings.debug; // Here to maintain v2 API + + // Event Handlers + this.settings.return_upload_start_handler = this.returnUploadStart; + this.ensureDefault("swfupload_preload_handler", null); + this.ensureDefault("swfupload_load_failed_handler", null); + this.ensureDefault("swfupload_loaded_handler", null); + this.ensureDefault("file_dialog_start_handler", null); + this.ensureDefault("file_queued_handler", null); + this.ensureDefault("file_queue_error_handler", null); + this.ensureDefault("file_dialog_complete_handler", null); + + this.ensureDefault("upload_resize_start_handler", null); + this.ensureDefault("upload_start_handler", null); + this.ensureDefault("upload_progress_handler", null); + this.ensureDefault("upload_error_handler", null); + this.ensureDefault("upload_success_handler", null); + this.ensureDefault("upload_complete_handler", null); + + this.ensureDefault("mouse_click_handler", null); + this.ensureDefault("mouse_out_handler", null); + this.ensureDefault("mouse_over_handler", null); + + this.ensureDefault("debug_handler", this.debugMessage); + + this.ensureDefault("custom_settings", {}); + + // Other settings + this.customSettings = this.settings.custom_settings; + + // Update the flash url if needed + if (!!this.settings.prevent_swf_caching) { + this.settings.flash_url = this.settings.flash_url + (this.settings.flash_url.indexOf("?") < 0 ? "?" : "&") + "preventswfcaching=" + new Date().getTime(); + this.settings.flash9_url = this.settings.flash9_url + (this.settings.flash9_url.indexOf("?") < 0 ? "?" : "&") + "preventswfcaching=" + new Date().getTime(); + } + + if (!this.settings.preserve_relative_urls) { + this.settings.upload_url = SWFUpload.completeURL(this.settings.upload_url); + this.settings.button_image_url = SWFUpload.completeURL(this.settings.button_image_url); + } + + delete this.ensureDefault; +}; + +// Initializes the supported functionality based the Flash Player version, state, and event that occur during initialization +SWFUpload.prototype.loadSupport = function () { + this.support = { + loading : swfobject.hasFlashPlayerVersion("9.0.28"), + imageResize : swfobject.hasFlashPlayerVersion("10.0.0") + }; + +}; + +// Private: loadFlash replaces the button_placeholder element with the flash movie. +SWFUpload.prototype.loadFlash = function () { + var targetElement, tempParent, wrapperType, flashHTML, els; + + if (!this.support.loading) { + this.queueEvent("swfupload_load_failed_handler", ["Flash Player doesn't support SWFUpload"]); + return; + } + + // Make sure an element with the ID we are going to use doesn't already exist + if (document.getElementById(this.movieName) !== null) { + this.support.loading = false; + this.queueEvent("swfupload_load_failed_handler", ["Element ID already in use"]); + return; + } + + // Get the element where we will be placing the flash movie + targetElement = document.getElementById(this.settings.button_placeholder_id) || this.settings.button_placeholder; + + if (targetElement == undefined) { + this.support.loading = false; + this.queueEvent("swfupload_load_failed_handler", ["button place holder not found"]); + return; + } + + wrapperType = (targetElement.currentStyle && targetElement.currentStyle["display"] || window.getComputedStyle && document.defaultView.getComputedStyle(targetElement, null).getPropertyValue("display")) !== "block" ? "span" : "div"; + + // Append the container and load the flash + tempParent = document.createElement(wrapperType); + + flashHTML = this.getFlashHTML(); + + try { + tempParent.innerHTML = flashHTML; // Using innerHTML is non-standard but the only sensible way to dynamically add Flash in IE (and maybe other browsers) + } catch (ex) { + this.support.loading = false; + this.queueEvent("swfupload_load_failed_handler", ["Exception loading Flash HTML into placeholder"]); + return; + } + + // Try to get the movie element immediately + els = tempParent.getElementsByTagName("object"); + if (!els || els.length > 1 || els.length === 0) { + this.support.loading = false; + this.queueEvent("swfupload_load_failed_handler", ["Unable to find movie after adding to DOM"]); + return; + } else if (els.length === 1) { + this.movieElement = els[0]; + } + + targetElement.parentNode.replaceChild(tempParent.firstChild, targetElement); + + // Fix IE Flash/Form bug + if (window[this.movieName] == undefined) { + window[this.movieName] = this.getMovieElement(); + } +}; + +// Private: getFlashHTML generates the object tag needed to embed the flash in to the document +SWFUpload.prototype.getFlashHTML = function (flashVersion) { + // Flash Satay object syntax: http://www.alistapart.com/articles/flashsatay + + var classid = ""; + //�����IE9,10 ������calssid + if (window.ActiveXObject){ + try{ + if (+navigator.userAgent.toLowerCase().match(/msie ([\d.]+)/)[1].substring(0, 1) >= 9) + classid = ' classid = "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" '; + }catch(e){ + classid = "" + } + } + + + + return ['', + '', + '', + '', + '', + '', + ''].join(""); +}; + +// Private: getFlashVars builds the parameter string that will be passed +// to flash in the flashvars param. +SWFUpload.prototype.getFlashVars = function () { + // Build a string from the post param object + var httpSuccessString, paramString; + + paramString = this.buildParamString(); + httpSuccessString = this.settings.http_success.join(","); + + // Build the parameter string + return ["movieName=", encodeURIComponent(this.movieName), + "&uploadURL=", encodeURIComponent(this.settings.upload_url), + "&useQueryString=", encodeURIComponent(this.settings.use_query_string), + "&requeueOnError=", encodeURIComponent(this.settings.requeue_on_error), + "&httpSuccess=", encodeURIComponent(httpSuccessString), + "&assumeSuccessTimeout=", encodeURIComponent(this.settings.assume_success_timeout), + "&params=", encodeURIComponent(paramString), + "&filePostName=", encodeURIComponent(this.settings.file_post_name), + "&fileTypes=", encodeURIComponent(this.settings.file_types), + "&fileTypesDescription=", encodeURIComponent(this.settings.file_types_description), + "&fileSizeLimit=", encodeURIComponent(this.settings.file_size_limit), + "&fileUploadLimit=", encodeURIComponent(this.settings.file_upload_limit), + "&fileQueueLimit=", encodeURIComponent(this.settings.file_queue_limit), + "&debugEnabled=", encodeURIComponent(this.settings.debug_enabled), + "&buttonImageURL=", encodeURIComponent(this.settings.button_image_url), + "&buttonWidth=", encodeURIComponent(this.settings.button_width), + "&buttonHeight=", encodeURIComponent(this.settings.button_height), + "&buttonText=", encodeURIComponent(this.settings.button_text), + "&buttonTextTopPadding=", encodeURIComponent(this.settings.button_text_top_padding), + "&buttonTextLeftPadding=", encodeURIComponent(this.settings.button_text_left_padding), + "&buttonTextStyle=", encodeURIComponent(this.settings.button_text_style), + "&buttonAction=", encodeURIComponent(this.settings.button_action), + "&buttonDisabled=", encodeURIComponent(this.settings.button_disabled), + "&buttonCursor=", encodeURIComponent(this.settings.button_cursor) + ].join(""); +}; + +// Public: get retrieves the DOM reference to the Flash element added by SWFUpload +// The element is cached after the first lookup +SWFUpload.prototype.getMovieElement = function () { + if (this.movieElement == undefined) { + this.movieElement = document.getElementById(this.movieName); + } + + if (this.movieElement === null) { + throw "Could not find Flash element"; + } + + return this.movieElement; +}; + +// Private: buildParamString takes the name/value pairs in the post_params setting object +// and joins them up in to a string formatted "name=value&name=value" +SWFUpload.prototype.buildParamString = function () { + var name, postParams, paramStringPairs = []; + + postParams = this.settings.post_params; + + if (typeof(postParams) === "object") { + for (name in postParams) { + if (postParams.hasOwnProperty(name)) { + paramStringPairs.push(encodeURIComponent(name.toString()) + "=" + encodeURIComponent(postParams[name].toString())); + } + } + } + + return paramStringPairs.join("&"); +}; + +// Public: Used to remove a SWFUpload instance from the page. This method strives to remove +// all references to the SWF, and other objects so memory is properly freed. +// Returns true if everything was destroyed. Returns a false if a failure occurs leaving SWFUpload in an inconsistant state. +// Credits: Major improvements provided by steffen +SWFUpload.prototype.destroy = function () { + var movieElement; + + try { + // Make sure Flash is done before we try to remove it + this.cancelUpload(null, false); + + movieElement = this.cleanUp(); + + // Remove the SWFUpload DOM nodes + if (movieElement) { + // Remove the Movie Element from the page + try { + movieElement.parentNode.removeChild(movieElement); + } catch (ex) {} + } + + // Remove IE form fix reference + window[this.movieName] = null; + + // Destroy other references + SWFUpload.instances[this.movieName] = null; + delete SWFUpload.instances[this.movieName]; + + this.movieElement = null; + this.settings = null; + this.customSettings = null; + this.eventQueue = null; + this.movieName = null; + + + return true; + } catch (ex2) { + return false; + } +}; + + +// Public: displayDebugInfo prints out settings and configuration +// information about this SWFUpload instance. +// This function (and any references to it) can be deleted when placing +// SWFUpload in production. +SWFUpload.prototype.displayDebugInfo = function () { + this.debug( + [ + "---SWFUpload Instance Info---\n", + "Version: ", SWFUpload.version, "\n", + "Movie Name: ", this.movieName, "\n", + "Settings:\n", + "\t", "upload_url: ", this.settings.upload_url, "\n", + "\t", "flash_url: ", this.settings.flash_url, "\n", + "\t", "flash9_url: ", this.settings.flash9_url, "\n", + "\t", "use_query_string: ", this.settings.use_query_string.toString(), "\n", + "\t", "requeue_on_error: ", this.settings.requeue_on_error.toString(), "\n", + "\t", "http_success: ", this.settings.http_success.join(", "), "\n", + "\t", "assume_success_timeout: ", this.settings.assume_success_timeout, "\n", + "\t", "file_post_name: ", this.settings.file_post_name, "\n", + "\t", "post_params: ", this.settings.post_params.toString(), "\n", + "\t", "file_types: ", this.settings.file_types, "\n", + "\t", "file_types_description: ", this.settings.file_types_description, "\n", + "\t", "file_size_limit: ", this.settings.file_size_limit, "\n", + "\t", "file_upload_limit: ", this.settings.file_upload_limit, "\n", + "\t", "file_queue_limit: ", this.settings.file_queue_limit, "\n", + "\t", "debug: ", this.settings.debug.toString(), "\n", + + "\t", "prevent_swf_caching: ", this.settings.prevent_swf_caching.toString(), "\n", + + "\t", "button_placeholder_id: ", this.settings.button_placeholder_id.toString(), "\n", + "\t", "button_placeholder: ", (this.settings.button_placeholder ? "Set" : "Not Set"), "\n", + "\t", "button_image_url: ", this.settings.button_image_url.toString(), "\n", + "\t", "button_width: ", this.settings.button_width.toString(), "\n", + "\t", "button_height: ", this.settings.button_height.toString(), "\n", + "\t", "button_text: ", this.settings.button_text.toString(), "\n", + "\t", "button_text_style: ", this.settings.button_text_style.toString(), "\n", + "\t", "button_text_top_padding: ", this.settings.button_text_top_padding.toString(), "\n", + "\t", "button_text_left_padding: ", this.settings.button_text_left_padding.toString(), "\n", + "\t", "button_action: ", this.settings.button_action.toString(), "\n", + "\t", "button_cursor: ", this.settings.button_cursor.toString(), "\n", + "\t", "button_disabled: ", this.settings.button_disabled.toString(), "\n", + + "\t", "custom_settings: ", this.settings.custom_settings.toString(), "\n", + "Event Handlers:\n", + "\t", "swfupload_preload_handler assigned: ", (typeof this.settings.swfupload_preload_handler === "function").toString(), "\n", + "\t", "swfupload_load_failed_handler assigned: ", (typeof this.settings.swfupload_load_failed_handler === "function").toString(), "\n", + "\t", "swfupload_loaded_handler assigned: ", (typeof this.settings.swfupload_loaded_handler === "function").toString(), "\n", + "\t", "mouse_click_handler assigned: ", (typeof this.settings.mouse_click_handler === "function").toString(), "\n", + "\t", "mouse_over_handler assigned: ", (typeof this.settings.mouse_over_handler === "function").toString(), "\n", + "\t", "mouse_out_handler assigned: ", (typeof this.settings.mouse_out_handler === "function").toString(), "\n", + "\t", "file_dialog_start_handler assigned: ", (typeof this.settings.file_dialog_start_handler === "function").toString(), "\n", + "\t", "file_queued_handler assigned: ", (typeof this.settings.file_queued_handler === "function").toString(), "\n", + "\t", "file_queue_error_handler assigned: ", (typeof this.settings.file_queue_error_handler === "function").toString(), "\n", + "\t", "upload_resize_start_handler assigned: ", (typeof this.settings.upload_resize_start_handler === "function").toString(), "\n", + "\t", "upload_start_handler assigned: ", (typeof this.settings.upload_start_handler === "function").toString(), "\n", + "\t", "upload_progress_handler assigned: ", (typeof this.settings.upload_progress_handler === "function").toString(), "\n", + "\t", "upload_error_handler assigned: ", (typeof this.settings.upload_error_handler === "function").toString(), "\n", + "\t", "upload_success_handler assigned: ", (typeof this.settings.upload_success_handler === "function").toString(), "\n", + "\t", "upload_complete_handler assigned: ", (typeof this.settings.upload_complete_handler === "function").toString(), "\n", + "\t", "debug_handler assigned: ", (typeof this.settings.debug_handler === "function").toString(), "\n", + + "Support:\n", + "\t", "Load: ", (this.support.loading ? "Yes" : "No"), "\n", + "\t", "Image Resize: ", (this.support.imageResize ? "Yes" : "No"), "\n" + + ].join("") + ); +}; + +/* Note: addSetting and getSetting are no longer used by SWFUpload but are included + the maintain v2 API compatibility +*/ +// Public: (Deprecated) addSetting adds a setting value. If the value given is undefined or null then the default_value is used. +SWFUpload.prototype.addSetting = function (name, value, default_value) { + if (value == undefined) { + return (this.settings[name] = default_value); + } else { + return (this.settings[name] = value); + } +}; + +// Public: (Deprecated) getSetting gets a setting. Returns an empty string if the setting was not found. +SWFUpload.prototype.getSetting = function (name) { + if (this.settings[name] != undefined) { + return this.settings[name]; + } + + return ""; +}; + + + +// Private: callFlash handles function calls made to the Flash element. +// Calls are made with a setTimeout for some functions to work around +// bugs in the ExternalInterface library. +SWFUpload.prototype.callFlash = function (functionName, argumentArray) { + var movieElement, returnValue, returnString; + + argumentArray = argumentArray || []; + movieElement = this.getMovieElement(); + + // Flash's method if calling ExternalInterface methods (code adapted from MooTools). + try { + if (movieElement != undefined) { + returnString = movieElement.CallFunction('' + __flash__argumentsToXML(argumentArray, 0) + ''); + returnValue = eval(returnString); + } else { + this.debug("Can't call flash because the movie wasn't found."); + } + } catch (ex) { + this.debug("Exception calling flash function '" + functionName + "': " + ex.message); + } + + // Unescape file post param values + if (returnValue != undefined && typeof returnValue.post === "object") { + returnValue = this.unescapeFilePostParams(returnValue); + } + + return returnValue; +}; + +/* ***************************** + -- Flash control methods -- + Your UI should use these + to operate SWFUpload + ***************************** */ + +// WARNING: this function does not work in Flash Player 10 +// Public: selectFile causes a File Selection Dialog window to appear. This +// dialog only allows 1 file to be selected. +SWFUpload.prototype.selectFile = function () { + this.callFlash("SelectFile"); +}; + +// WARNING: this function does not work in Flash Player 10 +// Public: selectFiles causes a File Selection Dialog window to appear/ This +// dialog allows the user to select any number of files +// Flash Bug Warning: Flash limits the number of selectable files based on the combined length of the file names. +// If the selection name length is too long the dialog will fail in an unpredictable manner. There is no work-around +// for this bug. +SWFUpload.prototype.selectFiles = function () { + this.callFlash("SelectFiles"); +}; + + +// Public: startUpload starts uploading the first file in the queue unless +// the optional parameter 'fileID' specifies the ID +SWFUpload.prototype.startUpload = function (fileID) { + this.callFlash("StartUpload", [fileID]); +}; + +// Public: startUpload starts uploading the first file in the queue unless +// the optional parameter 'fileID' specifies the ID +SWFUpload.prototype.startResizedUpload = function (fileID, width, height, encoding, quality, allowEnlarging) { + this.callFlash("StartUpload", [fileID, { "width": width, "height" : height, "encoding" : encoding, "quality" : quality, "allowEnlarging" : allowEnlarging }]); +}; + +// Public: cancelUpload cancels any queued file. The fileID parameter may be the file ID or index. +// If you do not specify a fileID the current uploading file or first file in the queue is cancelled. +// If you do not want the uploadError event to trigger you can specify false for the triggerErrorEvent parameter. +SWFUpload.prototype.cancelUpload = function (fileID, triggerErrorEvent) { + if (triggerErrorEvent !== false) { + triggerErrorEvent = true; + } + this.callFlash("CancelUpload", [fileID, triggerErrorEvent]); +}; + +// Public: stopUpload stops the current upload and requeues the file at the beginning of the queue. +// If nothing is currently uploading then nothing happens. +SWFUpload.prototype.stopUpload = function () { + this.callFlash("StopUpload"); +}; + + +// Public: requeueUpload requeues any file. If the file is requeued or already queued true is returned. +// If the file is not found or is currently uploading false is returned. Requeuing a file bypasses the +// file size, queue size, upload limit and other queue checks. Certain files can't be requeued (e.g, invalid or zero bytes files). +SWFUpload.prototype.requeueUpload = function (indexOrFileID) { + return this.callFlash("RequeueUpload", [indexOrFileID]); +}; + + +/* ************************ + * Settings methods + * These methods change the SWFUpload settings. + * SWFUpload settings should not be changed directly on the settings object + * since many of the settings need to be passed to Flash in order to take + * effect. + * *********************** */ + +// Public: getStats gets the file statistics object. +SWFUpload.prototype.getStats = function () { + return this.callFlash("GetStats"); +}; + +// Public: setStats changes the SWFUpload statistics. You shouldn't need to +// change the statistics but you can. Changing the statistics does not +// affect SWFUpload accept for the successful_uploads count which is used +// by the upload_limit setting to determine how many files the user may upload. +SWFUpload.prototype.setStats = function (statsObject) { + this.callFlash("SetStats", [statsObject]); +}; + +// Public: getFile retrieves a File object by ID or Index. If the file is +// not found then 'null' is returned. +SWFUpload.prototype.getFile = function (fileID) { + if (typeof(fileID) === "number") { + return this.callFlash("GetFileByIndex", [fileID]); + } else { + return this.callFlash("GetFile", [fileID]); + } +}; + +// Public: getFileFromQueue retrieves a File object by ID or Index. If the file is +// not found then 'null' is returned. +SWFUpload.prototype.getQueueFile = function (fileID) { + if (typeof(fileID) === "number") { + return this.callFlash("GetFileByQueueIndex", [fileID]); + } else { + return this.callFlash("GetFile", [fileID]); + } +}; + + +// Public: addFileParam sets a name/value pair that will be posted with the +// file specified by the Files ID. If the name already exists then the +// exiting value will be overwritten. +SWFUpload.prototype.addFileParam = function (fileID, name, value) { + return this.callFlash("AddFileParam", [fileID, name, value]); +}; + +// Public: removeFileParam removes a previously set (by addFileParam) name/value +// pair from the specified file. +SWFUpload.prototype.removeFileParam = function (fileID, name) { + this.callFlash("RemoveFileParam", [fileID, name]); +}; + +// Public: setUploadUrl changes the upload_url setting. +SWFUpload.prototype.setUploadURL = function (url) { + this.settings.upload_url = url.toString(); + this.callFlash("SetUploadURL", [url]); +}; + +// Public: setPostParams changes the post_params setting +SWFUpload.prototype.setPostParams = function (paramsObject) { + this.settings.post_params = paramsObject; + this.callFlash("SetPostParams", [paramsObject]); +}; + +// Public: addPostParam adds post name/value pair. Each name can have only one value. +SWFUpload.prototype.addPostParam = function (name, value) { + this.settings.post_params[name] = value; + this.callFlash("SetPostParams", [this.settings.post_params]); +}; + +// Public: removePostParam deletes post name/value pair. +SWFUpload.prototype.removePostParam = function (name) { + delete this.settings.post_params[name]; + this.callFlash("SetPostParams", [this.settings.post_params]); +}; + +// Public: setFileTypes changes the file_types setting and the file_types_description setting +SWFUpload.prototype.setFileTypes = function (types, description) { + this.settings.file_types = types; + this.settings.file_types_description = description; + this.callFlash("SetFileTypes", [types, description]); +}; + +// Public: setFileSizeLimit changes the file_size_limit setting +SWFUpload.prototype.setFileSizeLimit = function (fileSizeLimit) { + this.settings.file_size_limit = fileSizeLimit; + this.callFlash("SetFileSizeLimit", [fileSizeLimit]); +}; + +// Public: setFileUploadLimit changes the file_upload_limit setting +SWFUpload.prototype.setFileUploadLimit = function (fileUploadLimit) { + this.settings.file_upload_limit = fileUploadLimit; + this.callFlash("SetFileUploadLimit", [fileUploadLimit]); +}; + +// Public: setFileQueueLimit changes the file_queue_limit setting +SWFUpload.prototype.setFileQueueLimit = function (fileQueueLimit) { + this.settings.file_queue_limit = fileQueueLimit; + this.callFlash("SetFileQueueLimit", [fileQueueLimit]); +}; + +// Public: setFilePostName changes the file_post_name setting +SWFUpload.prototype.setFilePostName = function (filePostName) { + this.settings.file_post_name = filePostName; + this.callFlash("SetFilePostName", [filePostName]); +}; + +// Public: setUseQueryString changes the use_query_string setting +SWFUpload.prototype.setUseQueryString = function (useQueryString) { + this.settings.use_query_string = useQueryString; + this.callFlash("SetUseQueryString", [useQueryString]); +}; + +// Public: setRequeueOnError changes the requeue_on_error setting +SWFUpload.prototype.setRequeueOnError = function (requeueOnError) { + this.settings.requeue_on_error = requeueOnError; + this.callFlash("SetRequeueOnError", [requeueOnError]); +}; + +// Public: setHTTPSuccess changes the http_success setting +SWFUpload.prototype.setHTTPSuccess = function (http_status_codes) { + if (typeof http_status_codes === "string") { + http_status_codes = http_status_codes.replace(" ", "").split(","); + } + + this.settings.http_success = http_status_codes; + this.callFlash("SetHTTPSuccess", [http_status_codes]); +}; + +// Public: setHTTPSuccess changes the http_success setting +SWFUpload.prototype.setAssumeSuccessTimeout = function (timeout_seconds) { + this.settings.assume_success_timeout = timeout_seconds; + this.callFlash("SetAssumeSuccessTimeout", [timeout_seconds]); +}; + +// Public: setDebugEnabled changes the debug_enabled setting +SWFUpload.prototype.setDebugEnabled = function (debugEnabled) { + this.settings.debug_enabled = debugEnabled; + this.callFlash("SetDebugEnabled", [debugEnabled]); +}; + +// Public: setButtonImageURL loads a button image sprite +SWFUpload.prototype.setButtonImageURL = function (buttonImageURL) { + if (buttonImageURL == undefined) { + buttonImageURL = ""; + } + + this.settings.button_image_url = buttonImageURL; + this.callFlash("SetButtonImageURL", [buttonImageURL]); +}; + +// Public: setButtonDimensions resizes the Flash Movie and button +SWFUpload.prototype.setButtonDimensions = function (width, height) { + this.settings.button_width = width; + this.settings.button_height = height; + + var movie = this.getMovieElement(); + if (movie != undefined) { + movie.style.width = width + "px"; + movie.style.height = height + "px"; + } + + this.callFlash("SetButtonDimensions", [width, height]); +}; +// Public: setButtonText Changes the text overlaid on the button +SWFUpload.prototype.setButtonText = function (html) { + this.settings.button_text = html; + this.callFlash("SetButtonText", [html]); +}; +// Public: setButtonTextPadding changes the top and left padding of the text overlay +SWFUpload.prototype.setButtonTextPadding = function (left, top) { + this.settings.button_text_top_padding = top; + this.settings.button_text_left_padding = left; + this.callFlash("SetButtonTextPadding", [left, top]); +}; + +// Public: setButtonTextStyle changes the CSS used to style the HTML/Text overlaid on the button +SWFUpload.prototype.setButtonTextStyle = function (css) { + this.settings.button_text_style = css; + this.callFlash("SetButtonTextStyle", [css]); +}; +// Public: setButtonDisabled disables/enables the button +SWFUpload.prototype.setButtonDisabled = function (isDisabled) { + this.settings.button_disabled = isDisabled; + this.callFlash("SetButtonDisabled", [isDisabled]); +}; +// Public: setButtonAction sets the action that occurs when the button is clicked +SWFUpload.prototype.setButtonAction = function (buttonAction) { + this.settings.button_action = buttonAction; + this.callFlash("SetButtonAction", [buttonAction]); +}; + +// Public: setButtonCursor changes the mouse cursor displayed when hovering over the button +SWFUpload.prototype.setButtonCursor = function (cursor) { + this.settings.button_cursor = cursor; + this.callFlash("SetButtonCursor", [cursor]); +}; + +/* ******************************* + Flash Event Interfaces + These functions are used by Flash to trigger the various + events. + + All these functions a Private. + + Because the ExternalInterface library is buggy the event calls + are added to a queue and the queue then executed by a setTimeout. + This ensures that events are executed in a determinate order and that + the ExternalInterface bugs are avoided. +******************************* */ + +SWFUpload.prototype.queueEvent = function (handlerName, argumentArray) { + // Warning: Don't call this.debug inside here or you'll create an infinite loop + var self = this; + + if (argumentArray == undefined) { + argumentArray = []; + } else if (!(argumentArray instanceof Array)) { + argumentArray = [argumentArray]; + } + + if (typeof this.settings[handlerName] === "function") { + // Queue the event + this.eventQueue.push(function () { + this.settings[handlerName].apply(this, argumentArray); + }); + + // Execute the next queued event + setTimeout(function () { + self.executeNextEvent(); + }, 0); + + } else if (this.settings[handlerName] !== null) { + throw "Event handler " + handlerName + " is unknown or is not a function"; + } +}; + +// Private: Causes the next event in the queue to be executed. Since events are queued using a setTimeout +// we must queue them in order to garentee that they are executed in order. +SWFUpload.prototype.executeNextEvent = function () { + // Warning: Don't call this.debug inside here or you'll create an infinite loop + + var f = this.eventQueue ? this.eventQueue.shift() : null; + if (typeof(f) === "function") { + f.apply(this); + } +}; + +// Private: unescapeFileParams is part of a workaround for a flash bug where objects passed through ExternalInterface cannot have +// properties that contain characters that are not valid for JavaScript identifiers. To work around this +// the Flash Component escapes the parameter names and we must unescape again before passing them along. +SWFUpload.prototype.unescapeFilePostParams = function (file) { + var reg = /[$]([0-9a-f]{4})/i, unescapedPost = {}, uk, k, match; + + if (file != undefined) { + for (k in file.post) { + if (file.post.hasOwnProperty(k)) { + uk = k; + while ((match = reg.exec(uk)) !== null) { + uk = uk.replace(match[0], String.fromCharCode(parseInt("0x" + match[1], 16))); + } + unescapedPost[uk] = file.post[k]; + } + } + + file.post = unescapedPost; + } + + return file; +}; + +// Private: This event is called by SWFUpload Init after we've determined what the user's Flash Player supports. +// Use the swfupload_preload_handler event setting to execute custom code when SWFUpload has loaded. +// Return false to prevent SWFUpload from loading and allow your script to do something else if your required feature is +// not supported +SWFUpload.prototype.swfuploadPreload = function () { + var returnValue; + if (typeof this.settings.swfupload_preload_handler === "function") { + returnValue = this.settings.swfupload_preload_handler.call(this); + } else if (this.settings.swfupload_preload_handler != undefined) { + throw "upload_start_handler must be a function"; + } + + // Convert undefined to true so if nothing is returned from the upload_start_handler it is + // interpretted as 'true'. + if (returnValue === undefined) { + returnValue = true; + } + + return !!returnValue; +} + +// Private: This event is called by Flash when it has finished loading. Don't modify this. +// Use the swfupload_loaded_handler event setting to execute custom code when SWFUpload has loaded. +SWFUpload.prototype.flashReady = function () { + // Check that the movie element is loaded correctly with its ExternalInterface methods defined + var movieElement = this.cleanUp(); + + if (!movieElement) { + this.debug("Flash called back ready but the flash movie can't be found."); + return; + } + + this.queueEvent("swfupload_loaded_handler"); +}; + +// Private: removes Flash added fuctions to the DOM node to prevent memory leaks in IE. +// This function is called by Flash each time the ExternalInterface functions are created. +SWFUpload.prototype.cleanUp = function () { + var key, movieElement = this.getMovieElement(); + + // Pro-actively unhook all the Flash functions + try { + if (movieElement && typeof(movieElement.CallFunction) === "unknown") { // We only want to do this in IE + this.debug("Removing Flash functions hooks (this should only run in IE and should prevent memory leaks)"); + for (key in movieElement) { + try { + if (typeof(movieElement[key]) === "function") { + movieElement[key] = null; + } + } catch (ex) { + } + } + } + } catch (ex1) { + + } + + // Fix Flashes own cleanup code so if the SWF Movie was removed from the page + // it doesn't display errors. + window["__flash__removeCallback"] = function (instance, name) { + try { + if (instance) { + instance[name] = null; + } + } catch (flashEx) { + + } + }; + + return movieElement; +}; + +/* When the button_action is set to None this event gets fired and executes the mouse_click_handler */ +SWFUpload.prototype.mouseClick = function () { + this.queueEvent("mouse_click_handler"); +}; +SWFUpload.prototype.mouseOver = function () { + this.queueEvent("mouse_over_handler"); +}; +SWFUpload.prototype.mouseOut = function () { + this.queueEvent("mouse_out_handler"); +}; + +/* This is a chance to do something before the browse window opens */ +SWFUpload.prototype.fileDialogStart = function () { + this.queueEvent("file_dialog_start_handler"); +}; + + +/* Called when a file is successfully added to the queue. */ +SWFUpload.prototype.fileQueued = function (file) { + file = this.unescapeFilePostParams(file); + this.queueEvent("file_queued_handler", file); +}; + + +/* Handle errors that occur when an attempt to queue a file fails. */ +SWFUpload.prototype.fileQueueError = function (file, errorCode, message) { + file = this.unescapeFilePostParams(file); + this.queueEvent("file_queue_error_handler", [file, errorCode, message]); +}; + +/* Called after the file dialog has closed and the selected files have been queued. + You could call startUpload here if you want the queued files to begin uploading immediately. */ +SWFUpload.prototype.fileDialogComplete = function (numFilesSelected, numFilesQueued, numFilesInQueue) { + this.queueEvent("file_dialog_complete_handler", [numFilesSelected, numFilesQueued, numFilesInQueue]); +}; + +SWFUpload.prototype.uploadResizeStart = function (file, resizeSettings) { + file = this.unescapeFilePostParams(file); + this.queueEvent("upload_resize_start_handler", [file, resizeSettings.width, resizeSettings.height, resizeSettings.encoding, resizeSettings.quality]); +}; + +SWFUpload.prototype.uploadStart = function (file) { + file = this.unescapeFilePostParams(file); + this.queueEvent("return_upload_start_handler", file); +}; + +SWFUpload.prototype.returnUploadStart = function (file) { + var returnValue; + if (typeof this.settings.upload_start_handler === "function") { + file = this.unescapeFilePostParams(file); + returnValue = this.settings.upload_start_handler.call(this, file); + } else if (this.settings.upload_start_handler != undefined) { + throw "upload_start_handler must be a function"; + } + + // Convert undefined to true so if nothing is returned from the upload_start_handler it is + // interpretted as 'true'. + if (returnValue === undefined) { + returnValue = true; + } + + returnValue = !!returnValue; + + this.callFlash("ReturnUploadStart", [returnValue]); +}; + + + +SWFUpload.prototype.uploadProgress = function (file, bytesComplete, bytesTotal) { + file = this.unescapeFilePostParams(file); + this.queueEvent("upload_progress_handler", [file, bytesComplete, bytesTotal]); +}; + +SWFUpload.prototype.uploadError = function (file, errorCode, message) { + file = this.unescapeFilePostParams(file); + this.queueEvent("upload_error_handler", [file, errorCode, message]); +}; + +SWFUpload.prototype.uploadSuccess = function (file, serverData, responseReceived) { + file = this.unescapeFilePostParams(file); + this.queueEvent("upload_success_handler", [file, serverData, responseReceived]); +}; + +SWFUpload.prototype.uploadComplete = function (file) { + file = this.unescapeFilePostParams(file); + this.queueEvent("upload_complete_handler", file); +}; + +/* Called by SWFUpload JavaScript and Flash functions when debug is enabled. By default it writes messages to the + internal debug console. You can override this event and have messages written where you want. */ +SWFUpload.prototype.debug = function (message) { + this.queueEvent("debug_handler", message); +}; + + +/* ********************************** + Debug Console + The debug console is a self contained, in page location + for debug message to be sent. The Debug Console adds + itself to the body if necessary. + + The console is automatically scrolled as messages appear. + + If you are using your own debug handler or when you deploy to production and + have debug disabled you can remove these functions to reduce the file size + and complexity. +********************************** */ + +// Private: debugMessage is the default debug_handler. If you want to print debug messages +// call the debug() function. When overriding the function your own function should +// check to see if the debug setting is true before outputting debug information. +SWFUpload.prototype.debugMessage = function (message) { + var exceptionMessage, exceptionValues, key; + + if (this.settings.debug) { + exceptionValues = []; + + // Check for an exception object and print it nicely + if (typeof message === "object" && typeof message.name === "string" && typeof message.message === "string") { + for (key in message) { + if (message.hasOwnProperty(key)) { + exceptionValues.push(key + ": " + message[key]); + } + } + exceptionMessage = exceptionValues.join("\n") || ""; + exceptionValues = exceptionMessage.split("\n"); + exceptionMessage = "EXCEPTION: " + exceptionValues.join("\nEXCEPTION: "); + SWFUpload.Console.writeLine(exceptionMessage); + } else { + SWFUpload.Console.writeLine(message); + } + } +}; + +SWFUpload.Console = {}; +SWFUpload.Console.writeLine = function (message) { + var console, documentForm; + + try { + console = document.getElementById("SWFUpload_Console"); + + if (!console) { + documentForm = document.createElement("form"); + document.getElementsByTagName("body")[0].appendChild(documentForm); + + console = document.createElement("textarea"); + console.id = "SWFUpload_Console"; + console.style.fontFamily = "monospace"; + console.setAttribute("wrap", "off"); + console.wrap = "off"; + console.style.overflow = "auto"; + console.style.width = "700px"; + console.style.height = "350px"; + console.style.margin = "5px"; + documentForm.appendChild(console); + } + + console.value += message + "\n"; + + console.scrollTop = console.scrollHeight - console.clientHeight; + } catch (ex) { + alert("Exception: " + ex.name + " Message: " + ex.message); + } +}; + + +/* SWFObject v2.2 + is released under the MIT License +*/ +swfobject = function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab= Build.VERSION_CODES.O) createChannelId() - applyDayNight() - LiveEventBus - .config() - .supportBroadcast(this) + LanguageUtils.setConfiguration(this) + createNotificationChannels() + applyDayNight(this) + LiveEventBus.config() .lifecycleObserverAlwaysActive(true) .autoClear(false) - - registerActivityLifecycleCallbacks(ActivityHelp) + registerActivityLifecycleCallbacks(LifecycleHelp) + defaultSharedPreferences.registerOnSharedPreferenceChangeListener(AppConfig) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES, - Configuration.UI_MODE_NIGHT_NO -> applyDayNight() + Configuration.UI_MODE_NIGHT_NO -> applyDayNight(this) } } - /** - * 更新主题 - */ - fun applyTheme() { - when { - AppConfig.isEInkMode -> { - ThemeStore.editTheme(this) - .coloredNavigationBar(true) - .primaryColor(Color.WHITE) - .accentColor(Color.BLACK) - .backgroundColor(Color.WHITE) - .bottomBackground(Color.WHITE) - .apply() - } - AppConfig.isNightTheme -> { - val primary = - getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600)) - val accent = - getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_deep_orange_800)) - var background = - getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900)) - if (ColorUtils.isColorLight(background)) { - background = getCompatColor(R.color.md_grey_900) - putPrefInt(PreferKey.cNBackground, background) - } - val bBackground = - getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_850)) - ThemeStore.editTheme(this) - .coloredNavigationBar(true) - .primaryColor(ColorUtils.withAlpha(primary, 1f)) - .accentColor(ColorUtils.withAlpha(accent, 1f)) - .backgroundColor(ColorUtils.withAlpha(background, 1f)) - .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) - .apply() - } - else -> { - val primary = - getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500)) - val accent = - getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_red_600)) - var background = - getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100)) - if (!ColorUtils.isColorLight(background)) { - background = getCompatColor(R.color.md_grey_100) - putPrefInt(PreferKey.cBackground, background) - } - val bBackground = - getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200)) - ThemeStore.editTheme(this) - .coloredNavigationBar(true) - .primaryColor(ColorUtils.withAlpha(primary, 1f)) - .accentColor(ColorUtils.withAlpha(accent, 1f)) - .backgroundColor(ColorUtils.withAlpha(background, 1f)) - .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) - .apply() - } - } - } - - fun applyDayNight() { - ReadBookConfig.upBg() - applyTheme() - initNightMode() - postEvent(EventBus.RECREATE, "") - } - - private fun initNightMode() { - val targetMode = - if (AppConfig.isNightTheme) { - AppCompatDelegate.MODE_NIGHT_YES - } else { - AppCompatDelegate.MODE_NIGHT_NO - } - AppCompatDelegate.setDefaultNightMode(targetMode) - } - /** * 创建通知ID */ - @RequiresApi(Build.VERSION_CODES.O) - private fun createChannelId() { + private fun createNotificationChannels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return (getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let { - //用唯一的ID创建渠道对象 val downloadChannel = NotificationChannel( channelIdDownload, - getString(R.string.download_offline), - NotificationManager.IMPORTANCE_LOW - ) - //初始化channel - downloadChannel.enableLights(false) - downloadChannel.enableVibration(false) - downloadChannel.setSound(null, null) + getString(R.string.action_download), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + } - //用唯一的ID创建渠道对象 val readAloudChannel = NotificationChannel( channelIdReadAloud, getString(R.string.read_aloud), - NotificationManager.IMPORTANCE_LOW - ) - //初始化channel - readAloudChannel.enableLights(false) - readAloudChannel.enableVibration(false) - readAloudChannel.setSound(null, null) + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + } - //用唯一的ID创建渠道对象 val webChannel = NotificationChannel( channelIdWeb, getString(R.string.web_service), - NotificationManager.IMPORTANCE_LOW - ) - //初始化channel - webChannel.enableLights(false) - webChannel.enableVibration(false) - webChannel.setSound(null, null) + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + } //向notification manager 提交channel it.createNotificationChannels(listOf(downloadChannel, readAloudChannel, webChannel)) } } + companion object { + var navigationBarHeight = 0 + } + } diff --git a/app/src/main/java/io/legado/app/README.md b/app/src/main/java/io/legado/app/README.md index b23d4196d..a05f70415 100644 --- a/app/src/main/java/io/legado/app/README.md +++ b/app/src/main/java/io/legado/app/README.md @@ -1,4 +1,4 @@ -## 文件结构介绍 +# 文件结构介绍 * base 基类 * constant 常量 diff --git a/app/src/main/java/io/legado/app/api/ReaderProvider.kt b/app/src/main/java/io/legado/app/api/ReaderProvider.kt index fcf50aa9c..cc054f9c4 100644 --- a/app/src/main/java/io/legado/app/api/ReaderProvider.kt +++ b/app/src/main/java/io/legado/app/api/ReaderProvider.kt @@ -4,19 +4,14 @@ package io.legado.app.api import android.content.ContentProvider -import android.content.ContentResolver import android.content.ContentValues import android.content.UriMatcher -import android.database.CharArrayBuffer -import android.database.ContentObserver import android.database.Cursor -import android.database.DataSetObserver +import android.database.MatrixCursor import android.net.Uri -import android.os.Bundle import com.google.gson.Gson -import io.legado.app.web.controller.BookshelfController -import io.legado.app.web.controller.SourceController -import io.legado.app.web.utils.ReturnData +import io.legado.app.api.controller.BookController +import io.legado.app.api.controller.SourceController import java.util.* /** @@ -24,7 +19,8 @@ import java.util.* */ class ReaderProvider : ContentProvider() { private enum class RequestCode { - SaveSource, SaveSources, SaveBook, DeleteSources, GetSource, GetSources, GetBookshelf, GetChapterList, GetBookContent + SaveSource, SaveSources, SaveBook, DeleteSources, GetSource, GetSources, + GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover } private val postBodyKey = "json" @@ -38,8 +34,10 @@ class ReaderProvider : ContentProvider() { addURI(authority, "source/query", RequestCode.GetSource.ordinal) addURI(authority, "sources/query", RequestCode.GetSources.ordinal) addURI(authority, "books/query", RequestCode.GetBookshelf.ordinal) + addURI(authority, "book/refreshToc/query", RequestCode.RefreshToc.ordinal) addURI(authority, "book/chapter/query", RequestCode.GetChapterList.ordinal) addURI(authority, "book/content/query", RequestCode.GetBookContent.ordinal) + addURI(authority, "book/cover/query", RequestCode.GetBookCover.ordinal) } } } @@ -70,7 +68,7 @@ class ReaderProvider : ContentProvider() { SourceController.saveSource(values.getAsString(postBodyKey)) } RequestCode.SaveBook -> values?.let { - BookshelfController.saveBook(values.getAsString(postBodyKey)) + BookController.saveBook(values.getAsString(postBodyKey)) } RequestCode.SaveSources -> values?.let { SourceController.saveSources(values.getAsString(postBodyKey)) @@ -93,12 +91,17 @@ class ReaderProvider : ContentProvider() { uri.getQueryParameter("index")?.let { map["index"] = arrayListOf(it) } + uri.getQueryParameter("path")?.let { + map["path"] = arrayListOf(it) + } return if (sMatcher.match(uri) < 0) null else when (RequestCode.values()[sMatcher.match(uri)]) { RequestCode.GetSource -> SimpleCursor(SourceController.getSource(map)) RequestCode.GetSources -> SimpleCursor(SourceController.sources) - RequestCode.GetBookshelf -> SimpleCursor(BookshelfController.bookshelf) - RequestCode.GetBookContent -> SimpleCursor(BookshelfController.getBookContent(map)) - RequestCode.GetChapterList -> SimpleCursor(BookshelfController.getChapterList(map)) + RequestCode.GetBookshelf -> SimpleCursor(BookController.bookshelf) + RequestCode.GetBookContent -> SimpleCursor(BookController.getBookContent(map)) + RequestCode.RefreshToc -> SimpleCursor(BookController.refreshToc(map)) + RequestCode.GetChapterList -> SimpleCursor(BookController.getChapterList(map)) + RequestCode.GetBookCover -> SimpleCursor(BookController.getCover(map)) else -> throw IllegalStateException( "Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name ) @@ -109,99 +112,20 @@ class ReaderProvider : ContentProvider() { uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array? ) = throw UnsupportedOperationException("Not yet implemented") - + /** * Simple inner class to deliver json callback data. * * Only getString() makes sense. */ - private class SimpleCursor(data: ReturnData?) : Cursor { + private class SimpleCursor(data: ReturnData?) : MatrixCursor(arrayOf("result"), 1) { private val mData: String = Gson().toJson(data) - override fun getCount() = 1 - - override fun getPosition() = 0 - - override fun move(i: Int) = true - - override fun moveToPosition(i: Int) = true - - override fun moveToFirst() = true - - override fun moveToLast() = true - - override fun moveToNext() = true - - override fun moveToPrevious() = true - - override fun isFirst() = true - - override fun isLast() = true - - override fun isBeforeFirst() = true - - override fun isAfterLast() = true - - override fun getColumnIndex(s: String) = 0 - - @Throws(IllegalArgumentException::class) - override fun getColumnIndexOrThrow(s: String): Int { - throw UnsupportedOperationException("Not yet implemented") + init { + addRow(arrayOf(mData)) } - override fun getColumnName(i: Int) = null as String? - - override fun getColumnNames() = arrayOf() - - override fun getColumnCount() = 0 - - override fun getBlob(i: Int) = ByteArray(0) - - override fun getString(i: Int) = mData - - override fun copyStringToBuffer( - i: Int, - charArrayBuffer: CharArrayBuffer - ) { - } - - override fun getShort(i: Int) = 0.toShort() - - - override fun getInt(i: Int) = 0 - - override fun getLong(i: Int) = 0L - - override fun getFloat(i: Int) = 0F - - override fun getDouble(i: Int) = 0.toDouble() - - override fun getType(i: Int) = 0 - - override fun isNull(i: Int) = false - - override fun deactivate() {} - override fun requery() = false - - override fun close() {} - override fun isClosed() = false - - override fun registerContentObserver(contentObserver: ContentObserver) {} - override fun unregisterContentObserver(contentObserver: ContentObserver) {} - override fun registerDataSetObserver(dataSetObserver: DataSetObserver) {} - override fun unregisterDataSetObserver(dataSetObserver: DataSetObserver) {} - override fun setNotificationUri(contentResolver: ContentResolver, uri: Uri) {} - - override fun getNotificationUri() = null as Uri? - - override fun getWantsAllOnMoveCalls() = false - - override fun setExtras(bundle: Bundle) {} - override fun getExtras() = null as Bundle? - - override fun respond(bundle: Bundle) = null as Bundle? - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/web/utils/ReturnData.kt b/app/src/main/java/io/legado/app/api/ReturnData.kt similarity index 63% rename from app/src/main/java/io/legado/app/web/utils/ReturnData.kt rename to app/src/main/java/io/legado/app/api/ReturnData.kt index 7c460e5e2..f6d6aefdc 100644 --- a/app/src/main/java/io/legado/app/web/utils/ReturnData.kt +++ b/app/src/main/java/io/legado/app/api/ReturnData.kt @@ -1,4 +1,4 @@ -package io.legado.app.web.utils +package io.legado.app.api class ReturnData { @@ -6,18 +6,13 @@ class ReturnData { var isSuccess: Boolean = false private set - var errorMsg: String? = null + var errorMsg: String = "未知错误,请联系开发者!" private set var data: Any? = null private set - init { - this.isSuccess = false - this.errorMsg = "未知错误,请联系开发者!" - } - - fun setErrorMsg(errorMsg: String?): ReturnData { + fun setErrorMsg(errorMsg: String): ReturnData { this.isSuccess = false this.errorMsg = errorMsg return this diff --git a/app/src/main/java/io/legado/app/api/controller/BookController.kt b/app/src/main/java/io/legado/app/api/controller/BookController.kt new file mode 100644 index 000000000..509bd2e6e --- /dev/null +++ b/app/src/main/java/io/legado/app/api/controller/BookController.kt @@ -0,0 +1,231 @@ +package io.legado.app.api.controller + +import androidx.core.graphics.drawable.toBitmap +import fi.iki.elonen.NanoFileUpload +import fi.iki.elonen.NanoHTTPD +import io.legado.app.R +import io.legado.app.api.ReturnData +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.help.BookHelp +import io.legado.app.help.ContentProcessor +import io.legado.app.help.ImageLoader +import io.legado.app.model.localBook.EpubFile +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.localBook.UmdFile +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.help.ReadBook +import io.legado.app.ui.widget.image.CoverImageView +import io.legado.app.utils.* +import kotlinx.coroutines.runBlocking +import org.apache.commons.fileupload.disk.DiskFileItemFactory +import splitties.init.appCtx + +object BookController { + + /** + * 书架所有书籍 + */ + val bookshelf: ReturnData + get() { + val books = appDb.bookDao.all + val returnData = ReturnData() + return if (books.isEmpty()) { + returnData.setErrorMsg("还没有添加小说") + } else { + val data = when (appCtx.getPrefInt(PreferKey.bookshelfSort)) { + 1 -> books.sortedByDescending { it.latestChapterTime } + 2 -> books.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + 3 -> books.sortedBy { it.order } + else -> books.sortedByDescending { it.durChapterTime } + } + returnData.setData(data) + } + } + + fun getCover(parameters: Map>): ReturnData { + val returnData = ReturnData() + val coverPath = parameters["path"]?.firstOrNull() + val ftBitmap = ImageLoader.loadBitmap(appCtx, coverPath).submit() + return try { + returnData.setData(ftBitmap.get()) + } catch (e: Exception) { + returnData.setData(CoverImageView.defaultDrawable.toBitmap()) + } + } + + /** + * 更新目录 + */ + fun refreshToc(parameters: Map>): ReturnData { + val returnData = ReturnData() + try { + val bookUrl = parameters["url"]?.firstOrNull() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + val book = appDb.bookDao.getBook(bookUrl) + ?: return returnData.setErrorMsg("bookUrl不对") + if (book.isLocalBook()) { + val toc = LocalBook.getChapterList(book) + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + appDb.bookDao.update(book) + return if (toc.isEmpty()) { + returnData.setErrorMsg(appCtx.getString(R.string.error_load_toc)) + } else { + returnData.setData(toc) + } + } else { + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: return returnData.setErrorMsg("未找到对应书源,请换源") + val webBook = WebBook(bookSource) + val toc = runBlocking { + if (book.tocUrl.isBlank()) { + webBook.getBookInfoAwait(this, book) + } + webBook.getChapterListAwait(this, book) + } + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + appDb.bookDao.update(book) + return if (toc.isEmpty()) { + returnData.setErrorMsg(appCtx.getString(R.string.error_load_toc)) + } else { + returnData.setData(toc) + } + } + } catch (e: Exception) { + return returnData.setErrorMsg(e.localizedMessage ?: "refresh toc error") + } + } + + /** + * 获取目录 + */ + fun getChapterList(parameters: Map>): ReturnData { + val bookUrl = parameters["url"]?.firstOrNull() + val returnData = ReturnData() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + val chapterList = appDb.bookChapterDao.getChapterList(bookUrl) + if (chapterList.isEmpty()) { + return refreshToc(parameters) + } + return returnData.setData(chapterList) + } + + /** + * 获取正文 + */ + fun getBookContent(parameters: Map>): ReturnData { + val bookUrl = parameters["url"]?.firstOrNull() + val index = parameters["index"]?.firstOrNull()?.toInt() + val returnData = ReturnData() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + if (index == null) { + return returnData.setErrorMsg("参数index不能为空, 请指定目录序号") + } + val book = appDb.bookDao.getBook(bookUrl) + val chapter = appDb.bookChapterDao.getChapter(bookUrl, index) + if (book == null || chapter == null) { + return returnData.setErrorMsg("未找到") + } + var content: String? = BookHelp.getContent(book, chapter) + if (content != null) { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + saveBookReadIndex(book, index) + return returnData.setData( + contentProcessor.getContent(book, chapter.title, content) + .joinToString("\n") + ) + } + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: return returnData.setErrorMsg("未找到书源") + try { + content = runBlocking { + WebBook(bookSource).getContentAwait(this, book, chapter) + } + val contentProcessor = ContentProcessor.get(book.name, book.origin) + saveBookReadIndex(book, index) + returnData.setData( + contentProcessor.getContent(book, chapter.title, content) + .joinToString("\n") + ) + } catch (e: Exception) { + returnData.setErrorMsg(e.msg) + } + return returnData + } + + fun saveBook(postData: String?): ReturnData { + val book = GSON.fromJsonObject(postData) + val returnData = ReturnData() + if (book != null) { + book.save() + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.book = book + ReadBook.durChapterIndex = book.durChapterIndex + } + return returnData.setData("") + } + return returnData.setErrorMsg("格式不对") + } + + private fun saveBookReadIndex(book: Book, index: Int) { + if (index > book.durChapterIndex) { + book.durChapterIndex = index + book.durChapterTime = System.currentTimeMillis() + appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { + book.durChapterTitle = it.title + } + appDb.bookDao.update(book) + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.book = book + ReadBook.durChapterIndex = index + } + } + } + + private val uploader by lazy { + val dif = DiskFileItemFactory(0, LocalBook.cacheFolder) + NanoFileUpload(dif) + } + + fun addLocalBook(session: NanoHTTPD.IHTTPSession, postData: String?): ReturnData { + val returnData = ReturnData() + try { + uploader.parseRequest(session).forEach { + val path = FileUtils.getPath(LocalBook.cacheFolder, it.name) + val nameAuthor = LocalBook.analyzeNameAuthor(it.name) + val book = Book( + bookUrl = path, + name = nameAuthor.first, + author = nameAuthor.second, + originName = it.name, + coverUrl = FileUtils.getPath( + appCtx.externalFiles, + "covers", + "${MD5Utils.md5Encode16(path)}.jpg" + ) + ) + if (book.isEpub()) EpubFile.upBookInfo(book) + if (book.isUmd()) UmdFile.upBookInfo(book) + appDb.bookDao.insert(book) + } + } catch (e: Exception) { + e.printStackTrace() + return returnData.setErrorMsg( + e.localizedMessage ?: appCtx.getString(R.string.unknown_error) + ) + } + return returnData.setData(true) + } + +} diff --git a/app/src/main/java/io/legado/app/web/controller/SourceController.kt b/app/src/main/java/io/legado/app/api/controller/SourceController.kt similarity index 80% rename from app/src/main/java/io/legado/app/web/controller/SourceController.kt rename to app/src/main/java/io/legado/app/api/controller/SourceController.kt index a76fe31e9..f4a92cd6d 100644 --- a/app/src/main/java/io/legado/app/web/controller/SourceController.kt +++ b/app/src/main/java/io/legado/app/api/controller/SourceController.kt @@ -1,19 +1,20 @@ -package io.legado.app.web.controller +package io.legado.app.api.controller import android.text.TextUtils -import io.legado.app.App +import io.legado.app.api.ReturnData +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonObject -import io.legado.app.web.utils.ReturnData +import io.legado.app.utils.msg object SourceController { val sources: ReturnData get() { - val bookSources = App.db.bookSourceDao().all + val bookSources = appDb.bookSourceDao.all val returnData = ReturnData() return if (bookSources.isEmpty()) { returnData.setErrorMsg("设备书源列表为空") @@ -22,20 +23,20 @@ object SourceController { fun saveSource(postData: String?): ReturnData { val returnData = ReturnData() - try { + kotlin.runCatching { val bookSource = GSON.fromJsonObject(postData) if (bookSource != null) { if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) { returnData.setErrorMsg("书源名称和URL不能为空") } else { - App.db.bookSourceDao().insert(bookSource) + appDb.bookSourceDao.insert(bookSource) returnData.setData("") } } else { returnData.setErrorMsg("转换书源失败") } - } catch (e: Exception) { - returnData.setErrorMsg(e.localizedMessage) + }.onFailure { + returnData.setErrorMsg(it.msg) } return returnData } @@ -49,7 +50,7 @@ object SourceController { if (bookSource.bookSourceName.isBlank() || bookSource.bookSourceUrl.isBlank()) { continue } - App.db.bookSourceDao().insert(bookSource) + appDb.bookSourceDao.insert(bookSource) okSources.add(bookSource) } } @@ -58,12 +59,12 @@ object SourceController { } fun getSource(parameters: Map>): ReturnData { - val url = parameters["url"]?.getOrNull(0) + val url = parameters["url"]?.firstOrNull() val returnData = ReturnData() if (url.isNullOrEmpty()) { return returnData.setErrorMsg("参数url不能为空,请指定书源地址") } - val bookSource = App.db.bookSourceDao().getBookSource(url) + val bookSource = appDb.bookSourceDao.getBookSource(url) ?: return returnData.setErrorMsg("未找到书源,请检查书源地址") return returnData.setData(bookSource) } @@ -72,7 +73,7 @@ object SourceController { kotlin.runCatching { GSON.fromJsonArray(postData)?.let { it.forEach { source -> - App.db.bookSourceDao().delete(source) + appDb.bookSourceDao.delete(source) } } } diff --git a/app/src/main/java/io/legado/app/base/BaseActivity.kt b/app/src/main/java/io/legado/app/base/BaseActivity.kt index b1d3c76d9..6406c18bf 100644 --- a/app/src/main/java/io/legado/app/base/BaseActivity.kt +++ b/app/src/main/java/io/legado/app/base/BaseActivity.kt @@ -2,18 +2,24 @@ package io.legado.app.base import android.content.Context import android.content.res.Configuration +import android.os.Build import android.os.Bundle import android.util.AttributeSet import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.WindowManager import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import io.legado.app.App import io.legado.app.R import io.legado.app.constant.AppConst +import io.legado.app.constant.PreferKey import io.legado.app.constant.Theme +import io.legado.app.help.AppConfig +import io.legado.app.help.ThemeConfig import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.TitleBar @@ -23,18 +29,20 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel -abstract class BaseActivity( - private val layoutID: Int, +abstract class BaseActivity( val fullScreen: Boolean = true, private val theme: Theme = Theme.Auto, private val toolBarTheme: Theme = Theme.Auto, - private val transparent: Boolean = false + private val transparent: Boolean = false, + private val imageBg: Boolean = true ) : AppCompatActivity(), CoroutineScope by MainScope() { + protected abstract val binding: VB + val isInMultiWindow: Boolean get() { - return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { isInMultiWindowMode } else { false @@ -58,12 +66,32 @@ abstract class BaseActivity( } override fun onCreate(savedInstanceState: Bundle?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + getPrefBoolean(PreferKey.highBrush) + ) { + /** + * 添加高刷新率支持 + */ + // 获取系统window支持的模式 + @Suppress("DEPRECATION") + val modes = window.windowManager.defaultDisplay.supportedModes + // 对获取的模式,基于刷新率的大小进行排序,从小到大排序 + modes.sortBy { + it.refreshRate + } + window.let { + val lp = it.attributes + // 取出最大的那一个刷新率,直接设置给window + lp.preferredDisplayModeId = modes.last().modeId + it.attributes = lp + } + } window.decorView.disableAutoFill() initTheme() - setupSystemBar() super.onCreate(savedInstanceState) - setContentView(layoutID) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + setContentView(binding.root) + setupSystemBar() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { findViewById(R.id.title_bar) ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen) } @@ -71,6 +99,13 @@ abstract class BaseActivity( observeLiveBus() } + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + App.navigationBarHeight = navigationBarHeight + } + } + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) findViewById(R.id.title_bar) @@ -100,24 +135,19 @@ abstract class BaseActivity( } ?: super.onCreateOptionsMenu(menu) } - override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean { - menu?.let { - menu.applyOpenTint(this) - return super.onMenuOpened(featureId, menu) - } - return true + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.applyOpenTint(this) + return super.onMenuOpened(featureId, menu) } open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu) - final override fun onOptionsItemSelected(item: MenuItem?): Boolean { - item?.let { - if (it.itemId == android.R.id.home) { - supportFinishAfterTransition() - return true - } + final override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + supportFinishAfterTransition() + return true } - return item != null && onCompatOptionsItemSelected(item) + return onCompatOptionsItemSelected(item) } open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item) @@ -142,29 +172,39 @@ abstract class BaseActivity( ATH.applyBackgroundTint(window.decorView) } } + if (imageBg) { + try { + ThemeConfig.getBgImage(this)?.let { + window.decorView.background = it + } + } catch (e: OutOfMemoryError) { + toastOnUi(e.localizedMessage) + } catch (e: Exception) { + toastOnUi(e.localizedMessage) + } + } } private fun setupSystemBar() { if (fullScreen && !isInMultiWindow) { - 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.fullScreen(this) } ATH.setStatusBarColorAuto(this, fullScreen) if (toolBarTheme == Theme.Dark) { - ATH.setLightStatusBar(window, false) + ATH.setLightStatusBar(this, false) } else if (toolBarTheme == Theme.Light) { - ATH.setLightStatusBar(window, true) + ATH.setLightStatusBar(this, true) } upNavigationBarColor() } open fun upNavigationBarColor() { - ATH.setNavigationBarColorAuto(this) + if (AppConfig.immNavigationBar) { + ATH.setNavigationBarColorAuto(this, ThemeStore.navigationBarColor(this)) + } else { + val nbColor = ColorUtils.darkenColor(ThemeStore.navigationBarColor(this)) + ATH.setNavigationBarColorAuto(this, nbColor) + } } open fun observeLiveBus() { diff --git a/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt index 6b2ca45ba..555ef1306 100644 --- a/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt @@ -8,19 +8,12 @@ import io.legado.app.help.coroutine.Coroutine import io.legado.app.lib.theme.ThemeStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext -abstract class BaseDialogFragment : DialogFragment(), CoroutineScope { - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - private lateinit var job: Job - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - job = Job() - } +abstract class BaseDialogFragment : DialogFragment(), CoroutineScope by MainScope() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -32,19 +25,16 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope { abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) override fun show(manager: FragmentManager, tag: String?) { - try { + kotlin.runCatching { //在每个add事务前增加一个remove事务,防止连续的add manager.beginTransaction().remove(this).commit() super.show(manager, tag) - } catch (e: Exception) { - //同一实例使用不同的tag会异常,这里捕获一下 - e.printStackTrace() } } override fun onDestroy() { super.onDestroy() - job.cancel() + cancel() } fun execute( diff --git a/app/src/main/java/io/legado/app/base/BaseFragment.kt b/app/src/main/java/io/legado/app/base/BaseFragment.kt index 155bb6076..c95a5ab67 100644 --- a/app/src/main/java/io/legado/app/base/BaseFragment.kt +++ b/app/src/main/java/io/legado/app/base/BaseFragment.kt @@ -3,7 +3,10 @@ package io.legado.app.base import android.annotation.SuppressLint import android.content.res.Configuration import android.os.Bundle -import android.view.* +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment @@ -11,13 +14,13 @@ import io.legado.app.R import io.legado.app.ui.widget.TitleBar import io.legado.app.utils.applyTint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +@Suppress("MemberVisibilityCanBePrivate") abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), - CoroutineScope { - lateinit var job: Job + CoroutineScope by MainScope() { + var supportToolbar: Toolbar? = null private set @@ -25,18 +28,6 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), @SuppressLint("RestrictedApi") get() = SupportMenuInflater(requireContext()) - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - job = Job() - return super.onCreateView(inflater, container, savedInstanceState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) onMultiWindowModeChanged() @@ -57,7 +48,7 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), } private fun onMultiWindowModeChanged() { - (activity as? BaseActivity)?.let { + (activity as? BaseActivity<*>)?.let { view?.findViewById(R.id.title_bar) ?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen) } @@ -65,7 +56,7 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), override fun onDestroy() { super.onDestroy() - job.cancel() + cancel() } fun setSupportToolbar(toolbar: Toolbar) { diff --git a/app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt b/app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt index b2e467b7c..f444985e3 100644 --- a/app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt +++ b/app/src/main/java/io/legado/app/base/BasePreferenceFragment.kt @@ -53,7 +53,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() { ) } } + @Suppress("DEPRECATION") f.setTargetFragment(this, 0) + f.show(parentFragmentManager, dialogFragmentTag) } diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt index f00540526..54b409f39 100644 --- a/app/src/main/java/io/legado/app/base/BaseService.kt +++ b/app/src/main/java/io/legado/app/base/BaseService.kt @@ -3,6 +3,8 @@ package io.legado.app.base import android.app.Service import android.content.Intent import android.os.IBinder +import androidx.annotation.CallSuper +import io.legado.app.help.LifecycleHelp import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -18,10 +20,26 @@ abstract class BaseService : Service(), CoroutineScope by MainScope() { block: suspend CoroutineScope.() -> T ) = Coroutine.async(scope, context) { block() } - override fun onBind(intent: Intent?) = null + @CallSuper + override fun onCreate() { + super.onCreate() + LifecycleHelp.onServiceCreate(this) + } + + @CallSuper + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + stopSelf() + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + @CallSuper override fun onDestroy() { super.onDestroy() cancel() + LifecycleHelp.onServiceDestroy(this) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt index c8332e0cc..2422a8a15 100644 --- a/app/src/main/java/io/legado/app/base/BaseViewModel.kt +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -2,23 +2,22 @@ package io.legado.app.base import android.app.Application import android.content.Context -import androidx.annotation.CallSuper import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope 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 kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext -open class BaseViewModel(application: Application) : AndroidViewModel(application), - CoroutineScope by MainScope(), - AnkoLogger { +@Suppress("unused") +open class BaseViewModel(application: Application) : AndroidViewModel(application) { val context: Context by lazy { this.getApplication() } fun execute( - scope: CoroutineScope = this, + scope: CoroutineScope = viewModelScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> T ): Coroutine { @@ -26,40 +25,11 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio } fun submit( - scope: CoroutineScope = this, + scope: CoroutineScope = viewModelScope, context: CoroutineContext = Dispatchers.IO, block: suspend CoroutineScope.() -> Deferred ): Coroutine { return Coroutine.async(scope, context) { block().await() } } - @CallSuper - override fun onCleared() { - super.onCleared() - cancel() - } - - open fun toast(message: Int) { - launch { - context.toast(message) - } - } - - open fun toast(message: CharSequence?) { - launch { - context.toast(message ?: toString()) - } - } - - open fun longToast(message: Int) { - launch { - context.toast(message) - } - } - - open fun longToast(message: CharSequence?) { - launch { - context.toast(message ?: toString()) - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/README.md b/app/src/main/java/io/legado/app/base/README.md index d28e27909..ba3d001f1 100644 --- a/app/src/main/java/io/legado/app/base/README.md +++ b/app/src/main/java/io/legado/app/base/README.md @@ -1 +1 @@ -## 基类 \ No newline at end of file +# 基类 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt index f8de615de..3eeb5c91b 100644 --- a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt +++ b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt @@ -1,14 +1,16 @@ package io.legado.app.base import androidx.lifecycle.ViewModel +import androidx.viewbinding.ViewBinding import io.legado.app.constant.Theme -abstract class VMBaseActivity( - layoutID: Int, +abstract class VMBaseActivity( fullScreen: Boolean = true, theme: Theme = Theme.Auto, - toolBarTheme: Theme = Theme.Auto -) : BaseActivity(layoutID, fullScreen, theme, toolBarTheme) { + toolBarTheme: Theme = Theme.Auto, + transparent: Boolean = false, + imageBg: Boolean = true +) : BaseActivity(fullScreen, theme, toolBarTheme, transparent, imageBg) { protected abstract val viewModel: VM diff --git a/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt new file mode 100644 index 000000000..2575ea067 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt @@ -0,0 +1,216 @@ +package io.legado.app.base.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import splitties.views.onLongClick + +/** + * Created by Invincible on 2017/12/15. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +abstract class DiffRecyclerAdapter(protected val context: Context) : + RecyclerView.Adapter() { + + val inflater: LayoutInflater = LayoutInflater.from(context) + + private val asyncListDiffer: AsyncListDiffer by lazy { + AsyncListDiffer(this, diffItemCallback).apply { + addListListener { _, _ -> + onCurrentListChanged() + } + } + } + + private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null + private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null + + var itemAnimation: ItemAnimation? = null + + abstract val diffItemCallback: DiffUtil.ItemCallback + + fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { + itemClickListener = listener + } + + fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { + itemLongClickListener = listener + } + + fun bindToRecyclerView(recyclerView: RecyclerView) { + recyclerView.adapter = this + } + + @Synchronized + fun setItems(items: List?) { + kotlin.runCatching { + asyncListDiffer.submitList(items) + } + } + + @Synchronized + fun setItem(position: Int, item: ITEM) { + kotlin.runCatching { + val list = ArrayList(asyncListDiffer.currentList) + list[position] = item + asyncListDiffer.submitList(list) + } + } + + @Synchronized + fun updateItem(item: ITEM) { + kotlin.runCatching { + val index = asyncListDiffer.currentList.indexOf(item) + if (index >= 0) { + asyncListDiffer.currentList[index] = item + notifyItemChanged(index) + } + } + } + + @Synchronized + fun updateItem(position: Int, payload: Any) { + kotlin.runCatching { + val size = itemCount + if (position in 0 until size) { + notifyItemChanged(position, payload) + } + } + } + + @Synchronized + fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) { + kotlin.runCatching { + val size = itemCount + if (fromPosition in 0 until size && toPosition in 0 until size) { + notifyItemRangeChanged( + fromPosition, + toPosition - fromPosition + 1, + payloads + ) + } + } + } + + fun isEmpty() = asyncListDiffer.currentList.isEmpty() + + fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty() + + fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position) + + fun getItems(): List = asyncListDiffer.currentList + + /** + * grid 模式下使用 + */ + protected open fun getSpanSize(viewType: Int, position: Int) = 1 + + final override fun getItemCount() = getItems().size + + final override fun getItemViewType(position: Int): Int { + return 0 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + val holder = ItemViewHolder(getViewBinding(parent)) + + @Suppress("UNCHECKED_CAST") + registerListener(holder, (holder.binding as VB)) + + if (itemClickListener != null) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + itemClickListener?.invoke(holder, it) + } + } + } + + if (itemLongClickListener != null) { + holder.itemView.onLongClick { + getItem(holder.layoutPosition)?.let { + itemLongClickListener?.invoke(holder, it) + } + } + } + + return holder + } + + protected abstract fun getViewBinding(parent: ViewGroup): VB + + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} + + open fun onCurrentListChanged() { + + } + + @Suppress("UNCHECKED_CAST") + final override fun onBindViewHolder( + holder: ItemViewHolder, + position: Int, + payloads: MutableList + ) { + getItem(holder.layoutPosition)?.let { + convert(holder, (holder.binding as VB), it, payloads) + } + } + + override fun onViewAttachedToWindow(holder: ItemViewHolder) { + super.onViewAttachedToWindow(holder) + addAnimation(holder) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + val manager = recyclerView.layoutManager + if (manager is GridLayoutManager) { + manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return getSpanSize(getItemViewType(position), position) + } + } + } + } + + private fun addAnimation(holder: ItemViewHolder) { + itemAnimation?.let { + if (it.itemAnimEnabled) { + if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { + startAnimation(holder, it) + it.itemAnimStartPosition = holder.layoutPosition + } + } + } + } + + protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { + item.itemAnimation?.let { + for (anim in it.getAnimators(holder.itemView)) { + anim.setDuration(item.itemAnimDuration).start() + anim.interpolator = item.itemAnimInterpolator + } + } + } + + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item + */ + abstract fun convert( + holder: ItemViewHolder, + binding: VB, + item: ITEM, + payloads: MutableList + ) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder, binding: VB) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt deleted file mode 100644 index a679f3e08..000000000 --- a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt +++ /dev/null @@ -1,32 +0,0 @@ -package io.legado.app.base.adapter - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -/** - * Created by Invincible on 2017/12/15. - * - * 上拉加载更多 - */ -abstract class InfiniteScrollListener() : RecyclerView.OnScrollListener() { - private val loadMoreRunnable = Runnable { onLoadMore() } - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { -// if (dy < 0 || dataLoading.isDataLoading()) return - - val layoutManager: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager - val visibleItemCount = recyclerView.childCount - val totalItemCount = layoutManager.itemCount - val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() - - if (totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) { - recyclerView.post(loadMoreRunnable) - } - } - - abstract fun onLoadMore() - - companion object { - private const val VISIBLE_THRESHOLD = 5 - } -} diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt index e2c37d708..2285c8242 100644 --- a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt +++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt @@ -7,6 +7,7 @@ import io.legado.app.base.adapter.animations.* /** * Created by Invincible on 2017/12/15. */ +@Suppress("unused") class ItemAnimation private constructor() { var itemAnimEnabled = false @@ -52,28 +53,33 @@ class ItemAnimation private constructor() { companion object { const val NONE: Int = 0x00000000 + /** * Use with [.openLoadAnimation] */ const val FADE_IN: Int = 0x00000001 + /** * Use with [.openLoadAnimation] */ const val SCALE_IN: Int = 0x00000002 + /** * Use with [.openLoadAnimation] */ const val BOTTOM_SLIDE_IN: Int = 0x00000003 + /** * Use with [.openLoadAnimation] */ const val LEFT_SLIDE_IN: Int = 0x00000004 + /** * Use with [.openLoadAnimation] */ const val RIGHT_SLIDE_IN: Int = 0x00000005 fun create() = ItemAnimation() - + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt deleted file mode 100644 index 866a53b6f..000000000 --- a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.legado.app.base.adapter - -import android.content.Context - -/** - * Created by Invincible on 2017/11/24. - * - * item代理, - */ -abstract class ItemViewDelegate(protected val context: Context, val layoutId: Int) { - - /** - * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, - * 使用getItem(holder.layoutPosition)来获取item, - * 或者使用registerListener(holder: ItemViewHolder, position: Int) - */ - abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) - - - /** - * 注册事件 - */ - abstract fun registerListener(holder: ItemViewHolder) - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt index c415fa4b6..da57d4627 100644 --- a/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt @@ -1,9 +1,10 @@ package io.legado.app.base.adapter -import android.view.View import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding /** * Created by Invincible on 2017/11/28. */ -class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) \ No newline at end of file +@Suppress("MemberVisibilityCanBePrivate") +class ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt similarity index 57% rename from app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt rename to app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt index 48e72de45..d6749e63a 100644 --- a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt @@ -1,13 +1,15 @@ package io.legado.app.base.adapter +import android.annotation.SuppressLint import android.content.Context import android.util.SparseArray import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import splitties.views.onLongClick import java.util.* /** @@ -15,117 +17,78 @@ import java.util.* * * 通用的adapter 可添加header,footer,以及不同类型item */ -abstract class CommonRecyclerAdapter(protected val context: Context): +@Suppress("unused", "MemberVisibilityCanBePrivate") +abstract class RecyclerAdapter(protected val context: Context) : RecyclerView.Adapter() { - - constructor(context: Context, vararg delegates: ItemViewDelegate): this(context) { - addItemViewDelegates(*delegates) - } - - constructor( - context: Context, - vararg delegates: Pair> - ): this(context) { - addItemViewDelegates(*delegates) - } - - private val inflater: LayoutInflater = LayoutInflater.from(context) - - private var headerItems: SparseArray? = null - private var footerItems: SparseArray? = null - - private val itemDelegates: HashMap> = hashMapOf() + + val inflater: LayoutInflater = LayoutInflater.from(context) + + private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } + private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } + private val items: MutableList = mutableListOf() - - private val lock = Object() - + private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null - - // 这个用Kotlin的setter就行了, 不需要手动开一个函数进行设置 + var itemAnimation: ItemAnimation? = null - + fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { itemClickListener = listener } - + fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { itemLongClickListener = listener } - + fun bindToRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = this } - - fun > addItemViewDelegate(viewType: Int, delegate: DELEGATE) { - itemDelegates[viewType] = delegate - } - - fun > addItemViewDelegate(delegate: DELEGATE) { - itemDelegates[itemDelegates.size] = delegate - } - - fun > addItemViewDelegates(vararg delegates: DELEGATE) { - delegates.forEach { - addItemViewDelegate(it) + + @Synchronized + fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = headerItems.size() + headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header) + notifyItemInserted(index) } } - - fun addItemViewDelegates(vararg delegates: Pair>) = - delegates.forEach { - addItemViewDelegate(it.first, it.second) - } - - fun addHeaderView(header: View) { - synchronized(lock) { - if (headerItems == null) { - headerItems = SparseArray() - } - headerItems?.let { - val index = it.size() - it.put(TYPE_HEADER_VIEW + it.size(), header) - notifyItemInserted(index) - } + + @Synchronized + fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = getActualItemCount() + footerItems.size() + footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer) + notifyItemInserted(index) } } - - fun addFooterView(footer: View) = - synchronized(lock) { - if (footerItems == null) { - footerItems = SparseArray() - } - footerItems?.let { - val index = getActualItemCount() + it.size() - it.put(TYPE_FOOTER_VIEW + it.size(), footer) - notifyItemInserted(index) - } - } - - - fun removeHeaderView(header: View) = - synchronized(lock) { - headerItems?.let { - val index = it.indexOfValue(header) - if (index >= 0) { - it.remove(index) - notifyItemRemoved(index) - } + + @Synchronized + fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = headerItems.indexOfValue(header) + if (index >= 0) { + headerItems.remove(index) + notifyItemRemoved(index) } } - - fun removeFooterView(footer: View) = - synchronized(lock) { - footerItems?.let { - val index = it.indexOfValue(footer) - if (index >= 0) { - it.remove(index) - notifyItemRemoved(getActualItemCount() + index - 2) - } + } + + @Synchronized + fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = footerItems.indexOfValue(footer) + if (index >= 0) { + footerItems.remove(index) + notifyItemRemoved(getActualItemCount() + index - 2) } } - + } + + @SuppressLint("NotifyDataSetChanged") + @Synchronized fun setItems(items: List?) { - synchronized(lock) { + kotlin.runCatching { if (this.items.isNotEmpty()) { this.items.clear() } @@ -133,11 +96,50 @@ abstract class CommonRecyclerAdapter(protected val context: Context): this.items.addAll(items) } notifyDataSetChanged() + onCurrentListChanged() } } - - fun setItems(items: List?, diffResult: DiffUtil.DiffResult) { - synchronized(lock) { + + @Synchronized + fun setItems(items: List?, itemCallback: DiffUtil.ItemCallback) { + kotlin.runCatching { + val callback = object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return itemCount + } + + override fun getNewListSize(): Int { + return (items?.size ?: 0) + getHeaderCount() + getFooterCount() + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = getItem(oldItemPosition - getHeaderCount()) + ?: return true + val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) + ?: return true + return itemCallback.areItemsTheSame(oldItem, newItem) + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem = getItem(oldItemPosition - getHeaderCount()) + ?: return true + val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) + ?: return true + return itemCallback.areContentsTheSame(oldItem, newItem) + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldItem = getItem(oldItemPosition - getHeaderCount()) + ?: return null + val newItem = items?.getOrNull(newItemPosition - getHeaderCount()) + ?: return null + return itemCallback.getChangePayload(oldItem, newItem) + } + } + val diffResult = DiffUtil.calculateDiff(callback) if (this.items.isNotEmpty()) { this.items.clear() } @@ -145,38 +147,46 @@ abstract class CommonRecyclerAdapter(protected val context: Context): this.items.addAll(items) } diffResult.dispatchUpdatesTo(this) + onCurrentListChanged() } } - + + @Synchronized fun setItem(position: Int, item: ITEM) { - synchronized(lock) { + kotlin.runCatching { val oldSize = getActualItemCount() if (position in 0 until oldSize) { this.items[position] = item notifyItemChanged(position + getHeaderCount()) } + onCurrentListChanged() } } - + + @Synchronized fun addItem(item: ITEM) { - synchronized(lock) { + kotlin.runCatching { val oldSize = getActualItemCount() if (this.items.add(item)) { notifyItemInserted(oldSize + getHeaderCount()) } + onCurrentListChanged() } } - + + @Synchronized fun addItems(position: Int, newItems: List) { - synchronized(lock) { + kotlin.runCatching { if (this.items.addAll(position, newItems)) { notifyItemRangeInserted(position + getHeaderCount(), newItems.size) } + onCurrentListChanged() } } - + + @Synchronized fun addItems(newItems: List) { - synchronized(lock) { + kotlin.runCatching { val oldSize = getActualItemCount() if (this.items.addAll(newItems)) { if (oldSize == 0 && getHeaderCount() == 0) { @@ -185,65 +195,79 @@ abstract class CommonRecyclerAdapter(protected val context: Context): notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size) } } + onCurrentListChanged() } } - + + @Synchronized fun removeItem(position: Int) { - synchronized(lock) { + kotlin.runCatching { if (this.items.removeAt(position) != null) { notifyItemRemoved(position + getHeaderCount()) } + onCurrentListChanged() } } - + + @Synchronized fun removeItem(item: ITEM) { - synchronized(lock) { + kotlin.runCatching { if (this.items.remove(item)) { notifyItemRemoved(this.items.indexOf(item) + getHeaderCount()) } + onCurrentListChanged() } } - + + @Synchronized fun removeItems(items: List) { - synchronized(lock) { + kotlin.runCatching { if (this.items.removeAll(items)) { notifyDataSetChanged() } + onCurrentListChanged() } } - + + @Synchronized fun swapItem(oldPosition: Int, newPosition: Int) { - synchronized(lock) { + kotlin.runCatching { val size = getActualItemCount() if (oldPosition in 0 until size && newPosition in 0 until size) { val srcPosition = oldPosition + getHeaderCount() val targetPosition = newPosition + getHeaderCount() Collections.swap(this.items, srcPosition, targetPosition) - notifyItemChanged(srcPosition) - notifyItemChanged(targetPosition) + notifyItemMoved(srcPosition, targetPosition) } + onCurrentListChanged() } } - - fun updateItem(item: ITEM) = - synchronized(lock) { + + @Synchronized + fun updateItem(item: ITEM) { + kotlin.runCatching { val index = this.items.indexOf(item) if (index >= 0) { this.items[index] = item notifyItemChanged(index) } + onCurrentListChanged() } - - fun updateItem(position: Int, payload: Any) = - synchronized(lock) { + } + + @Synchronized + fun updateItem(position: Int, payload: Any) { + kotlin.runCatching { val size = getActualItemCount() if (position in 0 until size) { notifyItemChanged(position + getHeaderCount(), payload) } } - - fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) = - synchronized(lock) { + } + + @Synchronized + fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) { + kotlin.runCatching { val size = getActualItemCount() if (fromPosition in 0 until size && toPosition in 0 until size) { notifyItemRangeChanged( @@ -253,43 +277,47 @@ abstract class CommonRecyclerAdapter(protected val context: Context): ) } } - - fun clearItems() = - synchronized(lock) { + } + + @Synchronized + fun clearItems() { + kotlin.runCatching { this.items.clear() notifyDataSetChanged() + onCurrentListChanged() } - + } + fun isEmpty() = items.isEmpty() - + fun isNotEmpty() = items.isNotEmpty() - + /** * 除去header和footer */ fun getActualItemCount() = items.size - - - fun getHeaderCount() = headerItems?.size() ?: 0 - - - fun getFooterCount() = footerItems?.size() ?: 0 - + + + fun getHeaderCount() = headerItems.size() + + + fun getFooterCount() = footerItems.size() + fun getItem(position: Int): ITEM? = items.getOrNull(position) - + fun getItemByLayoutPosition(position: Int) = items.getOrNull(position - getHeaderCount()) - + fun getItems(): List = items - + protected open fun getItemViewType(item: ITEM, position: Int) = 0 - + /** * grid 模式下使用 */ - protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int) = 1 - + protected open fun getSpanSize(viewType: Int, position: Int) = 1 + final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount() - + final override fun getItemViewType(position: Int) = when { isHeader(position) -> TYPE_HEADER_VIEW + position isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() @@ -297,28 +325,26 @@ abstract class CommonRecyclerAdapter(protected val context: Context): getItemViewType(it, getActualPosition(position)) } ?: 0 } - + + open fun onCurrentListChanged() { + + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when { viewType < TYPE_HEADER_VIEW + getHeaderCount() -> { - ItemViewHolder(headerItems!!.get(viewType)) + ItemViewHolder(headerItems.get(viewType).invoke(parent)) } - + viewType >= TYPE_FOOTER_VIEW -> { - ItemViewHolder(footerItems!!.get(viewType)) + ItemViewHolder(footerItems.get(viewType).invoke(parent)) } - + else -> { - val holder = ItemViewHolder( - inflater.inflate( - itemDelegates.getValue(viewType).layoutId, - parent, - false - ) - ) - - itemDelegates.getValue(viewType) - .registerListener(holder) - + val holder = ItemViewHolder(getViewBinding(parent)) + + @Suppress("UNCHECKED_CAST") + registerListener(holder, (holder.binding as VB)) + if (itemClickListener != null) { holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { @@ -326,21 +352,24 @@ abstract class CommonRecyclerAdapter(protected val context: Context): } } } - + if (itemLongClickListener != null) { - holder.itemView.setOnLongClickListener { + holder.itemView.onLongClick { getItem(holder.layoutPosition)?.let { - itemLongClickListener?.invoke(holder, it) ?: true - } ?: true + itemLongClickListener?.invoke(holder, it) + } } } - + holder } } - + + protected abstract fun getViewBinding(parent: ViewGroup): VB + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} - + + @Suppress("UNCHECKED_CAST") final override fun onBindViewHolder( holder: ItemViewHolder, position: Int, @@ -348,41 +377,36 @@ abstract class CommonRecyclerAdapter(protected val context: Context): ) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { getItem(holder.layoutPosition - getHeaderCount())?.let { - itemDelegates.getValue(getItemViewType(holder.layoutPosition)) - .convert(holder, it, payloads) + convert(holder, (holder.binding as VB), it, payloads) } } } - + override fun onViewAttachedToWindow(holder: ItemViewHolder) { super.onViewAttachedToWindow(holder) if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { addAnimation(holder) } } - + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) val manager = recyclerView.layoutManager if (manager is GridLayoutManager) { - manager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() { + manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return getItem(position)?.let { - if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( - it, getItemViewType(position), position - ) - } ?: manager.spanCount + return getSpanSize(getItemViewType(position), position) } } } } - + private fun isHeader(position: Int) = position < getHeaderCount() - + private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount() - + private fun getActualPosition(position: Int) = position - getHeaderCount() - + private fun addAnimation(holder: ItemViewHolder) { itemAnimation?.let { if (it.itemAnimEnabled) { @@ -393,7 +417,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context): } } } - + protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { item.itemAnimation?.let { for (anim in it.getAnimators(holder.itemView)) { @@ -402,12 +426,28 @@ abstract class CommonRecyclerAdapter(protected val context: Context): } } } - + + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item + */ + abstract fun convert( + holder: ItemViewHolder, + binding: VB, + item: ITEM, + payloads: MutableList + ) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder, binding: VB) + companion object { private const val TYPE_HEADER_VIEW = Int.MIN_VALUE private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999 } - + } diff --git a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt deleted file mode 100644 index 81c831ff4..000000000 --- a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.legado.app.base.adapter - -import android.content.Context - -/** - * Created by Invincible on 2017/12/15. - */ -abstract class SimpleRecyclerAdapter(context: Context, private val layoutId: Int) : - CommonRecyclerAdapter(context) { - - init { - addItemViewDelegate(object : ItemViewDelegate(context, layoutId) { - - override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) { - this@SimpleRecyclerAdapter.convert(holder, item, payloads) - } - - override fun registerListener(holder: ItemViewHolder) { - this@SimpleRecyclerAdapter.registerListener(holder) - } - }) - } - - /** - * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, - * 使用getItem(holder.layoutPosition)来获取item - */ - abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) - - /** - * 注册事件 - */ - abstract fun registerListener(holder: ItemViewHolder) -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt index e3a5b523a..f5932a100 100644 --- a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt +++ b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt @@ -5,7 +5,8 @@ import android.animation.ObjectAnimator import android.view.View -class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation { +class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : + BaseAnimation { override fun getAnimators(view: View): Array = arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt index 3464f4079..9566e2ad0 100644 --- a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt +++ b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt @@ -5,7 +5,8 @@ import android.animation.ObjectAnimator import android.view.View -class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation { +class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : + BaseAnimation { override fun getAnimators(view: View): Array { val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f) diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt index d6bb81302..24ea55d1e 100644 --- a/app/src/main/java/io/legado/app/constant/AppConst.kt +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -1,9 +1,10 @@ package io.legado.app.constant import android.annotation.SuppressLint -import io.legado.app.App +import android.provider.Settings +import io.legado.app.BuildConfig import io.legado.app.R -import io.legado.app.data.entities.BookGroup +import splitties.init.appCtx import java.text.SimpleDateFormat import javax.script.ScriptEngine import javax.script.ScriptEngineManager @@ -19,10 +20,6 @@ object AppConst { const val UA_NAME = "User-Agent" - val userAgent: String by lazy { - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36" - } - val SCRIPT_ENGINE: ScriptEngine by lazy { ScriptEngineManager().getEngineByName("rhino") } @@ -41,16 +38,16 @@ object AppConst { val keyboardToolChars: List by lazy { arrayListOf( - "※", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", - "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", - "id", "class", "tag" + "❓", "@css:", "", "{{}}", "##", "&&", "%%", "||", "//", "\\", "$.", + "@", ":", "class", "text", "href", "textNodes", "ownText", "all", "html", + "[", "]", "<", ">", "#", "!", ".", "+", "-", "*", "=", "{'webView': true}" ) } - 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)) - val bookGroupNone = BookGroup(-4, App.INSTANCE.getString(R.string.no_group)) + const val bookGroupAllId = -1L + const val bookGroupLocalId = -2L + const val bookGroupAudioId = -3L + const val bookGroupNoneId = -4L const val notificationIdRead = 1144771 const val notificationIdAudio = 1144772 @@ -60,10 +57,12 @@ object AppConst { val urlOption: String by lazy { """ ,{ - "charset": "", - "method": "POST", - "body": "", - "headers": {"User-Agent": ""} + 'charset': '', + 'method': 'POST', + 'body': '', + 'headers': { + 'User-Agent': '' + } } """.trimIndent() } @@ -72,4 +71,42 @@ object AppConst { "com.android.internal.view.menu.ListMenuItemView", "androidx.appcompat.view.menu.ListMenuItemView" ) -} \ No newline at end of file + + val sysElevation = appCtx.resources.getDimension(R.dimen.design_appbar_elevation).toInt() + + val darkWebViewJs by lazy { + """ + document.body.style.backgroundColor = "#222222"; + document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a'; + """.trimIndent() + } + + val androidId: String by lazy { + Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) + } + + val appInfo: AppInfo by lazy { + val appInfo = AppInfo() + appCtx.packageManager.getPackageInfo(appCtx.packageName, 0)?.let { + appInfo.versionName = it.versionName + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + appInfo.versionCode = it.longVersionCode + } else { + @Suppress("DEPRECATION") + appInfo.versionCode = it.versionCode.toLong() + } + } + appInfo + } + + val charsets = + arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") + + data class AppInfo( + var versionCode: Long = 0L, + var versionName: String = "" + ) + + const val authority = BuildConfig.APPLICATION_ID + ".fileProvider" + +} diff --git a/app/src/main/java/io/legado/app/constant/AppPattern.kt b/app/src/main/java/io/legado/app/constant/AppPattern.kt index 2faf70daf..884b2717d 100644 --- a/app/src/main/java/io/legado/app/constant/AppPattern.kt +++ b/app/src/main/java/io/legado/app/constant/AppPattern.kt @@ -2,15 +2,17 @@ package io.legado.app.constant import java.util.regex.Pattern +@Suppress("RegExpRedundantEscape") object AppPattern { val JS_PATTERN: Pattern = - Pattern.compile("([\\w\\W]*?|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE) + Pattern.compile("([\\w\\W]*?)|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE) val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") - val imgPattern: Pattern = - Pattern.compile("", Pattern.CASE_INSENSITIVE) - val nameRegex = Regex("\\s+作\\s*者.*") - val authorRegex = Regex(".*?作\\s*?者[::]") + //匹配格式化后的图片格式 + val imgPattern: Pattern = Pattern.compile("]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>") + + val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") + val authorRegex = Regex("^.*?作\\s*者[::\\s]*|\\s+著") val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") - val splitGroupRegex = Regex("[,;]") -} \ No newline at end of file + val splitGroupRegex = Regex("[,;,;]") +} diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt index 6fe8b5cea..04f165a60 100644 --- a/app/src/main/java/io/legado/app/constant/EventBus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -3,7 +3,8 @@ package io.legado.app.constant object EventBus { const val MEDIA_BUTTON = "mediaButton" const val RECREATE = "RECREATE" - const val UP_BOOK = "upBookToc" + const val UP_BOOKSHELF = "upBookToc" + const val BOOKSHELF_REFRESH = "bookshelfRefresh" const val ALOUD_STATE = "aloud_state" const val TTS_PROGRESS = "ttsStart" const val TTS_DS = "ttsDs" @@ -16,8 +17,11 @@ object EventBus { const val AUDIO_PROGRESS = "audioProgress" const val AUDIO_SIZE = "audioSize" const val AUDIO_SPEED = "audioSpeed" - const val SHOW_RSS = "showRss" - const val WEB_SERVICE_STOP = "webServiceStop" + const val NOTIFY_MAIN = "notifyMain" + const val WEB_SERVICE = "webService" const val UP_DOWNLOAD = "upDownload" const val SAVE_CONTENT = "saveContent" + const val CHECK_SOURCE = "checkSource" + const val CHECK_SOURCE_DONE = "checkSourceDone" + const val TIP_COLOR = "tipColor" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index c6e222849..b92ad091c 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -1,27 +1,40 @@ package io.legado.app.constant object PreferKey { - const val versionCode = "versionCode" const val language = "language" const val themeMode = "themeMode" + const val userAgent = "userAgent" + const val showUnread = "showUnread" + const val bookGroupStyle = "bookGroupStyle" + const val useDefaultCover = "useDefaultCover" const val hideStatusBar = "hideStatusBar" - const val clickTurnPage = "clickTurnPage" - const val clickAllNext = "clickAllNext" + const val clickActionTL = "clickActionTopLeft" + const val clickActionTC = "clickActionTopCenter" + const val clickActionTR = "clickActionTopRight" + const val clickActionML = "clickActionMiddleLeft" + const val clickActionMC = "clickActionMiddleCenter" + const val clickActionMR = "clickActionMiddleRight" + const val clickActionBL = "clickActionBottomLeft" + const val clickActionBC = "clickActionBottomCenter" + const val clickActionBR = "clickActionBottomRight" const val hideNavigationBar = "hideNavigationBar" const val precisionSearch = "precisionSearch" const val speakEngine = "speakEngine" const val readAloudByPage = "readAloudByPage" + const val ttsEngine = "ttsEngine" const val ttsSpeechRate = "ttsSpeechRate" - const val prevKey = "prevKeyCode" - const val nextKey = "nextKeyCode" + const val prevKeys = "prevKeyCodes" + const val nextKeys = "nextKeyCodes" + const val showDiscovery = "showDiscovery" const val showRss = "showRss" const val bookshelfLayout = "bookshelfLayout" const val bookshelfSort = "bookshelfSort" + const val bookExportFileName = "bookExportFileName" + const val bookImportFileName = "bookImportFileName" const val recordLog = "recordLog" const val processText = "process_text" const val cleanCache = "cleanCache" const val saveTabPosition = "saveTabPosition" - const val pageAnim = "pageAnim" const val fontFolder = "fontFolder" const val backupPath = "backupUri" const val restoreIgnore = "restoreIgnore" @@ -33,12 +46,15 @@ object PreferKey { const val webDavAccount = "web_dav_account" const val webDavPassword = "web_dav_password" const val webDavCreateDir = "webDavCreateDir" + const val exportToWebDav = "webDavCacheBackup" + const val exportType = "exportType" + const val changeSourceCheckAuthor = "changeSourceCheckAuthor" const val changeSourceLoadToc = "changeSourceLoadToc" + const val changeSourceLoadInfo = "changeSourceLoadInfo" const val chineseConverterType = "chineseConverterType" const val launcherIcon = "launcherIcon" const val textSelectAble = "selectText" const val lastBackup = "lastBackup" - const val bodyIndent = "textIndent" const val shareLayout = "shareLayout" const val readStyleSelect = "readStyleSelect" const val systemTypefaces = "system_typefaces" @@ -48,18 +64,38 @@ object PreferKey { const val autoReadSpeed = "autoReadSpeed" const val barElevation = "barElevation" const val transparentStatusBar = "transparentStatusBar" + const val immNavigationBar = "immNavigationBar" const val defaultCover = "defaultCover" + const val defaultCoverDark = "defaultCoverDark" const val replaceEnableDefault = "replaceEnableDefault" const val showBrightnessView = "showBrightnessView" + const val autoClearExpired = "autoClearExpired" + const val autoChangeSource = "autoChangeSource" + const val importKeepName = "importKeepName" + const val screenOrientation = "screenOrientation" + const val syncBookProgress = "syncBookProgress" + const val preDownloadNum = "preDownloadNum" + const val autoRefresh = "auto_refresh" + const val defaultToRead = "defaultToRead" + const val exportCharset = "exportCharset" + const val exportUseReplace = "exportUseReplace" + const val useZhLayout = "useZhLayout" + const val fullScreenGesturesSupport = "fullScreenGesturesSupport" + const val highBrush = "highBrush" + const val brightness = "brightness" + const val nightBrightness = "nightBrightness" + const val expandTextMenu = "expandTextMenu" const val cPrimary = "colorPrimary" const val cAccent = "colorAccent" const val cBackground = "colorBackground" const val cBBackground = "colorBottomBackground" + const val bgImage = "backgroundImage" const val cNPrimary = "colorPrimaryNight" const val cNAccent = "colorAccentNight" const val cNBackground = "colorBackgroundNight" const val cNBBackground = "colorBottomBackgroundNight" + const val bgImageN = "backgroundImageNight" -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/constant/Theme.kt b/app/src/main/java/io/legado/app/constant/Theme.kt index 896e73ea0..8145f4d01 100644 --- a/app/src/main/java/io/legado/app/constant/Theme.kt +++ b/app/src/main/java/io/legado/app/constant/Theme.kt @@ -4,16 +4,18 @@ import io.legado.app.help.AppConfig import io.legado.app.utils.ColorUtils enum class Theme { - Dark, Light, Auto, Transparent; + Dark, Light, Auto, Transparent, EInk; companion object { - fun getTheme() = - if (AppConfig.isNightTheme) Dark - else Light + fun getTheme() = when { + AppConfig.isEInkMode -> EInk + AppConfig.isNightTheme -> Dark + else -> Light + } fun getTheme(backgroundColor: Int) = if (ColorUtils.isColorLight(backgroundColor)) Light else Dark - + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index a865f8d86..fe4319b13 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -6,132 +6,331 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import io.legado.app.App +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.androidId import io.legado.app.data.dao.* import io.legado.app.data.entities.* +import io.legado.app.help.AppConfig +import splitties.init.appCtx +import java.util.* +val appDb by lazy { + AppDatabase.createDatabase(appCtx) +} @Database( entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, - RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class], - version = 20, + RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, + RuleSub::class], + version = 33, exportSchema = true ) -abstract class AppDatabase: RoomDatabase() { - +abstract class AppDatabase : RoomDatabase() { + + abstract val bookDao: BookDao + abstract val bookGroupDao: BookGroupDao + abstract val bookSourceDao: BookSourceDao + abstract val bookChapterDao: BookChapterDao + abstract val replaceRuleDao: ReplaceRuleDao + abstract val searchBookDao: SearchBookDao + abstract val searchKeywordDao: SearchKeywordDao + abstract val rssSourceDao: RssSourceDao + abstract val bookmarkDao: BookmarkDao + abstract val rssArticleDao: RssArticleDao + abstract val rssStarDao: RssStarDao + abstract val cookieDao: CookieDao + abstract val txtTocRuleDao: TxtTocRuleDao + abstract val readRecordDao: ReadRecordDao + abstract val httpTTSDao: HttpTTSDao + abstract val cacheDao: CacheDao + abstract val ruleSubDao: RuleSubDao + companion object { - + private const val DATABASE_NAME = "legado.db" - + fun createDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .fallbackToDestructiveMigration() .addMigrations( - migration_10_11, - migration_11_12, - migration_12_13, - migration_13_14, - migration_14_15, - migration_15_17, - migration_17_18, - migration_18_19, - migration_19_20 + migration_10_11, migration_11_12, migration_12_13, migration_13_14, + migration_14_15, migration_15_17, migration_17_18, migration_18_19, + migration_19_20, migration_20_21, migration_21_22, migration_22_23, + migration_23_24, migration_24_25, migration_25_26, migration_26_27, + migration_27_28, migration_28_29, migration_29_30, migration_30_31, + migration_31_32, migration_32_33 ) .allowMainThreadQueries() + .addCallback(dbCallback) .build() - - private val migration_10_11 = object: Migration(10, 11) { + + private val dbCallback = object : Callback() { + + override fun onCreate(db: SupportSQLiteDatabase) { + db.setLocale(Locale.CHINESE) + } + + override fun onOpen(db: SupportSQLiteDatabase) { + db.execSQL( + """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAllId}, '全部', -10, 1 + where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})""" + ) + db.execSQL( + """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupLocalId}, '本地', -9, 1 + where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})""" + ) + db.execSQL( + """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAudioId}, '音频', -8, 1 + where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})""" + ) + db.execSQL( + """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 + where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})""" + ) + if (AppConfig.isGooglePlay) { + db.execSQL( + """ + delete from rssSources where sourceUrl = 'https://github.com/gedoor/legado/releases' + """ + ) + } + } + } + + private val migration_10_11 = object : Migration(10, 11) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("DROP TABLE txtTocRules") database.execSQL( - """ - CREATE TABLE txtTocRules(id INTEGER NOT NULL, + """CREATE TABLE txtTocRules(id INTEGER NOT NULL, name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL, - enable INTEGER NOT NULL, PRIMARY KEY (id)) - """ + enable INTEGER NOT NULL, PRIMARY KEY (id))""" ) } } - - private val migration_11_12 = object: Migration(11, 12) { + + private val migration_11_12 = object : Migration(11, 12) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE rssSources ADD style TEXT ") } } - - private val migration_12_13 = object: Migration(12, 13) { + + private val migration_12_13 = object : Migration(12, 13) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ") } } - - private val migration_13_14 = object: Migration(13, 14) { + + private val migration_13_14 = object : Migration(13, 14) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( - """ - CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, - `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, - `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, - `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, - `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, - `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`)) - """ + """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, + `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, + `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, + `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, + `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, + `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, + `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))""" ) - database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books_new` (`name`, `author`) ") database.execSQL("INSERT INTO books_new select * from books ") database.execSQL("DROP TABLE books") database.execSQL("ALTER TABLE books_new RENAME TO books") + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ") } } - - private val migration_14_15 = object: Migration(14, 15) { + + private val migration_14_15 = object : Migration(14, 15) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''") } } - - private val migration_15_17 = object: Migration(15, 17) { + + private val migration_15_17 = object : Migration(15, 17) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))") } } - - private val migration_17_18 = object: Migration(17, 18) { + + private val migration_17_18 = object : Migration(17, 18) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))") } } - - private val migration_18_19 = object: Migration(18, 19) { + + private val migration_18_19 = object : Migration(18, 19) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))") - database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord") + database.execSQL( + """CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, + PRIMARY KEY(`androidId`, `bookName`))""" + ) + database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${androidId}' as androidId, bookName, readTime from readRecord") database.execSQL("DROP TABLE readRecord") database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord") } } - private val migration_19_20 = object: Migration(19, 20) { + private val migration_19_20 = object : Migration(19, 20) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT") } } + + private val migration_20_21 = object : Migration(20, 21) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1") + } + } + + private val migration_21_22 = object : Migration(21, 22) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, + `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, + `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, + `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, + `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, + `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, + `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))""" + ) + database.execSQL( + """INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`, + `customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`, + `lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`, + `order`, `originOrder`, `variable`, null + from books""" + ) + database.execSQL("DROP TABLE books") + database.execSQL("ALTER TABLE books_new RENAME TO books") + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ") + } + } + + private val migration_22_23 = object : Migration(22, 23) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''") + } + } + + private val migration_23_24 = object : Migration(23, 24) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))") + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)") + } + } + + private val migration_24_25 = object : Migration(24, 25) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `sourceSubs` + (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, + PRIMARY KEY(`id`))""" + ) + } + } + + private val migration_25_26 = object : Migration(25, 26) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, + `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))""" + ) + database.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ") + database.execSQL("DROP TABLE `sourceSubs`") + } + } + + private val migration_26_27 = object : Migration(26, 27) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ") + database.execSQL( + """CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, + `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, + `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))""" + ) + database.execSQL( + """insert into `bookmarks1` + select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content` + from bookmarks""" + ) + database.execSQL(" DROP TABLE `bookmarks` ") + database.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ") + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)") + } + } + + private val migration_27_28 = object : Migration(27, 28) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE rssArticles ADD variable TEXT") + database.execSQL("ALTER TABLE rssStars ADD variable TEXT") + } + } + + private val migration_28_29 = object : Migration(28, 29) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT") + } + } + + private val migration_29_30 = object : Migration(29, 30) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT") + database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `epubChapters` + (`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT, + PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE ) + """ + ) + database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)") + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)") + } + } + + private val migration_30_31 = object : Migration(30, 31) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`)) + """ + ) + database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1") + } + } + + private val migration_31_32 = object : Migration(31, 32) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE `epubChapters`") + } + } + + private val migration_32_33 = object : Migration(32, 33) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL, + `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, + `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, + `content` TEXT NOT NULL, PRIMARY KEY(`time`)) + """ + ) + database.execSQL( + """ + CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`) + """ + ) + database.execSQL( + """ + insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content) + select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor, + chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o + left join books b on o.bookUrl = b.bookUrl + """ + ) + } + } } - - abstract fun bookDao(): BookDao - abstract fun bookGroupDao(): BookGroupDao - abstract fun bookSourceDao(): BookSourceDao - abstract fun bookChapterDao(): BookChapterDao - abstract fun replaceRuleDao(): ReplaceRuleDao - abstract fun searchBookDao(): SearchBookDao - abstract fun searchKeywordDao(): SearchKeywordDao - abstract fun rssSourceDao(): RssSourceDao - abstract fun bookmarkDao(): BookmarkDao - abstract fun rssArticleDao(): RssArticleDao - abstract fun rssStarDao(): RssStarDao - abstract fun cookieDao(): CookieDao - abstract fun txtTocRule(): TxtTocRuleDao - abstract fun readRecordDao(): ReadRecordDao - abstract fun httpTTSDao(): HttpTTSDao + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/README.md b/app/src/main/java/io/legado/app/data/README.md index 9b5f490b7..a9354610f 100644 --- a/app/src/main/java/io/legado/app/data/README.md +++ b/app/src/main/java/io/legado/app/data/README.md @@ -1,4 +1,4 @@ -## 存储数据用 +# 存储数据用 * dao 数据操作 * entities 数据模型 * \Book 书籍信息 diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt index d0588bec8..eaeaabd5c 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -1,20 +1,20 @@ 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 +import kotlinx.coroutines.flow.Flow @Dao interface BookChapterDao { @Query("select * from chapters where bookUrl = :bookUrl order by `index`") - fun observeByBook(bookUrl: String): LiveData> + fun flowByBook(bookUrl: String): Flow> @Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`") - fun liveDataSearch(bookUrl: String, key: String): LiveData> + fun flowSearch(bookUrl: String, key: String): Flow> @Query("select * from chapters where bookUrl = :bookUrl order by `index`") fun getChapterList(bookUrl: String): List @@ -25,6 +25,9 @@ interface BookChapterDao { @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") fun getChapter(bookUrl: String, index: Int): BookChapter? + @Query("select * from chapters where bookUrl = :bookUrl and `title` = :title") + fun getChapter(bookUrl: String, title: String): BookChapter? + @Query("select count(url) from chapters where bookUrl = :bookUrl") fun getChapterCount(bookUrl: String): Int diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt index 9aaf5d3fe..bfc933f64 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt @@ -1,40 +1,36 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.constant.BookType import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookProgress +import kotlinx.coroutines.flow.Flow @Dao interface BookDao { @Query("SELECT * FROM books order by durChapterTime desc") - fun observeAll(): LiveData> + fun flowAll(): Flow> @Query("SELECT * FROM books WHERE type = ${BookType.audio}") - fun observeAudio(): LiveData> + fun flowAudio(): Flow> @Query("SELECT * FROM books WHERE origin = '${BookType.local}'") - fun observeLocal(): LiveData> + fun flowLocal(): Flow> + + @Query("select * from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0") + fun flowNoGroup(): Flow> @Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'") - fun observeLocalUri(): LiveData> + fun flowLocalUri(): Flow> @Query("SELECT * FROM books WHERE (`group` & :group) > 0") - fun observeByGroup(group: Int): LiveData> - - @Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0") - fun observeNoGroup(): LiveData> - - @Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0") - fun observeNoGroupSize(): LiveData + fun flowByGroup(group: Long): Flow> @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") - fun liveDataSearch(key: String): LiveData> + fun flowSearch(key: String): Flow> @Query("SELECT * FROM books WHERE (`group` & :group) > 0") - fun getBooksByGroup(group: Int): List + fun getBooksByGroup(group: Long): List @Query("SELECT * FROM books WHERE `name` in (:names)") fun findByName(vararg names: String): List @@ -69,6 +65,9 @@ interface BookDao { @get:Query("select max(`order`) from books") val maxOrder: Int + @Query("select 1 from books where bookUrl = :bookUrl") + fun has(bookUrl: String): Boolean? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg book: Book) @@ -82,24 +81,6 @@ interface BookDao { fun upProgress(bookUrl: String, pos: Int) @Query("update books set `group` = :newGroupId where `group` = :oldGroupId") - fun upGroup(oldGroupId: Int, newGroupId: Int) - - @get:Query("select bookUrl, tocUrl, origin, originName, durChapterIndex, durChapterPos, durChapterTime, durChapterTitle from books") - val allBookProgress: List - - @Query( - """ - update books set - durChapterIndex = :durChapterIndex, durChapterPos = :durChapterPos, - durChapterTime = :durChapterTime, durChapterTitle = :durChapterTitle - where bookUrl = :bookUrl and durChapterTime < :durChapterTime - """ - ) - fun upBookProgress( - bookUrl: String, - durChapterIndex: Int, - durChapterPos: Int, - durChapterTime: Long, - durChapterTitle: String? - ) + fun upGroup(oldGroupId: Long, newGroupId: Long) + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt index c262d60e1..346992faa 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt @@ -1,27 +1,51 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* +import io.legado.app.constant.BookType import io.legado.app.data.entities.BookGroup +import kotlinx.coroutines.flow.Flow @Dao interface BookGroupDao { @Query("select * from book_groups where groupId = :id") - fun getByID(id: Int): BookGroup? + fun getByID(id: Long): BookGroup? - @Query("SELECT * FROM book_groups ORDER BY `order`") - fun liveDataAll(): LiveData> - - @get:Query("SELECT sum(groupId) FROM book_groups") - val idsSum: Int + @Query("select * from book_groups where groupName = :groupName") + fun getByName(groupName: String): BookGroup? - @get:Query("SELECT MAX(`order`) FROM book_groups") + @Query("SELECT * FROM book_groups ORDER BY `order`") + fun flowAll(): Flow> + + @Query( + """ + SELECT * FROM book_groups where (groupId >= 0 and show > 0) + or (groupId = -4 and show > 0 and (select count(bookUrl) from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0) > 0) + or (groupId = -3 and show > 0 and (select count(bookUrl) from books where type = ${BookType.audio}) > 0) + or (groupId = -2 and show > 0 and (select count(bookUrl) from books where origin = '${BookType.local}') > 0) + or (groupId = -1 and show > 0) + ORDER BY `order`""" + ) + fun flowShow(): Flow> + + @Query("SELECT * FROM book_groups where groupId >= 0 ORDER BY `order`") + fun flowSelect(): Flow> + + @get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0") + val idsSum: Long + + @get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0") val maxOrder: Int @get:Query("SELECT * FROM book_groups ORDER BY `order`") val all: List + @Query("update book_groups set show = 1 where groupId = :groupId") + fun enableGroup(groupId: Long) + + @Query("select groupName from book_groups where groupId > 0 and (groupId & :id) > 0") + fun getGroupNames(id: Long): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg bookGroup: BookGroup) diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt index 0c0b8e49a..99677054c 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt @@ -1,45 +1,69 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData -import androidx.paging.DataSource import androidx.room.* import io.legado.app.data.entities.BookSource +import kotlinx.coroutines.flow.Flow @Dao interface BookSourceDao { @Query("select * from book_sources order by customOrder asc") - fun liveDataAll(): LiveData> + fun flowAll(): Flow> - @Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc") - fun liveDataSearch(searchKey: String = ""): LiveData> + @Query( + """select * from book_sources + where bookSourceName like :searchKey + or bookSourceGroup like :searchKey + or bookSourceUrl like :searchKey + or bookSourceComment like :searchKey + order by customOrder asc""" + ) + fun flowSearch(searchKey: String): Flow> + + @Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc") + fun flowGroupSearch(searchKey: String): Flow> @Query("select * from book_sources where enabled = 1 order by customOrder asc") - fun liveDataEnabled(): LiveData> + fun flowEnabled(): Flow> @Query("select * from book_sources where enabled = 0 order by customOrder asc") - fun liveDataDisabled(): LiveData> + fun flowDisabled(): Flow> @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc") - fun liveExplore(): LiveData> - - @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc") - fun liveExplore(key: String): LiveData> - - @Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") - fun liveGroup(): LiveData> - - @Query("select bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''") - fun liveGroupEnabled(): LiveData> - - @Query("select bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''") - fun liveGroupExplore(): LiveData> - - @Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey") - fun searchIsEnable(searchKey: String = ""): List - - @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc") - fun observeFind(): DataSource.Factory + fun flowExplore(): Flow> + + @Query( + """select * from book_sources + where enabledExplore = 1 + and trim(exploreUrl) <> '' + and (bookSourceGroup like :key or bookSourceName like :key) + order by customOrder asc""" + ) + fun flowExplore(key: String): Flow> + + @Query( + """select * from book_sources + where enabledExplore = 1 + and trim(exploreUrl) <> '' + and (bookSourceGroup like :key) + order by customOrder asc""" + ) + fun flowGroupExplore(key: String): Flow> + + @Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") + fun flowGroup(): Flow> + + @Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''") + fun flowGroupEnabled(): Flow> + + @Query( + """select distinct bookSourceGroup from book_sources + where enabledExplore = 1 + and trim(exploreUrl) <> '' + and trim(bookSourceGroup) <> '' + order by customOrder""" + ) + fun flowExploreGroup(): Flow> @Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'") fun getByGroup(group: String): List @@ -62,6 +86,9 @@ interface BookSourceDao { @get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder") val allTextEnabled: List + @get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") + val allGroup: List + @Query("select * from book_sources where bookSourceUrl = :key") fun getBookSource(key: String): BookSource? diff --git a/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt index 16fdff455..830e83d6c 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt @@ -1,8 +1,8 @@ package io.legado.app.data.dao -import androidx.paging.DataSource import androidx.room.* import io.legado.app.data.entities.Bookmark +import kotlinx.coroutines.flow.Flow @Dao @@ -11,15 +11,20 @@ interface BookmarkDao { @get:Query("select * from bookmarks") val all: List - @Query("select * from bookmarks where bookUrl = :bookUrl or (bookName = :bookName and bookAuthor = :bookAuthor)") - fun observeByBook( - bookUrl: String, - bookName: String, - bookAuthor: String - ): DataSource.Factory - - @Query("SELECT * FROM bookmarks where bookUrl = :bookUrl and chapterName like '%'||:key||'%' or content like '%'||:key||'%'") - fun liveDataSearch(bookUrl: String, key: String): DataSource.Factory + @Query( + """select * from bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + order by chapterIndex""" + ) + fun flowByBook(bookName: String, bookAuthor: String): Flow> + + @Query( + """SELECT * FROM bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + and chapterName like '%'||:key||'%' or content like '%'||:key||'%' + order by chapterIndex""" + ) + fun flowSearch(bookName: String, bookAuthor: String, key: String): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg bookmark: Bookmark) @@ -30,7 +35,4 @@ interface BookmarkDao { @Delete fun delete(vararg bookmark: Bookmark) - @Query("delete from bookmarks where bookUrl = :bookUrl and chapterName like '%'||:chapterName||'%'") - fun delByBookmark(bookUrl: String, chapterName: String) - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/CacheDao.kt b/app/src/main/java/io/legado/app/data/dao/CacheDao.kt new file mode 100644 index 000000000..863199b75 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/CacheDao.kt @@ -0,0 +1,24 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.legado.app.data.entities.Cache + +@Dao +interface CacheDao { + + @Query("select value from caches where `key` = :key and (deadline = 0 or deadline > :now)") + fun get(key: String, now: Long): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg cache: Cache) + + @Query("delete from caches where `key` = :key") + fun delete(key: String) + + @Query("delete from caches where deadline > 0 and deadline < :now") + fun clearDeadline(now: Long) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt index f71169bb1..70ff64717 100644 --- a/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt @@ -1,8 +1,8 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.HttpTTS +import kotlinx.coroutines.flow.Flow @Dao interface HttpTTSDao { @@ -11,7 +11,7 @@ interface HttpTTSDao { val all: List @Query("select * from httpTTS order by name") - fun observeAll(): LiveData> + fun flowAll(): Flow> @get:Query("select count(*) from httpTTS") val count: Int @@ -28,4 +28,6 @@ interface HttpTTSDao { @Update fun update(vararg httpTTS: HttpTTS) + @Query("delete from httpTTS where id < 0") + fun deleteDefault() } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt index 3e964ea73..7aaaf916e 100644 --- a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt @@ -10,7 +10,7 @@ interface ReadRecordDao { @get:Query("select * from readRecord") val all: List - @get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName") + @get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName collate localized") val allShow: List @get:Query("select sum(readTime) from readRecord") @@ -19,7 +19,7 @@ interface ReadRecordDao { @Query("select sum(readTime) from readRecord where bookName = :bookName") fun getReadTime(bookName: String): Long? - @Query("select readTime from readRecord where androidId = :androidId and bookName = :bookName") + @Query("select readTime from readRecord where deviceId = :androidId and bookName = :bookName") fun getReadTime(androidId: String, bookName: String): Long? @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt index 5643ef975..b82014839 100644 --- a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt @@ -1,18 +1,24 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.ReplaceRule +import kotlinx.coroutines.flow.Flow @Dao interface ReplaceRuleDao { @Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") - fun liveDataAll(): LiveData> + fun flowAll(): Flow> @Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC") - fun liveDataSearch(key: String): LiveData> + fun flowSearch(key: String): Flow> + + @Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC") + fun flowGroupSearch(key: String): Flow> + + @Query("select `group` from replace_rules where `group` is not null and `group` <> ''") + fun flowGroup(): Flow> @get:Query("SELECT MIN(sortOrder) FROM replace_rules") val minOrder: Int @@ -33,26 +39,12 @@ interface ReplaceRuleDao { fun findByIds(vararg ids: Long): List @Query( - """ - SELECT * FROM replace_rules WHERE isEnabled = 1 - AND (scope LIKE '%' || :scope || '%' or scope is null or scope = '') - order by sortOrder - """ - ) - fun findEnabledByScope(scope: String): List - - @Query( - """ - SELECT * FROM replace_rules WHERE isEnabled = 1 + """SELECT * FROM replace_rules WHERE isEnabled = 1 AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '') - order by sortOrder - """ + order by sortOrder""" ) fun findEnabledByScope(name: String, origin: String): List - @Query("select `group` from replace_rules where `group` is not null and `group` <> ''") - fun liveGroup(): LiveData> - @Query("select * from replace_rules where `group` like '%' || :group || '%'") fun getByGroup(group: String): List diff --git a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt index 10bf63933..30caf63da 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt @@ -1,9 +1,9 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssReadRecord +import kotlinx.coroutines.flow.Flow @Dao interface RssArticleDao { @@ -12,12 +12,13 @@ interface RssArticleDao { fun get(origin: String, link: String): RssArticle? @Query( - """select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read + """select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, + t1.description, t1.image, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read from rssArticles as t1 left join rssReadRecords as t2 on t1.link = t2.record where origin = :origin and sort = :sort order by `order` desc""" ) - fun liveByOriginSort(origin: String, sort: String): LiveData> + fun flowByOriginSort(origin: String, sort: String): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg rssArticle: RssArticle) diff --git a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt index 5436ee26c..85605b3cf 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt @@ -1,8 +1,8 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.RssSource +import kotlinx.coroutines.flow.Flow @Dao interface RssSourceDao { @@ -11,7 +11,7 @@ interface RssSourceDao { fun getByKey(key: String): RssSource? @Query("select * from rssSources where sourceUrl in (:sourceUrls)") - fun getRssSources(vararg sourceUrls: String):List + fun getRssSources(vararg sourceUrls: String): List @get:Query("SELECT * FROM rssSources") val all: List @@ -20,16 +20,33 @@ interface RssSourceDao { val size: Int @Query("SELECT * FROM rssSources order by customOrder") - fun liveAll(): LiveData> + fun flowAll(): Flow> @Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder") - fun liveSearch(key: String): LiveData> + fun flowSearch(key: String): Flow> + + @Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder") + fun flowGroupSearch(key: String): Flow> @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") - fun liveEnabled(): LiveData> + fun flowEnabled(): Flow> + + @Query( + """SELECT * FROM rssSources + where enabled = 1 + and (sourceName like :searchKey or sourceGroup like :searchKey or sourceUrl like :searchKey) + order by customOrder""" + ) + fun flowEnabled(searchKey: String): Flow> + + @Query("SELECT * FROM rssSources where enabled = 1 and sourceGroup like :searchKey order by customOrder") + fun flowEnabledByGroup(searchKey: String): Flow> + + @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") + fun flowGroup(): Flow> - @Query("select sourceGroup from rssSources where trim(sourceGroup) <> ''") - fun liveGroup(): LiveData> + @get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") + val allGroup: List @get:Query("select min(customOrder) from rssSources") val minOrder: Int diff --git a/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt index faaf85b9f..2c40349c0 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt @@ -1,8 +1,8 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.RssStar +import kotlinx.coroutines.flow.Flow @Dao interface RssStarDao { @@ -14,7 +14,7 @@ interface RssStarDao { fun get(origin: String, link: String): RssStar? @Query("select * from rssStars order by starTime desc") - fun liveAll(): LiveData> + fun liveAll(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg rssStar: RssStar) diff --git a/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt b/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt new file mode 100644 index 000000000..56d87b262 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt @@ -0,0 +1,30 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.RuleSub +import kotlinx.coroutines.flow.Flow + +@Dao +interface RuleSubDao { + + @get:Query("select * from ruleSubs order by customOrder") + val all: List + + @Query("select * from ruleSubs order by customOrder") + fun flowAll(): Flow> + + @get:Query("select customOrder from ruleSubs order by customOrder limit 0,1") + val maxOrder: Int + + @Query("select * from ruleSubs where url = :url") + fun findByUrl(url: String): RuleSub? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg ruleSub: RuleSub) + + @Delete + fun delete(vararg ruleSub: RuleSub) + + @Update + fun update(vararg ruleSub: RuleSub) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt index e2586e3e0..0c5a6183f 100644 --- a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt @@ -1,21 +1,11 @@ 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 androidx.room.* import io.legado.app.data.entities.SearchBook @Dao interface SearchBookDao { - @Query("SELECT * FROM searchBooks") - fun observeAll(): DataSource.Factory - - @Query("SELECT * FROM searchBooks where time >= :time") - fun observeNew(time: Long): DataSource.Factory - @Query("select * from searchBooks where bookUrl = :bookUrl") fun getSearchBook(bookUrl: String): SearchBook? @@ -23,26 +13,34 @@ interface SearchBookDao { fun getFirstByNameAuthor(name: String, author: String): SearchBook? @Query( - """ - select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder + """select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, + t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, + t1.wordCount, t2.customOrder as originOrder from searchBooks as t1 inner join book_sources as t2 on t1.origin = t2.bookSourceUrl - where t1.name = :name and t1.author = :author and t2.enabled = 1 - order by t2.customOrder - """ + where t1.name = :name and t1.author like '%'||:author||'%' + and t2.enabled = 1 and t2.bookSourceGroup like '%'||:sourceGroup||'%' + order by t2.customOrder""" ) - fun getByNameAuthorEnable(name: String, author: String): List + fun getChangeSourceSearch(name: String, author: String, sourceGroup: String): List @Query( - """ - select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder + """select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, + t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, + t1.wordCount, t2.customOrder as originOrder from searchBooks as t1 inner join book_sources as t2 on t1.origin = t2.bookSourceUrl - where t1.name = :name and t1.author = :author and originName like '%'||:key||'%' and t2.enabled = 1 - order by t2.customOrder - """ + where t1.name = :name and t1.author = :author + and originName like '%'||:key||'%' and t2.enabled = 1 + and t2.bookSourceGroup like '%'||:sourceGroup||'%' + order by t2.customOrder""" ) - fun getChangeSourceSearch(name: String, author: String, key: String): List + fun getChangeSourceSearch( + name: String, + author: String, + key: String, + sourceGroup: String + ): List @Query( """ @@ -58,6 +56,15 @@ interface SearchBookDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg searchBook: SearchBook): List + @Query("delete from searchBooks where name = :name and author = :author") + fun clear(name: String, author: String) + @Query("delete from searchBooks where time < :time") fun clearExpired(time: Long) + + @Update + fun update(vararg searchBook: SearchBook) + + @Delete + fun delete(vararg searchBook: SearchBook) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt index be7ad75c5..2b433e488 100644 --- a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt @@ -1,21 +1,24 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.SearchKeyword +import kotlinx.coroutines.flow.Flow @Dao interface SearchKeywordDao { + @get:Query("SELECT * FROM search_keywords") + val all: List + @Query("SELECT * FROM search_keywords ORDER BY usage DESC") - fun liveDataByUsage(): LiveData> + fun flowByUsage(): Flow> @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC") - fun liveDataByTime(): LiveData> + fun flowByTime(): Flow> @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC") - fun liveDataSearch(key: String): LiveData> + fun flowSearch(key: String): Flow> @Query("select * from search_keywords where word = :key") fun get(key: String): SearchKeyword? diff --git a/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt index d1b023896..98925bc78 100644 --- a/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt @@ -1,14 +1,14 @@ package io.legado.app.data.dao -import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.TxtTocRule +import kotlinx.coroutines.flow.Flow @Dao interface TxtTocRuleDao { @Query("select * from txtTocRules order by serialNumber") - fun observeAll(): LiveData> + fun observeAll(): Flow> @get:Query("select * from txtTocRules order by serialNumber") val all: List diff --git a/app/src/main/java/io/legado/app/data/entities/BaseBook.kt b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt index 31346a50c..5b3f76e0c 100644 --- a/app/src/main/java/io/legado/app/data/entities/BaseBook.kt +++ b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt @@ -1,18 +1,18 @@ package io.legado.app.data.entities +import io.legado.app.model.analyzeRule.RuleDataInterface import io.legado.app.utils.splitNotBlank -interface BaseBook { +interface BaseBook : RuleDataInterface { + var name: String + var author: String var bookUrl: String - val variableMap: HashMap var kind: String? var wordCount: String? var infoHtml: String? var tocHtml: String? - fun putVariable(key: String, value: String) {} - fun getKindList(): List { val kindList = arrayListOf() wordCount?.let { diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index 7996e3ea9..4a603f3db 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -1,24 +1,23 @@ 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 androidx.room.* import io.legado.app.constant.AppPattern import io.legado.app.constant.BookType +import io.legado.app.data.appDb import io.legado.app.help.AppConfig import io.legado.app.service.help.ReadBook import io.legado.app.utils.GSON import io.legado.app.utils.MD5Utils import io.legado.app.utils.fromJsonObject -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize import java.nio.charset.Charset import kotlin.math.max +import kotlin.math.min @Parcelize +@TypeConverters(Book.Converters::class) @Entity( tableName = "books", indices = [Index(value = ["name", "author"], unique = true)] @@ -29,8 +28,8 @@ data class Book( var tocUrl: String = "", // 目录页Url (toc=table of Contents) var origin: String = BookType.local, // 书源URL(默认BookType.local) var originName: String = "", //书源名称 or 本地书籍文件名 - var name: String = "", // 书籍名称(书源获取) - var author: String = "", // 作者名称(书源获取) + override var name: String = "", // 书籍名称(书源获取) + override var author: String = "", // 作者名称(书源获取) override var kind: String? = null, // 分类信息(书源获取) var customTag: String? = null, // 分类信息(用户修改) var coverUrl: String? = null, // 封面Url(书源获取) @@ -39,7 +38,7 @@ data class Book( var customIntro: String? = null, // 简介内容(用户修改) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var type: Int = 0, // 0:text 1:audio - var group: Int = 0, // 自定义分组索引号 + var group: Long = 0, // 自定义分组索引号 var latestChapterTitle: String? = null, // 最新章节标题 var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间 var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间 @@ -53,73 +52,144 @@ data class Book( var canUpdate: Boolean = true, // 刷新书架时更新书籍信息 var order: Int = 0, // 手动排序 var originOrder: Int = 0, //书源排序 - var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则 - var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息) -): Parcelable, BaseBook { - + var variable: String? = null, // 自定义书籍变量信息(用于书源规则检索书籍信息) + var readConfig: ReadConfig? = null +) : Parcelable, BaseBook { + fun isLocalBook(): Boolean { return origin == BookType.local } - + fun isLocalTxt(): Boolean { return isLocalBook() && originName.endsWith(".txt", true) } - + fun isEpub(): Boolean { return originName.endsWith(".epub", true) } - + + fun isUmd(): Boolean { + return originName.endsWith(".umd", true) + } + fun isOnLineTxt(): Boolean { return !isLocalBook() && type == 0 } - + override fun equals(other: Any?): Boolean { if (other is Book) { return other.bookUrl == bookUrl } return false } - + override fun hashCode(): Int { return bookUrl.hashCode() } - + @delegate:Transient @delegate:Ignore @IgnoredOnParcel override val variableMap by lazy { GSON.fromJsonObject>(variable) ?: HashMap() } - + override fun putVariable(key: String, value: String) { variableMap[key] = value variable = GSON.toJson(variableMap) } - + @Ignore @IgnoredOnParcel override var infoHtml: String? = null - + @Ignore @IgnoredOnParcel override var tocHtml: String? = null - + fun getRealAuthor() = author.replace(AppPattern.authorRegex, "") - + fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) - + + fun getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag + fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl - + fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro - + + //自定义简介有自动更新的需求时,可通过更新intro再调用upCustomIntro()完成 + @Suppress("unused") + fun upCustomIntro() { + customIntro = intro + } + fun fileCharset(): Charset { return charset(charset ?: "UTF-8") } - + + private fun config(): ReadConfig { + if (readConfig == null) { + readConfig = ReadConfig() + } + return readConfig!! + } + + fun setReverseToc(reverseToc: Boolean) { + config().reverseToc = reverseToc + } + + fun getReverseToc(): Boolean { + return config().reverseToc + } + + fun setUseReplaceRule(useReplaceRule: Boolean) { + config().useReplaceRule = useReplaceRule + } + + fun getUseReplaceRule(): Boolean { + return config().useReplaceRule + } + + fun getReSegment(): Boolean { + return config().reSegment + } + + fun setReSegment(reSegment: Boolean) { + config().reSegment = reSegment + } + + fun getPageAnim(): Int { + return config().pageAnim + } + + fun setPageAnim(pageAnim: Int) { + config().pageAnim = pageAnim + } + + fun getImageStyle(): String? { + return config().imageStyle + } + + fun setImageStyle(imageStyle: String?) { + config().imageStyle = imageStyle + } + + fun setDelTag(tag: Long) { + config().delTag = + if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag + } + + fun getDelTag(tag: Long): Boolean { + return config().delTag and tag == tag + } + fun getFolderName(): String { - return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl) + //防止书名过长,只取9位 + var folderName = name.replace(AppPattern.fileNameRegex, "") + folderName = folderName.substring(0, min(9, folderName.length)) + return folderName + MD5Utils.md5Encode16(bookUrl) } - + fun toSearchBook() = SearchBook( name = name, author = author, @@ -139,7 +209,7 @@ data class Book( this.infoHtml = this@Book.infoHtml this.tocHtml = this@Book.tocHtml } - + fun changeTo(newBook: Book) { newBook.group = group newBook.order = order @@ -147,16 +217,9 @@ data class Book( newBook.customIntro = customIntro newBook.customTag = customTag newBook.canUpdate = canUpdate - newBook.useReplaceRule = useReplaceRule - delete() - App.db.bookDao().insert(newBook) - } - - fun delete() { - if (ReadBook.book?.bookUrl == bookUrl) { - ReadBook.book = null - } - App.db.bookDao().delete(this) + newBook.readConfig = readConfig + delete(this) + appDb.bookDao.insert(newBook) } fun upInfoFromOld(oldBook: Book?) { @@ -173,4 +236,55 @@ data class Book( } } } -} \ No newline at end of file + + fun createBookMark(): Bookmark { + return Bookmark( + bookName = name, + bookAuthor = author, + ) + } + + fun save() { + if (appDb.bookDao.has(bookUrl) == true) { + appDb.bookDao.update(this) + } else { + appDb.bookDao.insert(this) + } + } + + companion object { + const val hTag = 2L + const val rubyTag = 4L + const val imgTag = 8L + const val imgStyleDefault = "DEFAULT" + const val imgStyleFull = "FULL" + const val imgStyleText = "TEXT" + + fun delete(book: Book?) { + book ?: return + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.book = null + } + appDb.bookDao.delete(book) + } + } + + @Parcelize + data class ReadConfig( + var reverseToc: Boolean = false, + var pageAnim: Int = -1, + var reSegment: Boolean = false, + var imageStyle: String? = null, + var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则 + var delTag: Long = 0L,//去除标签 + ) : Parcelable + + class Converters { + + @TypeConverter + fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config) + + @TypeConverter + fun stringToReadConfig(json: String?) = GSON.fromJsonObject(json) + } +} diff --git a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt index 607704e6a..0fc72569a 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt @@ -5,11 +5,13 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Ignore import androidx.room.Index +import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.NetworkUtils import io.legado.app.utils.fromJsonObject -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize - +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize @Parcelize @Entity( @@ -26,35 +28,49 @@ import kotlinx.android.parcel.Parcelize ) // 删除书籍时自动删除章节 data class BookChapter( var url: String = "", // 章节地址 - var title: String = "", // 章节标题 + var title: String = "", // 章节标题 + var baseUrl: String = "", //用来拼接相对url var bookUrl: String = "", // 书籍地址 var index: Int = 0, // 章节序号 var resourceUrl: String? = null, // 音频真实URL var tag: String? = null, // var start: Long? = null, // 章节起始位置 - var end: Long? = null, // 章节终止位置 + var end: Long? = null, // 章节终止位置 + var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId + var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId var variable: String? = null //变量 ) : Parcelable { - @Ignore + @delegate:Transient + @delegate:Ignore @IgnoredOnParcel - var variableMap: HashMap? = null - private set - get() { - if (field == null) { - field = GSON.fromJsonObject>(variable) ?: HashMap() - } - return field - } + val variableMap by lazy { + GSON.fromJsonObject>(variable) ?: HashMap() + } fun putVariable(key: String, value: String) { - variableMap?.put(key, value) + variableMap[key] = value variable = GSON.toJson(variableMap) } override fun hashCode() = url.hashCode() - override fun equals(other: Any?) = if (other is BookChapter) other.url == url else false + override fun equals(other: Any?): Boolean { + if (other is BookChapter) { + return other.url == url + } + return false + } + + fun getAbsoluteURL():String{ + val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) + val urlBefore = if(urlMatcher.find())url.substring(0,urlMatcher.start()) else url + val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl,urlBefore) + return if(urlBefore.length == url.length) urlAbsoluteBefore else urlAbsoluteBefore + ',' + url.substring(urlMatcher.end()) + } + + fun getFileName(): String = String.format("%05d-%s.nb", index, MD5Utils.md5Encode16(title)) + fun getFontName(): String = String.format("%05d-%s.ttf", index, MD5Utils.md5Encode16(title)) } diff --git a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt index 9cd74b331..a4e71a0c3 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt @@ -1,15 +1,45 @@ package io.legado.app.data.entities +import android.content.Context import android.os.Parcelable import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.android.parcel.Parcelize +import io.legado.app.R +import io.legado.app.constant.AppConst +import kotlinx.parcelize.Parcelize @Parcelize @Entity(tableName = "book_groups") data class BookGroup( @PrimaryKey - val groupId: Int = 0b1, + val groupId: Long = 0b1, var groupName: String, - var order: Int = 0 -) : Parcelable \ No newline at end of file + var order: Int = 0, + var show: Boolean = true +) : Parcelable { + + fun getManageName(context: Context): String { + return when (groupId) { + AppConst.bookGroupAllId -> "$groupName(${context.getString(R.string.all)})" + AppConst.bookGroupAudioId -> "$groupName(${context.getString(R.string.audio)})" + AppConst.bookGroupLocalId -> "$groupName(${context.getString(R.string.local)})" + AppConst.bookGroupNoneId -> "$groupName(${context.getString(R.string.no_group)})" + else -> groupName + } + } + + override fun hashCode(): Int { + return groupId.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is BookGroup) { + return other.groupId == groupId + && other.groupName == groupName + && other.order == order + && other.show == show + } + return false + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BookProgress.kt b/app/src/main/java/io/legado/app/data/entities/BookProgress.kt index bb9d4e0c5..e5d1e5c35 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookProgress.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookProgress.kt @@ -1,10 +1,8 @@ package io.legado.app.data.entities data class BookProgress( - val bookUrl: String, - val tocUrl: String = "", - var origin: String = "", - var originName: String = "", + val name: String, + val author: String, val durChapterIndex: Int, val durChapterPos: Int, val durChapterTime: Long, diff --git a/app/src/main/java/io/legado/app/data/entities/BookSource.kt b/app/src/main/java/io/legado/app/data/entities/BookSource.kt index 3af66f972..d5c43ef7e 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookSource.kt @@ -3,14 +3,17 @@ package io.legado.app.data.entities import android.os.Parcelable import android.text.TextUtils import androidx.room.* -import io.legado.app.App import io.legado.app.constant.AppConst -import io.legado.app.constant.AppConst.userAgent import io.legado.app.constant.BookType import io.legado.app.data.entities.rule.* +import io.legado.app.help.AppConfig +import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions +import io.legado.app.help.http.CookieStore import io.legado.app.utils.* -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import splitties.init.appCtx import javax.script.SimpleBindings @Parcelize @@ -41,17 +44,64 @@ data class BookSource( var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则 var ruleToc: TocRule? = null, // 目录页规则 var ruleContent: ContentRule? = null // 正文页规则 -): Parcelable, JsExtensions { - +) : Parcelable, JsExtensions { + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + val exploreKinds by lazy { + val exploreUrl = exploreUrl ?: return@lazy emptyList() + val kinds = arrayListOf() + var ruleStr = exploreUrl + if (ruleStr.isNotBlank()) { + kotlin.runCatching { + if (exploreUrl.startsWith("", false) + || exploreUrl.startsWith("@js", false) + ) { + val aCache = ACache.get(appCtx, "explore") + ruleStr = aCache.getAsString(bookSourceUrl) ?: "" + if (ruleStr.isBlank()) { + val bindings = SimpleBindings() + bindings["baseUrl"] = bookSourceUrl + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + val jsStr = if (exploreUrl.startsWith("@")) { + exploreUrl.substring(3) + } else { + exploreUrl.substring(4, exploreUrl.lastIndexOf("<")) + } + ruleStr = AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString().trim() + aCache.put(bookSourceUrl, ruleStr) + } + } + if (ruleStr.isJsonArray()) { + GSON.fromJsonArray(ruleStr)?.let { + kinds.addAll(it) + } + } else { + ruleStr.split("(&&|\n)+".toRegex()).forEach { kindStr -> + val kindCfg = kindStr.split("::") + kinds.add(ExploreKind(kindCfg.first(), kindCfg.getOrNull(1))) + } + } + }.onFailure { + kinds.add(ExploreKind(it.localizedMessage ?: "")) + } + } + return@lazy kinds + } + override fun hashCode(): Int { return bookSourceUrl.hashCode() } - - override fun equals(other: Any?) = if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false - + + override fun equals(other: Any?) = + if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false + @Throws(Exception::class) fun getHeaderMap() = (HashMap().apply { - this[AppConst.UA_NAME] = App.INSTANCE.getPrefString("user_agent") ?: userAgent + this[AppConst.UA_NAME] = AppConfig.userAgent header?.let { GSON.fromJsonObject>( when { @@ -66,17 +116,17 @@ data class BookSource( } } }) as Map - + fun getSearchRule() = ruleSearch ?: SearchRule() - + fun getExploreRule() = ruleExplore ?: ExploreRule() - + fun getBookInfoRule() = ruleBookInfo ?: BookInfoRule() - + fun getTocRule() = ruleToc ?: TocRule() - + fun getContentRule() = ruleContent ?: ContentRule() - + fun addGroup(group: String) { bookSourceGroup?.let { if (!it.contains(group)) { @@ -86,46 +136,14 @@ data class BookSource( bookSourceGroup = group } } - + fun removeGroup(group: String) { bookSourceGroup?.splitNotBlank("[,;,;]".toRegex())?.toHashSet()?.let { it.remove(group) bookSourceGroup = TextUtils.join(",", it) } } - - fun getExploreKinds() = arrayListOf().apply { - exploreUrl?.let { - var a = it - if (a.isNotBlank()) { - try { - if (it.startsWith("", false)) { - val aCache = ACache.get(App.INSTANCE, "explore") - a = aCache.getAsString(bookSourceUrl) ?: "" - if (a.isBlank()) { - val bindings = SimpleBindings() - bindings["baseUrl"] = bookSourceUrl - bindings["java"] = this - a = AppConst.SCRIPT_ENGINE.eval( - it.substring(4, it.lastIndexOf("<")), - bindings - ).toString() - aCache.put(bookSourceUrl, a) - } - } - val b = a.split("(&&|\n)+".toRegex()) - b.forEach { c -> - val d = c.split("::") - if (d.size > 1) - add(ExploreKind(d[0], d[1])) - } - } catch (e: Exception) { - add(ExploreKind(e.localizedMessage ?: "")) - } - } - } - } - + /** * 执行JS */ @@ -133,65 +151,62 @@ data class BookSource( private fun evalJS(jsStr: String): Any { val bindings = SimpleBindings() bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings) } - + fun equal(source: BookSource) = equal(bookSourceName, source.bookSourceName) - && equal(bookSourceUrl, source.bookSourceUrl) - && equal(bookSourceGroup, source.bookSourceGroup) - && bookSourceType == source.bookSourceType - && equal(bookUrlPattern, source.bookUrlPattern) - && equal(bookSourceComment, source.bookSourceComment) - && enabled == source.enabled - && enabledExplore == source.enabledExplore - && equal(header, source.header) - && equal(loginUrl, source.loginUrl) - && equal(exploreUrl, source.exploreUrl) - && equal(searchUrl, source.searchUrl) - && getSearchRule() == source.getSearchRule() - && getExploreRule() == source.getExploreRule() - && getBookInfoRule() == source.getBookInfoRule() - && getTocRule() == source.getTocRule() - && getContentRule() == source.getContentRule() - + && equal(bookSourceUrl, source.bookSourceUrl) + && equal(bookSourceGroup, source.bookSourceGroup) + && bookSourceType == source.bookSourceType + && equal(bookUrlPattern, source.bookUrlPattern) + && equal(bookSourceComment, source.bookSourceComment) + && enabled == source.enabled + && enabledExplore == source.enabledExplore + && equal(header, source.header) + && equal(loginUrl, source.loginUrl) + && equal(exploreUrl, source.exploreUrl) + && equal(searchUrl, source.searchUrl) + && getSearchRule() == source.getSearchRule() + && getExploreRule() == source.getExploreRule() + && getBookInfoRule() == source.getBookInfoRule() + && getTocRule() == source.getTocRule() + && getContentRule() == source.getContentRule() + private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) - - data class ExploreKind( - var title: String, - var url: String? = null - ) - + class Converters { @TypeConverter fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule) - + @TypeConverter fun stringToExploreRule(json: String?) = GSON.fromJsonObject(json) @TypeConverter fun searchRuleToString(searchRule: SearchRule?): String = GSON.toJson(searchRule) - + @TypeConverter fun stringToSearchRule(json: String?) = GSON.fromJsonObject(json) @TypeConverter fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String = GSON.toJson(bookInfoRule) - + @TypeConverter fun stringToBookInfoRule(json: String?) = GSON.fromJsonObject(json) @TypeConverter fun tocRuleToString(tocRule: TocRule?): String = GSON.toJson(tocRule) - + @TypeConverter fun stringToTocRule(json: String?) = GSON.fromJsonObject(json) - + @TypeConverter fun contentRuleToString(contentRule: ContentRule?): String = GSON.toJson(contentRule) - + @TypeConverter fun stringToContentRule(json: String?) = GSON.fromJsonObject(json) - + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Bookmark.kt b/app/src/main/java/io/legado/app/data/entities/Bookmark.kt index e7222bf33..9ee4a8ea0 100644 --- a/app/src/main/java/io/legado/app/data/entities/Bookmark.kt +++ b/app/src/main/java/io/legado/app/data/entities/Bookmark.kt @@ -4,19 +4,22 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize -@Entity(tableName = "bookmarks", indices = [(Index(value = ["time"], unique = true))]) +@Entity( + tableName = "bookmarks", + indices = [(Index(value = ["bookName", "bookAuthor"], unique = false))] +) data class Bookmark( @PrimaryKey - var time: Long = System.currentTimeMillis(), - var bookUrl: String = "", - var bookName: String = "", + val time: Long = System.currentTimeMillis(), + val bookName: String = "", val bookAuthor: String = "", var chapterIndex: Int = 0, - var pageIndex: Int = 0, + var chapterPos: Int = 0, var chapterName: String = "", + var bookText: String = "", var content: String = "" ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Cache.kt b/app/src/main/java/io/legado/app/data/entities/Cache.kt new file mode 100644 index 000000000..f9687dea7 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Cache.kt @@ -0,0 +1,13 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "caches", indices = [(Index(value = ["key"], unique = true))]) +data class Cache( + @PrimaryKey + val key: String = "", + var value: String? = null, + var deadline: Long = 0L +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ExploreKind.kt b/app/src/main/java/io/legado/app/data/entities/ExploreKind.kt new file mode 100644 index 000000000..5a44bd419 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/ExploreKind.kt @@ -0,0 +1,39 @@ +package io.legado.app.data.entities + +data class ExploreKind( + val title: String, + val url: String? = null, + val style: Style? = null +) { + + companion object { + val defaultStyle = Style() + } + + fun style(): Style { + return style ?: defaultStyle + } + + data class Style( + val layout_flexGrow: Float = 0F, + val layout_flexShrink: Float = 1F, + val layout_alignSelf: String = "auto", + val layout_flexBasisPercent: Float = -1F, + val layout_wrapBefore: Boolean = false, + ) { + + fun alignSelf(): Int { + return when (layout_alignSelf) { + "auto" -> -1 + "flex_start" -> 0 + "flex_end" -> 1 + "center" -> 2 + "baseline" -> 3 + "stretch" -> 4 + else -> -1 + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt index 60f577477..2b2b5caf4 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt @@ -2,9 +2,9 @@ package io.legado.app.data.entities import androidx.room.Entity -@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"]) +@Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"]) data class ReadRecord( - var androidId: String = "", + var deviceId: String = "", var bookName: String = "", var readTime: Long = 0L ) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt index dc1364559..29f0072fd 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt @@ -6,7 +6,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import java.util.regex.Pattern import java.util.regex.PatternSyntaxException @@ -41,19 +41,18 @@ data class ReplaceRule( return id.hashCode() } - fun isValid(): Boolean{ - if (TextUtils.isEmpty(pattern)){ - return false; + fun isValid(): Boolean { + if (TextUtils.isEmpty(pattern)) { + return false } //判断正则表达式是否正确 - if (isRegex){ + if (isRegex) { try { - Pattern.compile(pattern); - } - catch (ex: PatternSyntaxException){ - return false; + Pattern.compile(pattern) + } catch (ex: PatternSyntaxException) { + return false } } - return true; + return true } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt index fda4769d2..9e0f91b2d 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt @@ -1,6 +1,11 @@ package io.legado.app.data.entities import androidx.room.Entity +import androidx.room.Ignore +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel @Entity( @@ -17,16 +22,29 @@ data class RssArticle( var description: String? = null, var content: String? = null, var image: String? = null, - var read: Boolean = false -) { - + var read: Boolean = false, + var variable: String? = null +) : RuleDataInterface { + override fun hashCode() = link.hashCode() - + override fun equals(other: Any?): Boolean { other ?: return false return if (other is RssArticle) origin == other.origin && link == other.link else false } - + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap by lazy { + GSON.fromJsonObject>(variable) ?: HashMap() + } + + override fun putVariable(key: String, value: String) { + variableMap[key] = value + variable = GSON.toJson(variableMap) + } + fun toStar() = RssStar( origin = origin, sort = sort, diff --git a/app/src/main/java/io/legado/app/data/entities/RssSource.kt b/app/src/main/java/io/legado/app/data/entities/RssSource.kt index 300049f82..5c3280812 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssSource.kt @@ -4,13 +4,16 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import io.legado.app.App import io.legado.app.constant.AppConst +import io.legado.app.help.AppConfig +import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions +import io.legado.app.help.http.CookieStore +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.Parcelize +import kotlinx.parcelize.Parcelize +import splitties.init.appCtx import java.util.* import javax.script.SimpleBindings @@ -22,8 +25,10 @@ data class RssSource( var sourceName: String = "", var sourceIcon: String = "", var sourceGroup: String? = null, + var sourceComment: String? = null, var enabled: Boolean = true, var sortUrl: String? = null, + var singleUrl: Boolean = false, var articleStyle: Int = 0, //列表规则 var ruleArticles: String? = null, @@ -39,17 +44,22 @@ data class RssSource( var header: String? = null, var enableJs: Boolean = false, var loadWithBaseUrl: Boolean = false, - + var customOrder: Int = 0 -): Parcelable, JsExtensions { - - override fun equals(other: Any?) = if (other is RssSource) other.sourceUrl == sourceUrl else false - +) : Parcelable, JsExtensions { + + override fun equals(other: Any?): Boolean { + if (other is RssSource) { + return other.sourceUrl == sourceUrl + } + return false + } + override fun hashCode() = sourceUrl.hashCode() - + @Throws(Exception::class) fun getHeaderMap() = HashMap().apply { - this[AppConst.UA_NAME] = App.INSTANCE.getPrefString("user_agent") ?: AppConst.userAgent + this[AppConst.UA_NAME] = AppConfig.userAgent header?.let { GSON.fromJsonObject>( when { @@ -64,36 +74,63 @@ data class RssSource( } } } - + /** * 执行JS */ @Throws(Exception::class) - private fun evalJS(jsStr: String): Any = AppConst.SCRIPT_ENGINE.eval(jsStr, SimpleBindings().apply { this["java"] = this@RssSource }) - + private fun evalJS(jsStr: String): Any? { + val bindings = SimpleBindings() + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings) + } + fun equal(source: RssSource): Boolean { return equal(sourceUrl, source.sourceUrl) - && equal(sourceIcon, source.sourceIcon) - && enabled == source.enabled - && equal(sourceGroup, source.sourceGroup) - && equal(ruleArticles, source.ruleArticles) - && equal(ruleNextPage, source.ruleNextPage) - && equal(ruleTitle, source.ruleTitle) - && equal(rulePubDate, source.rulePubDate) - && equal(ruleDescription, source.ruleDescription) - && equal(ruleLink, source.ruleLink) - && equal(ruleContent, source.ruleContent) - && enableJs == source.enableJs - && loadWithBaseUrl == source.loadWithBaseUrl + && equal(sourceIcon, source.sourceIcon) + && enabled == source.enabled + && equal(sourceGroup, source.sourceGroup) + && equal(ruleArticles, source.ruleArticles) + && equal(ruleNextPage, source.ruleNextPage) + && equal(ruleTitle, source.ruleTitle) + && equal(rulePubDate, source.rulePubDate) + && equal(ruleDescription, source.ruleDescription) + && equal(ruleLink, source.ruleLink) + && equal(ruleContent, source.ruleContent) + && enableJs == source.enableJs + && loadWithBaseUrl == source.loadWithBaseUrl } - + private fun equal(a: String?, b: String?): Boolean { return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) } - - fun sortUrls(): LinkedHashMap = - linkedMapOf().apply { - sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c -> + + fun sortUrls(): LinkedHashMap = linkedMapOf().apply { + kotlin.runCatching { + var a = sortUrl + if (sortUrl?.startsWith("", false) == true + || sortUrl?.startsWith("@js", false) == true + ) { + val aCache = ACache.get(appCtx, "rssSortUrl") + a = aCache.getAsString(sourceUrl) ?: "" + if (a.isBlank()) { + val bindings = SimpleBindings() + bindings["baseUrl"] = sourceUrl + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + val jsStr = if (sortUrl!!.startsWith("@")) { + sortUrl!!.substring(3) + } else { + sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<")) + } + a = AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString() + aCache.put(sourceUrl, a) + } + } + a?.split("(&&|\n)+".toRegex())?.forEach { c -> val d = c.split("::") if (d.size > 1) this[d[0]] = d[1] @@ -102,4 +139,5 @@ data class RssSource( this[""] = sourceUrl } } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssStar.kt b/app/src/main/java/io/legado/app/data/entities/RssStar.kt index 7e78550bb..dcd8ea737 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssStar.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssStar.kt @@ -1,6 +1,11 @@ package io.legado.app.data.entities import androidx.room.Entity +import androidx.room.Ignore +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel @Entity( @@ -16,16 +21,30 @@ data class RssStar( var pubDate: String? = null, var description: String? = null, var content: String? = null, - var image: String? = null -) { + var image: String? = null, + var variable: String? = null +) : RuleDataInterface { + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap by lazy { + GSON.fromJsonObject>(variable) ?: HashMap() + } + + override fun putVariable(key: String, value: String) { + variableMap[key] = value + variable = GSON.toJson(variableMap) + } + fun toRssArticle() = RssArticle( - origin = origin, - sort = sort, - title = title, - link = link, - pubDate = pubDate, - description = description, - content = content, - image = image - ) + origin = origin, + sort = sort, + title = title, + link = link, + pubDate = pubDate, + description = description, + content = content, + image = image + ) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RuleSub.kt b/app/src/main/java/io/legado/app/data/entities/RuleSub.kt new file mode 100644 index 000000000..2b43787fe --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RuleSub.kt @@ -0,0 +1,16 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "ruleSubs") +data class RuleSub( + @PrimaryKey + val id: Long = System.currentTimeMillis(), + var name: String = "", + var url: String = "", + var type: Int = 0, + var customOrder: Int = 0, + var autoUpdate: Boolean = false, + var update: Long = System.currentTimeMillis() +) diff --git a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt index 71bd9016f..70896b9da 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt @@ -1,11 +1,13 @@ package io.legado.app.data.entities +import android.content.Context import android.os.Parcelable import androidx.room.* +import io.legado.app.R import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize @Parcelize @Entity( @@ -25,8 +27,8 @@ data class SearchBook( var origin: String = "", // 书源规则 var originName: String = "", var type: Int = 0, // @BookType - var name: String = "", - var author: String = "", + override var name: String = "", + override var author: String = "", override var kind: String? = null, var coverUrl: String? = null, var intro: String? = null, @@ -36,45 +38,45 @@ data class SearchBook( var time: Long = System.currentTimeMillis(), var variable: String? = null, var originOrder: Int = 0 -): Parcelable, BaseBook, Comparable { - +) : Parcelable, BaseBook, Comparable { + @Ignore @IgnoredOnParcel override var infoHtml: String? = null - + @Ignore @IgnoredOnParcel override var tocHtml: String? = null - + override fun equals(other: Any?) = other is SearchBook && other.bookUrl == bookUrl - + override fun hashCode() = bookUrl.hashCode() - + override fun compareTo(other: SearchBook): Int { return other.originOrder - this.originOrder } - + @delegate:Transient @delegate:Ignore @IgnoredOnParcel override val variableMap by lazy { GSON.fromJsonObject>(variable) ?: HashMap() } - + override fun putVariable(key: String, value: String) { variableMap[key] = value variable = GSON.toJson(variableMap) } - + @delegate:Transient @delegate:Ignore @IgnoredOnParcel val origins: LinkedHashSet by lazy { linkedSetOf(origin) } - + fun addOrigin(origin: String) { origins.add(origin) } - + fun getDisplayLastChapterTitle(): String { latestChapterTitle?.let { if (it.isNotEmpty()) { @@ -83,7 +85,16 @@ data class SearchBook( } return "无最新章节" } - + + fun trimIntro(context: Context): String { + val trimIntro = intro?.trim() + return if (trimIntro.isNullOrEmpty()) { + context.getString(R.string.intro_show_null) + } else { + context.getString(R.string.intro_show, trimIntro) + } + } + fun toBook() = Book( name = name, author = author, diff --git a/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt index 89d1c3baf..955e5fe3d 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt @@ -4,7 +4,7 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize diff --git a/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt index 882ffbad7..9f3a0c9d0 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt @@ -1,7 +1,8 @@ package io.legado.app.data.entities.rule import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize + @Parcelize data class BookInfoRule( @@ -14,5 +15,6 @@ data class BookInfoRule( var updateTime: String? = null, var coverUrl: String? = null, var tocUrl: String? = null, - var wordCount: String? = null + var wordCount: String? = null, + var canReName: String? = null ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt index a3c565652..7da6e9d57 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt @@ -1,7 +1,7 @@ package io.legado.app.data.entities.rule import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class ContentRule( @@ -9,6 +9,6 @@ data class ContentRule( var nextContentUrl: String? = null, var webJs: String? = null, var sourceRegex: String? = null, - var replaceRegex: String? = null, - var imageStyle: String? = null //默认大小居中,1最大宽度 + var replaceRegex: String? = null, //替换规则 + var imageStyle: String? = null, //默认大小居中,FULL最大宽度 ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt index 41031e61e..bc2a425f5 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt @@ -1,7 +1,7 @@ package io.legado.app.data.entities.rule import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class ExploreRule( diff --git a/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt index f5683f05b..4c627cbc3 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt @@ -1,7 +1,8 @@ package io.legado.app.data.entities.rule import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize + @Parcelize data class SearchRule( diff --git a/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt index d040f3905..745d55519 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt @@ -1,7 +1,7 @@ package io.legado.app.data.entities.rule import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class TocRule( diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index e2538e902..5a381cf73 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -1,13 +1,56 @@ package io.legado.app.help -import android.annotation.SuppressLint import android.content.Context -import io.legado.app.App -import io.legado.app.R +import android.content.SharedPreferences +import io.legado.app.constant.AppConst import io.legado.app.constant.PreferKey import io.legado.app.utils.* - -object AppConfig { +import splitties.init.appCtx + +@Suppress("MemberVisibilityCanBePrivate") +object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { + val isGooglePlay = appCtx.channel == "google" + val isCronet = appCtx.channel == "cronet" + var userAgent: String = getPrefUserAgent() + var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3" + var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2) + var clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2) + var clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1) + var clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2) + var clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0) + var clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1) + var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2) + var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1) + var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1) + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3" + PreferKey.clickActionTL -> clickActionTL = + appCtx.getPrefInt(PreferKey.clickActionTL, 2) + PreferKey.clickActionTC -> clickActionTC = + appCtx.getPrefInt(PreferKey.clickActionTC, 2) + PreferKey.clickActionTR -> clickActionTR = + appCtx.getPrefInt(PreferKey.clickActionTR, 2) + PreferKey.clickActionML -> clickActionML = + appCtx.getPrefInt(PreferKey.clickActionML, 2) + PreferKey.clickActionMC -> clickActionMC = + appCtx.getPrefInt(PreferKey.clickActionMC, 2) + PreferKey.clickActionMR -> clickActionMR = + appCtx.getPrefInt(PreferKey.clickActionMR, 2) + PreferKey.clickActionBL -> clickActionBL = + appCtx.getPrefInt(PreferKey.clickActionBL, 2) + PreferKey.clickActionBC -> clickActionBC = + appCtx.getPrefInt(PreferKey.clickActionBC, 2) + PreferKey.clickActionBR -> clickActionBR = + appCtx.getPrefInt(PreferKey.clickActionBR, 2) + PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = + appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + PreferKey.useZhLayout -> ReadBookConfig.useZhLayout = + appCtx.getPrefBoolean(PreferKey.useZhLayout) + PreferKey.userAgent -> userAgent = getPrefUserAgent() + } + } fun isNightTheme(context: Context): Boolean { return when (context.getPrefString(PreferKey.themeMode, "0")) { @@ -19,127 +62,192 @@ object AppConfig { } var isNightTheme: Boolean - get() = isNightTheme(App.INSTANCE) + get() = isNightTheme(appCtx) set(value) { if (isNightTheme != value) { if (value) { - App.INSTANCE.putPrefString(PreferKey.themeMode, "2") + appCtx.putPrefString(PreferKey.themeMode, "2") } else { - App.INSTANCE.putPrefString(PreferKey.themeMode, "1") + appCtx.putPrefString(PreferKey.themeMode, "1") } } } - val isEInkMode: Boolean - get() = App.INSTANCE.getPrefString(PreferKey.themeMode) == "3" + var showUnread: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showUnread, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.showUnread, value) + } + + var readBrightness: Int + get() = if (isNightTheme) { + appCtx.getPrefInt(PreferKey.nightBrightness, 100) + } else { + appCtx.getPrefInt(PreferKey.brightness, 100) + } + set(value) { + if (isNightTheme) { + appCtx.putPrefInt(PreferKey.nightBrightness, value) + } else { + appCtx.putPrefInt(PreferKey.brightness, value) + } + } + + val useDefaultCover: Boolean + get() = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false) + + val isTransparentStatusBar: Boolean + get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true) + + val immNavigationBar: Boolean + get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true) + + val screenOrientation: String? + get() = appCtx.getPrefString(PreferKey.screenOrientation) - var isTransparentStatusBar: Boolean - get() = App.INSTANCE.getPrefBoolean(PreferKey.transparentStatusBar) + var bookGroupStyle: Int + get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0) set(value) { - App.INSTANCE.putPrefBoolean(PreferKey.transparentStatusBar, value) + appCtx.putPrefInt(PreferKey.bookGroupStyle, value) } - val requestedDirection: String? - get() = App.INSTANCE.getPrefString(R.string.pk_requested_direction) + var bookExportFileName: String? + get() = appCtx.getPrefString(PreferKey.bookExportFileName) + set(value) { + appCtx.putPrefString(PreferKey.bookExportFileName, value) + } + + var bookImportFileName: String? + get() = appCtx.getPrefString(PreferKey.bookImportFileName) + set(value) { + appCtx.putPrefString(PreferKey.bookImportFileName, value) + } var backupPath: String? - get() = App.INSTANCE.getPrefString(PreferKey.backupPath) + get() = appCtx.getPrefString(PreferKey.backupPath) set(value) { if (value.isNullOrEmpty()) { - App.INSTANCE.removePref(PreferKey.backupPath) + appCtx.removePref(PreferKey.backupPath) } else { - App.INSTANCE.putPrefString(PreferKey.backupPath, value) + appCtx.putPrefString(PreferKey.backupPath, value) } } - var isShowRSS: Boolean - get() = App.INSTANCE.getPrefBoolean(PreferKey.showRss, true) - set(value) { - App.INSTANCE.putPrefBoolean(PreferKey.showRss, value) - } + val showDiscovery: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) + + val showRSS: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showRss, true) val autoRefreshBook: Boolean - get() = App.INSTANCE.getPrefBoolean(R.string.pk_auto_refresh) + get() = appCtx.getPrefBoolean(PreferKey.autoRefresh) var threadCount: Int - get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) + get() = appCtx.getPrefInt(PreferKey.threadCount, 8) set(value) { - App.INSTANCE.putPrefInt(PreferKey.threadCount, value) + appCtx.putPrefInt(PreferKey.threadCount, value) } var importBookPath: String? - get() = App.INSTANCE.getPrefString("importBookPath") + get() = appCtx.getPrefString("importBookPath") set(value) { if (value == null) { - App.INSTANCE.removePref("importBookPath") + appCtx.removePref("importBookPath") } else { - App.INSTANCE.putPrefString("importBookPath", value) + appCtx.putPrefString("importBookPath", value) } } var ttsSpeechRate: Int - get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5) + get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, 5) set(value) { - App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value) + appCtx.putPrefInt(PreferKey.ttsSpeechRate, value) } - val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) - var chineseConverterType: Int - get() = App.INSTANCE.getPrefInt(PreferKey.chineseConverterType) + get() = appCtx.getPrefInt(PreferKey.chineseConverterType) set(value) { - App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value) + appCtx.putPrefInt(PreferKey.chineseConverterType, value) } var systemTypefaces: Int - get() = App.INSTANCE.getPrefInt(PreferKey.systemTypefaces) + get() = appCtx.getPrefInt(PreferKey.systemTypefaces) set(value) { - App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value) + appCtx.putPrefInt(PreferKey.systemTypefaces, value) } - var bookGroupAllShow: Boolean - get() = App.INSTANCE.getPrefBoolean("bookGroupAll", true) + var elevation: Int + get() = appCtx.getPrefInt(PreferKey.barElevation, AppConst.sysElevation) set(value) { - App.INSTANCE.putPrefBoolean("bookGroupAll", value) + appCtx.putPrefInt(PreferKey.barElevation, value) } - var bookGroupLocalShow: Boolean - get() = App.INSTANCE.getPrefBoolean("bookGroupLocal", false) + var exportCharset: String + get() { + val c = appCtx.getPrefString(PreferKey.exportCharset) + if (c.isNullOrBlank()) { + return "UTF-8" + } + return c + } set(value) { - App.INSTANCE.putPrefBoolean("bookGroupLocal", value) + appCtx.putPrefString(PreferKey.exportCharset, value) } - var bookGroupAudioShow: Boolean - get() = App.INSTANCE.getPrefBoolean("bookGroupAudio", false) + var exportUseReplace: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportUseReplace, true) set(value) { - App.INSTANCE.putPrefBoolean("bookGroupAudio", value) + appCtx.putPrefBoolean(PreferKey.exportUseReplace, value) } - var bookGroupNoneShow: Boolean - get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false) + var exportToWebDav: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportToWebDav) set(value) { - App.INSTANCE.putPrefBoolean("bookGroupNone", value) + appCtx.putPrefBoolean(PreferKey.exportToWebDav, value) } - var elevation: Int - @SuppressLint("PrivateResource") - get() = App.INSTANCE.getPrefInt( - PreferKey.barElevation, - App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt() - ) + var exportType: Int + get() = appCtx.getPrefInt(PreferKey.exportType) + set(value) { + appCtx.putPrefInt(PreferKey.exportType, value) + } + + var changeSourceCheckAuthor: Boolean + get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor) set(value) { - App.INSTANCE.putPrefInt(PreferKey.barElevation, value) + appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value) } - var replaceEnableDefault: Boolean = - App.INSTANCE.getPrefBoolean(PreferKey.replaceEnableDefault, true) + val autoChangeSource: Boolean + get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true) + + val changeSourceLoadInfo get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) + + val changeSourceLoadToc get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) + + val importKeepName get() = appCtx.getPrefBoolean(PreferKey.importKeepName) + + val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true) - val autoChangeSource: Boolean get() = App.INSTANCE.getPrefBoolean("autoChangeSource", true) + var preDownloadNum + get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10) + set(value) { + appCtx.putPrefInt(PreferKey.preDownloadNum, value) + } + + val mediaButtonOnExit get() = appCtx.getPrefBoolean("mediaButtonOnExit", true) - val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true) + val replaceEnableDefault get() = appCtx.getPrefBoolean(PreferKey.replaceEnableDefault, true) - val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google" + val fullScreenGesturesSupport: Boolean + get() = appCtx.getPrefBoolean(PreferKey.fullScreenGesturesSupport, false) - val isCoolApk: Boolean get() = App.INSTANCE.channel == "coolApk" + private fun getPrefUserAgent(): String { + val ua = appCtx.getPrefString(PreferKey.userAgent) + if (ua.isNullOrBlank()) { + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + return ua + } } diff --git a/app/src/main/java/io/legado/app/help/BlurTransformation.kt b/app/src/main/java/io/legado/app/help/BlurTransformation.kt index 4493228c4..af4ab506a 100644 --- a/app/src/main/java/io/legado/app/help/BlurTransformation.kt +++ b/app/src/main/java/io/legado/app/help/BlurTransformation.kt @@ -23,12 +23,17 @@ class BlurTransformation(context: Context, private val radius: Int) : CenterCrop 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 { + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { val transform = super.transform(pool, toTransform, outWidth, outHeight) //图片缩小1/2 val width = (min(outWidth, transform.width) / 2f).roundToInt() val height = (min(outHeight, transform.height) / 2f).roundToInt() - val blurredBitmap = Bitmap.createScaledBitmap(transform, width, height, false); + val blurredBitmap = Bitmap.createScaledBitmap(transform, width, height, false) // Allocate memory for Renderscript to work with //分配用于渲染脚本的内存 val input = Allocation.createFromBitmap( diff --git a/app/src/main/java/io/legado/app/help/BookHelp.kt b/app/src/main/java/io/legado/app/help/BookHelp.kt index 89a2bf41b..1c7733f9f 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -1,40 +1,31 @@ package io.legado.app.help -import com.hankcs.hanlp.HanLP -import io.legado.app.App +import android.net.Uri import io.legado.app.constant.AppPattern import io.legado.app.constant.EventBus +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.ReplaceRule import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.localBook.LocalBook import io.legado.app.utils.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext import org.apache.commons.text.similarity.JaccardSimilarity -import org.jetbrains.anko.toast +import splitties.init.appCtx import java.io.File import java.util.concurrent.CopyOnWriteArraySet +import java.util.regex.Pattern +import kotlin.math.abs +import kotlin.math.max import kotlin.math.min object BookHelp { private const val cacheFolderName = "book_cache" private const val cacheImageFolderName = "images" - private val downloadDir: File = App.INSTANCE.externalFilesDir + private val downloadDir: File = appCtx.externalFiles private val downloadImages = CopyOnWriteArraySet() - fun formatChapterName(bookChapter: BookChapter): String { - return String.format( - "%05d-%s.nb", - bookChapter.index, - MD5Utils.md5Encode16(bookChapter.title) - ) - } - fun clearCache() { FileUtils.deleteFile( FileUtils.getPath(downloadDir, cacheFolderName) @@ -47,12 +38,12 @@ object BookHelp { } /** - * 清楚已删除书的缓存 + * 清除已删除书的缓存 */ fun clearRemovedCache() { Coroutine.async { val bookFolderNames = arrayListOf() - App.db.bookDao().all.forEach { + appDb.bookDao.all.forEach { bookFolderNames.add(it.getFolderName()) } val file = FileUtils.getFile(downloadDir, cacheFolderName) @@ -64,6 +55,28 @@ object BookHelp { } } + fun getEpubFile(book: Book): File { + val file = FileUtils.getFile( + downloadDir, + cacheFolderName, + book.getFolderName(), + "index.epubx" + ) + if (!file.exists()) { + val input = if (book.bookUrl.isContentScheme()) { + val uri = Uri.parse(book.bookUrl) + appCtx.contentResolver.openInputStream(uri) + } else { + File(book.bookUrl).inputStream() + } + if (input != null) { + FileUtils.writeInputStream(file, input) + } + + } + return file + } + suspend fun saveContent(book: Book, bookChapter: BookChapter, content: String) { if (content.isEmpty()) return //保存文本 @@ -71,16 +84,15 @@ object BookHelp { downloadDir, cacheFolderName, book.getFolderName(), - formatChapterName(bookChapter), + bookChapter.getFileName(), ).writeText(content) //保存图片 content.split("\n").forEach { val matcher = AppPattern.imgPattern.matcher(it) if (matcher.find()) { - var src = matcher.group(1) - src = NetworkUtils.getAbsoluteURL(bookChapter.url, src) - src?.let { - saveImage(book, src) + matcher.group(1)?.let { src -> + val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src) + saveImage(book, mSrc) } } } @@ -97,7 +109,7 @@ object BookHelp { downloadImages.add(src) val analyzeUrl = AnalyzeUrl(src) try { - analyzeUrl.getImageBytes(book.origin)?.let { + analyzeUrl.getByteArray(book.origin).let { FileUtils.createFileIfNotExist( downloadDir, cacheFolderName, @@ -123,7 +135,7 @@ object BookHelp { ) } - private fun getImageSuffix(src: String): String { + fun getImageSuffix(src: String): String { var suffix = src.substringAfterLast(".").substringBefore(",") if (suffix.length > 5) { suffix = ".jpg" @@ -133,7 +145,7 @@ object BookHelp { fun getChapterFiles(book: Book): List { val fileNameList = arrayListOf() - if (book.isLocalBook()) { + if (book.isLocalTxt()) { return fileNameList } FileUtils.createFolderIfNotExist( @@ -145,28 +157,58 @@ object BookHelp { return fileNameList } + // 检测该章节是否下载 fun hasContent(book: Book, bookChapter: BookChapter): Boolean { - return if (book.isLocalBook()) { + return if (book.isLocalTxt()) { true } else { FileUtils.exists( downloadDir, cacheFolderName, book.getFolderName(), - formatChapterName(bookChapter) + bookChapter.getFileName() ) } } + fun hasImageContent(book: Book, bookChapter: BookChapter): Boolean { + if (!hasContent(book, bookChapter)) { + return false + } + getContent(book, bookChapter)?.let { + val matcher = AppPattern.imgPattern.matcher(it) + while (matcher.find()) { + matcher.group(1)?.let { src -> + val image = getImage(book, src) + if (!image.exists()) { + return false + } + } + } + } + return true + } + fun getContent(book: Book, bookChapter: BookChapter): String? { - if (book.isLocalBook()) { + if (book.isLocalTxt() || book.isUmd()) { return LocalBook.getContext(book, bookChapter) + } else if (book.isEpub() && !hasContent(book, bookChapter)) { + val string = LocalBook.getContext(book, bookChapter) + string?.let { + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName(), + ).writeText(it) + } + return string } else { val file = FileUtils.getFile( downloadDir, cacheFolderName, book.getFolderName(), - formatChapterName(bookChapter) + bookChapter.getFileName() ) if (file.exists()) { return file.readText() @@ -175,15 +217,34 @@ object BookHelp { return null } + fun reverseContent(book: Book, bookChapter: BookChapter) { + if (!book.isLocalBook()) { + val file = FileUtils.getFile( + downloadDir, + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName() + ) + if (file.exists()) { + val text = file.readText() + val stringBuilder = StringBuilder() + text.toStringArray().forEach { + stringBuilder.insert(0, it) + } + file.writeText(stringBuilder.toString()) + } + } + } + fun delContent(book: Book, bookChapter: BookChapter) { - if (book.isLocalBook()) { + if (book.isLocalTxt()) { return } else { FileUtils.createFileIfNotExist( downloadDir, cacheFolderName, book.getFolderName(), - formatChapterName(bookChapter) + bookChapter.getFileName() ).delete() } } @@ -200,138 +261,118 @@ object BookHelp { .trim { it <= ' ' } } + private val jaccardSimilarity by lazy { + JaccardSimilarity() + } + /** - * 找到相似度最高的章节 + * 根据目录名获取当前章节 */ - fun getDurChapterIndexByChapterTitle( - title: String?, - index: Int, - chapters: List, + fun getDurChapter( + oldDurChapterIndex: Int, + oldChapterListSize: Int, + oldDurChapterName: String?, + newChapterList: List ): Int { - if (title.isNullOrEmpty()) { - return min(index, chapters.lastIndex) - } - if (chapters.size > index && title == chapters[index].title) { - return index - } - + if (oldChapterListSize == 0) return oldDurChapterIndex + if (newChapterList.isEmpty()) return oldDurChapterIndex + val oldChapterNum = getChapterNum(oldDurChapterName) + val oldName = getPureChapterName(oldDurChapterName) + val newChapterSize = newChapterList.size + val min = max( + 0, + min( + oldDurChapterIndex, + oldDurChapterIndex - oldChapterListSize + newChapterSize + ) - 10 + ) + val max = min( + newChapterSize - 1, + max( + oldDurChapterIndex, + oldDurChapterIndex - oldChapterListSize + newChapterSize + ) + 10 + ) + var nameSim = 0.0 var newIndex = 0 - val jSimilarity = JaccardSimilarity() - var similarity = if (chapters.size > index) { - jSimilarity.apply(title, chapters[index].title) - } else 0.0 - if (similarity == 1.0) { - return index - } else { - for (i in 1..50) { - if (index - i in chapters.indices) { - jSimilarity.apply(title, chapters[index - i].title).let { - if (it > similarity) { - similarity = it - newIndex = index - i - if (similarity == 1.0) { - return newIndex - } - } - } + var newNum = 0 + if (oldName.isNotEmpty()) { + for (i in min..max) { + val newName = getPureChapterName(newChapterList[i].title) + val temp = jaccardSimilarity.apply(oldName, newName) + if (temp > nameSim) { + nameSim = temp + newIndex = i } - if (index + i in chapters.indices) { - jSimilarity.apply(title, chapters[index + i].title).let { - if (it > similarity) { - similarity = it - newIndex = index + i - if (similarity == 1.0) { - return newIndex - } - } - } + } + } + if (nameSim < 0.96 && oldChapterNum > 0) { + for (i in min..max) { + val temp = getChapterNum(newChapterList[i].title) + if (temp == oldChapterNum) { + newNum = temp + newIndex = i + break + } else if (abs(temp - oldChapterNum) < abs(newNum - oldChapterNum)) { + newNum = temp + newIndex = i } } } - return newIndex + return if (nameSim > 0.96 || abs(newNum - oldChapterNum) < 1) { + newIndex + } else { + min(max(0, newChapterList.size - 1), oldDurChapterIndex) + } } - private var bookName: String? = null - private var bookOrigin: String? = null - private var replaceRules: List = arrayListOf() + private val chapterNamePattern1 by lazy { + Pattern.compile(".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话]") + } - @Synchronized - suspend fun upReplaceRules() { - withContext(IO) { - synchronized(this) { - val o = bookOrigin - bookName?.let { - replaceRules = if (o.isNullOrEmpty()) { - App.db.replaceRuleDao().findEnabledByScope(it) - } else { - App.db.replaceRuleDao().findEnabledByScope(it, o) - } - } - } - } + private val chapterNamePattern2 by lazy { + Pattern.compile("^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、]|\\.[^\\d])") } - suspend fun disposeContent( - title: String, - name: String, - origin: String?, - content: String, - enableReplace: Boolean, - ): List { - var c = content - if (enableReplace) { - synchronized(this) { - if (bookName != name || bookOrigin != origin) { - bookName = name - bookOrigin = origin - replaceRules = if (origin.isNullOrEmpty()) { - App.db.replaceRuleDao().findEnabledByScope(name) - } else { - App.db.replaceRuleDao().findEnabledByScope(name, origin) - } - } - } - replaceRules.forEach { item -> - item.pattern.let { - if (it.isNotEmpty()) { - try { - c = if (item.isRegex) { - c.replace(it.toRegex(), item.replacement) - } else { - c.replace(it, item.replacement) - } - } catch (e: Exception) { - withContext(Main) { - App.INSTANCE.toast("${item.name}替换出错") - } - } - } - } - } - } - try { - when (AppConfig.chineseConverterType) { - 1 -> c = HanLP.convertToSimplifiedChinese(c) - 2 -> c = HanLP.convertToTraditionalChinese(c) - } - } catch (e: Exception) { - withContext(Main) { - App.INSTANCE.toast("简繁转换出错") - } - } - val contents = arrayListOf() - c.split("\n").forEach { - val str = it.replace("^\\s+".toRegex(), "") - .replace("\r", "") - if (contents.isEmpty()) { - contents.add(title) - if (str != title && it.isNotEmpty()) { - contents.add("${ReadBookConfig.bodyIndent}$str") - } - } else if (str.isNotEmpty()) { - contents.add("${ReadBookConfig.bodyIndent}$str") - } - } - return contents + private val regexA by lazy { + return@lazy "\\s".toRegex() + } + + private fun getChapterNum(chapterName: String?): Int { + chapterName ?: return -1 + val chapterName1 = StringUtils.fullToHalf(chapterName).replace(regexA, "") + return StringUtils.stringToInt( + ( + chapterNamePattern1.matcher(chapterName1).takeIf { it.find() } + ?: chapterNamePattern2.matcher(chapterName1).takeIf { it.find() } + )?.group(1) + ?: "-1" + ) + } + + @Suppress("SpellCheckingInspection") + private val regexOther by lazy { + // 所有非字母数字中日韩文字 CJK区+扩展A-F区 + @Suppress("RegExpDuplicateCharacterInClass") + return@lazy "[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]".toRegex() + } + + private val regexB by lazy { + //章节序号,排除处于结尾的状况,避免将章节名替换为空字串 + return@lazy "^.*?第(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话](?!$)|^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、](?!$)|\\.(?=[^\\d]))".toRegex() } -} \ No newline at end of file + + private val regexC by lazy { + //前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串 + return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex() + } + + private fun getPureChapterName(chapterName: String?): String { + return if (chapterName == null) "" else StringUtils.fullToHalf(chapterName) + .replace(regexA, "") + .replace(regexB, "") + .replace(regexC, "") + .replace(regexOther, "") + } + +} diff --git a/app/src/main/java/io/legado/app/help/CacheManager.kt b/app/src/main/java/io/legado/app/help/CacheManager.kt new file mode 100644 index 000000000..6194a6175 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/CacheManager.kt @@ -0,0 +1,62 @@ +package io.legado.app.help + +import io.legado.app.data.appDb +import io.legado.app.data.entities.Cache +import io.legado.app.model.analyzeRule.QueryTTF +import io.legado.app.utils.ACache +import splitties.init.appCtx + +@Suppress("unused") +object CacheManager { + + private val queryTTFMap = hashMapOf>() + + /** + * saveTime 单位为秒 + */ + @JvmOverloads + fun put(key: String, value: Any, saveTime: Int = 0) { + val deadline = + if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 + when (value) { + is QueryTTF -> queryTTFMap[key] = Pair(deadline, value) + is ByteArray -> ACache.get(appCtx).put(key, value, saveTime) + else -> { + val cache = Cache(key, value.toString(), deadline) + appDb.cacheDao.insert(cache) + } + } + } + + fun get(key: String): String? { + return appDb.cacheDao.get(key, System.currentTimeMillis()) + } + + fun getInt(key: String): Int? { + return get(key)?.toIntOrNull() + } + + fun getLong(key: String): Long? { + return get(key)?.toLongOrNull() + } + + fun getDouble(key: String): Double? { + return get(key)?.toDoubleOrNull() + } + + fun getFloat(key: String): Float? { + return get(key)?.toFloatOrNull() + } + + fun getByteArray(key: String): ByteArray? { + return ACache.get(appCtx).getAsBinary(key) + } + + fun getQueryTTF(key: String): QueryTTF? { + val cache = queryTTFMap[key] ?: return null + if (cache.first == 0L || cache.first > System.currentTimeMillis()) { + return cache.second + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ContentHelp.kt b/app/src/main/java/io/legado/app/help/ContentHelp.kt new file mode 100644 index 000000000..b31580cbd --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ContentHelp.kt @@ -0,0 +1,621 @@ +package io.legado.app.help + +import java.util.* +import java.util.regex.Pattern +import kotlin.math.max +import kotlin.math.min + +@Suppress("SameParameterValue", "RegExpRedundantEscape") +object ContentHelp { + + /** + * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分 + * + * @param content 正文 + * @param chapterName 标题 + * @return + */ + fun reSegment(content: String, chapterName: String): String { + var content1 = content + val dict = makeDict(content1) + var p = content1 + .replace(""".toRegex(), "“") + .replace("[::]['\"‘”“]+".toRegex(), ":“") + .replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .split("\n(\\s*)".toRegex()).toTypedArray() + + //初始化StringBuffer的长度,在原content的长度基础上做冗余 + var buffer = StringBuffer((content1.length * 1.15).toInt()) + // 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。 + buffer.append(" ") + if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) { + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), "")) + } + + //如果原文存在分段错误,需要把段落重新黏合 + for (i in 1 until p.size) { + if (match(MARK_SENTENCES_END, buffer[buffer.length - 1])) buffer.append("\n") + // 段落开头以外的地方不应该有空格 + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[i].replace("[\u3000\\s]".toRegex(), "")) + } + // 预分段预处理 + // ”“处理为”\n“。 + // ”。“处理为”。\n“。不考虑“?” “!”的情况。 + // ”。xxx处理为 ”。\n xxx + p = buffer.toString() + .replace("[\"”“]+[\\s]*[\"”“]+".toRegex(), "”\n“") + .replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“") + .replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2") + .replace( + "([问说喊唱叫骂道着答])[\\.。]".toRegex(), + "$1。\n" + ) + .split("\n".toRegex()).toTypedArray() + buffer = StringBuffer((content1.length * 1.15).toInt()) + for (s in p) { + buffer.append("\n") + buffer.append(findNewLines(s, dict)) + } + buffer = reduceLength(buffer) + content1 = ("$chapterName\n\n" + buffer.toString() // 处理章节头部空格和换行 + .replaceFirst("^\\s+".toRegex(), "") + .replace("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .replace("[::][”“\"\\s]+".toRegex(), ":“") + .replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3") + .replace("\n(\\s*)".toRegex(), "\n")) + return content1 + } + + /** + * 强制切分,减少段落内的句子 + * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落 + * 如果引号内的内容长于5句,可能引号状态有误,随机分段 + * 如果引号外的内容长于3句,随机分段 + * + * @param str + * @return + */ + private fun reduceLength(str: StringBuffer): StringBuffer { + val p = str.toString().split("\n".toRegex()).toTypedArray() + val l = p.size + val b = BooleanArray(l) + for (i in 0 until l) { + b[i] = p[i].matches(PARAGRAPH_DIAGLOG) + } + var dialogue = 0 + for (i in 0 until l) { + if (b[i]) { + if (dialogue < 0) dialogue = 1 else if (dialogue < 2) dialogue++ + } else { + if (dialogue > 1) { + p[i] = splitQuote(p[i]) + dialogue-- + } else if (dialogue > 0 && i < l - 2) { + if (b[i + 1]) p[i] = splitQuote(p[i]) + } + } + } + val string = StringBuffer() + for (i in 0 until l) { + string.append('\n') + string.append(p[i]) + //System.out.print(" "+b[i]); + } + //System.out.println(" " + str); + return string + } + + // 强制切分进入对话模式后,未构成 “xxx” 形式的段落 + private fun splitQuote(str: String): String { + val length = str.length + if (length < 3) return str + if (match(MARK_QUOTATION, str[0])) { + val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1 + if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.substring(0, i)}\n${str.substring(i)}" + } + } else if (match(MARK_QUOTATION, str[length - 1])) { + val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false) + if (i > 1) { + if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.substring(0, i)}\n${str.substring(i)}" + } + } + } + return str + } + + /** + * 计算随机插入换行符的位置。 + * @param str 字符串 + * @param offset 传回的结果需要叠加的偏移量 + * @param min 最低几个句子,随机插入换行 + * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 + * @return + */ + private fun forceSplit( + str: String, + offset: Int, + min: Int, + gain: Int, + tigger: Int + ): ArrayList { + val result = ArrayList() + val arrayEnd = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length - 2, true) + val arrayMid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length - 2, true) + if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result + var j = 0 + var i = min + while (i < arrayEnd.size) { + var k = 0 + while (j < arrayMid.size) { + if (arrayMid[j] < arrayEnd[i]) k++ + j++ + } + if (Math.random() * gain < 0.8 + k / 2.5) { + result.add(arrayEnd[i] + offset) + i = max(i + min, i) + } + i++ + } + return result + } + + // 对内容重新划分段落.输入参数str已经使用换行符预分割 + private fun findNewLines(str: String, dict: List): String { + val string = StringBuffer(str) + // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。 + val arrayQuote: MutableList = ArrayList() + // 标记插入换行符的位置,int为插入位置(str的char下标) + var insN = ArrayList() + + //mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。 + //长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间 + //0未知,正数引号内,负数引号外。 + //如果相邻的两个标记都为+1,那么需要增加1个引号。 + //引号内不进行断句 + val mod = IntArray(str.length) + var waitClose = false + for (i in str.indices) { + val c = str[i] + if (match(MARK_QUOTATION, c)) { + val size = arrayQuote.size + + // 把“xxx”、“yy”合并为“xxx_yy”进行处理 + if (size > 0) { + val quotePre = arrayQuote[size - 1] + if (i - quotePre == 2) { + var remove = false + if (waitClose) { + if (match(",,、/", str[i - 1])) { + // 考虑出现“和”这种特殊情况 + remove = true + } + } else if (match(",,、/和与或", str[i - 1])) { + remove = true + } + if (remove) { + string.setCharAt(i, '“') + string.setCharAt(i - 2, '”') + arrayQuote.removeAt(size - 1) + mod[size - 1] = 1 + mod[size] = -1 + continue + } + } + } + arrayQuote.add(i) + + // 为xxx:“xxx”做标记 + if (i > 1) { + // 当前发言的正引号的前一个字符 + val charB1 = str[i - 1] + // 上次发言的正引号的前一个字符 + var charB2 = 0.toChar() + if (match(MARK_QUOTATION_BEFORE, charB1)) { + // 如果不是第一处引号,寻找上一处断句,进行分段 + if (arrayQuote.size > 1) { + val lastQuote = arrayQuote[arrayQuote.size - 2] + var p = 0 + if (charB1 == ',' || charB1 == ',') { + if (arrayQuote.size > 2) { + p = arrayQuote[arrayQuote.size - 3] + if (p > 0) { + charB2 = str[p - 1] + } + } + } + //if(char_b2=='.' || char_b2=='。') + if (match(MARK_SENTENCES_END_P, charB2)) { + insN.add(p - 1) + } else if (!match("的", charB2)) { + val lastEnd = seekLast(str, MARK_SENTENCES_END, i, lastQuote) + if (lastEnd > 0) insN.add(lastEnd) else insN.add(lastQuote) + } + } + waitClose = true + mod[size] = 1 + if (size > 0) { + mod[size - 1] = -1 + if (size > 1) { + mod[size - 2] = 1 + } + } + } else if (waitClose) { + run { + waitClose = false + insN.add(i) + } + } + } + } + } + val size = arrayQuote.size + + + //标记循环状态,此位置前的引号是否已经配对 + var opend = false + if (size > 0) { + //第1次遍历array_quote,令其元素的值不为0 + for (i in 0 until size) { + if (mod[i] > 0) { + opend = true + } else if (mod[i] < 0) { + //连续2个反引号表明存在冲突,强制把前一个设为正引号 + if (!opend) { + if (i > 0) mod[i] = 3 + } + opend = false + } else { + opend = !opend + if (opend) mod[i] = 2 else mod[i] = -2 + } + } + // 修正,断尾必须封闭引号 + if (opend) { + if (arrayQuote[size - 1] - string.length > -3) { + //if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){ + if (size > 1) mod[size - 2] = 4 + // 0<=i=1 + mod[size - 1] = -4 + } else if (!match(MARK_SENTENCES_SAY, string[string.length - 2])) string.append( + "”" + ) + } + + + //第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行 + var loop2Mod1 = -1 //上一个引号跟随内容的状态 + var loop2Mod2: Int //当前引号跟随内容的状态 + var i = 0 + var j = arrayQuote[0] - 1 //当前引号前一字符的序号 + if (j < 0) { + i = 1 + loop2Mod1 = 0 + } + while (i < size) { + j = arrayQuote[i] - 1 + loop2Mod2 = mod[i] + if (loop2Mod1 < 0 && loop2Mod2 > 0) { + if (match(MARK_SENTENCES_END, string[j])) insN.add(j) + } + loop2Mod1 = loop2Mod2 + i++ + } + } + + //第3次循环,匹配并插入换行。 + //"xxxx" xxxx。\n xxx“xxxx” + //未实现 + + // 使用字典验证ins_n , 避免插入不必要的换行。 + // 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题 + val insN1 = ArrayList() + for (i in insN) { + if (match("\"'”“", string[i])) { + val start: Int = seekLast( + str, + "\"'”“", + i - 1, + i - WORD_MAX_LENGTH + ) + if (start > 0) { + val word = str.substring(start + 1, i) + if (dict.contains(word)) { + //System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word); + //引号内如果是字典词条,后方不插入换行符(前方不需要优化) + continue + } else { + //System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word); + if (match("的地得", str[start])) { + //xx的“xx”,后方不插入换行符(前方不需要优化) + continue + } + } + } + } + insN1.add(i) + } + insN = insN1 + +// 随机在句末插入换行符 + insN = ArrayList(HashSet(insN)) + insN.sort() + run { + var subs: String + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.size > 0) nextLine = insN[j] + var gain = 3 + var min = 0 + var trigger = 2 + for (i in arrayQuote.indices) { + val qutoe = arrayQuote[i] + if (qutoe > 0) { + gain = 4 + min = 2 + trigger = 4 + } else { + gain = 3 + min = 0 + trigger = 2 + } + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= qutoe) break + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < qutoe) { + subs = string.substring(progress, qutoe + 1) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = qutoe + 1 + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + subs = string.substring(progress, string.length) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + } + } + +// 根据段落状态修正引号方向、计算需要插入引号的位置 +// ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' + val insQuote = BooleanArray(size) + opend = false + for (i in 0 until size) { + val p = arrayQuote[i] + if (mod[i] > 0) { + string.setCharAt(p, '“') + if (opend) insQuote[i] = true + opend = true + } else if (mod[i] < 0) { + string.setCharAt(p, '”') + opend = false + } else { + opend = !opend + if (opend) string.setCharAt(p, '“') else string.setCharAt(p, '”') + } + } + insN = ArrayList(HashSet(insN)) + insN.sort() + +// 完成字符串拼接(从string复制、插入引号和换行 +// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' +// ins_n 插入换行。数组的值表示插入换行符的位置 + val buffer = StringBuffer((str.length * 1.15).toInt()) + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.size > 0) nextLine = insN[j] + for (i in arrayQuote.indices) { + val qutoe = arrayQuote[i] + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= qutoe) break + nextLine = insN[j] + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + j++ + } + if (progress < qutoe) { + buffer.append(string, progress, qutoe + 1) + progress = qutoe + 1 + } + if (insQuote[i] && buffer.length > 2) { + if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert( + buffer.length - 1, + "”\n" + ) + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress <= nextLine) { + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + buffer.append(string, progress, string.length) + } + return buffer.toString() + } + + /** + * 从字符串提取引号包围,且不止出现一次的内容为字典 + * + * @param str + * @return 词条列表 + */ + private fun makeDict(str: String): List { + + // 引号中间不包含任何标点 + val patten = Pattern.compile( + """ + (?<=["'”“])([^ + \p{P}]{1,${WORD_MAX_LENGTH}})(?=["'”“]) + """.trimIndent() + ) + //Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\"'”“]{1,16})(?=[\"'”“])"); + val matcher = patten.matcher(str) + val cache: MutableList = ArrayList() + val dict: MutableList = ArrayList() + while (matcher.find()) { + val word = matcher.group() + if (cache.contains(word)) { + if (!dict.contains(word)) dict.add(word) + } else cache.add(word) + } + return dict + } + + /** + * 计算匹配到字典的每个字符的位置 + * + * @param str 待匹配的字符串 + * @param key 字典 + * @param from 从字符串的第几个字符开始匹配 + * @param to 匹配到第几个字符结束 + * @param inOrder 是否按照从前向后的顺序匹配 + * @return 返回距离构成的ArrayList + */ + private fun seekIndexs( + str: String, + key: String, + from: Int, + to: Int, + inOrder: Boolean + ): ArrayList { + val list = ArrayList() + if (str.length - from < 1) return list + var i = 0 + if (from > i) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + list.add(i) + } + i++ + } + return list + } + + /** + * 计算字符串最后出现与字典中字符匹配的位置 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认最末位 + * @param to 匹配到哪个字符(不包含此字符)默认0 + * @return 位置(正向计算) + */ + private fun seekLast(str: String, key: String, from: Int, to: Int): Int { + if (str.length - from < 1) return -1 + var i = str.lastIndex + if (from < i && i > 0) i = from + var t = 0 + if (to > 0) t = to + var c: Char + while (i > t) { + c = str[i] + if (key.indexOf(c) != -1) { + return i + } + i-- + } + return -1 + } + + /** + * 计算字符串与字典中字符的最短距离 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认0 + * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 + * @param inOrder 是否从正向开始匹配 + * @return 返回最短距离, 注意不是str的char的下标 + */ + private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int { + if (str.length - from < 1) return -1 + var i = 0 + if (from > i) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + return i + } + i++ + } + return -1 + } + + /* 搜寻引号并进行分段。处理了一、二、五三类常见情况 + 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。 + 一、完整引用说话内容,在反引号内侧有断句标点。例如: + 1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!” + 2)“哎呀,真是美极了!”皇帝说,“我十分满意!” + 3)“怕什么!海的美就在这里!”我说道。 + 二、部分引用,在反引号外侧有断句标点: + 4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。 + 5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。 + 6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。 + 三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。 + 四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑 + 五、反语和强调,周围没有断句符号。 + */ + + // 句子结尾的标点。因为引号可能存在误判,不包含引号。 + private const val MARK_SENTENCES_END = "?。!?!~" + private const val MARK_SENTENCES_END_P = ".?。!?!~" + + // 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断 + private const val MARK_SENTENCES_MID = ".,、,—…" + private const val MARK_SENTENCES_SAY = "问说喊唱叫骂道着答" + + // XXX说:“”的冒号 + private const val MARK_QUOTATION_BEFORE = ",:,:" + + // 引号 + private const val MARK_QUOTATION = "\"“”" + private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex() + + // 限制字典的长度 + private const val WORD_MAX_LENGTH = 16 + + private fun match(rule: String, chr: Char): Boolean { + return rule.indexOf(chr) != -1 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ContentProcessor.kt b/app/src/main/java/io/legado/app/help/ContentProcessor.kt new file mode 100644 index 000000000..ac1521ea3 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ContentProcessor.kt @@ -0,0 +1,101 @@ +package io.legado.app.help + +import com.github.liuyueyi.quick.transfer.ChineseUtils +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.lang.ref.WeakReference + +class ContentProcessor private constructor( + private val bookName: String, + private val bookOrigin: String +) { + + companion object { + private val processors = hashMapOf>() + + fun get(bookName: String, bookOrigin: String): ContentProcessor { + val processorWr = processors[bookName + bookOrigin] + var processor: ContentProcessor? = processorWr?.get() + if (processor == null) { + processor = ContentProcessor(bookName, bookOrigin) + processors[bookName + bookOrigin] = WeakReference(processor) + } + return processor + } + + fun upReplaceRules() { + processors.forEach { + it.value.get()?.upReplaceRules() + } + } + + } + + private var replaceRules = arrayListOf() + + init { + upReplaceRules() + } + + @Synchronized + fun upReplaceRules() { + replaceRules.clear() + replaceRules.addAll(appDb.replaceRuleDao.findEnabledByScope(bookName, bookOrigin)) + } + + @Synchronized + fun getContent( + book: Book, + title: String, //已经经过简繁转换 + content: String, + isRead: Boolean = true, + useReplace: Boolean = book.getUseReplaceRule() + ): List { + var content1 = content + if (useReplace) { + replaceRules.forEach { item -> + if (item.pattern.isNotEmpty()) { + try { + content1 = if (item.isRegex) { + content1.replace(item.pattern.toRegex(), item.replacement) + } else { + content1.replace(item.pattern, item.replacement) + } + } catch (e: Exception) { + appCtx.toastOnUi("${item.name}替换出错") + } + } + } + } + if (isRead) { + if (book.getReSegment()) { + content1 = ContentHelp.reSegment(content1, title) + } + try { + when (AppConfig.chineseConverterType) { + 1 -> content1 = ChineseUtils.t2s(content1) + 2 -> content1 = ChineseUtils.s2t(content1) + } + } catch (e: Exception) { + appCtx.toastOnUi("简繁转换出错") + } + } + val contents = arrayListOf() + content1.split("\n").forEach { str -> + val paragraph = str.replace("^[\\n\\r]+".toRegex(), "").trim() + if (contents.isEmpty()) { + contents.add(title) + if (paragraph != title && paragraph.isNotEmpty()) { + contents.add("${ReadBookConfig.paragraphIndent}$paragraph") + } + } else if (paragraph.isNotEmpty()) { + contents.add("${ReadBookConfig.paragraphIndent}$paragraph") + } + } + return contents + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/CrashHandler.kt b/app/src/main/java/io/legado/app/help/CrashHandler.kt index e842707d3..b518d276f 100644 --- a/app/src/main/java/io/legado/app/help/CrashHandler.kt +++ b/app/src/main/java/io/legado/app/help/CrashHandler.kt @@ -4,12 +4,11 @@ import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageManager import android.os.Build -import android.os.Handler -import android.os.Looper import android.util.Log -import android.widget.Toast -import io.legado.app.service.TTSReadAloudService +import io.legado.app.service.help.ReadAloud import io.legado.app.utils.FileUtils +import io.legado.app.utils.longToastOnUi +import io.legado.app.utils.msg import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat @@ -26,7 +25,7 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { /** * 系统默认UncaughtExceptionHandler */ - private var mDefaultHandler: Thread.UncaughtExceptionHandler? = null + private var mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler() /** * 存储异常和参数信息 @@ -40,7 +39,6 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { private val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") init { - mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler() //设置该CrashHandler为系统默认的 Thread.setDefaultUncaughtExceptionHandler(this) } @@ -49,7 +47,7 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { * uncaughtException 回调函数 */ override fun uncaughtException(thread: Thread, ex: Throwable) { - TTSReadAloudService.clearTTS() + ReadAloud.stop(context) handleException(ex) mDefaultHandler?.uncaughtException(thread, ex) } @@ -63,18 +61,9 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { collectDeviceInfo(context) //添加自定义信息 addCustomInfo() - kotlin.runCatching { - //使用Toast来显示异常信息 - Handler(Looper.getMainLooper()).post { - Toast.makeText( - context, - ex.message, - Toast.LENGTH_LONG - ).show() - } - } //保存日志文件 saveCrashInfo2File(ex) + context.longToastOnUi(ex.msg) } /** @@ -116,35 +105,33 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { * 保存错误信息到文件中 */ private fun saveCrashInfo2File(ex: Throwable) { - kotlin.runCatching { - val sb = StringBuilder() - for ((key, value) in paramsMap) { - sb.append(key).append("=").append(value).append("\n") - } + val sb = StringBuilder() + for ((key, value) in paramsMap) { + sb.append(key).append("=").append(value).append("\n") + } - val writer = StringWriter() - val printWriter = PrintWriter(writer) - ex.printStackTrace(printWriter) - var cause: Throwable? = ex.cause - while (cause != null) { - cause.printStackTrace(printWriter) - cause = cause.cause - } - printWriter.close() - val result = writer.toString() - sb.append(result) - val timestamp = System.currentTimeMillis() - val time = format.format(Date()) - val fileName = "crash-$time-$timestamp.log" - context.externalCacheDir?.let { rootFile -> - FileUtils.getFile(rootFile, "crash").listFiles()?.forEach { - if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) { - it.delete() - } + val writer = StringWriter() + val printWriter = PrintWriter(writer) + ex.printStackTrace(printWriter) + var cause: Throwable? = ex.cause + while (cause != null) { + cause.printStackTrace(printWriter) + cause = cause.cause + } + printWriter.close() + val result = writer.toString() + sb.append(result) + val timestamp = System.currentTimeMillis() + val time = format.format(Date()) + val fileName = "crash-$time-$timestamp.log" + context.externalCacheDir?.let { rootFile -> + FileUtils.getFile(rootFile, "crash").listFiles()?.forEach { + if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) { + it.delete() } - FileUtils.createFileIfNotExist(rootFile, "crash", fileName) - .writeText(sb.toString()) } + FileUtils.createFileIfNotExist(rootFile, "crash", fileName) + .writeText(sb.toString()) } } diff --git a/app/src/main/java/io/legado/app/help/DefaultData.kt b/app/src/main/java/io/legado/app/help/DefaultData.kt new file mode 100644 index 000000000..b429ba05d --- /dev/null +++ b/app/src/main/java/io/legado/app/help/DefaultData.kt @@ -0,0 +1,71 @@ +package io.legado.app.help + +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.RssSource +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import splitties.init.appCtx +import java.io.File + +object DefaultData { + + const val httpTtsFileName = "httpTTS.json" + const val txtTocRuleFileName = "txtTocRule.json" + + val httpTTS by lazy { + val json = + String( + appCtx.assets.open("defaultData${File.separator}$httpTtsFileName") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val readConfigs by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val txtTocRules by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}$txtTocRuleFileName") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val themeConfigs by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + val rssSources by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}rssSources.json") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + + fun importDefaultHttpTTS() { + appDb.httpTTSDao.deleteDefault() + appDb.httpTTSDao.insert(*httpTTS.toTypedArray()) + } + + fun importDefaultTocRules() { + appDb.txtTocRuleDao.deleteDefault() + appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray()) + } + + fun importDefaultRssSources() { + appDb.rssSourceDao.insert(*rssSources.toTypedArray()) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt b/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt deleted file mode 100644 index 49338e960..000000000 --- a/app/src/main/java/io/legado/app/help/DefaultValueHelp.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.legado.app.help - -import io.legado.app.App -import io.legado.app.data.entities.HttpTTS -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonArray - -object DefaultValueHelp { - - - fun initHttpTTS() { - val json = String(App.INSTANCE.assets.open("httpTTS.json").readBytes()) - GSON.fromJsonArray(json)?.let { - App.db.httpTTSDao().insert(*it.toTypedArray()) - } - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/EventMessage.kt b/app/src/main/java/io/legado/app/help/EventMessage.kt index fe9c80994..31e8f662e 100644 --- a/app/src/main/java/io/legado/app/help/EventMessage.kt +++ b/app/src/main/java/io/legado/app/help/EventMessage.kt @@ -2,11 +2,10 @@ package io.legado.app.help import android.text.TextUtils -import java.util.Arrays - +@Suppress("unused") class EventMessage { - var what: Int?=null + var what: Int? = null var tag: String? = null var obj: Any? = null diff --git a/app/src/main/java/io/legado/app/help/ImageLoader.kt b/app/src/main/java/io/legado/app/help/ImageLoader.kt index 9a58f45bf..15ff2b4cb 100644 --- a/app/src/main/java/io/legado/app/help/ImageLoader.kt +++ b/app/src/main/java/io/legado/app/help/ImageLoader.kt @@ -7,9 +7,9 @@ import android.net.Uri import androidx.annotation.DrawableRes import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder -import io.legado.app.utils.isAbsUrl -import io.legado.app.utils.isContentPath import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isContentScheme import java.io.File object ImageLoader { @@ -21,15 +21,41 @@ object ImageLoader { return when { path.isNullOrEmpty() -> Glide.with(context).load(path) path.isAbsUrl() -> Glide.with(context).load(AnalyzeUrl(path).getGlideUrl()) - path.isContentPath() -> Glide.with(context).load(Uri.parse(path)) - else -> try { + path.isContentScheme() -> Glide.with(context).load(Uri.parse(path)) + else -> kotlin.runCatching { Glide.with(context).load(File(path)) - } catch (e: Exception) { + }.getOrElse { Glide.with(context).load(path) } } } + fun loadBitmap(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).asBitmap().load(path) + path.isAbsUrl() -> Glide.with(context).asBitmap().load(AnalyzeUrl(path).getGlideUrl()) + path.isContentScheme() -> Glide.with(context).asBitmap().load(Uri.parse(path)) + else -> kotlin.runCatching { + Glide.with(context).asBitmap().load(File(path)) + }.getOrElse { + Glide.with(context).asBitmap().load(path) + } + } + } + + fun loadFile(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).asFile().load(path) + path.isAbsUrl() -> Glide.with(context).asFile().load(AnalyzeUrl(path).getGlideUrl()) + path.isContentScheme() -> Glide.with(context).asFile().load(Uri.parse(path)) + else -> kotlin.runCatching { + Glide.with(context).asFile().load(File(path)) + }.getOrElse { + Glide.with(context).asFile().load(path) + } + } + } + fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder { return Glide.with(context).load(resId) } diff --git a/app/src/main/java/io/legado/app/help/IntentHelp.kt b/app/src/main/java/io/legado/app/help/IntentHelp.kt index d02e13392..64192b124 100644 --- a/app/src/main/java/io/legado/app/help/IntentHelp.kt +++ b/app/src/main/java/io/legado/app/help/IntentHelp.kt @@ -4,49 +4,54 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import io.legado.app.R -import org.jetbrains.anko.toast +import io.legado.app.utils.toastOnUi +@Suppress("unused") object IntentHelp { fun toTTSSetting(context: Context) { //跳转到文字转语音设置界面 - try { + kotlin.runCatching { val intent = Intent() intent.action = "com.android.settings.TTS_SETTINGS" intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) - } catch (ignored: Exception) { - context.toast(R.string.tip_cannot_jump_setting_page) + }.onFailure { + context.toastOnUi(R.string.tip_cannot_jump_setting_page) } } fun toInstallUnknown(context: Context) { - try { + kotlin.runCatching { val intent = Intent() intent.action = "android.settings.MANAGE_UNKNOWN_APP_SOURCES" intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK context.startActivity(intent) - } catch (ignored: Exception) { - context.toast("无法打开设置") + }.onFailure { + context.toastOnUi("无法打开设置") } } - inline fun servicePendingIntent(context: Context, action: String): PendingIntent? { - return PendingIntent.getService( - context, - 0, - Intent(context, T::class.java).apply { this.action = action }, - PendingIntent.FLAG_UPDATE_CURRENT - ) + inline fun servicePendingIntent( + context: Context, + action: String, + configIntent: Intent.() -> Unit = {} + ): PendingIntent? { + val intent = Intent(context, T::class.java) + intent.action = action + configIntent.invoke(intent) + return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - inline fun activityPendingIntent(context: Context, action: String): PendingIntent? { - return PendingIntent.getActivity( - context, - 0, - Intent(context, T::class.java).apply { this.action = action }, - PendingIntent.FLAG_UPDATE_CURRENT - ) + inline fun activityPendingIntent( + context: Context, + action: String, + configIntent: Intent.() -> Unit = {} + ): PendingIntent? { + val intent = Intent(context, T::class.java) + intent.action = action + configIntent.invoke(intent) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index 201f2e62a..edf8ae55b 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -1,42 +1,148 @@ package io.legado.app.help +import android.net.Uri import android.util.Base64 import androidx.annotation.Keep import io.legado.app.constant.AppConst.dateFormat +import io.legado.app.help.http.* +import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.utils.EncoderUtils -import io.legado.app.utils.MD5Utils -import io.legado.app.utils.htmlFormat -import io.legado.app.utils.msg +import io.legado.app.model.analyzeRule.QueryTTF +import io.legado.app.utils.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import org.jsoup.Connection +import org.jsoup.Jsoup +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File import java.net.URLEncoder +import java.nio.charset.Charset import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +/** + * js扩展类, 在js中通过java变量调用 + * 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件 + * /android/data/{package}/cache/... + */ @Keep @Suppress("unused") interface JsExtensions { /** - * js实现跨域访问,不能删 + * 访问网络,返回String */ fun ajax(urlStr: String): String? { - return try { - val analyzeUrl = AnalyzeUrl(urlStr, null, null, null, null, null) - val call = analyzeUrl.getResponse(urlStr) - val response = call.execute() - response.body() - } catch (e: Exception) { - e.msg + return runBlocking { + kotlin.runCatching { + val analyzeUrl = AnalyzeUrl(urlStr) + analyzeUrl.getStrResponse(urlStr).body + }.onFailure { + it.printStackTrace() + }.getOrElse { + it.msg + } } } + /** + * 并发访问网络 + */ + fun ajaxAll(urlList: Array): Array { + return runBlocking { + val asyncArray = Array(urlList.size) { + async(IO) { + val url = urlList[it] + val analyzeUrl = AnalyzeUrl(url) + analyzeUrl.getStrResponse(url) + } + } + val resArray = Array(urlList.size) { + asyncArray[it].await() + } + resArray + } + } + + /** + * 访问网络,返回Response + */ fun connect(urlStr: String): Any { - return try { - val analyzeUrl = AnalyzeUrl(urlStr, null, null, null, null, null) - val call = analyzeUrl.getResponse(urlStr) - val response = call.execute() - response - } catch (e: Exception) { - e.msg + return runBlocking { + kotlin.runCatching { + val analyzeUrl = AnalyzeUrl(urlStr) + analyzeUrl.getStrResponse(urlStr) + }.onFailure { + it.printStackTrace() + }.getOrElse { + it.msg + } + } + } + + /** + * 实现16进制字符串转文件 + * @param content 需要转成文件的16进制字符串 + * @param url 通过url里的参数来判断文件类型 + * @return 相对路径 + */ + fun downloadFile(content: String, url: String): String { + val type = AnalyzeUrl(url).type ?: return "" + val zipPath = FileUtils.getPath( + FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), + "${MD5Utils.md5Encode16(url)}.${type}" + ) + FileUtils.deleteFile(zipPath) + val zipFile = FileUtils.createFileIfNotExist(zipPath) + StringUtils.hexStringToByte(content).let { + if (it.isNotEmpty()) { + zipFile.writeBytes(it) + } + } + return zipPath.substring(FileUtils.getCachePath().length) + } + + /** + * js实现重定向拦截,网络访问get + */ + fun get(urlStr: String, headers: Map): Connection.Response { + return Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .headers(headers) + .method(Connection.Method.GET) + .execute() + } + + /** + * 网络访问post + */ + fun post(urlStr: String, body: String, headers: Map): Connection.Response { + return Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .requestBody(body) + .headers(headers) + .method(Connection.Method.POST) + .execute() + } + + /** + *js实现读取cookie + */ + fun getCookie(tag: String, key: String? = null): String { + val cookie = CookieStore.getCookie(tag) + val cookieMap = CookieStore.cookieToMap(cookie) + return if (key != null) { + cookieMap[key] ?: "" + } else { + cookie } } @@ -44,14 +150,32 @@ interface JsExtensions { * js实现解码,不能删 */ fun base64Decode(str: String): String { - return EncoderUtils.base64Decode(str) + return EncoderUtils.base64Decode(str, Base64.NO_WRAP) + } + + fun base64Decode(str: String, flags: Int): String { + return EncoderUtils.base64Decode(str, flags) + } + + fun base64DecodeToByteArray(str: String?): ByteArray? { + if (str.isNullOrBlank()) { + return null + } + return Base64.decode(str, Base64.DEFAULT) + } + + fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? { + if (str.isNullOrBlank()) { + return null + } + return Base64.decode(str, flags) } fun base64Encode(str: String): String? { - return EncoderUtils.base64Encode(str) + return EncoderUtils.base64Encode(str, Base64.NO_WRAP) } - fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { + fun base64Encode(str: String, flags: Int): String? { return EncoderUtils.base64Encode(str, flags) } @@ -63,11 +187,16 @@ interface JsExtensions { return MD5Utils.md5Encode16(str) } + /** + * 时间格式化 + */ fun timeFormat(time: Long): String { return dateFormat.format(Date(time)) } - //utf8编码转gbk编码 + /** + * utf8编码转gbk编码 + */ fun utf8ToGbk(str: String): String { val utf8 = String(str.toByteArray(charset("UTF-8"))) val unicode = String(utf8.toByteArray(), charset("UTF-8")) @@ -82,7 +211,351 @@ interface JsExtensions { } } + fun encodeURI(str: String, enc: String): String { + return try { + URLEncoder.encode(str, enc) + } catch (e: Exception) { + "" + } + } + fun htmlFormat(str: String): String { - return str.htmlFormat() + return HtmlFormatter.formatKeepImg(str) + } + + //****************文件操作******************// + + /** + * 获取本地文件 + * @param path 相对路径 + * @return File + */ + fun getFile(path: String): File { + val cachePath = appCtx.externalCache.absolutePath + val aPath = if (path.startsWith(File.separator)) { + cachePath + path + } else { + cachePath + File.separator + path + } + return File(aPath) + } + + fun readFile(path: String): ByteArray? { + val file = getFile(path) + if (file.exists()) { + return file.readBytes() + } + return null + } + + fun readTxtFile(path: String): String { + val file = getFile(path) + if (file.exists()) { + val charsetName = EncodingDetect.getEncode(file) + return String(file.readBytes(), charset(charsetName)) + } + return "" + } + + fun readTxtFile(path: String, charsetName: String): String { + val file = getFile(path) + if (file.exists()) { + return String(file.readBytes(), charset(charsetName)) + } + return "" + } + + /** + * 删除本地文件 + */ + fun deleteFile(path: String) { + val file = getFile(path) + FileUtils.delete(file, true) + } + + /** + * js实现压缩文件解压 + * @param zipPath 相对路径 + * @return 相对路径 + */ + fun unzipFile(zipPath: String): String { + if (zipPath.isEmpty()) return "" + val unzipPath = FileUtils.getPath( + FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), + FileUtils.getNameExcludeExtension(zipPath) + ) + FileUtils.deleteFile(unzipPath) + val zipFile = getFile(zipPath) + val unzipFolder = FileUtils.createFolderIfNotExist(unzipPath) + ZipUtils.unzipFile(zipFile, unzipFolder) + FileUtils.deleteFile(zipFile.absolutePath) + return unzipPath.substring(FileUtils.getCachePath().length) + } + + /** + * js实现文件夹内所有文件读取 + */ + fun getTxtInFolder(unzipPath: String): String { + if (unzipPath.isEmpty()) return "" + val unzipFolder = getFile(unzipPath) + val contents = StringBuilder() + unzipFolder.listFiles().let { + if (it != null) { + for (f in it) { + val charsetName = EncodingDetect.getEncode(f) + contents.append(String(f.readBytes(), charset(charsetName))) + .append("\n") + } + contents.deleteCharAt(contents.length - 1) + } + } + FileUtils.deleteFile(unzipFolder.absolutePath) + return contents.toString() + } + + /** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接或十六进制字符串 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ + fun getZipStringContent(url: String, path: String): String { + val byteArray = getZipByteArrayContent(url, path) ?: return "" + val charsetName = EncodingDetect.getEncode(byteArray) + return String(byteArray, Charset.forName(charsetName)) + } + + fun getZipStringContent(url: String, path: String, charsetName: String): String { + val byteArray = getZipByteArrayContent(url, path) ?: return "" + return String(byteArray, Charset.forName(charsetName)) + } + + /** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接或十六进制字符串 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ + fun getZipByteArrayContent(url: String, path: String): ByteArray? { + val bytes = if (url.startsWith("http://") || url.startsWith("https://")) { + runBlocking { + return@runBlocking okHttpClient.newCall { url(url) }.bytes() + } + } else { + StringUtils.hexStringToByte(url) + } + val bos = ByteArrayOutputStream() + val zis = ZipInputStream(ByteArrayInputStream(bytes)) + var entry: ZipEntry? = zis.nextEntry + while (entry != null) { + if (entry.name.equals(path)) { + zis.use { it.copyTo(bos) } + return bos.toByteArray() + } + entry = zis.nextEntry + } + Debug.log("getZipContent 未发现内容") + + return null + } + + //******************文件操作************************// + + /** + * 解析字体,返回字体解析类 + */ + fun queryBase64TTF(base64: String?): QueryTTF? { + base64DecodeToByteArray(base64)?.let { + return QueryTTF(it) + } + return null + } + + /** + * 返回字体解析类 + * @param str 支持url,本地文件,base64,自动判断,自动缓存 + */ + fun queryTTF(str: String?): QueryTTF? { + str ?: return null + val key = md5Encode16(str) + var qTTF = CacheManager.getQueryTTF(key) + if (qTTF != null) return qTTF + val font: ByteArray? = when { + str.isAbsUrl() -> runBlocking { + var x = CacheManager.getByteArray(key) + if (x == null) { + x = okHttpClient.newCall { url(str) }.bytes() + x.let { + CacheManager.put(key, it) + } + } + return@runBlocking x + } + str.isContentScheme() -> Uri.parse(str).readBytes(appCtx) + str.startsWith("/storage") -> File(str).readBytes() + else -> base64DecodeToByteArray(str) + } + font ?: return null + qTTF = QueryTTF(font) + CacheManager.put(key, qTTF) + return qTTF + } + + /** + * @param text 包含错误字体的内容 + * @param font1 错误的字体 + * @param font2 正确的字体 + */ + fun replaceFont( + text: String, + font1: QueryTTF?, + font2: QueryTTF? + ): String { + if (font1 == null || font2 == null) return text + val contentArray = text.toCharArray() + contentArray.forEachIndexed { index, s -> + val oldCode = s.code + if (font1.inLimit(s)) { + val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode)) + if (code != 0) contentArray[index] = code.toChar() + } + } + return contentArray.joinToString("") + } + + /** + * 输出调试日志 + */ + fun log(msg: String): String { + Debug.log(msg) + return msg } + + /** + * AES 解码为 ByteArray + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + fun aesDecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return EncoderUtils.decryptAES( + data = str.encodeToByteArray(), + key = key.encodeToByteArray(), + transformation, + iv.encodeToByteArray() + ) + } + + /** + * AES 解码为 String + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + + fun aesDecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) } + } + + /** + * 已经base64的AES 解码为 ByteArray + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + + fun aesBase64DecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return EncoderUtils.decryptBase64AES( + data = str.encodeToByteArray(), + key = key.encodeToByteArray(), + transformation, + iv.encodeToByteArray() + ) + } + + /** + * 已经base64的AES 解码为 String + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + + fun aesBase64DecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) } + } + + /** + * 加密aes为ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + fun aesEncodeToByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return EncoderUtils.encryptAES( + data.encodeToByteArray(), + key = key.encodeToByteArray(), + transformation, + iv.encodeToByteArray() + ) + } + + /** + * 加密aes为String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + fun aesEncodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) } + } + + /** + * 加密aes后Base64化的ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + fun aesEncodeToBase64ByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return EncoderUtils.encryptAES2Base64( + data.encodeToByteArray(), + key = key.encodeToByteArray(), + transformation, + iv.encodeToByteArray() + ) + } + + /** + * 加密aes后Base64化的String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + fun aesEncodeToBase64String( + data: String, key: String, transformation: String, iv: String + ): String? { + return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) } + } + } diff --git a/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt index 04723c21a..3a226e071 100644 --- a/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt +++ b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt @@ -3,30 +3,30 @@ package io.legado.app.help import android.content.ComponentName import android.content.pm.PackageManager import android.os.Build -import io.legado.app.App import io.legado.app.R import io.legado.app.ui.welcome.* -import org.jetbrains.anko.toast +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx /** * Created by GKF on 2018/2/27. * 更换图标 */ object LauncherIconHelp { - private val packageManager: PackageManager = App.INSTANCE.packageManager + private val packageManager: PackageManager = appCtx.packageManager private val componentNames = arrayListOf( - ComponentName(App.INSTANCE, Launcher1::class.java.name), - ComponentName(App.INSTANCE, Launcher2::class.java.name), - ComponentName(App.INSTANCE, Launcher3::class.java.name), - ComponentName(App.INSTANCE, Launcher4::class.java.name), - ComponentName(App.INSTANCE, Launcher5::class.java.name), - ComponentName(App.INSTANCE, Launcher6::class.java.name) + ComponentName(appCtx, Launcher1::class.java.name), + ComponentName(appCtx, Launcher2::class.java.name), + ComponentName(appCtx, Launcher3::class.java.name), + ComponentName(appCtx, Launcher4::class.java.name), + ComponentName(appCtx, Launcher5::class.java.name), + ComponentName(appCtx, Launcher6::class.java.name) ) fun changeIcon(icon: String?) { if (icon.isNullOrEmpty()) return if (Build.VERSION.SDK_INT < 26) { - App.INSTANCE.toast(R.string.change_icon_error) + appCtx.toastOnUi(R.string.change_icon_error) return } var hasEnabled = false @@ -50,13 +50,13 @@ object LauncherIconHelp { } if (hasEnabled) { packageManager.setComponentEnabledSetting( - ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), + ComponentName(appCtx, WelcomeActivity::class.java.name), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) } else { packageManager.setComponentEnabledSetting( - ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), + ComponentName(appCtx, WelcomeActivity::class.java.name), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP ) diff --git a/app/src/main/java/io/legado/app/help/LayoutManager.kt b/app/src/main/java/io/legado/app/help/LayoutManager.kt index e633993c7..4dee2ab06 100644 --- a/app/src/main/java/io/legado/app/help/LayoutManager.kt +++ b/app/src/main/java/io/legado/app/help/LayoutManager.kt @@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager +@Suppress("unused") object LayoutManager { interface LayoutManagerFactory { @@ -43,10 +44,19 @@ object LayoutManager { } - fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + fun grid( + spanCount: Int, + @Orientation orientation: Int, + reverseLayout: Boolean + ): LayoutManagerFactory { return object : LayoutManagerFactory { override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) + return GridLayoutManager( + recyclerView.context, + spanCount, + orientation, + reverseLayout + ) } } } diff --git a/app/src/main/java/io/legado/app/help/ActivityHelp.kt b/app/src/main/java/io/legado/app/help/LifecycleHelp.kt similarity index 57% rename from app/src/main/java/io/legado/app/help/ActivityHelp.kt rename to app/src/main/java/io/legado/app/help/LifecycleHelp.kt index b14c76670..0a9e50826 100644 --- a/app/src/main/java/io/legado/app/help/ActivityHelp.kt +++ b/app/src/main/java/io/legado/app/help/LifecycleHelp.kt @@ -3,24 +3,29 @@ package io.legado.app.help import android.app.Activity import android.app.Application import android.os.Bundle +import io.legado.app.base.BaseService +import io.legado.app.utils.LanguageUtils import java.lang.ref.WeakReference import java.util.* /** * Activity管理器,管理项目中Activity的状态 */ -object ActivityHelp : Application.ActivityLifecycleCallbacks { +@Suppress("unused") +object LifecycleHelp : Application.ActivityLifecycleCallbacks { private val activities: MutableList> = arrayListOf() + private val services: MutableList> = arrayListOf() + private var appFinishedListener: (() -> Unit)? = null - fun size(): Int { + fun activitySize(): Int { return activities.size } /** * 判断指定Activity是否存在 */ - fun isExist(activityClass: Class<*>): Boolean { + fun isExistActivity(activityClass: Class<*>): Boolean { activities.forEach { item -> if (item.get()?.javaClass == activityClass) { return true @@ -29,47 +34,6 @@ object ActivityHelp : Application.ActivityLifecycleCallbacks { 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) { - activities.forEach { activity -> - activity.finish() - } - } - /** * 关闭指定 activity(class) */ @@ -88,6 +52,10 @@ object ActivityHelp : Application.ActivityLifecycleCallbacks { } } + fun setOnAppFinishedListener(appFinishedListener: (() -> Unit)) { + this.appFinishedListener = appFinishedListener + } + override fun onActivityPaused(activity: Activity) { } @@ -99,16 +67,49 @@ object ActivityHelp : Application.ActivityLifecycleCallbacks { } override fun onActivityDestroyed(activity: Activity) { - remove(activity) + for (temp in activities) { + if (temp.get() != null && temp.get() === activity) { + activities.remove(temp) + if (services.size == 0 && activities.size == 0) { + onAppFinished() + } + break + } + } } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { } override fun onActivityStopped(activity: Activity) { } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - add(activity) + activities.add(WeakReference(activity)) + if (!LanguageUtils.isSameWithSetting(activity)) { + LanguageUtils.setConfiguration(activity) + } + } + + @Synchronized + fun onServiceCreate(service: BaseService) { + services.add(WeakReference(service)) + } + + @Synchronized + fun onServiceDestroy(service: BaseService) { + for (temp in services) { + if (temp.get() != null && temp.get() === service) { + services.remove(temp) + if (services.size == 0 && activities.size == 0) { + onAppFinished() + } + break + } + } + } + + private fun onAppFinished() { + appFinishedListener?.invoke() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/LocalConfig.kt b/app/src/main/java/io/legado/app/help/LocalConfig.kt new file mode 100644 index 000000000..012651fa1 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/LocalConfig.kt @@ -0,0 +1,76 @@ +package io.legado.app.help + +import android.content.Context +import androidx.core.content.edit +import splitties.init.appCtx + +object LocalConfig { + private const val versionCodeKey = "appVersionCode" + + private val localConfig = + appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) + + val readHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readHelpVersion", "firstRead") + + val backupHelpVersionIsLast: Boolean + get() = isLastVersion(1, "backupHelpVersion", "firstBackup") + + val readMenuHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu") + + val bookSourcesHelpVersionIsLast: Boolean + get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources") + + val debugHelpVersionIsLast: Boolean + get() = isLastVersion(1, "debugHelpVersion") + + val ruleHelpVersionIsLast: Boolean + get() = isLastVersion(1, "ruleHelpVersion") + + val needUpHttpTTS: Boolean + get() = !isLastVersion(3, "httpTtsVersion") + + val needUpTxtTocRule: Boolean + get() = !isLastVersion(1, "txtTocRuleVersion") + + val needUpRssSources: Boolean + get() = !isLastVersion(4, "rssSourceVersion") + + var versionCode + get() = localConfig.getLong(versionCodeKey, 0) + set(value) { + localConfig.edit { + putLong(versionCodeKey, value) + } + } + + val isFirstOpenApp: Boolean + get() { + val value = localConfig.getBoolean("firstOpen", true) + if (value) { + localConfig.edit { putBoolean("firstOpen", false) } + } + return value + } + + @Suppress("SameParameterValue") + private fun isLastVersion( + lastVersion: Int, + versionKey: String, + firstOpenKey: String? = null + ): Boolean { + var version = localConfig.getInt(versionKey, 0) + if (version == 0 && firstOpenKey != null) { + if (!localConfig.getBoolean(firstOpenKey, true)) { + version = 1 + } + } + if (version < lastVersion) { + localConfig.edit { putInt(versionKey, lastVersion) } + return false + } + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/MediaHelp.kt b/app/src/main/java/io/legado/app/help/MediaHelp.kt index 3ec860a4d..9153b9fd2 100644 --- a/app/src/main/java/io/legado/app/help/MediaHelp.kt +++ b/app/src/main/java/io/legado/app/help/MediaHelp.kt @@ -1,12 +1,12 @@ package io.legado.app.help import android.content.Context -import android.media.AudioAttributes -import android.media.AudioFocusRequest import android.media.AudioManager import android.media.MediaPlayer -import android.os.Build import android.support.v4.media.session.PlaybackStateCompat +import androidx.media.AudioAttributesCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat import io.legado.app.R object MediaHelp { @@ -32,20 +32,16 @@ object MediaHelp { or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED) - fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequest? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val mPlaybackAttributes = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .build() - AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - .setAudioAttributes(mPlaybackAttributes) - .setAcceptsDelayedFocusGain(true) - .setOnAudioFocusChangeListener(audioFocusChangeListener) - .build() - } else { - null - } + fun getFocusRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequestCompat? { + val mPlaybackAttributes = AudioAttributesCompat.Builder() + .setUsage(AudioAttributesCompat.USAGE_MEDIA) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) + .build() + return AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes(mPlaybackAttributes) + //.setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() } /** @@ -53,21 +49,11 @@ object MediaHelp { */ fun requestFocus( audioManager: AudioManager, - listener: AudioManager.OnAudioFocusChangeListener, - focusRequest: AudioFocusRequest? + focusRequest: AudioFocusRequestCompat? ): Boolean { - val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - focusRequest?.let { - audioManager.requestAudioFocus(focusRequest) - } ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED - } else { - @Suppress("DEPRECATION") - audioManager.requestAudioFocus( - listener, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN - ) - } + val request = focusRequest?.let { + AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) + } ?: AudioManager.AUDIOFOCUS_REQUEST_GRANTED return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED } diff --git a/app/src/main/java/io/legado/app/help/README.md b/app/src/main/java/io/legado/app/help/README.md index a614de113..9bf5306f7 100644 --- a/app/src/main/java/io/legado/app/help/README.md +++ b/app/src/main/java/io/legado/app/help/README.md @@ -1 +1 @@ -## 放置一些帮助类 \ No newline at end of file +# 放置一些帮助类 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt index 9d249fe2a..2598e9c94 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -4,15 +4,15 @@ import android.graphics.Color import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable -import android.os.Parcelable import androidx.annotation.Keep -import io.legado.app.App import io.legado.app.R import io.legado.app.constant.PreferKey import io.legado.app.help.coroutine.Coroutine import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.utils.* -import kotlinx.android.parcel.Parcelize +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import splitties.init.appCtx import java.io.File /** @@ -21,28 +21,28 @@ import java.io.File @Keep object ReadBookConfig { const val configFileName = "readConfig.json" - val configFilePath = FileUtils.getPath(App.INSTANCE.filesDir, configFileName) + const val shareConfigFileName = "shareReadConfig.json" + val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName) + val shareConfigFilePath = FileUtils.getPath(appCtx.filesDir, shareConfigFileName) val configList: ArrayList = arrayListOf() - private val defaultConfigs by lazy { - val json = String(App.INSTANCE.assets.open(configFileName).readBytes()) - GSON.fromJsonArray(json)!! - } + lateinit var shareConfig: Config var durConfig get() = getConfig(styleSelect) set(value) { configList[styleSelect] = value if (shareLayout) { - configList[5] = value + shareConfig = value } upBg() } var bg: Drawable? = null var bgMeanColor: Int = 0 - val textColor: Int get() = durConfig.textColor() + val textColor: Int get() = durConfig.curTextColor() init { - upConfig() + initConfigs() + initShareConfig() } @Synchronized @@ -50,68 +50,83 @@ object ReadBookConfig { if (configList.size < 5) { resetAll() } - if (configList.size < 6) { - configList.add(Config()) - } - return configList[index] + return configList.getOrNull(index) ?: configList[0] } - fun upConfig() { - (getConfigs() ?: defaultConfigs).let { + fun initConfigs() { + val configFile = File(configFilePath) + var configs: List? = null + if (configFile.exists()) { + try { + val json = configFile.readText() + configs = GSON.fromJsonArray(json) + } catch (e: Exception) { + e.printStackTrace() + } + } + (configs ?: DefaultData.readConfigs).let { configList.clear() configList.addAll(it) } } - private fun getConfigs(): List? { - val configFile = File(configFilePath) + fun initShareConfig() { + val configFile = File(shareConfigFilePath) + var c: Config? = null if (configFile.exists()) { try { val json = configFile.readText() - return GSON.fromJsonArray(json) + c = GSON.fromJsonObject(json) } catch (e: Exception) { e.printStackTrace() } } - return null + shareConfig = c ?: configList.getOrNull(5) ?: Config() } fun upBg() { - val resources = App.INSTANCE.resources + val resources = appCtx.resources val dm = resources.displayMetrics val width = dm.widthPixels val height = dm.heightPixels - bg = durConfig.bgDrawable(width, height).apply { + bg = durConfig.curBgDrawable(width, height).apply { if (this is BitmapDrawable) { bgMeanColor = BitmapUtils.getMeanColor(bitmap) } else if (this is ColorDrawable) { bgMeanColor = color } } - isScroll = pageAnim == 3 } fun save() { Coroutine.async { synchronized(this) { - val json = GSON.toJson(configList) - FileUtils.deleteFile(configFilePath) - FileUtils.createFileIfNotExist(configFilePath).writeText(json) + GSON.toJson(configList).let { + FileUtils.deleteFile(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(it) + } + GSON.toJson(shareConfig).let { + FileUtils.deleteFile(shareConfigFilePath) + FileUtils.createFileIfNotExist(shareConfigFilePath).writeText(it) + } } } } - fun resetDur() { - defaultConfigs[styleSelect].let { - durConfig.setBg(it.bgType(), it.bgStr()) - durConfig.setTextColor(it.textColor()) + fun deleteDur(): Boolean { + if (configList.size > 5) { + configList.removeAt(styleSelect) + if (styleSelect > 0) { + styleSelect -= 1 + } upBg() - save() + return true } + return false } private fun resetAll() { - defaultConfigs.let { + DefaultData.readConfigs.let { configList.clear() configList.addAll(it) save() @@ -119,48 +134,39 @@ object ReadBookConfig { } //配置写入读取 - var autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46) + var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 10) set(value) { field = value - App.INSTANCE.putPrefInt(PreferKey.autoReadSpeed, value) + appCtx.putPrefInt(PreferKey.autoReadSpeed, value) } - var styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) + var styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) set(value) { field = value - if (App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) != value) { - App.INSTANCE.putPrefInt(PreferKey.readStyleSelect, value) + if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) { + appCtx.putPrefInt(PreferKey.readStyleSelect, value) } } - var shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) + var shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout) set(value) { field = value - if (App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) != value) { - App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value) + if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) { + appCtx.putPrefBoolean(PreferKey.shareLayout, value) } } + val textFullJustify get() = appCtx.getPrefBoolean(PreferKey.textFullJustify, true) + val textBottomJustify get() = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true) + var hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar) + var hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) + var useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout) + + val config get() = if (shareLayout) shareConfig else durConfig + var pageAnim: Int - get() = if (AppConfig.isEInkMode) -1 else App.INSTANCE.getPrefInt(PreferKey.pageAnim) - set(value) { - App.INSTANCE.putPrefInt(PreferKey.pageAnim, value) - isScroll = pageAnim == 3 - } - var isScroll = pageAnim == 3 - val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true) - val textFullJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textFullJustify, true) - val textBottomJustify get() = App.INSTANCE.getPrefBoolean(PreferKey.textBottomJustify, true) - var bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) + get() = config.curPageAnim() set(value) { - field = value - bodyIndent = " ".repeat(value) - if (App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) != value) { - App.INSTANCE.putPrefInt(PreferKey.bodyIndent, value) - } + config.setCurPageAnim(value) } - var bodyIndent = " ".repeat(bodyIndentCount) - var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) - var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) - - val config get() = if (shareLayout) getConfig(5) else durConfig var textFont: String get() = config.textFont @@ -221,6 +227,12 @@ object ReadBookConfig { config.titleBottomSpacing = value } + var paragraphIndent: String + get() = config.paragraphIndent + set(value) { + config.paragraphIndent = value + } + var paddingBottom: Int get() = config.paddingBottom set(value) { @@ -308,68 +320,128 @@ object ReadBookConfig { fun getExportConfig(): Config { val exportConfig = GSON.fromJsonObject(GSON.toJson(durConfig))!! if (shareLayout) { - val shearConfig = getConfig(5) - exportConfig.textFont = shearConfig.textFont - exportConfig.textBold = shearConfig.textBold - exportConfig.textSize = shearConfig.textSize - exportConfig.letterSpacing = shearConfig.letterSpacing - exportConfig.lineSpacingExtra = shearConfig.lineSpacingExtra - exportConfig.paragraphSpacing = shearConfig.paragraphSpacing - exportConfig.titleMode = shearConfig.titleMode - exportConfig.titleSize = shearConfig.titleSize - exportConfig.titleTopSpacing = shearConfig.titleTopSpacing - exportConfig.titleBottomSpacing = shearConfig.titleBottomSpacing - exportConfig.paddingBottom = shearConfig.paddingBottom - exportConfig.paddingLeft = shearConfig.paddingLeft - exportConfig.paddingRight = shearConfig.paddingRight - exportConfig.paddingTop = shearConfig.paddingTop - exportConfig.headerPaddingBottom = shearConfig.headerPaddingBottom - exportConfig.headerPaddingLeft = shearConfig.headerPaddingLeft - exportConfig.headerPaddingRight = shearConfig.headerPaddingRight - exportConfig.headerPaddingTop = shearConfig.headerPaddingTop - exportConfig.footerPaddingBottom = shearConfig.footerPaddingBottom - exportConfig.footerPaddingLeft = shearConfig.footerPaddingLeft - exportConfig.footerPaddingRight = shearConfig.footerPaddingRight - exportConfig.footerPaddingTop = shearConfig.footerPaddingTop - exportConfig.showHeaderLine = shearConfig.showHeaderLine - exportConfig.showFooterLine = shearConfig.showFooterLine - exportConfig.tipHeaderLeft = shearConfig.tipHeaderLeft - exportConfig.tipHeaderMiddle = shearConfig.tipHeaderMiddle - exportConfig.tipHeaderRight = shearConfig.tipHeaderRight - exportConfig.tipFooterLeft = shearConfig.tipFooterLeft - exportConfig.tipFooterMiddle = shearConfig.tipFooterMiddle - exportConfig.tipFooterRight = shearConfig.tipFooterRight - exportConfig.hideHeader = shearConfig.hideHeader - exportConfig.hideFooter = shearConfig.hideFooter + exportConfig.textFont = shareConfig.textFont + exportConfig.textBold = shareConfig.textBold + exportConfig.textSize = shareConfig.textSize + exportConfig.letterSpacing = shareConfig.letterSpacing + exportConfig.lineSpacingExtra = shareConfig.lineSpacingExtra + exportConfig.paragraphSpacing = shareConfig.paragraphSpacing + exportConfig.titleMode = shareConfig.titleMode + exportConfig.titleSize = shareConfig.titleSize + exportConfig.titleTopSpacing = shareConfig.titleTopSpacing + exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing + exportConfig.paddingBottom = shareConfig.paddingBottom + exportConfig.paddingLeft = shareConfig.paddingLeft + exportConfig.paddingRight = shareConfig.paddingRight + exportConfig.paddingTop = shareConfig.paddingTop + exportConfig.headerPaddingBottom = shareConfig.headerPaddingBottom + exportConfig.headerPaddingLeft = shareConfig.headerPaddingLeft + exportConfig.headerPaddingRight = shareConfig.headerPaddingRight + exportConfig.headerPaddingTop = shareConfig.headerPaddingTop + exportConfig.footerPaddingBottom = shareConfig.footerPaddingBottom + exportConfig.footerPaddingLeft = shareConfig.footerPaddingLeft + exportConfig.footerPaddingRight = shareConfig.footerPaddingRight + exportConfig.footerPaddingTop = shareConfig.footerPaddingTop + exportConfig.showHeaderLine = shareConfig.showHeaderLine + exportConfig.showFooterLine = shareConfig.showFooterLine + exportConfig.tipHeaderLeft = shareConfig.tipHeaderLeft + exportConfig.tipHeaderMiddle = shareConfig.tipHeaderMiddle + exportConfig.tipHeaderRight = shareConfig.tipHeaderRight + exportConfig.tipFooterLeft = shareConfig.tipFooterLeft + exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle + exportConfig.tipFooterRight = shareConfig.tipFooterRight + exportConfig.tipColor = shareConfig.tipColor + exportConfig.headerMode = shareConfig.headerMode + exportConfig.footerMode = shareConfig.footerMode } return exportConfig } + suspend fun import(byteArray: ByteArray): Config { + return withContext(IO) { + val configZipPath = FileUtils.getPath(appCtx.externalCache, configFileName) + FileUtils.deleteFile(configZipPath) + val zipFile = FileUtils.createFileIfNotExist(configZipPath) + zipFile.writeBytes(byteArray) + val configDirPath = FileUtils.getPath(appCtx.externalCache, "readConfig") + FileUtils.deleteFile(configDirPath) + @Suppress("BlockingMethodInNonBlockingContext") + ZipUtils.unzipFile(zipFile, FileUtils.createFolderIfNotExist(configDirPath)) + val configDir = FileUtils.createFolderIfNotExist(configDirPath) + val configFile = FileUtils.getFile(configDir, "readConfig.json") + val config: Config = GSON.fromJsonObject(configFile.readText())!! + if (config.textFont.isNotEmpty()) { + val fontName = FileUtils.getName(config.textFont) + val fontPath = + FileUtils.getPath(appCtx.externalFiles, "font", fontName) + if (!FileUtils.exist(fontPath)) { + FileUtils.getFile(configDir, fontName).copyTo(File(fontPath)) + } + config.textFont = fontPath + } + if (config.bgType == 2) { + val bgName = FileUtils.getName(config.bgStr) + val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = FileUtils.getFile(configDir, bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + } + if (config.bgTypeNight == 2) { + val bgName = FileUtils.getName(config.bgStrNight) + val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = FileUtils.getFile(configDir, bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + } + if (config.bgTypeEInk == 2) { + val bgName = FileUtils.getName(config.bgStrEInk) + @Suppress("BlockingMethodInNonBlockingContext") val bgPath = + FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = FileUtils.getFile(configDir, bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + } + return@withContext config + } + } + @Keep - @Parcelize class Config( - private var bgStr: String = "#EEEEEE",//白天背景 - private var bgStrNight: String = "#000000",//夜间背景 - private var bgStrEInk: String = "#FFFFFF", - private var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片 - private var bgTypeNight: Int = 0,//夜间背景类型 - private var bgTypeEInk: Int = 0, + var name: String = "", + var bgStr: String = "#EEEEEE",//白天背景 + var bgStrNight: String = "#000000",//夜间背景 + var bgStrEInk: String = "#FFFFFF", + var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片 + var bgTypeNight: Int = 0,//夜间背景类型 + var bgTypeEInk: Int = 0, private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏 private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 private var darkStatusIconEInk: Boolean = true, private var textColor: String = "#3E3D3B",//白天文字颜色 private var textColorNight: String = "#ADADAD",//夜间文字颜色 private var textColorEInk: String = "#000000", + private var pageAnim: Int = 0, + private var pageAnimEInk: Int = 3, var textFont: String = "",//字体 var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体 var textSize: Int = 20,//文字大小 var letterSpacing: Float = 0.1f,//字间距 var lineSpacingExtra: Int = 12,//行间距 - var paragraphSpacing: Int = 4,//段距 + var paragraphSpacing: Int = 2,//段距 var titleMode: Int = 0,//标题居中 var titleSize: Int = 0, var titleTopSpacing: Int = 0, var titleBottomSpacing: Int = 0, + var paragraphIndent: String = "  ",//段落缩进 var paddingBottom: Int = 6, var paddingLeft: Int = 16, var paddingRight: Int = 16, @@ -390,27 +462,12 @@ object ReadBookConfig { var tipFooterLeft: Int = ReadTipConfig.chapterTitle, var tipFooterMiddle: Int = ReadTipConfig.none, var tipFooterRight: Int = ReadTipConfig.pageAndTotal, - var hideHeader: Boolean = true, - var hideFooter: Boolean = false - ) : Parcelable { - fun setBg(bgType: Int, bg: String) { - when { - AppConfig.isEInkMode -> { - bgTypeEInk = bgType - bgStrEInk = bg - } - AppConfig.isNightTheme -> { - bgTypeNight = bgType - bgStrNight = bg - } - else -> { - this.bgType = bgType - bgStr = bg - } - } - } + var tipColor: Int = 0, + var headerMode: Int = 0, + var footerMode: Int = 0 + ) { - fun setTextColor(color: Int) { + fun setCurTextColor(color: Int) { when { AppConfig.isEInkMode -> textColorEInk = "#${color.hexString}" AppConfig.isNightTheme -> textColorNight = "#${color.hexString}" @@ -419,7 +476,15 @@ object ReadBookConfig { ChapterProvider.upStyle() } - fun setStatusIconDark(isDark: Boolean) { + fun curTextColor(): Int { + return when { + AppConfig.isEInkMode -> Color.parseColor(textColorEInk) + AppConfig.isNightTheme -> Color.parseColor(textColorNight) + else -> Color.parseColor(textColor) + } + } + + fun setCurStatusIconDark(isDark: Boolean) { when { AppConfig.isEInkMode -> darkStatusIconEInk = isDark AppConfig.isNightTheme -> darkStatusIconNight = isDark @@ -427,7 +492,7 @@ object ReadBookConfig { } } - fun statusIconDark(): Boolean { + fun curStatusIconDark(): Boolean { return when { AppConfig.isEInkMode -> darkStatusIconEInk AppConfig.isNightTheme -> darkStatusIconNight @@ -435,15 +500,38 @@ object ReadBookConfig { } } - fun textColor(): Int { + fun setCurPageAnim(anim: Int) { + when { + AppConfig.isEInkMode -> pageAnimEInk = anim + else -> pageAnim = anim + } + } + + fun curPageAnim(): Int { return when { - AppConfig.isEInkMode -> Color.parseColor(textColorEInk) - AppConfig.isNightTheme -> Color.parseColor(textColorNight) - else -> Color.parseColor(textColor) + AppConfig.isEInkMode -> pageAnimEInk + else -> pageAnim + } + } + + fun setCurBg(bgType: Int, bg: String) { + when { + AppConfig.isEInkMode -> { + bgTypeEInk = bgType + bgStrEInk = bg + } + AppConfig.isNightTheme -> { + bgTypeNight = bgType + bgStrNight = bg + } + else -> { + this.bgType = bgType + bgStr = bg + } } } - fun bgStr(): String { + fun curBgStr(): String { return when { AppConfig.isEInkMode -> bgStrEInk AppConfig.isNightTheme -> bgStrNight @@ -451,7 +539,7 @@ object ReadBookConfig { } } - fun bgType(): Int { + fun curBgType(): Int { return when { AppConfig.isEInkMode -> bgTypeEInk AppConfig.isNightTheme -> bgTypeNight @@ -459,18 +547,18 @@ object ReadBookConfig { } } - fun bgDrawable(width: Int, height: Int): Drawable { + fun curBgDrawable(width: Int, height: Int): Drawable { var bgDrawable: Drawable? = null - val resources = App.INSTANCE.resources + val resources = appCtx.resources try { - bgDrawable = when (bgType()) { - 0 -> ColorDrawable(Color.parseColor(bgStr())) + bgDrawable = when (curBgType()) { + 0 -> ColorDrawable(Color.parseColor(curBgStr())) 1 -> { BitmapDrawable( resources, BitmapUtils.decodeAssetsBitmap( - App.INSTANCE, - "bg" + File.separator + bgStr(), + appCtx, + "bg" + File.separator + curBgStr(), width, height ) @@ -478,13 +566,13 @@ object ReadBookConfig { } else -> BitmapDrawable( resources, - BitmapUtils.decodeBitmap(bgStr(), width, height) + BitmapUtils.decodeBitmap(curBgStr(), width, height) ) } } catch (e: Exception) { e.printStackTrace() } - return bgDrawable ?: ColorDrawable(App.INSTANCE.getCompatColor(R.color.background)) + return bgDrawable ?: ColorDrawable(appCtx.getCompatColor(R.color.background)) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ReadTipConfig.kt b/app/src/main/java/io/legado/app/help/ReadTipConfig.kt index eb37a10e1..5bff7869d 100644 --- a/app/src/main/java/io/legado/app/help/ReadTipConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadTipConfig.kt @@ -1,10 +1,13 @@ package io.legado.app.help -import io.legado.app.App +import android.content.Context import io.legado.app.R +import splitties.init.appCtx object ReadTipConfig { - val tipArray: Array = App.INSTANCE.resources.getStringArray(R.array.read_tip) + val tips by lazy { + appCtx.resources.getStringArray(R.array.read_tip).toList() + } const val none = 0 const val chapterTitle = 1 const val time = 2 @@ -12,13 +15,15 @@ object ReadTipConfig { const val page = 4 const val totalProgress = 5 const val pageAndTotal = 6 + const val bookName = 7 + const val timeBattery = 8 - val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] } - val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] } - val tipHeaderRightStr: String get() = tipArray.getOrElse(tipHeaderRight) { tipArray[none] } - val tipFooterLeftStr: String get() = tipArray.getOrElse(tipFooterLeft) { tipArray[none] } - val tipFooterMiddleStr: String get() = tipArray.getOrElse(tipFooterMiddle) { tipArray[none] } - val tipFooterRightStr: String get() = tipArray.getOrElse(tipFooterRight) { tipArray[none] } + val tipHeaderLeftStr: String get() = tips.getOrElse(tipHeaderLeft) { tips[none] } + val tipHeaderMiddleStr: String get() = tips.getOrElse(tipHeaderMiddle) { tips[none] } + val tipHeaderRightStr: String get() = tips.getOrElse(tipHeaderRight) { tips[none] } + val tipFooterLeftStr: String get() = tips.getOrElse(tipFooterLeft) { tips[none] } + val tipFooterMiddleStr: String get() = tips.getOrElse(tipFooterMiddle) { tips[none] } + val tipFooterRightStr: String get() = tips.getOrElse(tipFooterRight) { tips[none] } var tipHeaderLeft: Int get() = ReadBookConfig.config.tipHeaderLeft @@ -56,15 +61,36 @@ object ReadTipConfig { ReadBookConfig.config.tipFooterRight = value } - var hideHeader: Boolean - get() = ReadBookConfig.config.hideHeader + var headerMode: Int + get() = ReadBookConfig.config.headerMode set(value) { - ReadBookConfig.config.hideHeader = value + ReadBookConfig.config.headerMode = value } - var hideFooter: Boolean - get() = ReadBookConfig.config.hideFooter + var footerMode: Int + get() = ReadBookConfig.config.footerMode set(value) { - ReadBookConfig.config.hideFooter = value + ReadBookConfig.config.footerMode = value } + + var tipColor: Int + get() = ReadBookConfig.config.tipColor + set(value) { + ReadBookConfig.config.tipColor = value + } + + fun getHeaderModes(context: Context): LinkedHashMap { + return linkedMapOf( + Pair(0, context.getString(R.string.hide_when_status_bar_show)), + Pair(1, context.getString(R.string.show)), + Pair(2, context.getString(R.string.hide)) + ) + } + + fun getFooterModes(context: Context): LinkedHashMap { + return linkedMapOf( + Pair(0, context.getString(R.string.show)), + Pair(1, context.getString(R.string.hide)) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/SourceHelp.kt b/app/src/main/java/io/legado/app/help/SourceHelp.kt index ebeb30d02..be977ec87 100644 --- a/app/src/main/java/io/legado/app/help/SourceHelp.kt +++ b/app/src/main/java/io/legado/app/help/SourceHelp.kt @@ -2,20 +2,21 @@ package io.legado.app.help import android.os.Handler import android.os.Looper -import io.legado.app.App +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.RssSource import io.legado.app.utils.EncoderUtils import io.legado.app.utils.NetworkUtils import io.legado.app.utils.splitNotBlank -import org.jetbrains.anko.toast +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx object SourceHelp { private val handler = Handler(Looper.getMainLooper()) private val list18Plus by lazy { try { - return@lazy String(App.INSTANCE.assets.open("18PlusList.txt").readBytes()) + return@lazy String(appCtx.assets.open("18PlusList.txt").readBytes()) .splitNotBlank("\n") } catch (e: Exception) { return@lazy arrayOf() @@ -26,10 +27,10 @@ object SourceHelp { rssSources.forEach { rssSource -> if (is18Plus(rssSource.sourceUrl)) { handler.post { - App.INSTANCE.toast("${rssSource.sourceName}是18+网址,禁止导入.") + appCtx.toastOnUi("${rssSource.sourceName}是18+网址,禁止导入.") } } else { - App.db.rssSourceDao().insert(rssSource) + appDb.rssSourceDao.insert(rssSource) } } } @@ -38,10 +39,10 @@ object SourceHelp { bookSources.forEach { bookSource -> if (is18Plus(bookSource.bookSourceUrl)) { handler.post { - App.INSTANCE.toast("${bookSource.bookSourceName}是18+网址,禁止导入.") + appCtx.toastOnUi("${bookSource.bookSourceName}是18+网址,禁止导入.") } } else { - App.db.bookSourceDao().insert(bookSource) + appDb.bookSourceDao.insert(bookSource) } } } diff --git a/app/src/main/java/io/legado/app/help/ThemeConfig.kt b/app/src/main/java/io/legado/app/help/ThemeConfig.kt index 2b19e0136..bdbe9eb8d 100644 --- a/app/src/main/java/io/legado/app/help/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/help/ThemeConfig.kt @@ -2,52 +2,83 @@ package io.legado.app.help import android.content.Context import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import androidx.annotation.Keep -import io.legado.app.App +import androidx.appcompat.app.AppCompatDelegate import io.legado.app.R import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.help.coroutine.Coroutine +import io.legado.app.constant.Theme +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.utils.* +import splitties.init.appCtx import java.io.File object ThemeConfig { const val configFileName = "themeConfig.json" - val configFilePath = FileUtils.getPath(App.INSTANCE.filesDir, configFileName) - private val defaultConfigs by lazy { - val json = String(App.INSTANCE.assets.open(configFileName).readBytes()) - GSON.fromJsonArray(json)!! + val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName) + + val configList: ArrayList by lazy { + val cList = getConfigs() ?: DefaultData.themeConfigs + ArrayList(cList) + } + + fun applyDayNight(context: Context) { + ReadBookConfig.upBg() + applyTheme(context) + initNightMode() + CoverImageView.upDefaultCover() + postEvent(EventBus.RECREATE, "") + } + + private fun initNightMode() { + val targetMode = + if (AppConfig.isNightTheme) { + AppCompatDelegate.MODE_NIGHT_YES + } else { + AppCompatDelegate.MODE_NIGHT_NO + } + AppCompatDelegate.setDefaultNightMode(targetMode) } - val configList = arrayListOf() - init { - upConfig() + fun getBgImage(context: Context): Drawable? { + val bgPath = when (Theme.getTheme()) { + Theme.Light -> context.getPrefString(PreferKey.bgImage) + Theme.Dark -> context.getPrefString(PreferKey.bgImageN) + else -> null + } + if (bgPath.isNullOrBlank()) return null + return BitmapDrawable.createFromPath(bgPath) } fun upConfig() { - (getConfigs() ?: defaultConfigs).let { - configList.clear() - configList.addAll(it) + getConfigs()?.forEach { config -> + addConfig(config) } } fun save() { - Coroutine.async { - synchronized(this) { - val json = GSON.toJson(configList) - FileUtils.deleteFile(configFilePath) - FileUtils.createFileIfNotExist(configFilePath).writeText(json) - } - } + val json = GSON.toJson(configList) + FileUtils.deleteFile(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(json) } - fun addConfig(json: String) { - GSON.fromJsonObject(json)?.let { + fun delConfig(index: Int) { + configList.removeAt(index) + save() + } + + fun addConfig(json: String): Boolean { + GSON.fromJsonObject(json.trim { it < ' ' })?.let { addConfig(it) + return true } + return false } - private fun addConfig(newConfig: Config) { + fun addConfig(newConfig: Config) { configList.forEachIndexed { index, config -> if (newConfig.themeName == config.themeName) { configList[index] = newConfig @@ -61,11 +92,11 @@ object ThemeConfig { private fun getConfigs(): List? { val configFile = File(configFilePath) if (configFile.exists()) { - try { + kotlin.runCatching { val json = configFile.readText() return GSON.fromJsonArray(json) - } catch (e: Exception) { - e.printStackTrace() + }.onFailure { + it.printStackTrace() } } return null @@ -88,8 +119,7 @@ object ThemeConfig { context.putPrefInt(PreferKey.cBBackground, bBackground) } AppConfig.isNightTheme = config.isNightTheme - App.INSTANCE.applyDayNight() - postEvent(EventBus.RECREATE, "") + applyDayNight(context) } fun saveDayTheme(context: Context, name: String) { @@ -114,9 +144,15 @@ object ThemeConfig { fun saveNightTheme(context: Context, name: String) { val primary = - context.getPrefInt(PreferKey.cNPrimary, context.getCompatColor(R.color.md_blue_grey_600)) + context.getPrefInt( + PreferKey.cNPrimary, + context.getCompatColor(R.color.md_blue_grey_600) + ) val accent = - context.getPrefInt(PreferKey.cNAccent, context.getCompatColor(R.color.md_deep_orange_800)) + context.getPrefInt( + PreferKey.cNAccent, + context.getCompatColor(R.color.md_deep_orange_800) + ) val background = context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900)) val bBackground = @@ -132,6 +168,62 @@ object ThemeConfig { addConfig(config) } + /** + * 更新主题 + */ + fun applyTheme(context: Context) = with(context) { + when { + AppConfig.isEInkMode -> { + ThemeStore.editTheme(this) + .primaryColor(Color.WHITE) + .accentColor(Color.BLACK) + .backgroundColor(Color.WHITE) + .bottomBackground(Color.WHITE) + .apply() + } + AppConfig.isNightTheme -> { + val primary = + getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600)) + val accent = + getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_deep_orange_800)) + var background = + getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900)) + if (ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_900) + putPrefInt(PreferKey.cNBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_850)) + ThemeStore.editTheme(this) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + else -> { + val primary = + getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500)) + val accent = + getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_red_600)) + var background = + getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100)) + if (!ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_100) + putPrefInt(PreferKey.cBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200)) + ThemeStore.editTheme(this) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + } + } + @Keep class Config( var themeName: String, diff --git a/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt index 1eb1102ba..14c123378 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt @@ -1,5 +1,6 @@ package io.legado.app.help.coroutine +@Suppress("unused") class CompositeCoroutine : CoroutineContainer { private var resources: HashSet>? = null diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt index 0811eee10..e2e89ffe2 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext +@Suppress("unused") class Coroutine( val scope: CoroutineScope, context: CoroutineContext = Dispatchers.IO, @@ -13,7 +14,7 @@ class Coroutine( companion object { - val DEFAULT = MainScope() + private val DEFAULT = MainScope() fun async( scope: CoroutineScope = DEFAULT, @@ -196,7 +197,9 @@ class Coroutine( return withContext(scope.coroutineContext.plus(context)) { if (timeMillis > 0L) withTimeout(timeMillis) { block() - } else block() + } else { + block() + } } } diff --git a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt index a12816486..8bf30ba8d 100644 --- a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt +++ b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt @@ -9,19 +9,15 @@ import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient -import io.legado.app.App import io.legado.app.constant.AppConst import org.apache.commons.text.StringEscapeUtils +import splitties.init.appCtx import java.lang.ref.WeakReference class AjaxWebView { var callback: Callback? = null - private var mHandler: AjaxHandler - - init { - mHandler = AjaxHandler(this) - } + private var mHandler: AjaxHandler = AjaxHandler(this) class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) { @@ -39,7 +35,7 @@ class AjaxWebView { mWebView = createAjaxWebView(params, this) } MSG_SUCCESS -> { - ajaxWebView.callback?.onResult(msg.obj as Res) + ajaxWebView.callback?.onResult(msg.obj as StrResponse) destroyWebView() } MSG_ERROR -> { @@ -51,7 +47,7 @@ class AjaxWebView { @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView { - val webView = WebView(App.INSTANCE) + val webView = WebView(appCtx) val settings = webView.settings settings.javaScriptEnabled = true settings.domStorageEnabled = true @@ -64,11 +60,12 @@ class AjaxWebView { webView.webViewClient = HtmlWebViewClient(params, handler) } when (params.requestMethod) { - RequestMethod.POST -> webView.postUrl(params.url, params.postData) - RequestMethod.GET -> webView.loadUrl( - params.url, - params.headerMap - ) + RequestMethod.POST -> params.postData?.let { + webView.postUrl(params.url, it) + } + RequestMethod.GET -> params.headerMap?.let { + webView.loadUrl(params.url, it) + } } return webView } @@ -159,13 +156,17 @@ class AjaxWebView { if (it.isNotEmpty() && it != "null") { val content = StringEscapeUtils.unescapeJson(it) .replace("^\"|\"$".toRegex(), "") - handler.obtainMessage(MSG_SUCCESS, Res(url, content)) - .sendToTarget() + try { + val response = StrResponse(url, content) + handler.obtainMessage(MSG_SUCCESS, response).sendToTarget() + } catch (e: Exception) { + handler.obtainMessage(MSG_ERROR, e).sendToTarget() + } handler.removeCallbacks(this) return@evaluateJavascript } if (retry > 30) { - handler.obtainMessage(MSG_ERROR, Exception("time out")) + handler.obtainMessage(MSG_ERROR, Exception("js执行超时")) .sendToTarget() handler.removeCallbacks(this) return@evaluateJavascript @@ -185,8 +186,12 @@ class AjaxWebView { override fun onLoadResource(view: WebView, url: String) { params.sourceRegex?.let { if (url.matches(it.toRegex())) { - handler.obtainMessage(MSG_SUCCESS, Res(view.url ?: params.url, url)) - .sendToTarget() + try { + val response = StrResponse(params.url, url) + handler.obtainMessage(MSG_SUCCESS, response).sendToTarget() + } catch (e: Exception) { + handler.obtainMessage(MSG_ERROR, e).sendToTarget() + } } } } @@ -225,7 +230,7 @@ class AjaxWebView { } abstract class Callback { - abstract fun onResult(response: Res) + abstract fun onResult(response: StrResponse) abstract fun onError(error: Throwable) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/ByteConverter.kt b/app/src/main/java/io/legado/app/help/http/ByteConverter.kt deleted file mode 100644 index bb42ec3a8..000000000 --- a/app/src/main/java/io/legado/app/help/http/ByteConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.legado.app.help.http - -import okhttp3.ResponseBody -import retrofit2.Converter -import retrofit2.Retrofit -import java.lang.reflect.Type - -class ByteConverter : Converter.Factory() { - - override fun responseBodyConverter( - type: Type?, - annotations: Array?, - retrofit: Retrofit? - ): Converter? { - return Converter { value -> - value.bytes() - } - } - -} diff --git a/app/src/main/java/io/legado/app/help/http/CookieStore.kt b/app/src/main/java/io/legado/app/help/http/CookieStore.kt index 067435ddb..c8d516091 100644 --- a/app/src/main/java/io/legado/app/help/http/CookieStore.kt +++ b/app/src/main/java/io/legado/app/help/http/CookieStore.kt @@ -1,23 +1,21 @@ +@file:Suppress("unused") + package io.legado.app.help.http import android.text.TextUtils -import com.franmontiel.persistentcookiejar.persistence.CookiePersistor -import com.franmontiel.persistentcookiejar.persistence.SerializableCookie -import io.legado.app.App +import io.legado.app.data.appDb import io.legado.app.data.entities.Cookie -import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.api.CookieManager import io.legado.app.utils.NetworkUtils -object CookieStore : CookiePersistor { +object CookieStore : CookieManager { - fun setCookie(url: String, cookie: String?) { - Coroutine.async { - val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "") - App.db.cookieDao().insert(cookieBean) - } + override fun setCookie(url: String, cookie: String?) { + val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "") + appDb.cookieDao.insert(cookieBean) } - fun replaceCookie(url: String, cookie: String) { + override fun replaceCookie(url: String, cookie: String) { if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) { return } @@ -32,16 +30,16 @@ object CookieStore : CookiePersistor { } } - fun getCookie(url: String): String { - val cookieBean = App.db.cookieDao().get(NetworkUtils.getSubDomain(url)) + override fun getCookie(url: String): String { + val cookieBean = appDb.cookieDao.get(NetworkUtils.getSubDomain(url)) return cookieBean?.cookie ?: "" } - fun removeCookie(url: String) { - App.db.cookieDao().delete(NetworkUtils.getSubDomain(url)) + override fun removeCookie(url: String) { + appDb.cookieDao.delete(NetworkUtils.getSubDomain(url)) } - private fun cookieToMap(cookie: String): MutableMap { + override fun cookieToMap(cookie: String): MutableMap { val cookieMap = mutableMapOf() if (cookie.isBlank()) { return cookieMap @@ -61,7 +59,7 @@ object CookieStore : CookiePersistor { return cookieMap } - private fun mapToCookie(cookieMap: Map?): String? { + override fun mapToCookie(cookieMap: Map?): String? { if (cookieMap == null || cookieMap.isEmpty()) { return null } @@ -78,36 +76,8 @@ object CookieStore : CookiePersistor { return builder.deleteCharAt(builder.lastIndexOf(";")).toString() } - override fun loadAll(): MutableList { - val cookies = arrayListOf() - App.db.cookieDao().getOkHttpCookies().forEach { - val serializedCookie = it.cookie - SerializableCookie().decode(serializedCookie)?.let { ck -> - cookies.add(ck) - } - } - return cookies - } - - override fun saveAll(cookies: MutableCollection?) { - val mCookies = arrayListOf() - cookies?.forEach { - mCookies.add(Cookie(createCookieKey(it), SerializableCookie().encode(it))) - } - App.db.cookieDao().insert(*mCookies.toTypedArray()) + fun clear() { + appDb.cookieDao.deleteOkHttp() } - override fun removeAll(cookies: MutableCollection?) { - cookies?.forEach { - App.db.cookieDao().delete(createCookieKey(it)) - } - } - - override fun clear() { - App.db.cookieDao().deleteOkHttp() - } - - private fun createCookieKey(cookie: okhttp3.Cookie): String { - return (if (cookie.secure()) "https" else "http") + "://" + cookie.domain() + cookie.path() + "|" + cookie.name() - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt b/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt deleted file mode 100644 index 0cad990f0..000000000 --- a/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.legado.app.help.http - -import io.legado.app.utils.EncodingDetect -import io.legado.app.utils.UTF8BOMFighter -import okhttp3.ResponseBody -import retrofit2.Converter -import retrofit2.Retrofit -import java.lang.reflect.Type -import java.nio.charset.Charset - -class EncodeConverter(private val encode: String? = null) : Converter.Factory() { - - override fun responseBodyConverter( - type: Type?, - annotations: Array?, - retrofit: Retrofit? - ): Converter? { - return Converter { value -> - val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()) - var charsetName: String? = encode - - charsetName?.let { - try { - return@Converter String(responseBytes, Charset.forName(charsetName)) - } catch (e: Exception) { - } - } - - //根据http头判断 - value.contentType()?.charset()?.let { - return@Converter String(responseBytes, it) - } - - //根据内容判断 - charsetName = EncodingDetect.getHtmlEncode(responseBytes) - String(responseBytes, Charset.forName(charsetName)) - } - } - -} diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index 6364e9ed8..e67090fb3 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -1,194 +1,119 @@ package io.legado.app.help.http -import io.legado.app.constant.AppConst -import io.legado.app.help.http.api.HttpGetApi -import io.legado.app.utils.NetworkUtils +import io.legado.app.help.AppConfig +import io.legado.app.help.http.cronet.CronetInterceptor +import io.legado.app.help.http.cronet.CronetLoader import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.* -import retrofit2.Retrofit +import okhttp3.ConnectionSpec +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import kotlin.coroutines.resume -@Suppress("unused") -object HttpHelper { - - val client: OkHttpClient by lazy { - - val specs = arrayListOf( - ConnectionSpec.MODERN_TLS, - ConnectionSpec.COMPATIBLE_TLS, - ConnectionSpec.CLEARTEXT - ) - - val builder = OkHttpClient.Builder() - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(15, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager) - .retryOnConnectionFailure(true) - .hostnameVerifier(SSLHelper.unsafeHostnameVerifier) - .connectionSpecs(specs) - .followRedirects(true) - .followSslRedirects(true) - .protocols(listOf(Protocol.HTTP_1_1)) - .addInterceptor(getHeaderInterceptor()) - - builder.build() - } - - fun simpleGet(url: String, encode: String? = null): String? { - NetworkUtils.getBaseUrl(url)?.let { baseUrl -> - val response = getApiService(baseUrl, encode) - .get(url, mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent))) - .execute() - return response.body() - } - return null - } - - fun getBytes(url: String, queryMap: Map, headers: Map): ByteArray? { - NetworkUtils.getBaseUrl(url)?.let { baseUrl -> - return getByteRetrofit(baseUrl) - .create(HttpGetApi::class.java) - .getMapByte(url, queryMap, headers) - .execute() - .body() - } - return null - } +private val proxyClientCache: ConcurrentHashMap by lazy { + ConcurrentHashMap() +} - suspend fun simpleGetAsync(url: String, encode: String? = null): String? { - NetworkUtils.getBaseUrl(url)?.let { baseUrl -> - val response = getApiService(baseUrl, encode) - .getAsync(url, mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent))) - return response.body() - } - return null +val okHttpClient: OkHttpClient by lazy { + val specs = arrayListOf( + ConnectionSpec.MODERN_TLS, + ConnectionSpec.COMPATIBLE_TLS, + ConnectionSpec.CLEARTEXT + ) + + val builder = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager) + .retryOnConnectionFailure(true) + .hostnameVerifier(SSLHelper.unsafeHostnameVerifier) + .connectionSpecs(specs) + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor(Interceptor { chain -> + val request = chain.request() + .newBuilder() + .addHeader("Keep-Alive", "300") + .addHeader("Connection", "Keep-Alive") + .addHeader("Cache-Control", "no-cache") + .build() + chain.proceed(request) + }) + if (AppConfig.isCronet && CronetLoader.install()) { + //提供CookieJar 用于同步Cookie + builder.addInterceptor(CronetInterceptor(null)) } - suspend fun simpleGetBytesAsync(url: String): ByteArray? { - NetworkUtils.getBaseUrl(url)?.let { baseUrl -> - return getByteRetrofit(baseUrl) - .create(HttpGetApi::class.java) - .getMapByteAsync(url, mapOf(), mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent))) - .body() - } - return null - } - inline fun getApiService(baseUrl: String, encode: String? = null): T { - return getRetrofit(baseUrl, encode).create(T::class.java) - } + builder.build() +} - inline fun getApiServiceWithProxy( - baseUrl: String, - encode: String? = null, - proxy: String? = null - ): T { - return getRetrofitWithProxy(baseUrl, encode, proxy).create(T::class.java) +/** + * 缓存代理okHttp + */ +fun getProxyClient(proxy: String? = null): OkHttpClient { + if (proxy.isNullOrBlank()) { + return okHttpClient } - - inline fun getBytesApiService(baseUrl: String): T { - return getByteRetrofit(baseUrl).create(T::class.java) + proxyClientCache[proxy]?.let { + return it } - - fun getRetrofit(baseUrl: String, encode: String? = null): Retrofit { - return Retrofit.Builder().baseUrl(baseUrl) - //增加返回值为字符串的支持(以实体类返回) - .addConverterFactory(EncodeConverter(encode)) - .client(client) - .build() + val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?") + val ms = r.findAll(proxy) + val group = ms.first() + var username = "" //代理服务器验证用户名 + var password = "" //代理服务器验证密码 + val type = if (group.groupValues[1] == "http") "http" else "socks" + val host = group.groupValues[2] + val port = group.groupValues[3].toInt() + if (group.groupValues[4] != "") { + username = group.groupValues[4].split("@")[1] + password = group.groupValues[4].split("@")[2] } - - fun getRetrofitWithProxy( - baseUrl: String, - encode: String? = null, - proxy: String? = null - ): Retrofit { - val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?") - val ms = proxy?.let { r.findAll(it) }; - val group = ms?.first() - var type = "direct" //直接连接 - var host = "127.0.0.1" //代理服务器hostname - var port = 1080 //代理服务器port - var username = "" //代理服务器验证用户名 - var password = "" //代理服务器验证密码 - if (group != null) { - type = if (group.groupValues[1] == "http") { - "http" - } else { - "socks" - } - host = group.groupValues[2] - port = group.groupValues[3].toInt() - if (group.groupValues[4] != "") { - username = group.groupValues[4].split("@")[1] - password = group.groupValues[4].split("@")[2] - } + if (type != "direct" && host != "") { + val builder = okHttpClient.newBuilder() + if (type == "http") { + builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))) + } else { + builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port))) } - val builder = client.newBuilder() - if (type != "direct" && host != "") { - if (type == "http") { - builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))); - } else { - builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port))); - } - if (username != "" && password != "") { - builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 - val credential: String = Credentials.basic(username, password) - response.request().newBuilder() - .header("Proxy-Authorization", credential) - .build() - } + if (username != "" && password != "") { + builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 + val credential: String = Credentials.basic(username, password) + response.request.newBuilder() + .header("Proxy-Authorization", credential) + .build() } - } - return Retrofit.Builder().baseUrl(baseUrl) - //增加返回值为字符串的支持(以实体类返回) - .addConverterFactory(EncodeConverter(encode)) - .client(builder.build()) - .build() - } - - fun getByteRetrofit(baseUrl: String): Retrofit { - return Retrofit.Builder().baseUrl(baseUrl) - .addConverterFactory(ByteConverter()) - .client(client) - .build() + val proxyClient = builder.build() + proxyClientCache[proxy] = proxyClient + return proxyClient } + return okHttpClient +} - private fun getHeaderInterceptor(): Interceptor { - return Interceptor { chain -> - val request = chain.request() - .newBuilder() - .addHeader("Keep-Alive", "300") - .addHeader("Connection", "Keep-Alive") - .addHeader("Cache-Control", "no-cache") - .build() - chain.proceed(request) +suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse = + suspendCancellableCoroutine { block -> + val webView = AjaxWebView() + block.invokeOnCancellation { + webView.destroyWebView() } - } + webView.callback = object : AjaxWebView.Callback() { + override fun onResult(response: StrResponse) { - suspend fun ajax(params: AjaxWebView.AjaxParams): Res = - suspendCancellableCoroutine { block -> - val webView = AjaxWebView() - block.invokeOnCancellation { - webView.destroyWebView() + if (!block.isCompleted) + block.resume(response) } - webView.callback = object : AjaxWebView.Callback() { - override fun onResult(response: Res) { - if (!block.isCompleted) - block.resume(response) - } - override fun onError(error: Throwable) { - if (!block.isCompleted) - block.resume(Res(params.url, error.localizedMessage)) - } + override fun onError(error: Throwable) { + if (!block.isCompleted) + block.cancel(error) } - webView.load(params) } - -} + webView.load(params) + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt new file mode 100644 index 000000000..2c8206123 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt @@ -0,0 +1,122 @@ +package io.legado.app.help.http + +import io.legado.app.constant.AppConst +import io.legado.app.help.AppConfig +import io.legado.app.utils.EncodingDetect +import io.legado.app.utils.UTF8BOMFighter +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.nio.charset.Charset +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun OkHttpClient.newCall( + retry: Int = 0, + builder: Request.Builder.() -> Unit +): ResponseBody { + val requestBuilder = Request.Builder() + requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) + requestBuilder.apply(builder) + var response: Response? = null + for (i in 0..retry) { + response = this.newCall(requestBuilder.build()).await() + if (response.isSuccessful) { + return response.body!! + } + } + return response!!.body ?: throw IOException(response.message) +} + +suspend fun OkHttpClient.newCallStrResponse( + retry: Int = 0, + builder: Request.Builder.() -> Unit +): StrResponse { + val requestBuilder = Request.Builder() + requestBuilder.header(AppConst.UA_NAME, AppConfig.userAgent) + requestBuilder.apply(builder) + var response: Response? = null + for (i in 0..retry) { + response = this.newCall(requestBuilder.build()).await() + if (response.isSuccessful) { + return StrResponse(response, response.body!!.text()) + } + } + return StrResponse(response!!, response.body?.text() ?: response.message) +} + +suspend fun Call.await(): Response = suspendCancellableCoroutine { block -> + + block.invokeOnCancellation { + cancel() + } + + enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + block.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + block.resume(response) + } + }) + +} + +fun ResponseBody.text(encode: String? = null): String { + val responseBytes = UTF8BOMFighter.removeUTF8BOM(bytes()) + var charsetName: String? = encode + + charsetName?.let { + return String(responseBytes, Charset.forName(charsetName)) + } + + //根据http头判断 + contentType()?.charset()?.let { + return String(responseBytes, it) + } + + //根据内容判断 + charsetName = EncodingDetect.getHtmlEncode(responseBytes) + return String(responseBytes, Charset.forName(charsetName)) +} + +fun Request.Builder.addHeaders(headers: Map) { + headers.forEach { + addHeader(it.key, it.value) + } +} + +fun Request.Builder.get(url: String, queryMap: Map, encoded: Boolean = false) { + val httpBuilder = url.toHttpUrl().newBuilder() + queryMap.forEach { + if (encoded) { + httpBuilder.addEncodedQueryParameter(it.key, it.value) + } else { + httpBuilder.addQueryParameter(it.key, it.value) + } + } + url(httpBuilder.build()) +} + +fun Request.Builder.postForm(form: Map, encoded: Boolean = false) { + val formBody = FormBody.Builder() + form.forEach { + if (encoded) { + formBody.addEncoded(it.key, it.value) + } else { + formBody.add(it.key, it.value) + } + } + post(formBody.build()) +} + +fun Request.Builder.postJson(json: String?) { + json?.let { + val requestBody = json.toRequestBody("application/json; charset=UTF-8".toMediaType()) + post(requestBody) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/Res.kt b/app/src/main/java/io/legado/app/help/http/Res.kt deleted file mode 100644 index e54832b40..000000000 --- a/app/src/main/java/io/legado/app/help/http/Res.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.legado.app.help.http - -data class Res( - val url: String, - val body: String?, -) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/RetryInterceptor.kt b/app/src/main/java/io/legado/app/help/http/RetryInterceptor.kt new file mode 100644 index 000000000..00c4a0881 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/RetryInterceptor.kt @@ -0,0 +1,12 @@ +package io.legado.app.help.http + +import okhttp3.Interceptor +import okhttp3.Response + +class RetryInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt index 800304b62..b0bd53484 100644 --- a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt @@ -12,6 +12,7 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.* +@Suppress("unused") object SSLHelper { /** @@ -78,7 +79,11 @@ object SSLHelper { * bksFile 和 password -> 客户端使用bks证书校验服务端证书 * certificates -> 用含有服务端公钥的证书校验服务端证书 */ - fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams? { + fun getSslSocketFactory( + bksFile: InputStream, + password: String, + vararg certificates: InputStream + ): SSLParams? { return getSslSocketFactoryBase(null, bksFile, password, *certificates) } @@ -87,7 +92,11 @@ object SSLHelper { * bksFile 和 password -> 客户端使用bks证书校验服务端证书 * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 */ - fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams? { + fun getSslSocketFactory( + bksFile: InputStream, + password: String, + trustManager: X509TrustManager + ): SSLParams? { return getSslSocketFactoryBase(trustManager, bksFile, password) } diff --git a/app/src/main/java/io/legado/app/help/http/StrResponse.kt b/app/src/main/java/io/legado/app/help/http/StrResponse.kt new file mode 100644 index 000000000..1b1f53395 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/StrResponse.kt @@ -0,0 +1,73 @@ +package io.legado.app.help.http + +import okhttp3.* +import okhttp3.Response.Builder + +/** + * An HTTP response. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +class StrResponse { + var raw: Response + private set + var body: String? = null + private set + var errorBody: ResponseBody? = null + private set + + constructor(rawResponse: Response, body: String?) { + this.raw = rawResponse + this.body = body + } + + constructor(url: String, body: String?) { + raw = Builder() + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .request(Request.Builder().url(url).build()) + .build() + this.body = body + } + + constructor(rawResponse: Response, errorBody: ResponseBody?) { + this.raw = rawResponse + this.errorBody = errorBody + } + + fun raw() = raw + + fun url(): String { + raw.networkResponse?.let { + return it.request.url.toString() + } + return raw.request.url.toString() + } + + val url: String get() = url() + + fun body() = body + + fun code(): Int { + return raw.code + } + + fun message(): String { + return raw.message + } + + fun headers(): Headers { + return raw.headers + } + + fun isSuccessful(): Boolean = raw.isSuccessful + + fun errorBody(): ResponseBody? { + return errorBody + } + + override fun toString(): String { + return raw.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/api/CookieManager.kt b/app/src/main/java/io/legado/app/help/http/api/CookieManager.kt new file mode 100644 index 000000000..525aa712d --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/api/CookieManager.kt @@ -0,0 +1,28 @@ +package io.legado.app.help.http.api + +interface CookieManager { + + /** + * 保存cookie + */ + fun setCookie(url: String, cookie: String?) + + /** + * 替换cookie + */ + fun replaceCookie(url: String, cookie: String) + + /** + * 获取cookie + */ + fun getCookie(url: String): String + + /** + * 移除cookie + */ + fun removeCookie(url: String) + + fun cookieToMap(cookie: String): MutableMap + + fun mapToCookie(cookieMap: Map?): String? +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt deleted file mode 100644 index 7138c8c20..000000000 --- a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt +++ /dev/null @@ -1,67 +0,0 @@ -package io.legado.app.help.http.api - -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 - */ -@Suppress("unused") -interface HttpGetApi { - @GET - suspend fun getAsync( - @Url url: String, - @HeaderMap headers: Map - ): Response - - @GET - suspend fun getMapAsync( - @Url url: String, - @QueryMap(encoded = true) queryMap: Map, - @HeaderMap headers: Map - ): Response - - @GET - fun get( - @Url url: String, - @HeaderMap headers: Map - ): Call - - @GET - fun getByte( - @Url url: String, - @HeaderMap headers: Map - ): Call - - @GET - fun getMap( - @Url url: String, - @QueryMap(encoded = true) queryMap: Map, - @HeaderMap headers: Map - ): Call - - @GET - fun getMapByte( - @Url url: String, - @QueryMap(encoded = true) queryMap: Map, - @HeaderMap headers: Map - ): Call - - @GET - suspend fun getByteAsync( - @Url url: String, - @HeaderMap headers: Map - ): Response - - @GET - suspend fun getMapByteAsync( - @Url url: String, - @QueryMap(encoded = true) queryMap: Map, - @HeaderMap headers: Map - ): Response -} diff --git a/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt deleted file mode 100644 index 170894390..000000000 --- a/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.legado.app.help.http.api - -import okhttp3.RequestBody -import retrofit2.Call -import retrofit2.Response -import retrofit2.http.* - -/** - * Created by GKF on 2018/1/29. - * post - */ -@Suppress("unused") -interface HttpPostApi { - - @FormUrlEncoded - @POST - suspend fun postMapAsync( - @Url url: String, - @FieldMap(encoded = true) fieldMap: Map, - @HeaderMap headers: Map - ): Response - - @POST - suspend fun postBodyAsync( - @Url url: String, - @Body body: RequestBody, - @HeaderMap headers: Map - ): Response - - @FormUrlEncoded - @POST - fun postMap( - @Url url: String, - @FieldMap(encoded = true) fieldMap: Map, - @HeaderMap headers: Map - ): Call - - @POST - fun postBody( - @Url url: String, - @Body body: RequestBody, - @HeaderMap headers: Map - ): Call - - @FormUrlEncoded - @POST - suspend fun postMapByteAsync( - @Url url: String, - @FieldMap(encoded = true) fieldMap: Map, - @HeaderMap headers: Map - ): Response - - @POST - suspend fun postBodyByteAsync( - @Url url: String, - @Body body: RequestBody, - @HeaderMap headers: Map - ): Response - -} diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt new file mode 100644 index 000000000..4d01657d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt @@ -0,0 +1,72 @@ +package io.legado.app.help.http.cronet + +import android.util.Log +import okhttp3.Headers +import okhttp3.MediaType +import okhttp3.Request +import okio.Buffer +import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK +import org.chromium.net.ExperimentalCronetEngine +import org.chromium.net.UploadDataProviders +import org.chromium.net.UrlRequest +import splitties.init.appCtx +import java.util.concurrent.Executor +import java.util.concurrent.Executors + + +val executor: Executor by lazy { Executors.newCachedThreadPool() } + +val cronetEngine: ExperimentalCronetEngine by lazy { + CronetLoader.preDownload() + + val builder = ExperimentalCronetEngine.Builder(appCtx) + .setLibraryLoader(CronetLoader)//设置自定义so库加载 + .setStoragePath(appCtx.externalCacheDir?.absolutePath)//设置缓存路径 + .enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))//设置缓存模式 + .enableQuic(true)//设置支持http/3 + .enableHttp2(true) //设置支持http/2 + .enablePublicKeyPinningBypassForLocalTrustAnchors(true) + //.enableNetworkQualityEstimator(true) + + //Brotli压缩 + builder.enableBrotli(true) + //builder.setExperimentalOptions("{\"quic_version\": \"h3-29\"}") + val engine = builder.build() + Log.d("Cronet", "Cronet Version:" + engine.versionString) + //这会导致Jsoup的网络请求出现问题,暂时不接管系统URL + //URL.setURLStreamHandlerFactory(CronetURLStreamHandlerFactory(engine)) + return@lazy engine + +} + + +fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest { + val url = request.url.toString() + val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor) + requestBuilder.setHttpMethod(request.method) + + val headers: Headers = request.headers + headers.forEachIndexed { index, _ -> + requestBuilder.addHeader(headers.name(index), headers.value(index)) + } + + val requestBody = request.body + if (requestBody != null) { + val contentType: MediaType? = requestBody.contentType() + if (contentType != null) { + requestBuilder.addHeader("Content-Type", contentType.toString()) + } else { + requestBuilder.addHeader("Content-Type", "text/plain") + } + val buffer = Buffer() + requestBody.writeTo(buffer) + requestBuilder.setUploadDataProvider( + UploadDataProviders.create(buffer.readByteArray()), + executor + ) + + } + + return requestBuilder.build() +} + diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt new file mode 100644 index 000000000..0a0e955b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt @@ -0,0 +1,62 @@ +package io.legado.app.help.http.cronet + +import io.legado.app.help.http.CookieStore +import okhttp3.* +import java.io.IOException + +class CronetInterceptor(private val cookieJar: CookieJar?) : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val original: Request = chain.request() + val builder: Request.Builder = original.newBuilder() + //Cronet未初始化 + return if (!CronetLoader.install()) { + chain.proceed(original) + } else try { + //移除Keep-Alive,手动设置会导致400 BadRequest + builder.removeHeader("Keep-Alive") + val cookieStr = getCookie(original.url) + //设置Cookie + if (cookieStr.length > 3) { + builder.header("Cookie", cookieStr) + } + val new = builder.build() + val response: Response = proceedWithCronet(new, chain.call()) + //从Response 中保存Cookie到CookieJar + cookieJar?.saveFromResponse(new.url, Cookie.parseAll(new.url, response.headers)) + response + } catch (e: Exception) { + //遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 + chain.proceed(original) + } + + + } + + @Throws(IOException::class) + private fun proceedWithCronet(request: Request, call: Call): Response { + + val callback = CronetUrlRequestCallback(request, call) + val urlRequest = buildRequest(request, callback) + urlRequest.start() + return callback.waitForDone() + } + + private fun getCookie(url: HttpUrl): String { + val sb = StringBuilder() + //处理从 Cookjar 获取到的Cookies + if (cookieJar != null) { + val cookies = cookieJar.loadForRequest(url) + for (cookie in cookies) { + sb.append(cookie.name).append("=").append(cookie.value).append("; ") + } + } + //处理自定义的Cookie + val cookie = CookieStore.getCookie(url.toString()) + if (cookie.length > 3) { + sb.append(cookie) + } + return sb.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt new file mode 100644 index 000000000..e7ea61ec1 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.kt @@ -0,0 +1,357 @@ +package io.legado.app.help.http.cronet + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.Build +import android.text.TextUtils +import android.util.Log +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.getPrefString +import io.legado.app.utils.putPrefString +import org.chromium.net.CronetEngine +import org.chromium.net.impl.ImplVersion +import splitties.init.appCtx +import java.io.* +import java.math.BigInteger +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest +import java.util.* + +object CronetLoader : CronetEngine.Builder.LibraryLoader() { + //https://storage.googleapis.com/chromium-cronet/android/92.0.4515.127/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.127.so + //https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@92.0.4515.127/cronet/92.0.4515.127/arm64-v8a/libcronet.92.0.4515.127.so.js + private const val TAG = "CronetLoader" + private val soName = "libcronet." + ImplVersion.getCronetVersion() + ".so" + private val soUrl: String + private val md5Url: String + private val soFile: File + private val downloadFile: File + private var cpuAbi: String? = null + private var md5: String? = appCtx.getPrefString("soMd5") + private val version: String? = appCtx.getPrefString("soVersion", ImplVersion.getCronetVersion()) + var download = false + + init { + soUrl = ("https://storage.googleapis.com/chromium-cronet/android/" + + ImplVersion.getCronetVersion() + "/Release/cronet/libs/" + + getCpuAbi(appCtx) + "/" + soName) + md5Url = ("https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@" + + ImplVersion.getCronetVersion() + "/cronet/" + ImplVersion.getCronetVersion() + "/" + + getCpuAbi(appCtx) + "/" + soName + ".js") + val dir = appCtx.getDir("lib", Context.MODE_PRIVATE) + soFile = File(dir.toString() + "/" + getCpuAbi(appCtx), soName) + downloadFile = File(appCtx.cacheDir.toString() + "/so_download", soName) + Log.e(TAG, "soName+:$soName") + Log.e(TAG, "destSuccessFile:$soFile") + Log.e(TAG, "tempFile:$downloadFile") + Log.e(TAG, "soUrl:$soUrl") + } + + fun install(): Boolean { + return soFile.exists() + } + + fun preDownload() { + Coroutine.async { + md5 = getUrlMd5(md5Url) + if (soFile.exists() && md5 == getFileMD5(soFile)) { + Log.e(TAG, "So 库已存在") + } else { + download(soUrl, md5, downloadFile, soFile) + } + Log.e(TAG, soName) + } + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + override fun loadLibrary(libName: String) { + Log.e(TAG, "libName:$libName") + val start = System.currentTimeMillis() + @Suppress("SameParameterValue") + try { + //非cronet的so调用系统方法加载 + if (!libName.contains("cronet")) { + System.loadLibrary(libName) + return + } + //以下逻辑为cronet加载,优先加载本地,否则从远程加载 + //首先调用系统行为进行加载 + System.loadLibrary(libName) + Log.i(TAG, "load from system") + } catch (e: Throwable) { + //如果找不到,则从远程下载 + //删除历史文件 + deleteHistoryFile(Objects.requireNonNull(soFile.parentFile), soFile) + md5 = getUrlMd5(md5Url) + Log.i(TAG, "soMD5:$md5") + if (md5 == null || md5!!.length != 32 || soUrl.isEmpty()) { + //如果md5或下载的url为空,则调用系统行为进行加载 + System.loadLibrary(libName) + return + } + if (!soFile.exists() || !soFile.isFile) { + soFile.delete() + download(soUrl, md5, downloadFile, soFile) + //如果文件不存在或不是文件,则调用系统行为进行加载 + System.loadLibrary(libName) + return + } + if (soFile.exists()) { + //如果文件存在,则校验md5值 + val fileMD5 = getFileMD5(soFile) + if (fileMD5 != null && fileMD5.equals(md5, ignoreCase = true)) { + //md5值一样,则加载 + System.load(soFile.absolutePath) + Log.e(TAG, "load from:$soFile") + return + } + //md5不一样则删除 + soFile.delete() + } + //不存在则下载 + download(soUrl, md5, downloadFile, soFile) + //使用系统加载方法 + System.loadLibrary(libName) + } finally { + Log.e(TAG, "time:" + (System.currentTimeMillis() - start)) + } + } + + @SuppressLint("DiscouragedPrivateApi") + private fun getCpuAbi(context: Context): String? { + if (cpuAbi != null) { + return cpuAbi + } + // 5.0以上Application才有primaryCpuAbi字段 + try { + val appInfo = context.applicationInfo + val abiField = ApplicationInfo::class.java.getDeclaredField("primaryCpuAbi") + abiField.isAccessible = true + cpuAbi = abiField[appInfo] as String + } catch (e: Exception) { + e.printStackTrace() + } + if (TextUtils.isEmpty(cpuAbi)) { + cpuAbi = Build.SUPPORTED_ABIS[0] + } + return cpuAbi + } + + @Suppress("SameParameterValue") + private fun getUrlMd5(url: String): String? { + //这样在下载成功后,遇到无网条件下,只要版本未发生变化也能获取md5 + if (md5 != null && md5!!.length == 32 && version == ImplVersion.getCronetVersion()) { + appCtx.putPrefString("soMd5", md5) + appCtx.putPrefString("soVersion", ImplVersion.getCronetVersion()) + return md5 + } + val inputStream: InputStream + val outputStream: OutputStream + return try { + outputStream = ByteArrayOutputStream() + val connection = URL(url).openConnection() as HttpURLConnection + inputStream = connection.inputStream + val buffer = ByteArray(1024) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + outputStream.flush() + } + val tmd5 = outputStream.toString() + //成功获取到md5后保存md5和版本 + if (tmd5.length == 32) { + appCtx.putPrefString("soMd5", tmd5) + appCtx.putPrefString("soVersion", ImplVersion.getCronetVersion()) + } + + return tmd5 + + } catch (e: IOException) { + null + } + } + + /** + * 删除历史文件 + */ + private fun deleteHistoryFile(dir: File, currentFile: File?) { + val files = dir.listFiles() + @Suppress("SameParameterValue") + if (files != null && files.isNotEmpty()) { + for (f in files) { + if (f.exists() && (currentFile == null || f.absolutePath != currentFile.absolutePath)) { + val delete = f.delete() + Log.e(TAG, "delete file: $f result: $delete") + if (!delete) { + f.deleteOnExit() + } + } + } + } + } + + /** + * 下载文件 + */ + private fun downloadFileIfNotExist(url: String, destFile: File): Boolean { + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + try { + val connection = URL(url).openConnection() as HttpURLConnection + inputStream = connection.inputStream + if (destFile.exists()) { + return true + } + destFile.parentFile!!.mkdirs() + destFile.createNewFile() + outputStream = FileOutputStream(destFile) + val buffer = ByteArray(32768) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + outputStream.flush() + } + return true + } catch (e: Throwable) { + e.printStackTrace() + if (destFile.exists() && !destFile.delete()) { + destFile.deleteOnExit() + } + } finally { + if (inputStream != null) { + try { + inputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + if (outputStream != null) { + try { + outputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + return false + } + + /** + * 下载并拷贝文件 + */ + @Suppress("SameParameterValue") + @Synchronized + private fun download( + url: String, + md5: String?, + downloadTempFile: File, + destSuccessFile: File + ) { + if (download) { + return + } + download = true + executor.execute { + val result = downloadFileIfNotExist(url, downloadTempFile) + Log.e(TAG, "download result:$result") + //文件md5再次校验 + val fileMD5 = getFileMD5(downloadTempFile) + if (md5 != null && !md5.equals(fileMD5, ignoreCase = true)) { + val delete = downloadTempFile.delete() + if (!delete) { + downloadTempFile.deleteOnExit() + } + download = false + return@execute + } + Log.e(TAG, "download success, copy to $destSuccessFile") + //下载成功拷贝文件 + copyFile(downloadTempFile, destSuccessFile) + val parentFile = downloadTempFile.parentFile + @Suppress("SameParameterValue") + deleteHistoryFile(parentFile!!, null) + } + } + + /** + * 拷贝文件 + */ + private fun copyFile(source: File?, dest: File?): Boolean { + if (source == null || !source.exists() || !source.isFile || dest == null) { + return false + } + if (source.absolutePath == dest.absolutePath) { + return true + } + var fileInputStream: FileInputStream? = null + var os: FileOutputStream? = null + val parent = dest.parentFile + if (parent != null && !parent.exists()) { + val mkdirs = parent.mkdirs() + if (!mkdirs) { + parent.mkdirs() + } + } + try { + fileInputStream = FileInputStream(source) + os = FileOutputStream(dest, false) + val buffer = ByteArray(1024 * 512) + var length: Int + while (fileInputStream.read(buffer).also { length = it } > 0) { + os.write(buffer, 0, length) + } + return true + } catch (e: Exception) { + e.printStackTrace() + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + if (os != null) { + try { + os.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + return false + } + + /** + * 获得文件md5 + */ + private fun getFileMD5(file: File): String? { + var fileInputStream: FileInputStream? = null + try { + fileInputStream = FileInputStream(file) + val md5 = MessageDigest.getInstance("MD5") + val buffer = ByteArray(1024) + var numRead: Int + while (fileInputStream.read(buffer).also { numRead = it } > 0) { + md5.update(buffer, 0, numRead) + } + return String.format("%032x", BigInteger(1, md5.digest())).lowercase() + } catch (e: Exception) { + e.printStackTrace() + } catch (e: OutOfMemoryError) { + e.printStackTrace() + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt new file mode 100644 index 000000000..41f6fab98 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt @@ -0,0 +1,222 @@ +package io.legado.app.help.http.cronet + +import android.os.ConditionVariable +import android.util.Log +import io.legado.app.help.http.okHttpClient +import okhttp3.* +import okhttp3.EventListener +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.ResponseBody.Companion.asResponseBody +import okio.Buffer +import org.chromium.net.CronetException +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +import java.io.IOException +import java.nio.ByteBuffer +import java.util.* + +class CronetUrlRequestCallback @JvmOverloads internal constructor( + private val originalRequest: Request, + private val mCall: Call, + eventListener: EventListener? = null, + responseCallback: Callback? = null +) : UrlRequest.Callback() { + + private val eventListener: EventListener? + private val responseCallback: Callback? + private var followCount = 0 + private var mResponse: Response + private var mException: IOException? = null + private val mResponseCondition = ConditionVariable() + private val mBuffer = Buffer() + + @Throws(IOException::class) + fun waitForDone(): Response { + mResponseCondition.block() + if (mException != null) { + throw mException as IOException + } + return this.mResponse + } + + override fun onRedirectReceived( + request: UrlRequest, + info: UrlResponseInfo, + newLocationUrl: String + ) { + if (followCount > MAX_FOLLOW_COUNT) { + request.cancel() + } + followCount += 1 + val client = okHttpClient + if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) { + request.followRedirect() + } else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) { + request.followRedirect() + } else if (client.followRedirects) { + request.followRedirect() + } else { + request.cancel() + } + } + + override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) { + this.mResponse = responseFromResponse(this.mResponse, info) +// 用于调试 +// val sb: StringBuilder = StringBuilder(info.url).append("\r\n") +// sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:") +// .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText) +// .append("][Protocol:").append(info.negotiatedProtocol).append("][ByteCount:") +// .append(info.receivedByteCount).append("]\r\n"); +// val httpHeaders=info.allHeadersAsList +// httpHeaders.forEach { h -> +// sb.append("[").append(h.key).append("]").append(h.value).append("\r\n"); +// } +// Log.e("Cronet", sb.toString()) + //打印协议,用于调试 + Log.e("Cronet", info.negotiatedProtocol) + if (eventListener != null) { + eventListener.responseHeadersEnd(mCall, this.mResponse) + eventListener.responseBodyStart(mCall) + } + request.read(ByteBuffer.allocateDirect(32 * 1024)) + } + + @Throws(Exception::class) + override fun onReadCompleted( + request: UrlRequest, + info: UrlResponseInfo, + byteBuffer: ByteBuffer + ) { + byteBuffer.flip() + try { + //mReceiveChannel.write(byteBuffer) + mBuffer.write(byteBuffer) + } catch (e: IOException) { + Log.i(TAG, "IOException during ByteBuffer read. Details: ", e) + throw e + } + byteBuffer.clear() + request.read(byteBuffer) + } + + override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { + eventListener?.responseBodyEnd(mCall, info.receivedByteCount) + val contentType: MediaType? = (this.mResponse.header("content-type") + ?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull() +// val responseBody: ResponseBody = +// mBytesReceived.toByteArray().toResponseBody(contentType) + val responseBody: ResponseBody = + mBuffer.asResponseBody(contentType) + val newRequest = originalRequest.newBuilder().url(info.url).build() + this.mResponse = this.mResponse.newBuilder().body(responseBody).request(newRequest).build() + mResponseCondition.open() + eventListener?.callEnd(mCall) + if (responseCallback != null) { + try { + responseCallback.onResponse(mCall, this.mResponse) + } catch (e: IOException) { + // Pass? + } + } + } + + //UrlResponseInfo可能为null + override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) { + Log.e(TAG, error.message.toString()) + val msg = error.localizedMessage + val e = IOException(msg?.substring(msg.indexOf("net::")), error) + mException = e + mResponseCondition.open() + + this.eventListener?.callFailed(mCall, e) + + + responseCallback?.onFailure(mCall, e) + } + + override fun onCanceled(request: UrlRequest, info: UrlResponseInfo) { + mResponseCondition.open() + + this.eventListener?.callEnd(mCall) + + + } + + companion object { + private const val TAG = "Callback" + private const val MAX_FOLLOW_COUNT = 20 + + @Suppress("DEPRECATION") + private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol { + val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault()) +// Log.e("Cronet", responseInfo.url) +// Log.e("Cronet", negotiatedProtocol) + return when { + negotiatedProtocol.contains("h3") -> { + return Protocol.QUIC + } + negotiatedProtocol.contains("quic") -> { + Protocol.QUIC + } + negotiatedProtocol.contains("spdy") -> { + Protocol.SPDY_3 + } + negotiatedProtocol.contains("h2") -> { + Protocol.HTTP_2 + } + negotiatedProtocol.contains("1.1") -> { + Protocol.HTTP_1_1 + } + else -> { + Protocol.HTTP_1_0 + } + } + } + + private fun headersFromResponse(responseInfo: UrlResponseInfo): Headers { + val headers = responseInfo.allHeadersAsList + val headerBuilder = Headers.Builder() + for ((key, value) in headers) { + try { + if (key.equals("content-encoding", ignoreCase = true)) { + // Strip all content encoding headers as decoding is done handled by cronet + continue + } + headerBuilder.add(key, value) + } catch (e: Exception) { + Log.w(TAG, "Invalid HTTP header/value: $key$value") + // Ignore that header + } + } + return headerBuilder.build() + } + + private fun responseFromResponse( + response: Response, + responseInfo: UrlResponseInfo + ): Response { + val protocol = protocolFromNegotiatedProtocol(responseInfo) + val headers = headersFromResponse(responseInfo) + return response.newBuilder() + .receivedResponseAtMillis(System.currentTimeMillis()) + .protocol(protocol) + .code(responseInfo.httpStatusCode) + .message(responseInfo.httpStatusText) + .headers(headers) + .build() + } + } + + init { + this.mResponse = Response.Builder() + .sentRequestAtMillis(System.currentTimeMillis()) + .request(originalRequest) + .protocol(Protocol.HTTP_1_0) + .code(0) + .message("") + .build() + this.responseCallback = responseCallback + this.eventListener = eventListener + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt deleted file mode 100644 index 4a51881b0..000000000 --- a/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.legado.app.help.permission - -interface OnPermissionsDeniedCallback { - - fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) - -} diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt deleted file mode 100644 index 59f6977d4..000000000 --- a/app/src/main/java/io/legado/app/help/permission/OnPermissionsGrantedCallback.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.legado.app.help.permission - -interface OnPermissionsGrantedCallback { - - fun onPermissionsGranted(requestCode: Int) - -} diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt deleted file mode 100644 index 3d7afa600..000000000 --- a/app/src/main/java/io/legado/app/help/permission/OnPermissionsResultCallback.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.legado.app.help.permission - -interface OnPermissionsResultCallback { - - fun onPermissionsGranted(requestCode: Int) - - fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt deleted file mode 100644 index 3674461bc..000000000 --- a/app/src/main/java/io/legado/app/help/permission/OnRequestPermissionsResultCallback.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.legado.app.help.permission - -import android.content.Intent - -interface OnRequestPermissionsResultCallback { - - fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) - - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) -} diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index b52688846..6bd8cd01c 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -3,15 +3,17 @@ package io.legado.app.help.storage import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile -import io.legado.app.App +import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.help.DefaultData import io.legado.app.help.ReadBookConfig import io.legado.app.help.ThemeConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext -import org.jetbrains.anko.defaultSharedPreferences +import splitties.init.appCtx import java.io.File import java.util.concurrent.TimeUnit @@ -19,7 +21,7 @@ import java.util.concurrent.TimeUnit object Backup { val backupPath: String by lazy { - FileUtils.getFile(App.INSTANCE.filesDir, "backup").absolutePath + FileUtils.getFile(appCtx.filesDir, "backup").absolutePath } val backupFileNames by lazy { @@ -28,13 +30,16 @@ object Backup { "bookmark.json", "bookGroup.json", "bookSource.json", - "rssSource.json", + "rssSources.json", "rssStar.json", "replaceRule.json", - "txtTocRule.json", "readRecord.json", - "httpTTS.json", + "searchHistory.json", + "sourceSub.json", + DefaultData.txtTocRuleFileName, + DefaultData.httpTtsFileName, ReadBookConfig.configFileName, + ReadBookConfig.shareConfigFileName, ThemeConfig.configFileName, "config.xml" ) @@ -45,6 +50,8 @@ object Backup { if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { Coroutine.async { backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true) + }.onError { + appCtx.toastOnUi(R.string.autobackup_fail) } } } @@ -52,48 +59,53 @@ object Backup { suspend fun backup(context: Context, path: String, isAuto: Boolean = false) { context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) withContext(IO) { - synchronized(this@Backup) { - writeListToJson(App.db.bookDao().all, "bookshelf.json", backupPath) - writeListToJson(App.db.bookmarkDao().all, "bookmark.json", backupPath) - writeListToJson(App.db.bookGroupDao().all, "bookGroup.json", backupPath) - writeListToJson(App.db.bookSourceDao().all, "bookSource.json", backupPath) - writeListToJson(App.db.rssSourceDao().all, "rssSource.json", backupPath) - writeListToJson(App.db.rssStarDao().all, "rssStar.json", backupPath) - writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath) - writeListToJson(App.db.txtTocRule().all, "txtTocRule.json", backupPath) - writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath) - writeListToJson(App.db.httpTTSDao().all, "httpTTS.json", backupPath) - GSON.toJson(ReadBookConfig.configList).let { - FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) - .writeText(it) - } - GSON.toJson(ThemeConfig.configList).let { - FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) - .writeText(it) - } - Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> - val edit = sp.edit() - App.INSTANCE.defaultSharedPreferences.all.map { - when (val value = it.value) { - is Int -> edit.putInt(it.key, value) - is Boolean -> edit.putBoolean(it.key, value) - is Long -> edit.putLong(it.key, value) - is Float -> edit.putFloat(it.key, value) - is String -> edit.putString(it.key, value) - else -> Unit - } + FileUtils.deleteFile(backupPath) + writeListToJson(appDb.bookDao.all, "bookshelf.json", backupPath) + writeListToJson(appDb.bookmarkDao.all, "bookmark.json", backupPath) + writeListToJson(appDb.bookGroupDao.all, "bookGroup.json", backupPath) + writeListToJson(appDb.bookSourceDao.all, "bookSource.json", backupPath) + writeListToJson(appDb.rssSourceDao.all, "rssSources.json", backupPath) + writeListToJson(appDb.rssStarDao.all, "rssStar.json", backupPath) + writeListToJson(appDb.replaceRuleDao.all, "replaceRule.json", backupPath) + writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath) + writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath) + writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath) + writeListToJson(appDb.txtTocRuleDao.all, DefaultData.txtTocRuleFileName, backupPath) + writeListToJson(appDb.httpTTSDao.all, DefaultData.httpTtsFileName, backupPath) + GSON.toJson(ReadBookConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) + .writeText(it) + } + GSON.toJson(ReadBookConfig.shareConfig).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) + .writeText(it) + } + GSON.toJson(ThemeConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) + .writeText(it) + } + Preferences.getSharedPreferences(appCtx, backupPath, "config")?.let { sp -> + val edit = sp.edit() + appCtx.defaultSharedPreferences.all.map { + when (val value = it.value) { + is Int -> edit.putInt(it.key, value) + is Boolean -> edit.putBoolean(it.key, value) + is Long -> edit.putLong(it.key, value) + is Float -> edit.putFloat(it.key, value) + is String -> edit.putString(it.key, value) + else -> Unit } - edit.commit() } - WebDavHelp.backUpWebDav(backupPath) - if (path.isContentPath()) { - copyBackup(context, Uri.parse(path), isAuto) + edit.commit() + } + BookWebDav.backUpWebDav(backupPath) + if (path.isContentScheme()) { + copyBackup(context, Uri.parse(path), isAuto) + } else { + if (path.isEmpty()) { + copyBackup(context.getExternalFilesDir(null)!!, false) } else { - if (path.isEmpty()) { - copyBackup(context.getExternalFilesDir(null)!!, false) - } else { - copyBackup(File(path), isAuto) - } + copyBackup(File(path), isAuto) } } } diff --git a/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt b/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt new file mode 100644 index 000000000..a194cf4e7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/BookWebDav.kt @@ -0,0 +1,176 @@ +package io.legado.app.help.storage + +import android.content.Context +import android.os.Handler +import android.os.Looper +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookProgress +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.webdav.HttpAuth +import io.legado.app.lib.webdav.WebDav +import io.legado.app.utils.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +object BookWebDav { + private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" + private val bookProgressUrl = "${rootWebDavUrl}bookProgress/" + private val zipFilePath = "${appCtx.externalFiles.absolutePath}${File.separator}backup.zip" + + private val rootWebDavUrl: String + get() { + var url = appCtx.getPrefString(PreferKey.webDavUrl) + if (url.isNullOrEmpty()) { + url = defaultWebDavUrl + } + if (!url.endsWith("/")) url = "${url}/" + if (appCtx.getPrefBoolean(PreferKey.webDavCreateDir, true)) { + url = "${url}legado/" + } + return url + } + + private suspend fun initWebDav(): Boolean { + val account = appCtx.getPrefString(PreferKey.webDavAccount) + val password = appCtx.getPrefString(PreferKey.webDavPassword) + if (!account.isNullOrBlank() && !password.isNullOrBlank()) { + HttpAuth.auth = HttpAuth.Auth(account, password) + WebDav(rootWebDavUrl).makeAsDir() + WebDav(bookProgressUrl).makeAsDir() + return true + } + return false + } + + @Throws(Exception::class) + private suspend fun getWebDavFileNames(): ArrayList { + val url = rootWebDavUrl + val names = arrayListOf() + if (initWebDav()) { + var files = WebDav(url).listFiles() + files = files.reversed() + files.forEach { + val name = it.displayName + if (name?.startsWith("backup") == true) { + names.add(name) + } + } + } + return names + } + + suspend fun showRestoreDialog(context: Context) { + val names = withContext(IO) { getWebDavFileNames() } + if (names.isNotEmpty()) { + withContext(Main) { + context.selector( + title = context.getString(R.string.select_restore_file), + items = names + ) { _, index -> + if (index in 0 until names.size) { + Coroutine.async { + restoreWebDav(names[index]) + }.onError { + appCtx.toastOnUi("WebDavError:${it.localizedMessage}") + } + } + } + } + } else { + throw Exception("Web dav no back up file") + } + } + + private suspend fun restoreWebDav(name: String) { + rootWebDavUrl.let { + val webDav = WebDav(it + name) + webDav.downloadTo(zipFilePath, true) + @Suppress("BlockingMethodInNonBlockingContext") + ZipUtils.unzipFile(zipFilePath, Backup.backupPath) + Restore.restoreDatabase() + Restore.restoreConfig() + } + } + + suspend fun backUpWebDav(path: String) { + try { + if (initWebDav() && NetworkUtils.isAvailable()) { + val paths = arrayListOf(*Backup.backupFileNames) + for (i in 0 until paths.size) { + paths[i] = path + File.separator + paths[i] + } + FileUtils.deleteFile(zipFilePath) + if (ZipUtils.zipFiles(paths, zipFilePath)) { + val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(Date(System.currentTimeMillis())) + val putUrl = "${rootWebDavUrl}backup${backupDate}.zip" + WebDav(putUrl).upload(zipFilePath) + } + } + } catch (e: Exception) { + appCtx.toastOnUi("WebDav\n${e.localizedMessage}") + } + } + + suspend fun exportWebDav(byteArray: ByteArray, fileName: String) { + try { + if (initWebDav() && NetworkUtils.isAvailable()) { + // 默认导出到legado文件夹下exports目录 + val exportsWebDavUrl = rootWebDavUrl + EncoderUtils.escape("exports") + "/" + // 在legado文件夹创建exports目录,如果不存在的话 + WebDav(exportsWebDavUrl).makeAsDir() + // 如果导出的本地文件存在,开始上传 + val putUrl = exportsWebDavUrl + fileName + WebDav(putUrl).upload(byteArray, "text/plain") + } + } catch (e: Exception) { + Handler(Looper.getMainLooper()).post { + appCtx.toastOnUi("WebDav导出\n${e.localizedMessage}") + } + } + } + + fun uploadBookProgress(book: Book) { + if (!NetworkUtils.isAvailable()) return + Coroutine.async { + val bookProgress = BookProgress( + name = book.name, + author = book.author, + durChapterIndex = book.durChapterIndex, + durChapterPos = book.durChapterPos, + durChapterTime = book.durChapterTime, + durChapterTitle = book.durChapterTitle + ) + val json = GSON.toJson(bookProgress) + val url = getProgressUrl(book) + if (initWebDav()) { + WebDav(url).upload(json.toByteArray(), "application/json") + } + } + } + + suspend fun getBookProgress(book: Book): BookProgress? { + if (initWebDav() && NetworkUtils.isAvailable()) { + val url = getProgressUrl(book) + WebDav(url).download()?.let { byteArray -> + val json = String(byteArray) + GSON.fromJsonObject(json)?.let { + return it + } + } + } + return null + } + + private fun getProgressUrl(book: Book): String { + return bookProgressUrl + book.name + "_" + book.author + ".json" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt index a1741c845..afaaa5e92 100644 --- a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt +++ b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt @@ -3,87 +3,91 @@ package io.legado.app.help.storage import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile -import io.legado.app.App +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.utils.DocumentUtils import io.legado.app.utils.FileUtils -import org.jetbrains.anko.toast +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.toastOnUi import java.io.File object ImportOldData { - fun import(context: Context, file: File) { - try {// 导入书架 - val shelfFile = - FileUtils.createFileIfNotExist(file, "myBookShelf.json") - val json = shelfFile.readText() - val importCount = importOldBookshelf(json) - context.toast("成功导入书籍${importCount}") - } catch (e: Exception) { - context.toast("导入书籍失败\n${e.localizedMessage}") - } - - try {// Book source - val sourceFile = - FileUtils.getFile(file, "myBookSource.json") - val json = sourceFile.readText() - val importCount = importOldSource(json) - context.toast("成功导入书源${importCount}") - } catch (e: Exception) { - context.toast("导入源失败\n${e.localizedMessage}") - } - - try {// Replace rules - val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json") - if (ruleFile.exists()) { - val json = ruleFile.readText() - val importCount = importOldReplaceRule(json) - context.toast("成功导入替换规则${importCount}") - } else { - context.toast("未找到替换规则") - } - } catch (e: Exception) { - context.toast("导入替换规则失败\n${e.localizedMessage}") - } - } - - fun importUri(uri: Uri) { - DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { - when (it.name) { - "myBookShelf.json" -> - try { - DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> - val importCount = importOldBookshelf(json) - App.INSTANCE.toast("成功导入书籍${importCount}") + fun importUri(context: Context, uri: Uri) { + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> + when (doc.name) { + "myBookShelf.json" -> + kotlin.runCatching { + DocumentUtils.readText(context, doc.uri)?.let { json -> + val importCount = importOldBookshelf(json) + context.toastOnUi("成功导入书籍${importCount}") + } + }.onFailure { + context.toastOnUi("导入书籍失败\n${it.localizedMessage}") } - } catch (e: java.lang.Exception) { - App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}") - } - "myBookSource.json" -> - try { - DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> - val importCount = importOldSource(json) - App.INSTANCE.toast("成功导入书源${importCount}") + "myBookSource.json" -> + kotlin.runCatching { + DocumentUtils.readText(context, doc.uri)?.let { json -> + val importCount = importOldSource(json) + context.toastOnUi("成功导入书源${importCount}") + } + }.onFailure { + context.toastOnUi("导入源失败\n${it.localizedMessage}") } - } catch (e: Exception) { - App.INSTANCE.toast("导入源失败\n${e.localizedMessage}") - } - "myBookReplaceRule.json" -> - try { - DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> - val importCount = importOldReplaceRule(json) - App.INSTANCE.toast("成功导入替换规则${importCount}") + "myBookReplaceRule.json" -> + kotlin.runCatching { + DocumentUtils.readText(context, doc.uri)?.let { json -> + val importCount = importOldReplaceRule(json) + context.toastOnUi("成功导入替换规则${importCount}") + } + }.onFailure { + context.toastOnUi("导入替换规则失败\n${it.localizedMessage}") } - } catch (e: Exception) { - App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}") + } + } + } else { + uri.path?.let { path -> + val file = File(path) + kotlin.runCatching {// 导入书架 + val shelfFile = + FileUtils.createFileIfNotExist(file, "myBookShelf.json") + val json = shelfFile.readText() + val importCount = importOldBookshelf(json) + context.toastOnUi("成功导入书籍${importCount}") + }.onFailure { + context.toastOnUi("导入书籍失败\n${it.localizedMessage}") + } + + kotlin.runCatching {// Book source + val sourceFile = + FileUtils.getFile(file, "myBookSource.json") + val json = sourceFile.readText() + val importCount = importOldSource(json) + context.toastOnUi("成功导入书源${importCount}") + }.onFailure { + context.toastOnUi("导入源失败\n${it.localizedMessage}") + } + + kotlin.runCatching {// Replace rules + val ruleFile = FileUtils.getFile(file, "myBookReplaceRule.json") + if (ruleFile.exists()) { + val json = ruleFile.readText() + val importCount = importOldReplaceRule(json) + context.toastOnUi("成功导入替换规则${importCount}") + } else { + context.toastOnUi("未找到替换规则") } + }.onFailure { + context.toastOnUi("导入替换规则失败\n${it.localizedMessage}") + } } } } private fun importOldBookshelf(json: String): Int { val books = OldBook.toNewBook(json) - App.db.bookDao().insert(*books.toTypedArray()) + appDb.bookDao.insert(*books.toTypedArray()) return books.size } @@ -96,13 +100,13 @@ object ImportOldData { bookSources.add(it) } } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + appDb.bookSourceDao.insert(*bookSources.toTypedArray()) return bookSources.size } - fun importOldReplaceRule(json: String): Int { + private fun importOldReplaceRule(json: String): Int { val rules = OldReplace.jsonToReplaceRules(json) - App.db.replaceRuleDao().insert(*rules.toTypedArray()) + appDb.replaceRuleDao.insert(*rules.toTypedArray()) return rules.size } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/OldBook.kt b/app/src/main/java/io/legado/app/help/storage/OldBook.kt index a782cf5f5..bc874d549 100644 --- a/app/src/main/java/io/legado/app/help/storage/OldBook.kt +++ b/app/src/main/java/io/legado/app/help/storage/OldBook.kt @@ -1,8 +1,8 @@ package io.legado.app.help.storage import android.util.Log -import io.legado.app.App import io.legado.app.constant.AppConst +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.utils.readBool import io.legado.app.utils.readInt @@ -14,7 +14,7 @@ object OldBook { fun toNewBook(json: String): List { val books = mutableListOf() val items: List> = Restore.jsonPath.parse(json).read("$") - val existingBooks = App.db.bookDao().allBookUrls.toSet() + val existingBooks = appDb.bookDao.allBookUrls.toSet() for (item in items) { val jsonItem = Restore.jsonPath.parse(item) val book = Book() @@ -44,8 +44,8 @@ object OldBook { book.latestChapterTitle = jsonItem.readString("$.lastChapterName") book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 book.order = jsonItem.readInt("$.serialNumber") ?: 0 - book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true book.variable = jsonItem.readString("$.variable") + book.setUseReplaceRule(jsonItem.readBool("$.useReplaceRule") == true) books.add(book) } return books diff --git a/app/src/main/java/io/legado/app/help/storage/OldRule.kt b/app/src/main/java/io/legado/app/help/storage/OldRule.kt index 83084d5f3..616d0d359 100644 --- a/app/src/main/java/io/legado/app/help/storage/OldRule.kt +++ b/app/src/main/java/io/legado/app/help/storage/OldRule.kt @@ -9,6 +9,7 @@ import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.utils.* import java.util.regex.Pattern +@Suppress("RegExpRedundantEscape") object OldRule { private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE) private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index 8f89a45fe..2d797472b 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -1,5 +1,7 @@ package io.legado.app.help.storage +import android.app.AlarmManager +import android.app.PendingIntent import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile @@ -7,28 +9,29 @@ import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.Option import com.jayway.jsonpath.ParseContext -import io.legado.app.App import io.legado.app.BuildConfig import io.legado.app.R -import io.legado.app.constant.EventBus +import io.legado.app.constant.AppConst.androidId import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.* -import io.legado.app.help.AppConfig +import io.legado.app.help.DefaultData import io.legado.app.help.LauncherIconHelp import io.legado.app.help.ReadBookConfig import io.legado.app.help.ThemeConfig -import io.legado.app.service.help.ReadBook -import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.toast +import splitties.init.appCtx +import splitties.systemservices.alarmManager import java.io.File +import kotlin.system.exitProcess + object Restore { - private val ignoreConfigPath = FileUtils.getPath(App.INSTANCE.filesDir, "restoreIgnore.json") + private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json") val ignoreConfig: HashMap by lazy { val file = FileUtils.createFileIfNotExist(ignoreConfigPath) val json = file.readText() @@ -46,25 +49,23 @@ object Restore { //忽略标题 val ignoreTitle = arrayOf( - App.INSTANCE.getString(R.string.read_config), - App.INSTANCE.getString(R.string.theme_mode), - App.INSTANCE.getString(R.string.bookshelf_layout), - App.INSTANCE.getString(R.string.show_rss), - App.INSTANCE.getString(R.string.thread_count) + appCtx.getString(R.string.read_config), + appCtx.getString(R.string.theme_mode), + appCtx.getString(R.string.bookshelf_layout), + appCtx.getString(R.string.show_rss), + appCtx.getString(R.string.thread_count) ) //默认忽略keys private val ignorePrefKeys = arrayOf( - PreferKey.versionCode, - PreferKey.defaultCover + PreferKey.defaultCover, + PreferKey.defaultCoverDark ) private val readPrefKeys = arrayOf( PreferKey.readStyleSelect, PreferKey.shareLayout, - PreferKey.pageAnim, PreferKey.hideStatusBar, PreferKey.hideNavigationBar, - PreferKey.bodyIndent, PreferKey.autoReadSpeed ) @@ -78,12 +79,12 @@ object Restore { suspend fun restore(context: Context, path: String) { withContext(IO) { - if (path.isContentPath()) { + if (path.isContentScheme()) { DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc -> for (fileName in Backup.backupFileNames) { if (doc.name == fileName) { DocumentUtils.readText(context, doc.uri)?.let { - FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName) + FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName") .writeText(it) } } @@ -96,7 +97,7 @@ object Restore { FileUtils.getFile(file, fileName).let { if (it.exists()) { it.copyTo( - FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName), + FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName"), true ) } @@ -114,46 +115,52 @@ object Restore { suspend fun restoreDatabase(path: String = Backup.backupPath) { withContext(IO) { fileToListT(path, "bookshelf.json")?.let { - App.db.bookDao().insert(*it.toTypedArray()) + appDb.bookDao.insert(*it.toTypedArray()) } fileToListT(path, "bookmark.json")?.let { - App.db.bookmarkDao().insert(*it.toTypedArray()) + appDb.bookmarkDao.insert(*it.toTypedArray()) } fileToListT(path, "bookGroup.json")?.let { - App.db.bookGroupDao().insert(*it.toTypedArray()) + appDb.bookGroupDao.insert(*it.toTypedArray()) } fileToListT(path, "bookSource.json")?.let { - App.db.bookSourceDao().insert(*it.toTypedArray()) + appDb.bookSourceDao.insert(*it.toTypedArray()) } - fileToListT(path, "rssSource.json")?.let { - App.db.rssSourceDao().insert(*it.toTypedArray()) + fileToListT(path, "rssSources.json")?.let { + appDb.rssSourceDao.insert(*it.toTypedArray()) } fileToListT(path, "rssStar.json")?.let { - App.db.rssStarDao().insert(*it.toTypedArray()) + appDb.rssStarDao.insert(*it.toTypedArray()) } fileToListT(path, "replaceRule.json")?.let { - App.db.replaceRuleDao().insert(*it.toTypedArray()) + appDb.replaceRuleDao.insert(*it.toTypedArray()) + } + fileToListT(path, "searchHistory.json")?.let { + appDb.searchKeywordDao.insert(*it.toTypedArray()) + } + fileToListT(path, "sourceSub.json")?.let { + appDb.ruleSubDao.insert(*it.toTypedArray()) + } + fileToListT(path, DefaultData.txtTocRuleFileName)?.let { + appDb.txtTocRuleDao.insert(*it.toTypedArray()) } - fileToListT(path, "txtTocRule.json")?.let { - App.db.txtTocRule().insert(*it.toTypedArray()) + fileToListT(path, DefaultData.httpTtsFileName)?.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) } fileToListT(path, "readRecord.json")?.let { it.forEach { readRecord -> //判断是不是本机记录 - if (readRecord.androidId != App.androidId) { - App.db.readRecordDao().insert(readRecord) + if (readRecord.deviceId != androidId) { + appDb.readRecordDao.insert(readRecord) } else { - val time = App.db.readRecordDao() - .getReadTime(readRecord.androidId, readRecord.bookName) + val time = appDb.readRecordDao + .getReadTime(readRecord.deviceId, readRecord.bookName) if (time == null || time < readRecord.readTime) { - App.db.readRecordDao().insert(readRecord) + appDb.readRecordDao.insert(readRecord) } } } } - fileToListT(path, "httpTTS.json")?.let { - App.db.httpTTSDao().insert(*it.toTypedArray()) - } } } @@ -161,7 +168,7 @@ object Restore { withContext(IO) { try { val file = - FileUtils.createFileIfNotExist(path + File.separator + ThemeConfig.configFileName) + FileUtils.createFileIfNotExist("$path${File.separator}${ThemeConfig.configFileName}") if (file.exists()) { FileUtils.deleteFile(ThemeConfig.configFilePath) file.copyTo(File(ThemeConfig.configFilePath)) @@ -173,18 +180,29 @@ object Restore { if (!ignoreReadConfig) { try { val file = - FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.configFileName) + FileUtils.createFileIfNotExist("$path${File.separator}${ReadBookConfig.configFileName}") if (file.exists()) { FileUtils.deleteFile(ReadBookConfig.configFilePath) file.copyTo(File(ReadBookConfig.configFilePath)) - ReadBookConfig.upConfig() + ReadBookConfig.initConfigs() + } + } catch (e: Exception) { + e.printStackTrace() + } + try { + val file = + FileUtils.createFileIfNotExist("$path${File.separator}${ReadBookConfig.shareConfigFileName}") + if (file.exists()) { + FileUtils.deleteFile(ReadBookConfig.shareConfigFilePath) + file.copyTo(File(ReadBookConfig.shareConfigFilePath)) + ReadBookConfig.initShareConfig() } } catch (e: Exception) { e.printStackTrace() } } - Preferences.getSharedPreferences(App.INSTANCE, path, "config")?.all?.let { map -> - val edit = App.INSTANCE.defaultSharedPreferences.edit() + Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map -> + val edit = appCtx.defaultSharedPreferences.edit() map.forEach { if (keyIsNotIgnore(it.key)) { when (val value = it.value) { @@ -193,35 +211,31 @@ object Restore { is Long -> edit.putLong(it.key, value) is Float -> edit.putFloat(it.key, value) is String -> edit.putString(it.key, value) - else -> Unit } } } edit.apply() - AppConfig.replaceEnableDefault = - App.INSTANCE.getPrefBoolean(PreferKey.replaceEnableDefault, true) } ReadBookConfig.apply { - styleSelect = App.INSTANCE.getPrefInt(PreferKey.readStyleSelect) - shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) - pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim) - hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) - hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) - bodyIndentCount = App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2) - autoReadSpeed = App.INSTANCE.getPrefInt(PreferKey.autoReadSpeed, 46) + styleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) + shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout) + hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar) + hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) + autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46) } - ChapterProvider.upStyle() - ReadBook.loadContent(resetPageOffset = false) } + appCtx.toastOnUi(R.string.restore_success) withContext(Main) { - App.INSTANCE.toast(R.string.restore_success) + delay(100) if (!BuildConfig.DEBUG) { - LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) + LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon)) + } + appCtx.packageManager.getLaunchIntentForPackage(appCtx.packageName)?.let { intent -> + val restartIntent = + PendingIntent.getActivity(appCtx, 0, intent, PendingIntent.FLAG_ONE_SHOT) + alarmManager[AlarmManager.RTC, System.currentTimeMillis() + 300] = restartIntent + exitProcess(0) } - LanguageUtils.setConfigurationOld(App.INSTANCE) - App.INSTANCE.applyDayNight() - postEvent(EventBus.SHOW_RSS, "") - postEvent(EventBus.RECREATE, "") } } diff --git a/app/src/main/java/io/legado/app/help/storage/SyncBookProgress.kt b/app/src/main/java/io/legado/app/help/storage/SyncBookProgress.kt deleted file mode 100644 index 4e09e7c63..000000000 --- a/app/src/main/java/io/legado/app/help/storage/SyncBookProgress.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.legado.app.help.storage - -import io.legado.app.App -import io.legado.app.data.entities.BookProgress -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.lib.webdav.WebDav -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonArray - -@Suppress("BlockingMethodInNonBlockingContext") -object SyncBookProgress { - const val fileName = "bookProgress.json" - private val file = FileUtils.createFileIfNotExist(App.INSTANCE.cacheDir, fileName) - private val webDavUrl = "${WebDavHelp.rootWebDavUrl}$fileName" - - fun uploadBookProgress() { - Coroutine.async { - val value = App.db.bookDao().allBookProgress - if (value.isNotEmpty()) { - val json = GSON.toJson(value) - file.writeText(json) - if (WebDavHelp.initWebDav()) { - WebDav(webDavUrl).upload(file.absolutePath) - } - } - } - } - - fun downloadBookProgress() { - Coroutine.async { - if (WebDavHelp.initWebDav()) { - WebDav(webDavUrl).downloadTo(file.absolutePath, true) - if (file.exists()) { - val json = file.readText() - GSON.fromJsonArray(json)?.forEach { - App.db.bookDao().upBookProgress( - bookUrl = it.bookUrl, - durChapterIndex = it.durChapterIndex, - durChapterPos = it.durChapterPos, - durChapterTime = it.durChapterTime, - durChapterTitle = it.durChapterTitle - ) - } - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt deleted file mode 100644 index e728c28de..000000000 --- a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt +++ /dev/null @@ -1,128 +0,0 @@ -package io.legado.app.help.storage - -import android.content.Context -import android.os.Handler -import android.os.Looper -import io.legado.app.App -import io.legado.app.R -import io.legado.app.constant.PreferKey -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.lib.dialogs.selector -import io.legado.app.lib.webdav.WebDav -import io.legado.app.lib.webdav.http.HttpAuth -import io.legado.app.utils.FileUtils -import io.legado.app.utils.ZipUtils -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getPrefString -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.withContext -import org.jetbrains.anko.toast -import java.io.File -import java.text.SimpleDateFormat -import java.util.* -import kotlin.math.min - -object WebDavHelp { - private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" - private val zipFilePath = "${FileUtils.getCachePath()}${File.separator}backup.zip" - - val rootWebDavUrl: String - get() { - var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl) - if (url.isNullOrEmpty()) { - url = defaultWebDavUrl - } - if (!url.endsWith("/")) url = "${url}/" - if (App.INSTANCE.getPrefBoolean(PreferKey.webDavCreateDir, true)) { - url = "${url}legado/" - } - return url - } - - fun initWebDav(): Boolean { - val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount) - val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword) - if (!account.isNullOrBlank() && !password.isNullOrBlank()) { - HttpAuth.auth = HttpAuth.Auth(account, password) - WebDav(rootWebDavUrl).makeAsDir() - return true - } - return false - } - - @Throws(Exception::class) - private fun getWebDavFileNames(): ArrayList { - val url = rootWebDavUrl - val names = arrayListOf() - if (initWebDav()) { - var files = WebDav(url).listFiles() - files = files.reversed() - for (index: Int in 0 until min(10, files.size)) { - files[index].displayName?.let { - names.add(it) - } - } - } - return names - } - - suspend fun showRestoreDialog(context: Context) { - val names = withContext(IO) { getWebDavFileNames() } - if (names.isNotEmpty()) { - withContext(Main) { - context.selector( - title = context.getString(R.string.select_restore_file), - items = names - ) { _, index -> - if (index in 0 until names.size) { - restoreWebDav(names[index]) - } - } - } - } else { - throw Exception("Web dav no back up file") - } - } - - private fun restoreWebDav(name: String) { - Coroutine.async { - rootWebDavUrl.let { - if (name == SyncBookProgress.fileName) { - SyncBookProgress.downloadBookProgress() - } else { - val webDav = WebDav(it + name) - webDav.downloadTo(zipFilePath, true) - @Suppress("BlockingMethodInNonBlockingContext") - ZipUtils.unzipFile(zipFilePath, Backup.backupPath) - Restore.restoreDatabase() - Restore.restoreConfig() - } - } - }.onError { - App.INSTANCE.toast("WebDavError:${it.localizedMessage}") - } - } - - fun backUpWebDav(path: String) { - try { - if (initWebDav()) { - val paths = arrayListOf(*Backup.backupFileNames) - for (i in 0 until paths.size) { - paths[i] = path + File.separator + paths[i] - } - FileUtils.deleteFile(zipFilePath) - if (ZipUtils.zipFiles(paths, zipFilePath)) { - val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - .format(Date(System.currentTimeMillis())) - val putUrl = "${rootWebDavUrl}backup${backupDate}.zip" - WebDav(putUrl).upload(zipFilePath) - } - } - } catch (e: Exception) { - Handler(Looper.getMainLooper()).post { - App.INSTANCE.toast("WebDav\n${e.localizedMessage}") - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/README.md b/app/src/main/java/io/legado/app/lib/README.md index 1089be064..de7b7bd78 100644 --- a/app/src/main/java/io/legado/app/lib/README.md +++ b/app/src/main/java/io/legado/app/lib/README.md @@ -1,4 +1,6 @@ -## 放置一些copy过来的库 +# 放置一些copy过来的库 * dialogs 弹出框 +* icu4j 编码识别库 +* permission 权限申请库 * theme 主题 * webDav 网络存储 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt index b1f0f494d..5dab676bb 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - @file:Suppress("NOTHING_TO_INLINE", "unused") package io.legado.app.lib.dialogs @@ -26,55 +10,59 @@ import android.view.KeyEvent import android.view.View import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER -import kotlin.DeprecationLevel.ERROR +import io.legado.app.R @SuppressLint("SupportAnnotationUsage") interface AlertBuilder { val ctx: Context - var title: CharSequence - @Deprecated(NO_GETTER, level = ERROR) get + fun setTitle(title: CharSequence) - var titleResource: Int - @Deprecated(NO_GETTER, level = ERROR) get + fun setTitle(titleResource: Int) - var message: CharSequence - @Deprecated(NO_GETTER, level = ERROR) get + fun setMessage(message: CharSequence) - var messageResource: Int - @Deprecated(NO_GETTER, level = ERROR) get + fun setMessage(messageResource: Int) - var icon: Drawable - @Deprecated(NO_GETTER, level = ERROR) get + fun setIcon(icon: Drawable) - @setparam:DrawableRes - var iconResource: Int - @Deprecated(NO_GETTER, level = ERROR) get + fun setIcon(@DrawableRes iconResource: Int) - var customTitle: View - @Deprecated(NO_GETTER, level = ERROR) get + fun setCustomTitle(customTitle: View) - var customView: View - @Deprecated(NO_GETTER, level = ERROR) get + fun setCustomView(customView: View) - var isCancelable: Boolean - @Deprecated(NO_GETTER, level = ERROR) get + fun setCancelable(isCancelable: Boolean) fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) - fun positiveButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun positiveButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) - fun negativeButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun negativeButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) - fun neutralButton(@StringRes buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun neutralButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) fun onCancelled(handler: (dialog: DialogInterface) -> Unit) fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) - fun items(items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit) + fun onDismiss(handler: (dialog: DialogInterface) -> Unit) + + fun items( + items: List, + onItemSelected: (dialog: DialogInterface, index: Int) -> Unit + ) + fun items( items: List, onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit @@ -94,24 +82,25 @@ interface AlertBuilder { fun build(): D fun show(): D -} -fun AlertBuilder<*>.customTitle(view: () -> View) { - customTitle = view() -} -fun AlertBuilder<*>.customView(view: () -> View) { - customView = view() -} + fun customTitle(view: () -> View) { + setCustomTitle(view()) + } + + fun customView(view: () -> View) { + setCustomView(view()) + } -inline fun AlertBuilder<*>.okButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = - positiveButton(android.R.string.ok, handler) + fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(android.R.string.ok, handler) -inline fun AlertBuilder<*>.cancelButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = - negativeButton(android.R.string.cancel, handler) + fun cancelButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(android.R.string.cancel, handler) -inline fun AlertBuilder<*>.yesButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = - positiveButton(android.R.string.yes, handler) + fun yesButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(R.string.yes, handler) -inline fun AlertBuilder<*>.noButton(noinline handler: ((dialog: DialogInterface) -> Unit)? = null) = - negativeButton(android.R.string.no, handler) \ No newline at end of file + fun noButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(R.string.no, handler) +} diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt b/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt index 9bfcab298..0c1f7950f 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package io.legado.app.lib.dialogs import android.content.Context @@ -22,50 +6,46 @@ import android.graphics.drawable.Drawable import android.view.KeyEvent import android.view.View import androidx.appcompat.app.AlertDialog -import org.jetbrains.anko.internals.AnkoInternals -import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER -import kotlin.DeprecationLevel.ERROR - -val Android: AlertBuilderFactory = ::AndroidAlertBuilder +import io.legado.app.utils.applyTint internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder { private val builder = AlertDialog.Builder(ctx) - override var title: CharSequence - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setTitle(value) } + override fun setTitle(title: CharSequence) { + builder.setTitle(title) + } - override var titleResource: Int - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setTitle(value) } + override fun setTitle(titleResource: Int) { + builder.setTitle(titleResource) + } - override var message: CharSequence - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setMessage(value) } + override fun setMessage(message: CharSequence) { + builder.setMessage(message) + } - override var messageResource: Int - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setMessage(value) } + override fun setMessage(messageResource: Int) { + builder.setMessage(messageResource) + } - override var icon: Drawable - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setIcon(value) } + override fun setIcon(icon: Drawable) { + builder.setIcon(icon) + } - override var iconResource: Int - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setIcon(value) } + override fun setIcon(iconResource: Int) { + builder.setIcon(iconResource) + } - override var customTitle: View - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setCustomTitle(value) } + override fun setCustomTitle(customTitle: View) { + builder.setCustomTitle(customTitle) + } - override var customView: View - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setView(value) } + override fun setCustomView(customView: View) { + builder.setView(customView) + } - override var isCancelable: Boolean - @Deprecated(NO_GETTER, level = ERROR) get() = AnkoInternals.noGetter() - set(value) { builder.setCancelable(value) } + override fun setCancelable(isCancelable: Boolean) { + builder.setCancelable(isCancelable) + } override fun onCancelled(handler: (DialogInterface) -> Unit) { builder.setOnCancelListener(handler) @@ -75,31 +55,56 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder Unit)?) { + override fun positiveButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun positiveButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + override fun positiveButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setPositiveButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { + override fun negativeButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setNegativeButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun negativeButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + override fun negativeButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setNegativeButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)?) { + override fun neutralButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setNeutralButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun neutralButton(buttonTextResource: Int, onClicked: ((dialog: DialogInterface) -> Unit)?) { + override fun neutralButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { builder.setNeutralButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } } - override fun items(items: List, onItemSelected: (dialog: DialogInterface, index: Int) -> Unit) { + override fun onDismiss(handler: (dialog: DialogInterface) -> Unit) { + builder.setOnDismissListener(handler) + } + + override fun items( + items: List, + onItemSelected: (dialog: DialogInterface, index: Int) -> Unit + ) { builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> onItemSelected(dialog, which) } @@ -136,5 +141,5 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder { return AndroidAlertBuilder(this).apply { if (title != null) { - this.title = title + this.setTitle(title) } if (message != null) { - this.message = message + this.setMessage(message) } if (init != null) init() } } inline fun Fragment.alert( - title: Int? = null, + titleResource: Int? = null, message: Int? = null, noinline init: (AlertBuilder.() -> Unit)? = null -) = requireActivity().alert(title, message, init) +) = requireActivity().alert(titleResource, message, init) fun Context.alert( titleResource: Int? = null, @@ -60,18 +43,18 @@ fun Context.alert( ): AlertBuilder { return AndroidAlertBuilder(this).apply { if (titleResource != null) { - this.titleResource = titleResource + this.setTitle(titleResource) } if (messageResource != null) { - this.messageResource = messageResource + this.setMessage(messageResource) } if (init != null) init() } } -inline fun AnkoContext<*>.alert(noinline init: AlertBuilder.() -> Unit) = ctx.alert(init) -inline fun Fragment.alert(noinline init: AlertBuilder.() -> Unit) = requireContext().alert(init) +inline fun Fragment.alert(noinline init: AlertBuilder.() -> Unit) = + requireContext().alert(init) fun Context.alert(init: AlertBuilder.() -> Unit): AlertBuilder = AndroidAlertBuilder(this).apply { init() } diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt b/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt index 1eb5dbff9..6033879de 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt @@ -21,7 +21,6 @@ package io.legado.app.lib.dialogs import android.content.Context import android.content.DialogInterface import androidx.fragment.app.Fragment -import io.legado.app.utils.applyTint inline fun Fragment.selector( title: CharSequence? = null, @@ -36,9 +35,23 @@ fun Context.selector( ) { with(AndroidAlertBuilder(this)) { if (title != null) { - this.title = title + this.setTitle(title) } items(items, onClick) - show().applyTint() + show() + } +} + +fun Context.selector( + titleSource: Int? = null, + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + if (titleSource != null) { + this.setTitle(titleSource) + } + items(items, onClick) + show() } } diff --git a/app/src/main/java/io/legado/app/lib/dialogs/Dialogs.kt b/app/src/main/java/io/legado/app/lib/dialogs/Dialogs.kt index 6a1e28428..0abcc36c0 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/Dialogs.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/Dialogs.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - @file:Suppress("NOTHING_TO_INLINE", "unused") package io.legado.app.lib.dialogs @@ -39,10 +23,10 @@ fun Context.alert( ): AlertBuilder { return factory(this).apply { if (title != null) { - this.title = title + this.setTitle(title) } if (message != null) { - this.message = message + this.setMessage(message) } if (init != null) init() } @@ -63,10 +47,10 @@ fun Context.alert( ): AlertBuilder { return factory(this).apply { if (titleResource != null) { - this.titleResource = titleResource + this.setTitle(titleResource) } if (messageResource != null) { - this.messageResource = messageResource + this.setMessage(messageResource) } if (init != null) init() } diff --git a/app/src/main/java/io/legado/app/lib/dialogs/Selectors.kt b/app/src/main/java/io/legado/app/lib/dialogs/Selectors.kt index b8fe1e1b4..c74535955 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/Selectors.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/Selectors.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - @file:Suppress("NOTHING_TO_INLINE", "unused") package io.legado.app.lib.dialogs @@ -37,7 +21,7 @@ fun Context.selector( ) { with(factory(this)) { if (title != null) { - this.title = title + this.setTitle(title) } items(items, onClick) show() diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java new file mode 100644 index 000000000..8d35b65a1 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java @@ -0,0 +1,567 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005-2016, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + + +/** + * CharsetDetector provides a facility for detecting the + * charset or encoding of character data in an unknown format. + * The input data can either be from an input stream or an array of bytes. + * The result of the detection operation is a list of possibly matching + * charsets, or, for simple use, you can just ask for a Java Reader that + * will will work over the input data. + *

    + * Character set detection is at best an imprecise operation. The detection + * process will attempt to identify the charset that best matches the characteristics + * of the byte data, but the process is partly statistical in nature, and + * the results can not be guaranteed to always be correct. + *

    + * For best accuracy in charset detection, the input data should be primarily + * in a single language, and a minimum of a few hundred bytes worth of plain text + * in the language are needed. The detection process will attempt to + * ignore html or xml style markup that could otherwise obscure the content. + *

    + * + * @stable ICU 3.4 + */ +@SuppressWarnings({"JavaDoc", "unused", "RedundantSuppression"}) +public class CharsetDetector { + +// Question: Should we have getters corresponding to the setters for input text +// and declared encoding? + +// A thought: If we were to create our own type of Java Reader, we could defer +// figuring out an actual charset for data that starts out with too much English +// only ASCII until the user actually read through to something that didn't look +// like 7 bit English. If nothing else ever appeared, we would never need to +// actually choose the "real" charset. All assuming that the application just +// wants the data, and doesn't care about a char set name. + + /** + * Constructor + * + * @stable ICU 3.4 + */ + public CharsetDetector() { + } + + /** + * Set the declared encoding for charset detection. + * The declared encoding of an input text is an encoding obtained + * from an http header or xml declaration or similar source that + * can be provided as additional information to the charset detector. + * A match between a declared encoding and a possible detected encoding + * will raise the quality of that detected encoding by a small delta, + * and will also appear as a "reason" for the match. + *

    + * A declared encoding that is incompatible with the input data being + * analyzed will not be added to the list of possible encodings. + * + * @param encoding The declared encoding + * @stable ICU 3.4 + */ + public CharsetDetector setDeclaredEncoding(String encoding) { + fDeclaredEncoding = encoding; + return this; + } + + /** + * Set the input text (byte) data whose charset is to be detected. + * + * @param in the input text of unknown encoding + * @return This CharsetDetector + * @stable ICU 3.4 + */ + public CharsetDetector setText(byte[] in) { + fRawInput = in; + fRawLength = in.length; + + return this; + } + + private static final int kBufSize = 8000; + + /** + * Set the input text (byte) data whose charset is to be detected. + *

    + * The input stream that supplies the character data must have markSupported() + * == true; the charset detection process will read a small amount of data, + * then return the stream to its original position via + * the InputStream.reset() operation. The exact amount that will + * be read depends on the characteristics of the data itself. + * + * @param in the input text of unknown encoding + * @return This CharsetDetector + * @stable ICU 3.4 + */ + + public CharsetDetector setText(InputStream in) throws IOException { + fInputStream = in; + fInputStream.mark(kBufSize); + fRawInput = new byte[kBufSize]; // Always make a new buffer because the + // previous one may have come from the caller, + // in which case we can't touch it. + fRawLength = 0; + int remainingLength = kBufSize; + while (remainingLength > 0) { + // read() may give data in smallish chunks, esp. for remote sources. Hence, this loop. + int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength); + if (bytesRead <= 0) { + break; + } + fRawLength += bytesRead; + remainingLength -= bytesRead; + } + fInputStream.reset(); + + return this; + } + + + /** + * Return the charset that best matches the supplied input data. + *

    + * Note though, that because the detection + * only looks at the start of the input data, + * there is a possibility that the returned charset will fail to handle + * the full set of input data. + * p> + * aise an exception if + *

      + *
    • no charset appears to match the data.
    • + *
    • no input text has been provided
    • + *
    + * + * @return a CharsetMatch object representing the best matching charset, or + * null if there are no matches. + * @stable ICU 3.4 + */ + public CharsetMatch detect() { +// TODO: A better implementation would be to copy the detect loop from +// detectAll(), and cut it short as soon as a match with a high confidence +// is found. This is something to be done later, after things are otherwise +// working. + CharsetMatch[] matches = detectAll(); + + if (matches == null || matches.length == 0) { + return null; + } + + return matches[0]; + } + + /** + * Return an array of all charsets that appear to be plausible + * matches with the input data. The array is ordered with the + * best quality match first. + *

    + * aise an exception if + *

      + *
    • no charsets appear to match the input data.
    • + *
    • no input text has been provided
    • + *
    + * + * @return An array of CharsetMatch objects representing possibly matching charsets. + * @stable ICU 3.4 + */ + public CharsetMatch[] detectAll() { + ArrayList matches = new ArrayList<>(); + + MungeInput(); // Strip html markup, collect byte stats. + + // Iterate over all possible charsets, remember all that + // give a match quality > 0. + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); + boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcinfo.isDefaultEnabled; + if (active) { + CharsetMatch m = rcinfo.recognizer.match(this); + if (m != null) { + matches.add(m); + } + } + } + Collections.sort(matches); // CharsetMatch compares on confidence + Collections.reverse(matches); // Put best match first. + CharsetMatch[] resultArray = new CharsetMatch[matches.size()]; + resultArray = matches.toArray(resultArray); + return resultArray; + } + + + /** + * Autodetect the charset of an inputStream, and return a Java Reader + * to access the converted input data. + *

    + * This is a convenience method that is equivalent to + * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader(); + *

    + * For the input stream that supplies the character data, markSupported() + * must be true; the charset detection will read a small amount of data, + * then return the stream to its original position via + * the InputStream.reset() operation. The exact amount that will + * be read depends on the characteristics of the data itself. + *

    + * Raise an exception if no charsets appear to match the input data. + * + * @param in The source of the byte data in the unknown charset. + * @param declaredEncoding A declared encoding for the data, if available, + * or null or an empty string if none is available. + * @stable ICU 3.4 + */ + public Reader getReader(InputStream in, String declaredEncoding) { + fDeclaredEncoding = declaredEncoding; + + try { + setText(in); + + CharsetMatch match = detect(); + + if (match == null) { + return null; + } + + return match.getReader(); + } catch (IOException e) { + return null; + } + } + + /** + * Autodetect the charset of an inputStream, and return a String + * containing the converted input data. + *

    + * This is a convenience method that is equivalent to + * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString(); + *

    + * Raise an exception if no charsets appear to match the input data. + * + * @param in The source of the byte data in the unknown charset. + * @param declaredEncoding A declared encoding for the data, if available, + * or null or an empty string if none is available. + * @stable ICU 3.4 + */ + public String getString(byte[] in, String declaredEncoding) { + fDeclaredEncoding = declaredEncoding; + + try { + setText(in); + + CharsetMatch match = detect(); + + if (match == null) { + return null; + } + + return match.getString(-1); + } catch (IOException e) { + return null; + } + } + + + /** + * Get the names of all charsets supported by CharsetDetector class. + *

    + * Note: Multiple different charset encodings in a same family may use + * a single shared name in this implementation. For example, this method returns + * an array including "ISO-8859-1" (ISO Latin 1), but not including "windows-1252" + * (Windows Latin 1). However, actual detection result could be "windows-1252" + * when the input data matches Latin 1 code points with any points only available + * in "windows-1252". + * + * @return an array of the names of all charsets supported by + * CharsetDetector class. + * @stable ICU 3.4 + */ + public static String[] getAllDetectableCharsets() { + String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()]; + for (int i = 0; i < allCharsetNames.length; i++) { + allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName(); + } + return allCharsetNames; + } + + /** + * Test whether or not input filtering is enabled. + * + * @return true if input text will be filtered. + * @stable ICU 3.4 + * @see #enableInputFilter + */ + public boolean inputFilterEnabled() { + return fStripTags; + } + + /** + * Enable filtering of input text. If filtering is enabled, + * text within angle brackets ("<" and ">") will be removed + * before detection. + * + * @param filter true to enable input text filtering. + * @return The previous setting. + * @stable ICU 3.4 + */ + public boolean enableInputFilter(boolean filter) { + boolean previous = fStripTags; + + fStripTags = filter; + + return previous; + } + + /* + * MungeInput - after getting a set of raw input data to be analyzed, preprocess + * it by removing what appears to be html markup. + */ + private void MungeInput() { + int srci = 0; + int dsti = 0; + byte b; + boolean inMarkup = false; + int openTags = 0; + int badTags = 0; + + // + // html / xml markup stripping. + // quick and dirty, not 100% accurate, but hopefully good enough, statistically. + // discard everything within < brackets > + // Count how many total '<' and illegal (nested) '<' occur, so we can make some + // guess as to whether the input was actually marked up at all. + if (fStripTags) { + for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) { + b = fRawInput[srci]; + if (b == (byte) '<') { + if (inMarkup) { + badTags++; + } + inMarkup = true; + openTags++; + } + + if (!inMarkup) { + fInputBytes[dsti++] = b; + } + + if (b == (byte) '>') { + inMarkup = false; + } + } + + fInputLen = dsti; + } + + // + // If it looks like this input wasn't marked up, or if it looks like it's + // essentially nothing but markup abandon the markup stripping. + // Detection will have to work on the unstripped input. + // + if (openTags < 5 || openTags / 5 < badTags || + (fInputLen < 100 && fRawLength > 600)) { + int limit = fRawLength; + + if (limit > kBufSize) { + limit = kBufSize; + } + + for (srci = 0; srci < limit; srci++) { + fInputBytes[srci] = fRawInput[srci]; + } + fInputLen = srci; + } + + // + // Tally up the byte occurence statistics. + // These are available for use by the various detectors. + // + Arrays.fill(fByteStats, (short) 0); + for (srci = 0; srci < fInputLen; srci++) { + int val = fInputBytes[srci] & 0x00ff; + fByteStats[val]++; + } + + fC1Bytes = false; + for (int i = 0x80; i <= 0x9F; i += 1) { + if (fByteStats[i] != 0) { + fC1Bytes = true; + break; + } + } + } + + /* + * The following items are accessed by individual CharsetRecongizers during + * the recognition process + * + */ + byte[] fInputBytes = // The text to be checked. Markup will have been + new byte[kBufSize]; // removed if appropriate. + + int fInputLen; // Length of the byte data in fInputBytes. + + short[] fByteStats = // byte frequency statistics for the input text. + new short[256]; // Value is percent, not absolute. + // Value is rounded up, so zero really means zero occurences. + + boolean fC1Bytes = // True if any bytes in the range 0x80 - 0x9F are in the input; + false; + + String fDeclaredEncoding; + + + byte[] fRawInput; // Original, untouched input bytes. + // If user gave us a byte array, this is it. + // If user gave us a stream, it's read to a + // buffer here. + int fRawLength; // Length of data in fRawInput array. + + InputStream fInputStream; // User's input stream, or null if the user + // gave us a byte array. + + // + // Stuff private to CharsetDetector + // + private boolean fStripTags = // If true, setText() will strip tags from input text. + false; + + private boolean[] fEnabledRecognizers; // If not null, active set of charset recognizers had + // been changed from the default. The array index is + // corresponding to ALL_RECOGNIZER. See setDetectableCharset(). + + private static class CSRecognizerInfo { + CharsetRecognizer recognizer; + boolean isDefaultEnabled; + + CSRecognizerInfo(CharsetRecognizer recognizer, boolean isDefaultEnabled) { + this.recognizer = recognizer; + this.isDefaultEnabled = isDefaultEnabled; + } + } + + /* + * List of recognizers for all charsets known to the implementation. + */ + private static final List ALL_CS_RECOGNIZERS; + + static { + List list = new ArrayList<>(); + + list.add(new CSRecognizerInfo(new CharsetRecog_UTF8(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true)); + + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true)); + + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true)); + + // IBM 420/424 recognizers are disabled by default + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false)); + + ALL_CS_RECOGNIZERS = Collections.unmodifiableList(list); + } + + /** + * Get the names of charsets that can be recognized by this CharsetDetector instance. + * + * @return an array of the names of charsets that can be recognized by this CharsetDetector + * instance. + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public String[] getDetectableCharsets() { + List csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size()); + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); + boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i]; + if (active) { + csnames.add(rcinfo.recognizer.getName()); + } + } + return csnames.toArray(new String[0]); + } + + /** + * Enable or disable individual charset encoding. + * A name of charset encoding must be included in the names returned by + * {@link #getAllDetectableCharsets()}. + * + * @param encoding the name of charset encoding. + * @param enabled true to enable, or false to disable the + * charset encoding. + * @return A reference to this CharsetDetector. + * @throws IllegalArgumentException when the name of charset encoding is + * not supported. + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public CharsetDetector setDetectableCharset(String encoding, boolean enabled) { + int modIdx = -1; + boolean isDefaultVal = false; + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i); + if (csrinfo.recognizer.getName().equals(encoding)) { + modIdx = i; + isDefaultVal = (csrinfo.isDefaultEnabled == enabled); + break; + } + } + if (modIdx < 0) { + // No matching encoding found + throw new IllegalArgumentException("Invalid encoding: " + "\"" + encoding + "\""); + } + + if (fEnabledRecognizers == null && !isDefaultVal) { + // Create an array storing the non default setting + fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()]; + + // Initialize the array with default info + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled; + } + } + + if (fEnabledRecognizers != null) { + fEnabledRecognizers[modIdx] = enabled; + } + + return this; + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java new file mode 100644 index 000000000..14aef7ad0 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java @@ -0,0 +1,241 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005-2016, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + + +/** + * This class represents a charset that has been identified by a CharsetDetector + * as a possible encoding for a set of input data. From an instance of this + * class, you can ask for a confidence level in the charset identification, + * or for Java Reader or String to access the original byte data in Unicode form. + *

    + * Instances of this class are created only by CharsetDetectors. + *

    + * Note: this class has a natural ordering that is inconsistent with equals. + * The natural ordering is based on the match confidence value. + * + * @stable ICU 3.4 + */ +@SuppressWarnings({"JavaDoc", "unused"}) +public class CharsetMatch implements Comparable { + + + /** + * Create a java.io.Reader for reading the Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + *

    + * CAUTION: if the source of the byte data was an InputStream, a Reader + * can be created for only one matching char set using this method. If more + * than one charset needs to be tried, the caller will need to reset + * the InputStream and create InputStreamReaders itself, based on the charset name. + * + * @return the Reader for the Unicode character data. + * @stable ICU 3.4 + */ + public Reader getReader() { + InputStream inputStream = fInputStream; + + if (inputStream == null) { + inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength); + } + + try { + inputStream.reset(); + return new InputStreamReader(inputStream, getName()); + } catch (IOException e) { + return null; + } + } + + /** + * Create a Java String from Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + * + * @return a String created from the converted input data. + * @stable ICU 3.4 + */ + public String getString() throws java.io.IOException { + return getString(-1); + + } + + /** + * Create a Java String from Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + * The length of the returned string is limited to the specified size; + * the string will be trunctated to this length if necessary. A limit value of + * zero or less is ignored, and treated as no limit. + * + * @param maxLength The maximium length of the String to be created when the + * source of the data is an input stream, or -1 for + * unlimited length. + * @return a String created from the converted input data. + * @stable ICU 3.4 + */ + public String getString(int maxLength) throws java.io.IOException { + String result = null; + if (fInputStream != null) { + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[1024]; + Reader reader = getReader(); + int max = maxLength < 0 ? Integer.MAX_VALUE : maxLength; + int bytesRead = 0; + + while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) { + sb.append(buffer, 0, bytesRead); + max -= bytesRead; + } + + reader.close(); + + return sb.toString(); + } else { + String name = getName(); + /* + * getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot + * be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr' + * should be stripped off before creating the string. + */ + int startSuffix = !name.contains("_rtl") ? name.indexOf("_ltr") : name.indexOf("_rtl"); + if (startSuffix > 0) { + name = name.substring(0, startSuffix); + } + result = new String(fRawInput, name); + } + return result; + + } + + /** + * Get an indication of the confidence in the charset detected. + * Confidence values range from 0-100, with larger numbers indicating + * a better match of the input data to the characteristics of the + * charset. + * + * @return the confidence in the charset match + * @stable ICU 3.4 + */ + public int getConfidence() { + return fConfidence; + } + + /** + * Get the name of the detected charset. + * The name will be one that can be used with other APIs on the + * platform that accept charset names. It is the "Canonical name" + * as defined by the class java.nio.charset.Charset; for + * charsets that are registered with the IANA charset registry, + * this is the MIME-preferred registerd name. + * + * @return The name of the charset. + * @stable ICU 3.4 + * @see java.nio.charset.Charset + * @see java.io.InputStreamReader + */ + public String getName() { + return fCharsetName; + } + + /** + * Get the ISO code for the language of the detected charset. + * + * @return The ISO code for the language or null if the language cannot be determined. + * @stable ICU 3.4 + */ + public String getLanguage() { + return fLang; + } + + /** + * Compare to other CharsetMatch objects. + * Comparison is based on the match confidence value, which + * allows CharsetDetector.detectAll() to order its results. + * + * @param other the CharsetMatch object to compare against. + * @return a negative integer, zero, or a positive integer as the + * confidence level of this CharsetMatch + * is less than, equal to, or greater than that of + * the argument. + * @throws ClassCastException if the argument is not a CharsetMatch. + * @stable ICU 4.4 + */ + @Override + public int compareTo(CharsetMatch other) { + int compareResult = 0; + if (this.fConfidence > other.fConfidence) { + compareResult = 1; + } else if (this.fConfidence < other.fConfidence) { + compareResult = -1; + } + return compareResult; + } + + /* + * Constructor. Implementation internal + */ + CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) { + fConfidence = conf; + + // The references to the original application input data must be copied out + // of the charset recognizer to here, in case the application resets the + // recognizer before using this CharsetMatch. + if (det.fInputStream == null) { + // We only want the existing input byte data if it came straight from the user, + // not if is just the head of a stream. + fRawInput = det.fRawInput; + fRawLength = det.fRawLength; + } + fInputStream = det.fInputStream; + fCharsetName = rec.getName(); + fLang = rec.getLanguage(); + } + + /* + * Constructor. Implementation internal + */ + CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf, String csName, String lang) { + fConfidence = conf; + + // The references to the original application input data must be copied out + // of the charset recognizer to here, in case the application resets the + // recognizer before using this CharsetMatch. + if (det.fInputStream == null) { + // We only want the existing input byte data if it came straight from the user, + // not if is just the head of a stream. + fRawInput = det.fRawInput; + fRawLength = det.fRawLength; + } + fInputStream = det.fInputStream; + fCharsetName = csName; + fLang = lang; + } + + + // + // Private Data + // + private final int fConfidence; + private byte[] fRawInput = null; // Original, untouched input bytes. + // If user gave us a byte array, this is it. + private int fRawLength; // Length of data in fRawInput array. + + private InputStream fInputStream = null; // User's input stream, or null if the user + // gave us a byte array. + + private final String fCharsetName; // The name of the charset this CharsetMatch + // represents. Filled in by the recognizer. + private final String fLang; // The language, if one was determined by + // the recognizer during the detect operation. +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java new file mode 100644 index 000000000..0b4d9fa65 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java @@ -0,0 +1,170 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + ******************************************************************************* + * Copyright (C) 2005 - 2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + */ +package io.legado.app.lib.icu4j; + +/** + * class CharsetRecog_2022 part of the ICU charset detection imlementation. + * This is a superclass for the individual detectors for + * each of the detectable members of the ISO 2022 family + * of encodings. + *

    + * The separate classes are nested within this class. + */ +abstract class CharsetRecog_2022 extends CharsetRecognizer { + + + /** + * Matching function shared among the 2022 detectors JP, CN and KR + * Counts up the number of legal an unrecognized escape sequences in + * the sample of text, and computes a score based on the total number & + * the proportion that fit the encoding. + * + * @param text the byte buffer containing text to analyse + * @param textLen the size of the text in the byte. + * @param escapeSequences the byte escape sequences to test for. + * @return match quality, in the range of 0-100. + */ + int match(byte[] text, int textLen, byte[][] escapeSequences) { + int i, j; + int escN; + int hits = 0; + int misses = 0; + int shifts = 0; + int quality; + scanInput: + for (i = 0; i < textLen; i++) { + if (text[i] == 0x1b) { + checkEscapes: + for (escN = 0; escN < escapeSequences.length; escN++) { + byte[] seq = escapeSequences[escN]; + + if ((textLen - i) < seq.length) { + continue checkEscapes; + } + + for (j = 1; j < seq.length; j++) { + if (seq[j] != text[i + j]) { + continue checkEscapes; + } + } + + hits++; + i += seq.length - 1; + continue scanInput; + } + + misses++; + } + + if (text[i] == 0x0e || text[i] == 0x0f) { + // Shift in/out + shifts++; + } + } + + if (hits == 0) { + return 0; + } + + // + // Initial quality is based on relative proportion of recongized vs. + // unrecognized escape sequences. + // All good: quality = 100; + // half or less good: quality = 0; + // linear inbetween. + quality = (100 * hits - 100 * misses) / (hits + misses); + + // Back off quality if there were too few escape sequences seen. + // Include shifts in this computation, so that KR does not get penalized + // for having only a single Escape sequence, but many shifts. + if (hits + shifts < 5) { + quality -= (5 - (hits + shifts)) * 10; + } + + if (quality < 0) { + quality = 0; + } + return quality; + } + + + static class CharsetRecog_2022JP extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x28, 0x43}, // KS X 1001:1992 + {0x1b, 0x24, 0x28, 0x44}, // JIS X 212-1990 + {0x1b, 0x24, 0x40}, // JIS C 6226-1978 + {0x1b, 0x24, 0x41}, // GB 2312-80 + {0x1b, 0x24, 0x42}, // JIS X 208-1983 + {0x1b, 0x26, 0x40}, // JIS X 208 1990, 1997 + {0x1b, 0x28, 0x42}, // ASCII + {0x1b, 0x28, 0x48}, // JIS-Roman + {0x1b, 0x28, 0x49}, // Half-width katakana + {0x1b, 0x28, 0x4a}, // JIS-Roman + {0x1b, 0x2e, 0x41}, // ISO 8859-1 + {0x1b, 0x2e, 0x46} // ISO 8859-7 + }; + + @Override + String getName() { + return "ISO-2022-JP"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_2022KR extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x29, 0x43} + }; + + @Override + String getName() { + return "ISO-2022-KR"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_2022CN extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x29, 0x41}, // GB 2312-80 + {0x1b, 0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 + {0x1b, 0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 + {0x1b, 0x24, 0x29, 0x45}, // ISO-IR-165 + {0x1b, 0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 + {0x1b, 0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 + {0x1b, 0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 + {0x1b, 0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 + {0x1b, 0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 + {0x1b, 0x4e}, // SS2 + {0x1b, 0x4f}, // SS3 + }; + + @Override + String getName() { + return "ISO-2022-CN"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + +} + diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java new file mode 100644 index 000000000..60ff6d746 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java @@ -0,0 +1,99 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005 - 2014, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +/** + * Charset recognizer for UTF-8 + */ +class CharsetRecog_UTF8 extends CharsetRecognizer { + + @Override + String getName() { + return "UTF-8"; + } + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) + */ + @Override + CharsetMatch match(CharsetDetector det) { + boolean hasBOM = false; + int numValid = 0; + int numInvalid = 0; + byte[] input = det.fRawInput; + int i; + int trailBytes = 0; + int confidence; + + if (det.fRawLength >= 3 && + (input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) { + hasBOM = true; + } + + // Scan for multi-byte sequences + for (i = 0; i < det.fRawLength; i++) { + int b = input[i]; + if ((b & 0x80) == 0) { + continue; // ASCII + } + + // Hi bit on char found. Figure out how long the sequence should be + if ((b & 0x0e0) == 0x0c0) { + trailBytes = 1; + } else if ((b & 0x0f0) == 0x0e0) { + trailBytes = 2; + } else if ((b & 0x0f8) == 0xf0) { + trailBytes = 3; + } else { + numInvalid++; + continue; + } + + // Verify that we've got the right number of trail bytes in the sequence + for (; ; ) { + i++; + if (i >= det.fRawLength) { + break; + } + b = input[i]; + if ((b & 0xc0) != 0x080) { + numInvalid++; + break; + } + if (--trailBytes == 0) { + numValid++; + break; + } + } + } + + // Cook up some sort of confidence score, based on presense of a BOM + // and the existence of valid and/or invalid multi-byte sequences. + confidence = 0; + if (hasBOM && numInvalid == 0) { + confidence = 100; + } else if (hasBOM && numValid > numInvalid * 10) { + confidence = 80; + } else if (numValid > 3 && numInvalid == 0) { + confidence = 100; + } else if (numValid > 0 && numInvalid == 0) { + confidence = 80; + } else if (numValid == 0 && numInvalid == 0) { + // Plain ASCII. Confidence must be > 10, it's more likely than UTF-16, which + // accepts ASCII with confidence = 10. + // TODO: add plain ASCII as an explicitly detected type. + confidence = 15; + } else if (numValid > numInvalid * 10) { + // Probably corruput utf-8 data. Valid sequences aren't likely by chance. + confidence = 25; + } + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java new file mode 100644 index 000000000..82380e654 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java @@ -0,0 +1,198 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + ******************************************************************************* + * Copyright (C) 1996-2013, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + * + */ + +package io.legado.app.lib.icu4j; + +/** + * This class matches UTF-16 and UTF-32, both big- and little-endian. The + * BOM will be used if it is present. + */ +abstract class CharsetRecog_Unicode extends CharsetRecognizer { + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#getName() + */ + @Override + abstract String getName(); + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) + */ + @Override + abstract CharsetMatch match(CharsetDetector det); + + static int codeUnit16FromBytes(byte hi, byte lo) { + return ((hi & 0xff) << 8) | (lo & 0xff); + } + + // UTF-16 confidence calculation. Very simple minded, but better than nothing. + // Any 8 bit non-control characters bump the confidence up. These have a zero high byte, + // and are very likely to be UTF-16, although they could also be part of a UTF-32 code. + // NULs are a contra-indication, they will appear commonly if the actual encoding is UTF-32. + // NULs should be rare in actual text. + static int adjustConfidence(int codeUnit, int confidence) { + if (codeUnit == 0) { + confidence -= 10; + } else if ((codeUnit >= 0x20 && codeUnit <= 0xff) || codeUnit == 0x0a) { + confidence += 10; + } + if (confidence < 0) { + confidence = 0; + } else if (confidence > 100) { + confidence = 100; + } + return confidence; + } + + static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode { + @Override + String getName() { + return "UTF-16BE"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int confidence = 10; + + int bytesToCheck = Math.min(input.length, 30); + for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { + int codeUnit = codeUnit16FromBytes(input[charIndex], input[charIndex + 1]); + if (charIndex == 0 && codeUnit == 0xFEFF) { + confidence = 100; + break; + } + confidence = adjustConfidence(codeUnit, confidence); + if (confidence == 0 || confidence == 100) { + break; + } + } + if (bytesToCheck < 4 && confidence < 100) { + confidence = 0; + } + if (confidence > 0) { + return new CharsetMatch(det, this, confidence); + } + return null; + } + } + + static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode { + @Override + String getName() { + return "UTF-16LE"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int confidence = 10; + + int bytesToCheck = Math.min(input.length, 30); + for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { + int codeUnit = codeUnit16FromBytes(input[charIndex + 1], input[charIndex]); + if (charIndex == 0 && codeUnit == 0xFEFF) { + confidence = 100; + break; + } + confidence = adjustConfidence(codeUnit, confidence); + if (confidence == 0 || confidence == 100) { + break; + } + } + if (bytesToCheck < 4 && confidence < 100) { + confidence = 0; + } + if (confidence > 0) { + return new CharsetMatch(det, this, confidence); + } + return null; + } + } + + static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode { + abstract int getChar(byte[] input, int index); + + @Override + abstract String getName(); + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int limit = (det.fRawLength / 4) * 4; + int numValid = 0; + int numInvalid = 0; + boolean hasBOM = false; + int confidence = 0; + + if (limit == 0) { + return null; + } + if (getChar(input, 0) == 0x0000FEFF) { + hasBOM = true; + } + + for (int i = 0; i < limit; i += 4) { + int ch = getChar(input, i); + + if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) { + numInvalid += 1; + } else { + numValid += 1; + } + } + + + // Cook up some sort of confidence score, based on presence of a BOM + // and the existence of valid and/or invalid multi-byte sequences. + if (hasBOM && numInvalid == 0) { + confidence = 100; + } else if (hasBOM && numValid > numInvalid * 10) { + confidence = 80; + } else if (numValid > 3 && numInvalid == 0) { + confidence = 100; + } else if (numValid > 0 && numInvalid == 0) { + confidence = 80; + } else if (numValid > numInvalid * 10) { + // Probably corrupt UTF-32BE data. Valid sequences aren't likely by chance. + confidence = 25; + } + + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32 { + @Override + int getChar(byte[] input, int index) { + return (input[index + 0] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 | + (input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF); + } + + @Override + String getName() { + return "UTF-32BE"; + } + } + + + static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32 { + @Override + int getChar(byte[] input, int index) { + return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 | + (input[index + 1] & 0xFF) << 8 | (input[index + 0] & 0xFF); + } + + @Override + String getName() { + return "UTF-32LE"; + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java new file mode 100644 index 000000000..e498e4e54 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java @@ -0,0 +1,554 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + **************************************************************************** + * Copyright (C) 2005-2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + **************************************************************************** + * + */ +package io.legado.app.lib.icu4j; + +import java.util.Arrays; + +/** + * CharsetRecognizer implemenation for Asian - double or multi-byte - charsets. + * Match is determined mostly by the input data adhering to the + * encoding scheme for the charset, and, optionally, + * frequency-of-occurence of characters. + *

    + * Instances of this class are singletons, one per encoding + * being recognized. They are created in the main + * CharsetDetector class and kept in the global list of available + * encodings to be checked. The specific encoding being recognized + * is determined by subclass. + */ +abstract class CharsetRecog_mbcs extends CharsetRecognizer { + + /** + * Get the IANA name of this charset. + * + * @return the charset name. + */ + @Override + abstract String getName(); + + + /** + * Test the match of this charset with the input text data + * which is obtained via the CharsetDetector object. + * + * @param det The CharsetDetector, which contains the input text + * to be checked for being in this charset. + * @return Two values packed into one int (Damn java, anyhow) + *
    + * bits 0-7: the match confidence, ranging from 0-100 + *
    + * bits 8-15: The match reason, an enum-like value. + */ + int match(CharsetDetector det, int[] commonChars) { + @SuppressWarnings("unused") + int singleByteCharCount = 0; //TODO Do we really need this? + int doubleByteCharCount = 0; + int commonCharCount = 0; + int badCharCount = 0; + int totalCharCount = 0; + int confidence = 0; + iteratedChar iter = new iteratedChar(); + + detectBlock: + { + for (iter.reset(); nextChar(iter, det); ) { + totalCharCount++; + if (iter.error) { + badCharCount++; + } else { + long cv = iter.charValue & 0xFFFFFFFFL; + + if (cv <= 0xff) { + singleByteCharCount++; + } else { + doubleByteCharCount++; + if (commonChars != null) { + // NOTE: This assumes that there are no 4-byte common chars. + if (Arrays.binarySearch(commonChars, (int) cv) >= 0) { + commonCharCount++; + } + } + } + } + if (badCharCount >= 2 && badCharCount * 5 >= doubleByteCharCount) { + // Bail out early if the byte data is not matching the encoding scheme. + break detectBlock; + } + } + + if (doubleByteCharCount <= 10 && badCharCount == 0) { + // Not many multi-byte chars. + if (doubleByteCharCount == 0 && totalCharCount < 10) { + // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes. + // We don't have enough data to have any confidence. + // Statistical analysis of single byte non-ASCII charcters would probably help here. + confidence = 0; + } else { + // ASCII or ISO file? It's probably not our encoding, + // but is not incompatible with our encoding, so don't give it a zero. + confidence = 10; + } + + break detectBlock; + } + + // + // No match if there are too many characters that don't fit the encoding scheme. + // (should we have zero tolerance for these?) + // + if (doubleByteCharCount < 20 * badCharCount) { + confidence = 0; + break detectBlock; + } + + if (commonChars == null) { + // We have no statistics on frequently occuring characters. + // Assess confidence purely on having a reasonable number of + // multi-byte characters (the more the better + confidence = 30 + doubleByteCharCount - 20 * badCharCount; + if (confidence > 100) { + confidence = 100; + } + } else { + // + // Frequency of occurence statistics exist. + // + double maxVal = Math.log((float) doubleByteCharCount / 4); + double scaleFactor = 90.0 / maxVal; + confidence = (int) (Math.log(commonCharCount + 1) * scaleFactor + 10); + confidence = Math.min(confidence, 100); + } + } // end of detectBlock: + + return confidence; + } + + // "Character" iterated character class. + // Recognizers for specific mbcs encodings make their "characters" available + // by providing a nextChar() function that fills in an instance of iteratedChar + // with the next char from the input. + // The returned characters are not converted to Unicode, but remain as the raw + // bytes (concatenated into an int) from the codepage data. + // + // For Asian charsets, use the raw input rather than the input that has been + // stripped of markup. Detection only considers multi-byte chars, effectively + // stripping markup anyway, and double byte chars do occur in markup too. + // + static class iteratedChar { + int charValue = 0; // 1-4 bytes from the raw input data + int nextIndex = 0; + boolean error = false; + boolean done = false; + + void reset() { + charValue = 0; + nextIndex = 0; + error = false; + done = false; + } + + int nextByte(CharsetDetector det) { + if (nextIndex >= det.fRawLength) { + done = true; + return -1; + } + int byteValue = det.fRawInput[nextIndex++] & 0x00ff; + return byteValue; + } + } + + /** + * Get the next character (however many bytes it is) from the input data + * Subclasses for specific charset encodings must implement this function + * to get characters according to the rules of their encoding scheme. + *

    + * This function is not a method of class iteratedChar only because + * that would require a lot of extra derived classes, which is awkward. + * + * @param it The iteratedChar "struct" into which the returned char is placed. + * @param det The charset detector, which is needed to get at the input byte data + * being iterated over. + * @return True if a character was returned, false at end of input. + */ + abstract boolean nextChar(iteratedChar it, CharsetDetector det); + + + /** + * Shift-JIS charset recognizer. + */ + static class CharsetRecog_sjis extends CharsetRecog_mbcs { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, + 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, + 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, + 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, + 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, + 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa}; + + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + return false; + } + + if (firstByte <= 0x7f || (firstByte > 0xa0 && firstByte <= 0xdf)) { + return true; + } + + int secondByte = it.nextByte(det); + if (secondByte < 0) { + return false; + } + it.charValue = (firstByte << 8) | secondByte; + if (!((secondByte >= 0x40 && secondByte <= 0x7f) || (secondByte >= 0x80 && secondByte <= 0xff))) { + // Illegal second byte value. + it.error = true; + } + return true; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + String getName() { + return "Shift_JIS"; + } + + @Override + public String getLanguage() { + return "ja"; + } + + + } + + + /** + * Big5 charset recognizer. + */ + static class CharsetRecog_big5 extends CharsetRecog_mbcs { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, + 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, + 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, + 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, + 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, + 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, + 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, + 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, + 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, + 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f}; + + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + return false; + } + + if (firstByte <= 0x7f || firstByte == 0xff) { + // single byte character. + return true; + } + + int secondByte = it.nextByte(det); + if (secondByte < 0) { + return false; + } + it.charValue = (it.charValue << 8) | secondByte; + + if (secondByte < 0x40 || + secondByte == 0x7f || + secondByte == 0xff) { + it.error = true; + } + return true; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + String getName() { + return "Big5"; + } + + + @Override + public String getLanguage() { + return "zh"; + } + } + + + /** + * EUC charset recognizers. One abstract class that provides the common function + * for getting the next character according to the EUC encoding scheme, + * and nested derived classes for EUC_KR, EUC_JP, EUC_CN. + */ + abstract static class CharsetRecog_euc extends CharsetRecog_mbcs { + + /* + * (non-Javadoc) + * Get the next character value for EUC based encodings. + * Character "value" is simply the raw bytes that make up the character + * packed into an int. + */ + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte = 0; + int secondByte = 0; + int thirdByte = 0; + //int fourthByte = 0; + + buildChar: + { + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + // Ran off the end of the input data + it.done = true; + break buildChar; + } + if (firstByte <= 0x8d) { + // single byte char + break buildChar; + } + + secondByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | secondByte; + + if (firstByte >= 0xA1 && firstByte <= 0xfe) { + // Two byte Char + if (secondByte < 0xa1) { + it.error = true; + } + break buildChar; + } + if (firstByte == 0x8e) { + // Code Set 2. + // In EUC-JP, total char size is 2 bytes, only one byte of actual char value. + // In EUC-TW, total char size is 4 bytes, three bytes contribute to char value. + // We don't know which we've got. + // Treat it like EUC-JP. If the data really was EUC-TW, the following two + // bytes will look like a well formed 2 byte char. + if (secondByte < 0xa1) { + it.error = true; + } + break buildChar; + } + + if (firstByte == 0x8f) { + // Code set 3. + // Three byte total char size, two bytes of actual char value. + thirdByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | thirdByte; + if (thirdByte < 0xa1) { + it.error = true; + } + } + } + + return (it.done == false); + } + + /** + * The charset recognize for EUC-JP. A singleton instance of this class + * is created and kept by the public CharsetDetector class + */ + static class CharsetRecog_euc_jp extends CharsetRecog_euc { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, + 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, + 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, + 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, + 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, + 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, + 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, + 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, + 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, + 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1}; + + @Override + String getName() { + return "EUC-JP"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "ja"; + } + } + + /** + * The charset recognize for EUC-KR. A singleton instance of this class + * is created and kept by the public CharsetDetector class + */ + static class CharsetRecog_euc_kr extends CharsetRecog_euc { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, + 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, + 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, + 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, + 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, + 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, + 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, + 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, + 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, + 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad}; + + @Override + String getName() { + return "EUC-KR"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "ko"; + } + } + } + + /** + * GB-18030 recognizer. Uses simplified Chinese statistics. + */ + static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs { + + /* + * (non-Javadoc) + * Get the next character value for EUC based encodings. + * Character "value" is simply the raw bytes that make up the character + * packed into an int. + */ + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte = 0; + int secondByte = 0; + int thirdByte = 0; + int fourthByte = 0; + + buildChar: + { + firstByte = it.charValue = it.nextByte(det); + + if (firstByte < 0) { + // Ran off the end of the input data + it.done = true; + break buildChar; + } + + if (firstByte <= 0x80) { + // single byte char + break buildChar; + } + + secondByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | secondByte; + + if (firstByte >= 0x81 && firstByte <= 0xFE) { + // Two byte Char + if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >= 80 && secondByte <= 0xFE)) { + break buildChar; + } + + // Four byte char + if (secondByte >= 0x30 && secondByte <= 0x39) { + thirdByte = it.nextByte(det); + + if (thirdByte >= 0x81 && thirdByte <= 0xFE) { + fourthByte = it.nextByte(det); + + if (fourthByte >= 0x30 && fourthByte <= 0x39) { + it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte; + break buildChar; + } + } + } + + it.error = true; + break buildChar; + } + } + + return (it.done == false); + } + + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, + 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, + 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, + 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, + 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, + 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, + 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, + 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, + 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, + 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0}; + + + @Override + String getName() { + return "GB18030"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "zh"; + } + } + + +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java new file mode 100644 index 000000000..dea9bac81 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java @@ -0,0 +1,1187 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + **************************************************************************** + * Copyright (C) 2005-2013, International Business Machines Corporation and * + * others. All Rights Reserved. * + ************************************************************************** * + * + */ + +package io.legado.app.lib.icu4j; + +/** + * This class recognizes single-byte encodings. Because the encoding scheme is so + * simple, language statistics are used to do the matching. + */ +abstract class CharsetRecog_sbcs extends CharsetRecognizer { + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#getName() + */ + @Override + abstract String getName(); + + static class NGramParser { + // private static final int N_GRAM_SIZE = 3; + private static final int N_GRAM_MASK = 0xFFFFFF; + + protected int byteIndex = 0; + private int ngram = 0; + + private final int[] ngramList; + protected byte[] byteMap; + + private int ngramCount; + private int hitCount; + + protected byte spaceChar; + + public NGramParser(int[] theNgramList, byte[] theByteMap) { + ngramList = theNgramList; + byteMap = theByteMap; + + ngram = 0; + + ngramCount = hitCount = 0; + } + + /* + * Binary search for value in table, which must have exactly 64 entries. + */ + private static int search(int[] table, int value) { + int index = 0; + + if (table[index + 32] <= value) { + index += 32; + } + + if (table[index + 16] <= value) { + index += 16; + } + + if (table[index + 8] <= value) { + index += 8; + } + + if (table[index + 4] <= value) { + index += 4; + } + + if (table[index + 2] <= value) { + index += 2; + } + + if (table[index + 1] <= value) { + index += 1; + } + + if (table[index] > value) { + index -= 1; + } + + if (index < 0 || table[index] != value) { + return -1; + } + + return index; + } + + private void lookup(int thisNgram) { + ngramCount += 1; + + if (search(ngramList, thisNgram) >= 0) { + hitCount += 1; + } + + } + + protected void addByte(int b) { + ngram = ((ngram << 8) + (b & 0xFF)) & N_GRAM_MASK; + lookup(ngram); + } + + private int nextByte(CharsetDetector det) { + if (byteIndex >= det.fInputLen) { + return -1; + } + + return det.fInputBytes[byteIndex++] & 0xFF; + } + + protected void parseCharacters(CharsetDetector det) { + int b; + boolean ignoreSpace = false; + + while ((b = nextByte(det)) >= 0) { + byte mb = byteMap[b]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + } + + } + + public int parse(CharsetDetector det) { + return parse(det, (byte) 0x20); + } + + public int parse(CharsetDetector det, byte spaceCh) { + + this.spaceChar = spaceCh; + + parseCharacters(det); + + // TODO: Is this OK? The buffer could have ended in the middle of a word... + addByte(spaceChar); + + double rawPercent = (double) hitCount / (double) ngramCount; + +// if (rawPercent <= 2.0) { +// return 0; +// } + + // TODO - This is a bit of a hack to take care of a case + // were we were getting a confidence of 135... + if (rawPercent > 0.33) { + return 98; + } + + return (int) (rawPercent * 300.0); + } + } + + static class NGramParser_IBM420 extends NGramParser { + private byte alef = 0x00; + + protected static byte[] unshapeMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x42, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x47, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F, +/* 5- */ (byte) 0x50, (byte) 0x49, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x56, (byte) 0x58, (byte) 0x58, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F, +/* 6- */ (byte) 0x60, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x63, (byte) 0x65, (byte) 0x65, (byte) 0x67, (byte) 0x67, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, +/* 7- */ (byte) 0x69, (byte) 0x71, (byte) 0x71, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x77, (byte) 0x79, (byte) 0x7A, (byte) 0x7B, (byte) 0x7C, (byte) 0x7D, (byte) 0x7E, (byte) 0x7F, +/* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x80, (byte) 0x8B, (byte) 0x8B, (byte) 0x8D, (byte) 0x8D, (byte) 0x8F, +/* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9E, (byte) 0x9E, +/* A- */ (byte) 0x9E, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x9E, (byte) 0xAB, (byte) 0xAB, (byte) 0xAD, (byte) 0xAD, (byte) 0xAF, +/* B- */ (byte) 0xAF, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xB1, (byte) 0xBB, (byte) 0xBB, (byte) 0xBD, (byte) 0xBD, (byte) 0xBF, +/* C- */ (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xBF, (byte) 0xCC, (byte) 0xBF, (byte) 0xCE, (byte) 0xCF, +/* D- */ (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDA, (byte) 0xDC, (byte) 0xDC, (byte) 0xDC, (byte) 0xDF, +/* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, +/* F- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + + public NGramParser_IBM420(int[] theNgramList, byte[] theByteMap) { + super(theNgramList, theByteMap); + } + + private byte isLamAlef(byte b) { + if (b == (byte) 0xb2 || b == (byte) 0xb3) { + return (byte) 0x47; + } else if (b == (byte) 0xb4 || b == (byte) 0xb5) { + return (byte) 0x49; + } else if (b == (byte) 0xb8 || b == (byte) 0xb9) { + return (byte) 0x56; + } else + return (byte) 0x00; + } + + /* + * Arabic shaping needs to be done manually. Cannot call ArabicShaping class + * because CharsetDetector is dealing with bytes not Unicode code points. We could + * convert the bytes to Unicode code points but that would leave us dependent + * on CharsetICU which we try to avoid. IBM420 converter amongst different versions + * of JDK can produce different results and therefore is also avoided. + */ + private int nextByte(CharsetDetector det) { + if (byteIndex >= det.fInputLen || det.fInputBytes[byteIndex] == 0) { + return -1; + } + int next; + + alef = isLamAlef(det.fInputBytes[byteIndex]); + if (alef != (byte) 0x00) + next = 0xB1 & 0xFF; + else + next = unshapeMap[det.fInputBytes[byteIndex] & 0xFF] & 0xFF; + + byteIndex++; + + return next; + } + + @Override + protected void parseCharacters(CharsetDetector det) { + int b; + boolean ignoreSpace = false; + + while ((b = nextByte(det)) >= 0) { + byte mb = byteMap[b]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + if (alef != (byte) 0x00) { + mb = byteMap[alef & 0xFF]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + + } + } + } + } + + + int match(CharsetDetector det, int[] ngrams, byte[] byteMap) { + return match(det, ngrams, byteMap, (byte) 0x20); + } + + int match(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { + NGramParser parser = new NGramParser(ngrams, byteMap); + return parser.parse(det, spaceChar); + } + + int matchIBM420(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { + NGramParser_IBM420 parser = new NGramParser_IBM420(ngrams, byteMap); + return parser.parse(det, spaceChar); + } + + static class NGramsPlusLang { + int[] fNGrams; + String fLang; + + NGramsPlusLang(String la, int[] ng) { + fLang = la; + fNGrams = ng; + } + } + + static class CharsetRecog_8859_1 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + + private static final NGramsPlusLang[] ngrams_8859_1 = new NGramsPlusLang[]{ + new NGramsPlusLang( + "da", + new int[]{ + 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, + 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, + 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, + 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, + }), + new NGramsPlusLang( + "de", + new int[]{ + 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, + 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, + 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, + 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, + }), + new NGramsPlusLang( + "en", + new int[]{ + 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, + 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, + 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, + 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, + }), + + new NGramsPlusLang( + "es", + new int[]{ + 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, + 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, + 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, + }), + + new NGramsPlusLang( + "fr", + new int[]{ + 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, + 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, + 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, + 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, + }), + + new NGramsPlusLang( + "it", + new int[]{ + 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, + 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, + 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, + 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, + }), + + new NGramsPlusLang( + "nl", + new int[]{ + 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, + 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, + 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, + 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, + }), + + new NGramsPlusLang( + "no", + new int[]{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, + 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, + 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, + }), + + new NGramsPlusLang( + "pt", + new int[]{ + 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, + 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, + 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, + + }), + + new NGramsPlusLang( + "sv", + new int[]{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, + 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, + 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, + }), + + }; + + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1252" : "ISO-8859-1"; + int bestConfidenceSoFar = -1; + String lang = null; + for (NGramsPlusLang ngl : ngrams_8859_1) { + int confidence = match(det, ngl.fNGrams, byteMap); + if (confidence > bestConfidenceSoFar) { + bestConfidenceSoFar = confidence; + lang = ngl.fLang; + } + } + return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); + } + + + @Override + public String getName() { + return "ISO-8859-1"; + } + } + + + static class CharsetRecog_8859_2 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0x20, + (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, + (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, + (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, + }; + + private static final NGramsPlusLang[] ngrams_8859_2 = new NGramsPlusLang[]{ + new NGramsPlusLang( + "cs", + new int[]{ + 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, + 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, + 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, + 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, + }), + new NGramsPlusLang( + "hu", + new int[]{ + 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, + 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, + 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, + 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, + }), + new NGramsPlusLang( + "pl", + new int[]{ + 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, + 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, + 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, + 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, + }), + new NGramsPlusLang( + "ro", + new int[]{ + 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, + 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, + 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, + 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, + }) + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1250" : "ISO-8859-2"; + int bestConfidenceSoFar = -1; + String lang = null; + for (NGramsPlusLang ngl : ngrams_8859_2) { + int confidence = match(det, ngl.fNGrams, byteMap); + if (confidence > bestConfidenceSoFar) { + bestConfidenceSoFar = confidence; + lang = ngl.fLang; + } + } + return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); + } + + @Override + public String getName() { + return "ISO-8859-2"; + } + + } + + + abstract static class CharsetRecog_8859_5 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "ISO-8859-5"; + } + } + + static class CharsetRecog_8859_5_ru extends CharsetRecog_8859_5 { + private static final int[] ngrams = { + 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, + 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, + 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, + 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, + }; + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_8859_6 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-6"; + } + } + + static class CharsetRecog_8859_6_ar extends CharsetRecog_8859_6 { + private static final int[] ngrams = { + 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, + 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, + 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, + 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, + }; + + @Override + public String getLanguage() { + return "ar"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_8859_7 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xA1, (byte) 0xA2, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xDC, (byte) 0x20, + (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0x20, (byte) 0xFC, (byte) 0x20, (byte) 0xFD, (byte) 0xFE, + (byte) 0xC0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0x20, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-7"; + } + } + + static class CharsetRecog_8859_7_el extends CharsetRecog_8859_7 { + private static final int[] ngrams = { + 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, + 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, + 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, + 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, + }; + + @Override + public String getLanguage() { + return "el"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1253" : "ISO-8859-7"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "el"); + } + } + + abstract static class CharsetRecog_8859_8 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-8"; + } + } + + static class CharsetRecog_8859_8_I_he extends CharsetRecog_8859_8 { + private static final int[] ngrams = { + 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, + 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, + 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, + 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, + }; + + @Override + public String getName() { + return "ISO-8859-8-I"; + } + + @Override + public String getLanguage() { + return "he"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8-I"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); + } + } + + static class CharsetRecog_8859_8_he extends CharsetRecog_8859_8 { + private static final int[] ngrams = { + 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, + 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, + 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, + 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, + }; + + @Override + public String getLanguage() { + return "he"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); + + } + } + + abstract static class CharsetRecog_8859_9 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x69, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "ISO-8859-9"; + } + } + + static class CharsetRecog_8859_9_tr extends CharsetRecog_8859_9 { + private static final int[] ngrams = { + 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, + 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, + 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, + 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, + }; + + @Override + public String getLanguage() { + return "tr"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1254" : "ISO-8859-9"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "tr"); + } + } + + static class CharsetRecog_windows_1251 extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, + 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, + 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, + 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x90, (byte) 0x83, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, + (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, + (byte) 0x20, (byte) 0xA2, (byte) 0xA2, (byte) 0xBC, (byte) 0x20, (byte) 0xB4, (byte) 0x20, (byte) 0x20, + (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBF, + (byte) 0x20, (byte) 0x20, (byte) 0xB3, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0xBC, (byte) 0xBE, (byte) 0xBE, (byte) 0xBF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "windows-1251"; + } + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_windows_1256 extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, + 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, + 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, + 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x81, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x88, (byte) 0x20, (byte) 0x8A, (byte) 0x20, (byte) 0x9C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, + (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x98, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x20, (byte) 0x20, (byte) 0x9F, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0x20, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF4, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xF9, (byte) 0x20, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0x20, (byte) 0xFF, + }; + + @Override + public String getName() { + return "windows-1256"; + } + + @Override + public String getLanguage() { + return "ar"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_KOI8_R extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, + 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, + 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, + 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + }; + + @Override + public String getName() { + return "KOI8-R"; + } + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_IBM424_he extends CharsetRecog_sbcs { + protected static byte[] byteMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 7- */ (byte) 0x40, (byte) 0x71, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x00, (byte) 0x40, (byte) 0x40, +/* 8- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 9- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* B- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, + }; + + @Override + public String getLanguage() { + return "he"; + } + } + + static class CharsetRecog_IBM424_he_rtl extends CharsetRecog_IBM424_he { + @Override + public String getName() { + return "IBM424_rtl"; + } + + private static final int[] ngrams = { + 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, + 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, + 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, + 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_IBM424_he_ltr extends CharsetRecog_IBM424_he { + @Override + public String getName() { + return "IBM424_ltr"; + } + + private static final int[] ngrams = { + 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, + 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, + 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, + 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651 + + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_IBM420_ar extends CharsetRecog_sbcs { + + protected static byte[] byteMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x40, (byte) 0x40, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 7- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, +/* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9B, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, +/* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, +/* B- */ (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x40, (byte) 0x40, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0xBD, (byte) 0xBE, (byte) 0xBF, +/* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0xCB, (byte) 0x40, (byte) 0xCD, (byte) 0x40, (byte) 0xCF, +/* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, +/* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xEA, (byte) 0xEB, (byte) 0x40, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, +/* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x40, + }; + + + @Override + public String getLanguage() { + return "ar"; + } + + } + + static class CharsetRecog_IBM420_ar_rtl extends CharsetRecog_IBM420_ar { + private static final int[] ngrams = { + 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, + 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, + 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, + 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, + }; + + @Override + public String getName() { + return "IBM420_rtl"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + } + + static class CharsetRecog_IBM420_ar_ltr extends CharsetRecog_IBM420_ar { + private static final int[] ngrams = { + 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, + 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, + 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, + 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156 + }; + + @Override + public String getName() { + return "IBM420_ltr"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java new file mode 100644 index 000000000..1adabd330 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java @@ -0,0 +1,53 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005-2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +/** + * Abstract class for recognizing a single charset. + * Part of the implementation of ICU's CharsetDetector. + *

    + * Each specific charset that can be recognized will have an instance + * of some subclass of this class. All interaction between the overall + * CharsetDetector and the stuff specific to an individual charset happens + * via the interface provided here. + *

    + * Instances of CharsetDetector DO NOT have or maintain + * state pertaining to a specific match or detect operation. + * The WILL be shared by multiple instances of CharsetDetector. + * They encapsulate const charset-specific information. + */ +abstract class CharsetRecognizer { + /** + * Get the IANA name of this charset. + * + * @return the charset name. + */ + abstract String getName(); + + /** + * Get the ISO language code for this charset. + * + * @return the language code, or null if the language cannot be determined. + */ + public String getLanguage() { + return null; + } + + /** + * Test the match of this charset with the input text data + * which is obtained via the CharsetDetector object. + * + * @param det The CharsetDetector, which contains the input text + * to be checked for being in this charset. + * @return A CharsetMatch object containing details of match + * with this charset, or null if there was no match. + */ + abstract CharsetMatch match(CharsetDetector det); + +} diff --git a/app/src/main/java/io/legado/app/help/permission/ActivitySource.kt b/app/src/main/java/io/legado/app/lib/permission/ActivitySource.kt similarity index 92% rename from app/src/main/java/io/legado/app/help/permission/ActivitySource.kt rename to app/src/main/java/io/legado/app/lib/permission/ActivitySource.kt index e0618067a..7db53385e 100644 --- a/app/src/main/java/io/legado/app/help/permission/ActivitySource.kt +++ b/app/src/main/java/io/legado/app/lib/permission/ActivitySource.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import android.content.Context import android.content.Intent diff --git a/app/src/main/java/io/legado/app/help/permission/FragmentSource.kt b/app/src/main/java/io/legado/app/lib/permission/FragmentSource.kt similarity index 92% rename from app/src/main/java/io/legado/app/help/permission/FragmentSource.kt rename to app/src/main/java/io/legado/app/lib/permission/FragmentSource.kt index b66e11b98..3b6bd8830 100644 --- a/app/src/main/java/io/legado/app/help/permission/FragmentSource.kt +++ b/app/src/main/java/io/legado/app/lib/permission/FragmentSource.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import android.content.Context import android.content.Intent diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt new file mode 100644 index 000000000..0cc6b715b --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.permission + +interface OnPermissionsDeniedCallback { + + fun onPermissionsDenied(deniedPermissions: Array) + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt new file mode 100644 index 000000000..fe43e947e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.permission + +interface OnPermissionsGrantedCallback { + + fun onPermissionsGranted() + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt new file mode 100644 index 000000000..41ae2f5fc --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.permission + +interface OnPermissionsResultCallback { + + fun onPermissionsGranted() + + fun onPermissionsDenied(deniedPermissions: Array) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt new file mode 100644 index 000000000..2934e4c77 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.permission + +interface OnRequestPermissionsResultCallback { + + fun onRequestPermissionsResult(permissions: Array, grantResults: IntArray) + + fun onSettingActivityResult() +} diff --git a/app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt b/app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt similarity index 75% rename from app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt rename to app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt index 04722997b..887ca158a 100644 --- a/app/src/main/java/io/legado/app/help/permission/PermissionActivity.kt +++ b/app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt @@ -1,17 +1,24 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.Settings import android.view.KeyEvent +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import io.legado.app.R -import org.jetbrains.anko.toast +import io.legado.app.utils.toastOnUi class PermissionActivity : AppCompatActivity() { + private val settingActivityResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + RequestPlugins.sRequestCallback?.onSettingActivityResult() + finish() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -30,24 +37,25 @@ class PermissionActivity : AppCompatActivity() { -> try { val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) settingIntent.data = Uri.fromParts("package", packageName, null) - startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING) + settingActivityResult.launch(settingIntent) } catch (e: Exception) { - toast(R.string.tip_cannot_jump_setting_page) + toastOnUi(R.string.tip_cannot_jump_setting_page) finish() } } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - RequestPlugins.sRequestCallback?.onRequestPermissionsResult(requestCode, permissions, grantResults) - finish() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - RequestPlugins.sRequestCallback?.onActivityResult(requestCode, resultCode, data) + RequestPlugins.sRequestCallback?.onRequestPermissionsResult( + permissions, + grantResults + ) finish() } diff --git a/app/src/main/java/io/legado/app/help/permission/Permissions.kt b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt similarity index 97% rename from app/src/main/java/io/legado/app/help/permission/Permissions.kt rename to app/src/main/java/io/legado/app/lib/permission/Permissions.kt index 236691f7f..620ace55b 100644 --- a/app/src/main/java/io/legado/app/help/permission/Permissions.kt +++ b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt @@ -1,5 +1,6 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission +@Suppress("unused") object Permissions { const val READ_CALENDAR = "android.permission.READ_CALENDAR" diff --git a/app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt b/app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt similarity index 60% rename from app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt rename to app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt index 17104108b..f127c77d6 100644 --- a/app/src/main/java/io/legado/app/help/permission/PermissionsCompat.kt +++ b/app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt @@ -1,10 +1,10 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import java.util.* +@Suppress("unused") class PermissionsCompat private constructor() { private var request: Request? = null @@ -13,17 +13,6 @@ class PermissionsCompat private constructor() { RequestManager.pushRequest(request) } - companion object { - // 检查权限, 如果已经拥有返回 true - fun check(activity: AppCompatActivity, vararg permissions: String): Boolean { - var request = Request(activity) - var pers = ArrayList() - pers.addAll(listOf(*permissions)) - var data = request.getDeniedPermissions(pers.toTypedArray()) - return data == null; - } - } - class Builder { private val request: Request @@ -40,24 +29,19 @@ class PermissionsCompat private constructor() { return this } - fun requestCode(requestCode: Int): Builder { - request.setRequestCode(requestCode) - return this - } - - fun onGranted(callback: (requestCode: Int) -> Unit): Builder { + fun onGranted(callback: () -> Unit): Builder { request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { - override fun onPermissionsGranted(requestCode: Int) { - callback(requestCode) + override fun onPermissionsGranted() { + callback() } }) return this } - fun onDenied(callback: (requestCode: Int, deniedPermissions: Array) -> Unit): Builder { + fun onDenied(callback: (deniedPermissions: Array) -> Unit): Builder { request.setOnDeniedCallback(object : OnPermissionsDeniedCallback { - override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { - callback(requestCode, deniedPermissions) + override fun onPermissionsDenied(deniedPermissions: Array) { + callback(deniedPermissions) } }) return this diff --git a/app/src/main/java/io/legado/app/help/permission/Request.kt b/app/src/main/java/io/legado/app/lib/permission/Request.kt similarity index 68% rename from app/src/main/java/io/legado/app/help/permission/Request.kt rename to app/src/main/java/io/legado/app/lib/permission/Request.kt index f7ed0293a..a5f3acacd 100644 --- a/app/src/main/java/io/legado/app/help/permission/Request.kt +++ b/app/src/main/java/io/legado/app/lib/permission/Request.kt @@ -1,6 +1,5 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission -import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.annotation.StringRes @@ -9,9 +8,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import io.legado.app.R -import org.jetbrains.anko.startActivity +import io.legado.app.utils.startActivity import java.util.* +@Suppress("MemberVisibilityCanBePrivate") internal class Request : OnRequestPermissionsResultCallback { internal val requestTime: Long @@ -46,10 +46,6 @@ internal class Request : OnRequestPermissionsResultCallback { this.permissions?.addAll(listOf(*permissions)) } - fun setRequestCode(requestCode: Int) { - this.requestCode = requestCode - } - fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { grantedCallback = callback } @@ -75,24 +71,27 @@ internal class Request : OnRequestPermissionsResultCallback { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (deniedPermissions == null) { - onPermissionsGranted(requestCode) + onPermissionsGranted() } else { - val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale + val rationale = + if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationale != null) { - showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } + showSettingDialog(rationale) { + onPermissionsDenied(deniedPermissions) + } } else { - onPermissionsDenied(requestCode, deniedPermissions) + onPermissionsDenied(deniedPermissions) } } } else { if (deniedPermissions != null) { - source?.context?.startActivity( - PermissionActivity.KEY_INPUT_REQUEST_TYPE to TYPE_REQUEST_PERMISSION, - PermissionActivity.KEY_INPUT_PERMISSIONS_CODE to requestCode, - PermissionActivity.KEY_INPUT_PERMISSIONS to deniedPermissions - ) + source?.context?.startActivity { + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } } else { - onPermissionsGranted(requestCode) + onPermissionsGranted() } } } @@ -132,9 +131,12 @@ internal class Request : OnRequestPermissionsResultCallback { .setTitle(R.string.dialog_title) .setMessage(rationale) .setPositiveButton(R.string.dialog_setting) { _, _ -> - it.startActivity( - PermissionActivity.KEY_INPUT_REQUEST_TYPE to TYPE_REQUEST_SETTING - ) + it.startActivity { + putExtra( + PermissionActivity.KEY_INPUT_REQUEST_TYPE, + TYPE_REQUEST_SETTING + ) + } } .setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() } .show() @@ -142,44 +144,48 @@ internal class Request : OnRequestPermissionsResultCallback { } } - private fun onPermissionsGranted(requestCode: Int) { + private fun onPermissionsGranted() { try { - grantedCallback?.onPermissionsGranted(requestCode) + grantedCallback?.onPermissionsGranted() } catch (ignore: Exception) { } - RequestPlugins.sResultCallback?.onPermissionsGranted(requestCode) + RequestPlugins.sResultCallback?.onPermissionsGranted() } - private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { + private fun onPermissionsDenied(deniedPermissions: Array) { try { - deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions) + deniedCallback?.onPermissionsDenied(deniedPermissions) } catch (ignore: Exception) { } - RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions) + RequestPlugins.sResultCallback?.onPermissionsDenied(deniedPermissions) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + override fun onRequestPermissionsResult( + permissions: Array, + grantResults: IntArray + ) { val deniedPermissions = getDeniedPermissions(permissions) if (deniedPermissions != null) { - val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale + val rationale = + if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale if (rationale != null) { - showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) } + showSettingDialog(rationale) { onPermissionsDenied(deniedPermissions) } } else { - onPermissionsDenied(requestCode, deniedPermissions) + onPermissionsDenied(deniedPermissions) } } else { - onPermissionsGranted(requestCode) + onPermissionsGranted() } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + override fun onSettingActivityResult() { val deniedPermissions = deniedPermissions if (deniedPermissions == null) { - onPermissionsGranted(this.requestCode) + onPermissionsGranted() } else { - onPermissionsDenied(this.requestCode, deniedPermissions) + onPermissionsDenied(deniedPermissions) } } diff --git a/app/src/main/java/io/legado/app/help/permission/RequestManager.kt b/app/src/main/java/io/legado/app/lib/permission/RequestManager.kt similarity index 86% rename from app/src/main/java/io/legado/app/help/permission/RequestManager.kt rename to app/src/main/java/io/legado/app/lib/permission/RequestManager.kt index 18bedb800..2bc6832c7 100644 --- a/app/src/main/java/io/legado/app/help/permission/RequestManager.kt +++ b/app/src/main/java/io/legado/app/lib/permission/RequestManager.kt @@ -1,6 +1,7 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import android.os.Handler +import android.os.Looper import java.util.* internal object RequestManager : OnPermissionsResultCallback { @@ -8,7 +9,7 @@ internal object RequestManager : OnPermissionsResultCallback { private var requests: Stack? = null private var request: Request? = null - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private val requestRunnable = Runnable { request?.start() @@ -57,11 +58,11 @@ internal object RequestManager : OnPermissionsResultCallback { } } - override fun onPermissionsGranted(requestCode: Int) { + override fun onPermissionsGranted() { startNextRequest() } - override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) { + override fun onPermissionsDenied(deniedPermissions: Array) { startNextRequest() } diff --git a/app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt b/app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt similarity index 92% rename from app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt rename to app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt index 16370193f..59bf0f0eb 100644 --- a/app/src/main/java/io/legado/app/help/permission/RequestPlugins.kt +++ b/app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission internal object RequestPlugins { diff --git a/app/src/main/java/io/legado/app/help/permission/RequestSource.kt b/app/src/main/java/io/legado/app/lib/permission/RequestSource.kt similarity index 80% rename from app/src/main/java/io/legado/app/help/permission/RequestSource.kt rename to app/src/main/java/io/legado/app/lib/permission/RequestSource.kt index a822ff5ad..3e029805f 100644 --- a/app/src/main/java/io/legado/app/help/permission/RequestSource.kt +++ b/app/src/main/java/io/legado/app/lib/permission/RequestSource.kt @@ -1,4 +1,4 @@ -package io.legado.app.help.permission +package io.legado.app.lib.permission import android.content.Context import android.content.Intent diff --git a/app/src/main/java/io/legado/app/lib/theme/ATH.kt b/app/src/main/java/io/legado/app/lib/theme/ATH.kt index f69c5d40a..dcecc9577 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ATH.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ATH.kt @@ -9,27 +9,26 @@ import android.graphics.drawable.GradientDrawable import android.os.Build import android.view.View import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR -import android.view.Window +import android.view.WindowInsetsController +import android.view.WindowManager import android.widget.EdgeEffect import android.widget.ScrollView import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.bottomnavigation.BottomNavigationView -import io.legado.app.App import io.legado.app.R import io.legado.app.help.AppConfig import io.legado.app.utils.ColorUtils import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor -import kotlinx.android.synthetic.main.activity_main.view.* -import org.jetbrains.anko.backgroundColor - +import splitties.init.appCtx /** * @author Karim Abou Zeid (kabouzeid) */ +@Suppress("unused", "MemberVisibilityCanBePrivate") object ATH { @SuppressLint("CommitPrefEdits") @@ -40,13 +39,28 @@ object ATH { ) > since } + fun fullScreen(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.window.setDecorFitsSystemWindows(true) + } + fullScreenO(activity) + activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + } + + @Suppress("DEPRECATION") + private fun fullScreenO(activity: Activity) { + activity.window.decorView.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + activity.window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + ) + } + fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) { val isTransparentStatusBar = AppConfig.isTransparentStatusBar - setStatusBarColor( - activity, - ThemeStore.statusBarColor(activity, isTransparentStatusBar), - isTransparentStatusBar, fullScreen - ) + val statusBarColor = ThemeStore.statusBarColor(activity, isTransparentStatusBar) + setStatusBarColor(activity, statusBarColor, isTransparentStatusBar, fullScreen) } fun setStatusBarColor( @@ -64,16 +78,36 @@ object ATH { } else { activity.window.statusBarColor = color } - setLightStatusBarAuto(activity.window, color) + setLightStatusBarAuto(activity, color) } - fun setLightStatusBarAuto(window: Window, bgColor: Int) { - setLightStatusBar(window, ColorUtils.isColorLight(bgColor)) + fun setLightStatusBarAuto(activity: Activity, bgColor: Int) { + setLightStatusBar(activity, ColorUtils.isColorLight(bgColor)) } - fun setLightStatusBar(window: Window, enabled: Boolean) { + fun setLightStatusBar(activity: Activity, enabled: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.window.insetsController?.let { + if (enabled) { + it.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } else { + it.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } + } + } + setLightStatusBarO(activity, enabled) + } + + @Suppress("DEPRECATION") + private fun setLightStatusBarO(activity: Activity, enabled: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val decorView = window.decorView + val decorView = activity.window.decorView val systemUiVisibility = decorView.systemUiVisibility if (enabled) { decorView.systemUiVisibility = @@ -85,7 +119,35 @@ object ATH { } } + fun setNavigationBarColorAuto( + activity: Activity, + color: Int, + ) { + activity.window.navigationBarColor = color + setLightNavigationBar(activity, ColorUtils.isColorLight(color)) + } + fun setLightNavigationBar(activity: Activity, enabled: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.window.insetsController?.let { + if (enabled) { + it.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + ) + } else { + it.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + ) + } + } + } + setLightNavigationBarO(activity, enabled) + } + + @Suppress("DEPRECATION") + private fun setLightNavigationBarO(activity: Activity, enabled: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val decorView = activity.window.decorView var systemUiVisibility = decorView.systemUiVisibility @@ -98,33 +160,20 @@ object ATH { } } - fun setNavigationBarColorAuto( - activity: Activity, - color: Int = ThemeStore.navigationBarColor(activity) - ) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.navigationBarColor = color - setLightNavigationBar(activity, ColorUtils.isColorLight(color)) - } - } - fun setTaskDescriptionColorAuto(activity: Activity) { setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity)) } fun setTaskDescriptionColor(activity: Activity, @ColorInt color: Int) { - val color1: Int - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - color1 = ColorUtils.stripAlpha(color) - @Suppress("DEPRECATION") - activity.setTaskDescription( - ActivityManager.TaskDescription( - activity.title as String, - null, - color1 - ) + val color1: Int = ColorUtils.stripAlpha(color) + @Suppress("DEPRECATION") + activity.setTaskDescription( + ActivityManager.TaskDescription( + activity.title as String, + null, + color1 ) - } + ) } fun setTint( @@ -170,9 +219,9 @@ object ATH { } } - fun setEdgeEffectColor(viewPager: ViewPager?, @ColorInt color: Int) { + fun setEdgeEffectColor(viewPager: ViewPager2?, @ColorInt color: Int) { try { - val clazz = ViewPager::class.java + val clazz = ViewPager2::class.java for (name in arrayOf("mLeftEdge", "mRightEdge")) { val field = clazz.getDeclaredField(name) field.isAccessible = true @@ -206,7 +255,7 @@ object ATH { val textColor = context.getSecondaryTextColor(textIsDark) val colorStateList = Selector.colorBuild() .setDefaultColor(textColor) - .setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create() + .setSelectedColor(ThemeStore.accentColor(context)).create() itemIconTintList = colorStateList itemTextColor = colorStateList } @@ -221,7 +270,7 @@ object ATH { fun applyBackgroundTint(view: View?) { view?.apply { if (background == null) { - backgroundColor = context.backgroundColor + setBackgroundColor(context.backgroundColor) } else { setBackgroundTint(this, context.backgroundColor) } @@ -231,7 +280,7 @@ object ATH { fun applyEdgeEffectColor(view: View?) { when (view) { is RecyclerView -> view.edgeEffectFactory = DEFAULT_EFFECT_FACTORY - is ViewPager -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) + is ViewPager2 -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) is ScrollView -> setEdgeEffectColor(view, ThemeStore.primaryColor(view.context)) } } @@ -239,7 +288,7 @@ object ATH { fun getDialogBackground(): GradientDrawable { val background = GradientDrawable() background.cornerRadius = 3F.dp - background.setColor(App.INSTANCE.backgroundColor) + background.setColor(appCtx.backgroundColor) return background } diff --git a/app/src/main/java/io/legado/app/lib/theme/DrawableUtils.kt b/app/src/main/java/io/legado/app/lib/theme/DrawableUtils.kt index 660448088..4e6636329 100644 --- a/app/src/main/java/io/legado/app/lib/theme/DrawableUtils.kt +++ b/app/src/main/java/io/legado/app/lib/theme/DrawableUtils.kt @@ -11,9 +11,13 @@ import androidx.core.graphics.drawable.DrawableCompat /** * @author Karim Abou Zeid (kabouzeid) */ +@Suppress("unused") object DrawableUtils { - fun createTransitionDrawable(@ColorInt startColor: Int, @ColorInt endColor: Int): TransitionDrawable { + fun createTransitionDrawable( + @ColorInt startColor: Int, + @ColorInt endColor: Int + ): TransitionDrawable { return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor)) } @@ -26,7 +30,11 @@ object DrawableUtils { return TransitionDrawable(drawables) } - fun setTintList(drawable: Drawable?, tint: ColorStateList, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP) { + fun setTintList( + drawable: Drawable?, + tint: ColorStateList, + tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP + ) { drawable?.let { val wrappedDrawable = DrawableCompat.wrap(it) wrappedDrawable.mutate() @@ -36,7 +44,11 @@ object DrawableUtils { } - fun setTint(drawable: Drawable?, @ColorInt tint: Int, tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP) { + fun setTint( + drawable: Drawable?, + @ColorInt tint: Int, + tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP + ) { drawable?.let { val wrappedDrawable = DrawableCompat.wrap(it) wrappedDrawable.mutate() diff --git a/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt b/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt index 018e3dd60..786db41c9 100644 --- a/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt +++ b/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package io.legado.app.lib.theme import android.annotation.SuppressLint @@ -99,9 +101,9 @@ val Fragment.secondaryDisabledTextColor: Int val Context.buttonDisabledColor: Int get() = if (isDarkTheme) { - ContextCompat.getColor(this, R.color.ate_button_disabled_dark) + ContextCompat.getColor(this, R.color.md_dark_disabled) } else { - ContextCompat.getColor(this, R.color.ate_button_disabled_light) + ContextCompat.getColor(this, R.color.md_light_disabled) } val Context.isDarkTheme: Boolean diff --git a/app/src/main/java/io/legado/app/lib/theme/NavigationViewUtils.kt b/app/src/main/java/io/legado/app/lib/theme/NavigationViewUtils.kt index c199ba855..1d28c0d1f 100644 --- a/app/src/main/java/io/legado/app/lib/theme/NavigationViewUtils.kt +++ b/app/src/main/java/io/legado/app/lib/theme/NavigationViewUtils.kt @@ -8,19 +8,34 @@ import com.google.android.material.navigation.NavigationView /** * @author Karim Abou Zeid (kabouzeid) */ +@Suppress("unused") object NavigationViewUtils { - fun setItemIconColors(navigationView: NavigationView, @ColorInt normalColor: Int, @ColorInt selectedColor: Int) { + fun setItemIconColors( + navigationView: NavigationView, + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int + ) { val iconSl = ColorStateList( - arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), + arrayOf( + intArrayOf(-android.R.attr.state_checked), + intArrayOf(android.R.attr.state_checked) + ), intArrayOf(normalColor, selectedColor) ) navigationView.itemIconTintList = iconSl } - fun setItemTextColors(navigationView: NavigationView, @ColorInt normalColor: Int, @ColorInt selectedColor: Int) { + fun setItemTextColors( + navigationView: NavigationView, + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int + ) { val textSl = ColorStateList( - arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), + arrayOf( + intArrayOf(-android.R.attr.state_checked), + intArrayOf(android.R.attr.state_checked) + ), intArrayOf(normalColor, selectedColor) ) navigationView.itemTextColor = textSl diff --git a/app/src/main/java/io/legado/app/lib/theme/README.md b/app/src/main/java/io/legado/app/lib/theme/README.md deleted file mode 100644 index cfba14d6d..000000000 --- a/app/src/main/java/io/legado/app/lib/theme/README.md +++ /dev/null @@ -1 +0,0 @@ -## 主题引擎 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/Selector.kt b/app/src/main/java/io/legado/app/lib/theme/Selector.kt index 89a71198b..f9985528f 100644 --- a/app/src/main/java/io/legado/app/lib/theme/Selector.kt +++ b/app/src/main/java/io/legado/app/lib/theme/Selector.kt @@ -13,6 +13,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.IntDef import androidx.core.content.ContextCompat +@Suppress("unused") object Selector { fun shapeBuild(): ShapeSelector { return ShapeSelector() @@ -62,7 +63,12 @@ object Selector { private var hasSetFocusedStrokeColor = false private var hasSetCheckedStrokeColor = false - @IntDef(GradientDrawable.RECTANGLE, GradientDrawable.OVAL, GradientDrawable.LINE, GradientDrawable.RING) + @IntDef( + GradientDrawable.RECTANGLE, + GradientDrawable.OVAL, + GradientDrawable.LINE, + GradientDrawable.RING + ) private annotation class Shape init { @@ -260,7 +266,8 @@ object Selector { * @author hjy * created at 2017/12/11 22:34 */ - class DrawableSelector constructor() { + @Suppress("MemberVisibilityCanBePrivate") + class DrawableSelector { private var mDefaultDrawable: Drawable? = null private var mDisabledDrawable: Drawable? = null @@ -355,7 +362,7 @@ object Selector { * @author hjy * created at 2017/12/11 22:26 */ - class ColorSelector constructor() { + class ColorSelector { private var mDefaultColor: Int = 0 private var mDisabledColor: Int = 0 diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt index 321d2a8f0..040a04da0 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt @@ -9,21 +9,18 @@ import androidx.annotation.CheckResult import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat -import io.legado.app.App import io.legado.app.R import io.legado.app.utils.ColorUtils +import splitties.init.appCtx /** * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) */ +@Suppress("unused") class ThemeStore @SuppressLint("CommitPrefEdits") private constructor(private val mContext: Context) : ThemeStoreInterface { - private val mEditor: SharedPreferences.Editor - - init { - mEditor = prefs(mContext).edit() - } + private val mEditor = prefs(mContext).edit() override fun primaryColor(@ColorInt color: Int): ThemeStore { mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR, color) @@ -154,16 +151,6 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { return this } - override fun coloredStatusBar(colored: Boolean): ThemeStore { - mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, colored) - return this - } - - override fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore { - mEditor.putBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar) - return this - } - override fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore { mEditor.putBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate) return this @@ -187,7 +174,10 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult internal fun prefs(context: Context): SharedPreferences { - return context.getSharedPreferences(ThemeStorePrefKeys.CONFIG_PREFS_KEY_DEFAULT, Context.MODE_PRIVATE) + return context.getSharedPreferences( + ThemeStorePrefKeys.CONFIG_PREFS_KEY_DEFAULT, + Context.MODE_PRIVATE + ) } fun markChanged(context: Context) { @@ -196,7 +186,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult @ColorInt - fun primaryColor(context: Context = App.INSTANCE): Int { + fun primaryColor(context: Context = appCtx): Int { return prefs(context).getInt( ThemeStorePrefKeys.KEY_PRIMARY_COLOR, ATHUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64")) @@ -214,7 +204,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult @ColorInt - fun accentColor(context: Context = App.INSTANCE): Int { + fun accentColor(context: Context = appCtx): Int { return prefs(context).getInt( ThemeStorePrefKeys.KEY_ACCENT_COLOR, ATHUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238")) @@ -224,21 +214,23 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult @ColorInt fun statusBarColor(context: Context, transparent: Boolean): Int { - return if (!coloredStatusBar(context)) { - Color.BLACK - } else if (transparent) { - prefs(context).getInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, primaryColor(context)) + return if (transparent) { + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColor(context) + ) } else { - prefs(context).getInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, primaryColorDark(context)) + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColorDark(context) + ) } } @CheckResult @ColorInt fun navigationBarColor(context: Context): Int { - return if (!coloredNavigationBar(context)) { - Color.BLACK - } else prefs(context).getInt( + return prefs(context).getInt( ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, bottomBackground(context) ) @@ -282,24 +274,29 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult @ColorInt - fun backgroundColor(context: Context = App.INSTANCE): Int { + fun backgroundColor(context: Context = appCtx): Int { return prefs(context).getInt( ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, ATHUtils.resolveColor(context, android.R.attr.colorBackground) ) } + @SuppressLint("PrivateResource") @CheckResult fun elevation(context: Context): Float { return prefs(context).getFloat( ThemeStorePrefKeys.KEY_ELEVATION, - ATHUtils.resolveFloat(context, android.R.attr.elevation, context.resources.getDimension(R.dimen.design_appbar_elevation)) + ATHUtils.resolveFloat( + context, + android.R.attr.elevation, + context.resources.getDimension(R.dimen.design_appbar_elevation) + ) ) } @CheckResult @ColorInt - fun bottomBackground(context: Context = App.INSTANCE): Int { + fun bottomBackground(context: Context = appCtx): Int { return prefs(context).getInt( ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, ATHUtils.resolveColor(context, android.R.attr.colorBackground) @@ -308,7 +305,10 @@ private constructor(private val mContext: Context) : ThemeStoreInterface { @CheckResult fun coloredStatusBar(context: Context): Boolean { - return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, true) + return prefs(context).getBoolean( + ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, + true + ) } @CheckResult diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt index 521b9d237..f7b3ae500 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt @@ -84,12 +84,6 @@ internal interface ThemeStoreInterface { fun bottomBackground(@ColorInt color: Int): ThemeStore - // Toggle configurations - - fun coloredStatusBar(colored: Boolean): ThemeStore - - fun coloredNavigationBar(applyToNavBar: Boolean): ThemeStore - // Commit/apply fun apply() diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt index ca8e85fc1..fe31b1c23 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt @@ -5,28 +5,28 @@ package io.legado.app.lib.theme */ object ThemeStorePrefKeys { - const val CONFIG_PREFS_KEY_DEFAULT = "app_themes" - const val IS_CONFIGURED_KEY = "is_configured" - const val IS_CONFIGURED_VERSION_KEY = "is_configured_version" - const val VALUES_CHANGED = "values_changed" + const val CONFIG_PREFS_KEY_DEFAULT = "app_themes" + const val IS_CONFIGURED_KEY = "is_configured" + const val IS_CONFIGURED_VERSION_KEY = "is_configured_version" + const val VALUES_CHANGED = "values_changed" - const val KEY_PRIMARY_COLOR = "primary_color" - const val KEY_PRIMARY_COLOR_DARK = "primary_color_dark" - const val KEY_ACCENT_COLOR = "accent_color" - const val KEY_STATUS_BAR_COLOR = "status_bar_color" - const val KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color" + const val KEY_PRIMARY_COLOR = "primary_color" + const val KEY_PRIMARY_COLOR_DARK = "primary_color_dark" + const val KEY_ACCENT_COLOR = "accent_color" + const val KEY_STATUS_BAR_COLOR = "status_bar_color" + const val KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color" - const val KEY_TEXT_COLOR_PRIMARY = "text_color_primary" - const val KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse" - const val KEY_TEXT_COLOR_SECONDARY = "text_color_secondary" - const val KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse" + const val KEY_TEXT_COLOR_PRIMARY = "text_color_primary" + const val KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse" + const val KEY_TEXT_COLOR_SECONDARY = "text_color_secondary" + const val KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse" - const val KEY_BACKGROUND_COLOR = "backgroundColor" - const val KEY_BOTTOM_BACKGROUND = "bottomBackground" + const val KEY_BACKGROUND_COLOR = "backgroundColor" + const val KEY_BOTTOM_BACKGROUND = "bottomBackground" - const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar" - const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" - const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" + const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar" + const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" + const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" - const val KEY_ELEVATION = "elevation" + const val KEY_ELEVATION = "elevation" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt index 76313cd46..a719cc1b1 100644 --- a/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt +++ b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt @@ -23,6 +23,7 @@ import io.legado.app.utils.ColorUtils /** * @author afollestad, plusCubed */ +@Suppress("MemberVisibilityCanBePrivate") object TintHelper { @SuppressLint("PrivateResource") @@ -37,7 +38,10 @@ object TintHelper { ) } - private fun getDisabledColorStateList(@ColorInt normal: Int, @ColorInt disabled: Int): ColorStateList { + private fun getDisabledColorStateList( + @ColorInt normal: Int, + @ColorInt disabled: Int + ): ColorStateList { return ColorStateList( arrayOf( intArrayOf(-android.R.attr.state_enabled), @@ -61,48 +65,52 @@ object TintHelper { ) val sl: ColorStateList - if (view is Button) { - sl = getDisabledColorStateList(color, disabled) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && view.getBackground() is RippleDrawable) { - val rd = view.getBackground() as RippleDrawable - rd.setColor(ColorStateList.valueOf(rippleColor)) - } + when (view) { + is Button -> { + sl = getDisabledColorStateList(color, disabled) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && view.getBackground() is RippleDrawable) { + val rd = view.getBackground() as RippleDrawable + rd.setColor(ColorStateList.valueOf(rippleColor)) + } - // Disabled text color state for buttons, may get overridden later by ATE tags - view.setTextColor( - getDisabledColorStateList( - textColor, - ContextCompat.getColor( - view.getContext(), - if (useDarkTheme) R.color.ate_button_text_disabled_dark else R.color.ate_button_text_disabled_light + // Disabled text color state for buttons, may get overridden later by ATE tags + view.setTextColor( + getDisabledColorStateList( + textColor, + ContextCompat.getColor( + view.getContext(), + if (useDarkTheme) R.color.ate_button_text_disabled_dark else R.color.ate_button_text_disabled_light + ) ) ) - ) - } else if (view is FloatingActionButton) { - // FloatingActionButton doesn't support disabled state? - sl = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_pressed) - ), intArrayOf(color, pressed) - ) + } + is FloatingActionButton -> { + // FloatingActionButton doesn't support disabled state? + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_pressed) + ), intArrayOf(color, pressed) + ) - view.rippleColor = rippleColor - view.backgroundTintList = sl - if (view.drawable != null) - view.setImageDrawable(createTintedDrawable(view.drawable, textColor)) - return - } else { - sl = ColorStateList( - arrayOf( - intArrayOf(-android.R.attr.state_enabled), - intArrayOf(android.R.attr.state_enabled), - intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed), - intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), - intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) - ), - intArrayOf(disabled, color, pressed, activated, activated) - ) + view.rippleColor = rippleColor + view.backgroundTintList = sl + if (view.drawable != null) + view.setImageDrawable(createTintedDrawable(view.drawable, textColor)) + return + } + else -> { + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf(disabled, color, pressed, activated, activated) + ) + } } var drawable: Drawable? = view.background @@ -386,7 +394,11 @@ object TintHelper { return createTintedDrawable(from, sl) } - fun setTint(switchView: Switch, @ColorInt color: Int, useDarker: Boolean) { + fun setTint( + @SuppressLint("UseSwitchCompatOrMaterialCode") switchView: Switch, + @ColorInt color: Int, + useDarker: Boolean + ) { if (switchView.trackDrawable != null) { switchView.trackDrawable = modifySwitchDrawable( switchView.context, diff --git a/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt b/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt index f3afa243c..bcbb794a1 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt @@ -10,6 +10,7 @@ import androidx.annotation.ColorInt /** * @author Karim Abou Zeid (kabouzeid) */ +@Suppress("unused") object ViewUtils { fun removeOnGlobalLayoutListener(v: View, listener: ViewTreeObserver.OnGlobalLayoutListener) { diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATECheckBox.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATECheckBox.kt index fa009b01b..daa41d6cd 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATECheckBox.kt +++ b/app/src/main/java/io/legado/app/lib/theme/view/ATECheckBox.kt @@ -12,6 +12,8 @@ import io.legado.app.lib.theme.accentColor class ATECheckBox(context: Context, attrs: AttributeSet) : AppCompatCheckBox(context, attrs) { init { - ATH.setTint(this, context.accentColor) + if (!isInEditMode) { + ATH.setTint(this, context.accentColor) + } } } diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEProgressBar.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATEProgressBar.kt index 20f13e03c..090c66f80 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEProgressBar.kt +++ b/app/src/main/java/io/legado/app/lib/theme/view/ATEProgressBar.kt @@ -12,6 +12,8 @@ import io.legado.app.lib.theme.ThemeStore class ATEProgressBar(context: Context, attrs: AttributeSet) : ProgressBar(context, attrs) { init { - ATH.setTint(this, ThemeStore.accentColor(context)) + if (!isInEditMode) { + ATH.setTint(this, ThemeStore.accentColor(context)) + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATERadioButton.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATERadioButton.kt index d416d6f8f..8654e1554 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATERadioButton.kt +++ b/app/src/main/java/io/legado/app/lib/theme/view/ATERadioButton.kt @@ -12,6 +12,8 @@ import io.legado.app.lib.theme.accentColor class ATERadioButton(context: Context, attrs: AttributeSet) : AppCompatRadioButton(context, attrs) { init { - ATH.setTint(this@ATERadioButton, context.accentColor) + if (!isInEditMode) { + ATH.setTint(this@ATERadioButton, context.accentColor) + } } } diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATESeekBar.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATESeekBar.kt index 2d793ba82..3b8f046aa 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATESeekBar.kt +++ b/app/src/main/java/io/legado/app/lib/theme/view/ATESeekBar.kt @@ -12,6 +12,8 @@ import io.legado.app.lib.theme.accentColor class ATESeekBar(context: Context, attrs: AttributeSet) : AppCompatSeekBar(context, attrs) { init { - ATH.setTint(this, context.accentColor) + if (!isInEditMode) { + ATH.setTint(this, context.accentColor) + } } } diff --git a/app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt b/app/src/main/java/io/legado/app/lib/webdav/HttpAuth.kt similarity index 75% rename from app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt rename to app/src/main/java/io/legado/app/lib/webdav/HttpAuth.kt index 07cab5855..d2f604a80 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/http/HttpAuth.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/HttpAuth.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.webdav.http +package io.legado.app.lib.webdav object HttpAuth { diff --git a/app/src/main/java/io/legado/app/lib/webdav/README.md b/app/src/main/java/io/legado/app/lib/webdav/README.md deleted file mode 100644 index f6ac57a9d..000000000 --- a/app/src/main/java/io/legado/app/lib/webdav/README.md +++ /dev/null @@ -1 +0,0 @@ -## 用于网络备份的WebDav \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index 03fc0d974..93e3752b2 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -1,139 +1,108 @@ package io.legado.app.lib.webdav -import io.legado.app.help.http.HttpHelper -import io.legado.app.lib.webdav.http.Handler -import io.legado.app.lib.webdav.http.HttpAuth +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.intellij.lang.annotations.Language import org.jsoup.Jsoup import java.io.File -import java.io.IOException import java.io.InputStream -import java.io.UnsupportedEncodingException import java.net.MalformedURLException import java.net.URL import java.net.URLEncoder import java.util.* +@Suppress("unused", "MemberVisibilityCanBePrivate") class WebDav(urlStr: String) { companion object { // 指定返回哪些属性 + @Language("xml") private const val DIR = """ - - - - - - - - %s - - """ + + + + + + + + %s + + """ } - private val url: URL = URL(null, urlStr, Handler) + private val url: URL = URL(urlStr) private val httpUrl: String? by lazy { val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") - try { - return@lazy URLEncoder.encode(raw, "UTF-8") + return@lazy kotlin.runCatching { + URLEncoder.encode(raw, "UTF-8") .replace("\\+".toRegex(), "%20") .replace("%3A".toRegex(), ":") .replace("%2F".toRegex(), "/") - } catch (e: UnsupportedEncodingException) { - e.printStackTrace() - return@lazy null - } + }.getOrNull() } - + val host: String? get() = url.host + val path get() = url.toString() var displayName: String? = null var size: Long = 0 var exists = false var parent = "" var urlName = "" - get() { - if (field.isEmpty()) { - this.urlName = ( - if (parent.isEmpty()) url.file - else url.toString().replace(parent, "") - ).replace("/", "") - } - return field - } - - fun getPath() = url.toString() - - fun getHost() = url.host + var contentType = "" /** * 填充文件信息。实例化WebDAVFile对象时,并没有将远程文件的信息填充到实例中。需要手动填充! - * * @return 远程文件是否存在 */ - @Throws(IOException::class) - fun indexFileInfo(): Boolean { - propFindResponse(ArrayList())?.let { response -> - if (!response.isSuccessful) { - this.exists = false - return false - } - response.body()?.let { - if (it.string().isNotEmpty()) { - return true - } - } - } - return false + suspend fun indexFileInfo(): Boolean { + return !propFindResponse(ArrayList()).isNullOrEmpty() } /** * 列出当前路径下的文件 * - * @param propsList 指定列出文件的哪些属性 * @return 文件列表 */ - @Throws(IOException::class) - @JvmOverloads - fun listFiles(propsList: ArrayList = ArrayList()): List { - propFindResponse(propsList)?.let { response -> - if (response.isSuccessful) { - response.body()?.let { body -> - return parseDir(body.string()) - } - } + suspend fun listFiles(): List { + propFindResponse()?.let { body -> + return parseDir(body) } return ArrayList() } - @Throws(IOException::class) - private fun propFindResponse(propsList: ArrayList, depth: Int = 1): Response? { + /** + * @param propsList 指定列出文件的哪些属性 + */ + private suspend fun propFindResponse(propsList: List = emptyList()): String? { val requestProps = StringBuilder() for (p in propsList) { requestProps.append("\n") } - val requestPropsStr: String - requestPropsStr = if (requestProps.toString().isEmpty()) { + val requestPropsStr: String = if (requestProps.toString().isEmpty()) { DIR.replace("%s", "") } else { String.format(DIR, requestProps.toString() + "\n") } - httpUrl?.let { url -> - val request = Request.Builder() - .url(url) - // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 - // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 - .method( - "PROPFIND", - RequestBody.create(MediaType.parse("text/plain"), requestPropsStr) - ) - - HttpAuth.auth?.let { - request.header( - "Authorization", - Credentials.basic(it.user, it.pass) - ) - } - request.header("Depth", if (depth < 0) "infinity" else depth.toString()) - return HttpHelper.client.newCall(request.build()).execute() + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + return kotlin.runCatching { + okHttpClient.newCall { + url(url) + addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + addHeader("Depth", "1") + // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 + // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 + val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType()) + method("PROPFIND", requestBody) + }.text() + }.onFailure { + it.printStackTrace() + }.getOrNull() } return null } @@ -142,8 +111,8 @@ class WebDav(urlStr: String) { val list = ArrayList() val document = Jsoup.parse(s) val elements = document.getElementsByTag("d:response") - httpUrl?.let { url -> - val baseUrl = if (url.endsWith("/")) url else "$url/" + httpUrl?.let { urlStr -> + val baseUrl = if (urlStr.endsWith("/")) urlStr else "$urlStr/" for (element in elements) { val href = element.getElementsByTag("d:href")[0].text() if (!href.endsWith("/")) { @@ -152,7 +121,16 @@ class WebDav(urlStr: String) { try { webDavFile = WebDav(baseUrl + fileName) webDavFile.displayName = fileName - webDavFile.urlName = href + webDavFile.contentType = element + .getElementsByTag("d:getcontenttype") + .getOrNull(0)?.text() ?: "" + if (href.isEmpty()) { + webDavFile.urlName = + if (parent.isEmpty()) url.file.replace("/", "") + else url.toString().replace(parent, "").replace("/", "") + } else { + webDavFile.urlName = href + } list.add(webDavFile) } catch (e: MalformedURLException) { e.printStackTrace() @@ -165,16 +143,20 @@ class WebDav(urlStr: String) { /** * 根据自己的URL,在远程处创建对应的文件夹 - * * @return 是否创建成功 */ - @Throws(IOException::class) - fun makeAsDir(): Boolean { - httpUrl?.let { url -> - val request = Request.Builder() - .url(url) - .method("MKCOL", null) - return execRequest(request) + suspend fun makeAsDir(): Boolean { + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + //防止报错 + return kotlin.runCatching { + okHttpClient.newCall { + url(url) + method("MKCOL", null) + addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + }.close() + }.isSuccess } return false } @@ -186,7 +168,7 @@ class WebDav(urlStr: String) { * @param replaceExisting 是否替换本地的同名文件 * @return 下载是否成功 */ - fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { + suspend fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean { if (File(savedPath).exists()) { if (!replaceExisting) return false } @@ -195,55 +177,63 @@ class WebDav(urlStr: String) { return true } + suspend fun download(): ByteArray? { + val inputS = getInputStream() ?: return null + return inputS.readBytes() + } + /** * 上传文件 */ - @Throws(IOException::class) - fun upload(localPath: String, contentType: String? = null): Boolean { + suspend fun upload( + localPath: String, + contentType: String = "application/octet-stream" + ): Boolean { val file = File(localPath) if (!file.exists()) return false - val mediaType = contentType?.let { MediaType.parse(it) } // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 - val fileBody = RequestBody.create(mediaType, file) - httpUrl?.let { - val request = Request.Builder() - .url(it) - .put(fileBody) - return execRequest(request) + val fileBody = file.asRequestBody(contentType.toMediaType()) + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + return kotlin.runCatching { + okHttpClient.newCall { + url(url) + put(fileBody) + addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + }.close() + }.isSuccess } return false } - /** - * 执行请求,获取响应结果 - * @param requestBuilder 因为还需要追加验证信息,所以此处传递Request.Builder的对象,而不是Request的对象 - * @return 请求执行的结果 - */ - @Throws(IOException::class) - private fun execRequest(requestBuilder: Request.Builder): Boolean { - HttpAuth.auth?.let { - requestBuilder.header( - "Authorization", - Credentials.basic(it.user, it.pass) - ) + suspend fun upload(byteArray: ByteArray, contentType: String): Boolean { + // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 + val fileBody = byteArray.toRequestBody(contentType.toMediaType()) + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + return kotlin.runCatching { + okHttpClient.newCall { + url(url) + put(fileBody) + addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + }.close() + }.isSuccess } - val response = HttpHelper.client.newCall(requestBuilder.build()).execute() - return response.isSuccessful + return false } - private fun getInputStream(): InputStream? { - httpUrl?.let { url -> - val request = Request.Builder().url(url) - HttpAuth.auth?.let { - request.header("Authorization", Credentials.basic(it.user, it.pass)) - } - try { - return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream() - } catch (e: IOException) { - e.printStackTrace() - } catch (e: IllegalArgumentException) { - e.printStackTrace() - } + private suspend fun getInputStream(): InputStream? { + val url = httpUrl + val auth = HttpAuth.auth + if (url != null && auth != null) { + return kotlin.runCatching { + okHttpClient.newCall { + url(url) + addHeader("Authorization", Credentials.basic(auth.user, auth.pass)) + }.byteStream() + }.getOrNull() } return null } diff --git a/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt b/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt deleted file mode 100644 index c3deec2e2..000000000 --- a/app/src/main/java/io/legado/app/lib/webdav/http/Handler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.legado.app.lib.webdav.http - -import java.net.URL -import java.net.URLConnection -import java.net.URLStreamHandler - -object Handler : URLStreamHandler() { - - override fun getDefaultPort(): Int { - return 80 - } - - public override fun openConnection(u: URL): URLConnection? { - return null - } -} diff --git a/app/src/main/java/io/legado/app/model/Debug.kt b/app/src/main/java/io/legado/app/model/Debug.kt index 4e419bef4..834d53efa 100644 --- a/app/src/main/java/io/legado/app/model/Debug.kt +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -1,19 +1,23 @@ package io.legado.app.model import android.annotation.SuppressLint -import io.legado.app.data.entities.* +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssSource import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.model.rss.Rss import io.legado.app.model.webBook.WebBook -import io.legado.app.utils.htmlFormat +import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.isAbsUrl import io.legado.app.utils.msg +import kotlinx.coroutines.CoroutineScope import java.text.SimpleDateFormat import java.util.* object Debug { - private var debugSource: String? = null var callback: Callback? = null + private var debugSource: String? = null private val tasks: CompositeCoroutine = CompositeCoroutine() @SuppressLint("ConstantLocale") @@ -29,16 +33,23 @@ object Debug { showTime: Boolean = true, state: Int = 1 ) { - if (debugSource != sourceUrl || callback == null || !print) return - var printMsg = msg ?: "" - if (isHtml) { - printMsg = printMsg.htmlFormat() - } - if (showTime) { - printMsg = - "${DEBUG_TIME_FORMAT.format(Date(System.currentTimeMillis() - startTime))} $printMsg" + callback?.let { + if (debugSource != sourceUrl || !print) return + var printMsg = msg ?: "" + if (isHtml) { + printMsg = HtmlFormatter.format(msg) + } + if (showTime) { + val time = DEBUG_TIME_FORMAT.format(Date(System.currentTimeMillis() - startTime)) + printMsg = "$time $printMsg" + } + it.printLog(state, printMsg) } - callback?.printLog(state, printMsg) + } + + @Synchronized + fun log(msg: String?) { + log(debugSource, msg, true) } fun cancelDebug(destroy: Boolean = false) { @@ -50,12 +61,12 @@ object Debug { } } - fun startDebug(rssSource: RssSource) { + fun startDebug(scope: CoroutineScope, rssSource: RssSource) { cancelDebug() debugSource = rssSource.sourceUrl log(debugSource, "︾开始解析") val sort = rssSource.sortUrls().entries.first() - Rss.getArticles(sort.key, sort.value, rssSource, 1) + Rss.getArticles(scope, sort.key, sort.value, rssSource, 1) .onSuccess { if (it.articles.isEmpty()) { log(debugSource, "⇒列表页解析成功,为空") @@ -68,7 +79,7 @@ object Debug { if (ruleContent.isNullOrEmpty()) { log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000) } else { - rssContentDebug(it.articles[0], ruleContent, rssSource) + rssContentDebug(scope, it.articles[0], ruleContent, rssSource) } } else { log(debugSource, "⇒存在描述规则,不解析内容页") @@ -81,9 +92,14 @@ object Debug { } } - private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String, rssSource: RssSource) { + private fun rssContentDebug( + scope: CoroutineScope, + rssArticle: RssArticle, + ruleContent: String, + rssSource: RssSource + ) { log(debugSource, "︾开始解析内容页") - Rss.getContent(rssArticle, ruleContent, rssSource) + Rss.getContent(scope, rssArticle, ruleContent, rssSource) .onSuccess { log(debugSource, it) log(debugSource, "︽内容页解析完成", state = 1000) @@ -93,7 +109,7 @@ object Debug { } } - fun startDebug(webBook: WebBook, key: String) { + fun startDebug(scope: CoroutineScope, webBook: WebBook, key: String) { cancelDebug() debugSource = webBook.sourceUrl startTime = System.currentTimeMillis() @@ -103,22 +119,22 @@ object Debug { book.origin = webBook.sourceUrl book.bookUrl = key log(webBook.sourceUrl, "⇒开始访问详情页:$key") - infoDebug(webBook, book) + infoDebug(scope, webBook, book) } key.contains("::") -> { - val url = key.substring(key.indexOf("::") + 2) + val url = key.substringAfter("::") log(webBook.sourceUrl, "⇒开始访问发现页:$url") - exploreDebug(webBook, url) + exploreDebug(scope, webBook, url) } - key.startsWith("++")-> { + key.startsWith("++") -> { val url = key.substring(2) val book = Book() book.origin = webBook.sourceUrl book.tocUrl = url log(webBook.sourceUrl, "⇒开始访目录页:$url") - tocDebug(webBook, book) + tocDebug(scope, webBook, book) } - key.startsWith("--")-> { + key.startsWith("--") -> { val url = key.substring(2) val book = Book() book.origin = webBook.sourceUrl @@ -126,24 +142,23 @@ object Debug { val chapter = BookChapter() chapter.title = "调试" chapter.url = url - contentDebug(webBook, book, chapter, null) + contentDebug(scope, webBook, book, chapter, null) } else -> { log(webBook.sourceUrl, "⇒开始搜索关键字:$key") - searchDebug(webBook, key) + searchDebug(scope, webBook, key) } } } - private fun exploreDebug(webBook: WebBook, url: String) { + private fun exploreDebug(scope: CoroutineScope, webBook: WebBook, url: String) { log(debugSource, "︾开始解析发现页") - val variableBook = SearchBook() - val explore = webBook.exploreBook(url, 1, variableBook) + val explore = webBook.exploreBook(scope, url, 1) .onSuccess { exploreBooks -> if (exploreBooks.isNotEmpty()) { log(debugSource, "︽发现页解析完成") log(debugSource, showTime = false) - infoDebug(webBook, exploreBooks[0].toBook()) + infoDebug(scope, webBook, exploreBooks[0].toBook()) } else { log(debugSource, "︽未获取到书籍", state = -1) } @@ -154,15 +169,14 @@ object Debug { tasks.add(explore) } - private fun searchDebug(webBook: WebBook, key: String) { + private fun searchDebug(scope: CoroutineScope, webBook: WebBook, key: String) { log(debugSource, "︾开始解析搜索页") - val variableBook = SearchBook() - val search = webBook.searchBook(key, 1, variableBook) + val search = webBook.searchBook(scope, key, 1) .onSuccess { searchBooks -> if (searchBooks.isNotEmpty()) { log(debugSource, "︽搜索页解析完成") log(debugSource, showTime = false) - infoDebug(webBook, searchBooks[0].toBook()) + infoDebug(scope, webBook, searchBooks[0].toBook()) } else { log(debugSource, "︽未获取到书籍", state = -1) } @@ -173,13 +187,19 @@ object Debug { tasks.add(search) } - private fun infoDebug(webBook: WebBook, book: Book) { + private fun infoDebug(scope: CoroutineScope, webBook: WebBook, book: Book) { + if (book.tocUrl.isNotBlank()) { + log(debugSource, "≡已获取目录链接,跳过详情页") + log(debugSource, showTime = false) + tocDebug(scope, webBook, book) + return + } log(debugSource, "︾开始解析详情页") - val info = webBook.getBookInfo(book) + val info = webBook.getBookInfo(scope, book) .onSuccess { log(debugSource, "︽详情页解析完成") log(debugSource, showTime = false) - tocDebug(webBook, book) + tocDebug(scope, webBook, book) } .onError { log(debugSource, it.msg, state = -1) @@ -187,15 +207,15 @@ object Debug { tasks.add(info) } - private fun tocDebug(webBook: WebBook, book: Book) { + private fun tocDebug(scope: CoroutineScope, webBook: WebBook, book: Book) { log(debugSource, "︾开始解析目录页") - val chapterList = webBook.getChapterList(book) + val chapterList = webBook.getChapterList(scope, book) .onSuccess { if (it.isNotEmpty()) { log(debugSource, "︽目录页解析完成") log(debugSource, showTime = false) - val nextChapterUrl = if (it.size > 1) it[1].url else null - contentDebug(webBook, book, it[0], nextChapterUrl) + val nextChapterUrl = it.getOrNull(1)?.url + contentDebug(scope, webBook, book, it[0], nextChapterUrl) } else { log(debugSource, "︽目录列表为空", state = -1) } @@ -207,13 +227,14 @@ object Debug { } private fun contentDebug( + scope: CoroutineScope, webBook: WebBook, book: Book, bookChapter: BookChapter, nextChapterUrl: String? ) { log(debugSource, "︾开始解析正文页") - val content = webBook.getContent(book, bookChapter, nextChapterUrl) + val content = webBook.getContent(scope, book, bookChapter, nextChapterUrl) .onSuccess { log(debugSource, "︽正文页解析完成", state = 1000) } diff --git a/app/src/main/java/io/legado/app/model/README.md b/app/src/main/java/io/legado/app/model/README.md index d791c861e..150cfb8bd 100644 --- a/app/src/main/java/io/legado/app/model/README.md +++ b/app/src/main/java/io/legado/app/model/README.md @@ -1,4 +1,4 @@ -## 放置一些模块类 +# 放置一些模块类 * analyzeRule 书源规则解析 * localBook 本地书籍解析 * rss 订阅规则解析 diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt index 1cc90b89e..5ca202521 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt @@ -1,75 +1,77 @@ package io.legado.app.model.analyzeRule -import android.text.TextUtils import androidx.annotation.Keep import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.ReadContext -import io.legado.app.utils.splitNotBlank import java.util.* -import java.util.regex.Pattern +@Suppress("RegExpRedundantEscape") @Keep -class AnalyzeByJSonPath { - private var ctx: ReadContext? = null +class AnalyzeByJSonPath(json: Any) { - fun parse(json: Any): AnalyzeByJSonPath { - ctx = if (json is String) { - JsonPath.parse(json) - } else { - JsonPath.parse(json) + companion object { + + fun parse(json: Any): ReadContext { + return when (json) { + is ReadContext -> json + is String -> JsonPath.parse(json) //JsonPath.parse(json) + else -> JsonPath.parse(json) //JsonPath.parse(json) + } } - return this } + private var ctx: ReadContext = parse(json) + + /** + * 改进解析方法 + * 解决阅读”&&“、”||“与jsonPath支持的”&&“、”||“之间的冲突 + * 解决{$.rule}形式规则可能匹配错误的问题,旧规则用正则解析内容含‘}’的json文本时,用规则中的字段去匹配这种内容会匹配错误.现改用平衡嵌套方法解决这个问题 + * */ fun getString(rule: String): String? { - if (TextUtils.isEmpty(rule)) return null - var result = "" - val rules: Array - val elementsType: String - if (rule.contains("&&")) { - rules = rule.splitNotBlank("&&") - elementsType = "&" - } else { - rules = rule.splitNotBlank("||") - elementsType = "|" - } + if (rule.isEmpty()) return null + var result: String + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||") + if (rules.size == 1) { - if (!rule.contains("{$.")) { - ctx?.let { - try { - val ob = it.read(rule) - result = if (ob is List<*>) { - val builder = StringBuilder() - for (o in ob) { - builder.append(o).append("\n") - } - builder.toString().replace("\\n$".toRegex(), "") - } else { - ob.toString() + + ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 + + result = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} + + if (result.isEmpty()) { //st为空,表明无成功替换的内嵌规则 + + try { + + val ob = ctx.read(rule) + + result = (if (ob is List<*>) { + + val builder = StringBuilder() + for (o in ob) { + builder.append(o).append("\n") } - } catch (ignored: Exception) { - } + builder.deleteCharAt(builder.lastIndex) //删除末尾赘余换行 + + builder + + } else ob).toString() + + } catch (ignored: Exception) { } - return result - } else { - result = rule - val matcher = jsonRulePattern.matcher(rule) - while (matcher.find()) { - result = result.replace( - String.format("{%s}", matcher.group()), - getString(matcher.group())!! - ) - } - return result + } + + return result + } else { val textList = arrayListOf() for (rl in rules) { val temp = getString(rl) if (!temp.isNullOrEmpty()) { textList.add(temp) - if (elementsType == "|") { + if (ruleAnalyzes.elementsType == "||") { break } } @@ -80,60 +82,48 @@ class AnalyzeByJSonPath { internal fun getStringList(rule: String): List { val result = ArrayList() - if (TextUtils.isEmpty(rule)) return result - val rules: Array - val elementsType: String - when { - rule.contains("&&") -> { - rules = rule.splitNotBlank("&&") - elementsType = "&" - } - rule.contains("%%") -> { - rules = rule.splitNotBlank("%%") - elementsType = "%" - } - else -> { - rules = rule.splitNotBlank("||") - elementsType = "|" - } - } + if (rule.isEmpty()) return result + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + if (rules.size == 1) { - if (!rule.contains("{$.")) { + + ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 + + val st = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} + + if (st.isEmpty()) { //st为空,表明无成功替换的内嵌规则 + try { - val obj = ctx!!.read(rule) ?: return result + + val obj = ctx.read(rule) //kotlin的Any型返回值不包含null ,删除赘余 ?: return result + if (obj is List<*>) { - for (o in obj) - result.add(o.toString()) - } else { - result.add(obj.toString()) - } + + for (o in obj) result.add(o.toString()) + + } else result.add(obj.toString()) + } catch (ignored: Exception) { } - return result - } else { - val matcher = jsonRulePattern.matcher(rule) - while (matcher.find()) { - val stringList = getStringList(matcher.group()) - for (s in stringList) { - result.add(rule.replace(String.format("{%s}", matcher.group()), s)) - } - } - return result - } + } else result.add(st) + + return result + } else { val results = ArrayList>() for (rl in rules) { val temp = getStringList(rl) if (temp.isNotEmpty()) { results.add(temp) - if (temp.isNotEmpty() && elementsType == "|") { + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { @@ -152,30 +142,16 @@ class AnalyzeByJSonPath { } internal fun getObject(rule: String): Any { - return ctx!!.read(rule) + return ctx.read(rule) } internal fun getList(rule: String): ArrayList? { val result = ArrayList() - if (TextUtils.isEmpty(rule)) return result - val elementsType: String - val rules: Array - when { - rule.contains("&&") -> { - rules = rule.splitNotBlank("&&") - elementsType = "&" - } - rule.contains("%%") -> { - rules = rule.splitNotBlank("%%") - elementsType = "%" - } - else -> { - rules = rule.splitNotBlank("||") - elementsType = "|" - } - } + if (rule.isEmpty()) return result + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") if (rules.size == 1) { - ctx?.let { + ctx.let { try { return it.read>(rules[0]) } catch (e: Exception) { @@ -189,13 +165,13 @@ class AnalyzeByJSonPath { val temp = getList(rl) if (temp != null && temp.isNotEmpty()) { results.add(temp) - if (temp.isNotEmpty() && elementsType == "|") { + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in 0 until results[0].size) { for (temp in results) { if (i < temp.size) { @@ -213,7 +189,4 @@ class AnalyzeByJSonPath { return result } - companion object { - private val jsonRulePattern = Pattern.compile("(?<=\\{)\\$\\..+?(?=\\})") - } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt index bf00f0d43..7bc7186e4 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt @@ -1,9 +1,6 @@ package io.legado.app.model.analyzeRule -import android.text.TextUtils.isEmpty -import android.text.TextUtils.join import androidx.annotation.Keep -import io.legado.app.utils.splitNotBlank import org.jsoup.Jsoup import org.jsoup.nodes.Element import org.jsoup.select.Collector @@ -17,107 +14,84 @@ import java.util.* * 书源规则解析 */ @Keep -class AnalyzeByJSoup { - private var element: Element? = null - - fun parse(doc: Any): AnalyzeByJSoup { - element = if (doc is Element) { - doc - } else if (doc is JXNode) { - if (doc.isElement) { - doc.asElement() - } else { - Jsoup.parse(doc.value().toString()) +class AnalyzeByJSoup(doc: Any) { + companion object { + + fun parse(doc: Any): Element { + return when (doc) { + is Element -> doc + is JXNode -> if (doc.isElement) doc.asElement() else Jsoup.parse(doc.toString()) + else -> Jsoup.parse(doc.toString()) } - } else { - Jsoup.parse(doc.toString()) } - return this + } + private var element: Element = parse(doc) + /** * 获取列表 */ - internal fun getElements(rule: String): Elements { - return getElements(element, rule) - } + internal fun getElements(rule: String) = getElements(element, rule) /** * 合并内容列表,得到内容 */ - internal fun getString(ruleStr: String): String? { - if (isEmpty(ruleStr)) { - return null - } - val textS = getStringList(ruleStr) - return if (textS.isEmpty()) { - null - } else { - textS.joinToString("\n") - } - - } + internal fun getString(ruleStr: String) = + if (ruleStr.isEmpty()) null + else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString("\n") /** * 获取一个字符串 */ - internal fun getString0(ruleStr: String): String { - val urlList = getStringList(ruleStr) - return if (urlList.isNotEmpty()) { - urlList[0] - } else "" - } + internal fun getString0(ruleStr: String) = + getStringList(ruleStr).let { if (it.isEmpty()) "" else it[0] } /** * 获取所有内容列表 */ internal fun getStringList(ruleStr: String): List { + val textS = ArrayList() - if (isEmpty(ruleStr)) { - return textS - } + + if (ruleStr.isEmpty()) return textS + //拆分规则 val sourceRule = SourceRule(ruleStr) - if (isEmpty(sourceRule.elementsRule)) { - textS.add(element?.data() ?: "") + + if (sourceRule.elementsRule.isEmpty()) { + + textS.add(element.data() ?: "") + } else { - val elementsType: String - val ruleStrS: Array - when { - sourceRule.elementsRule.contains("&&") -> { - elementsType = "&" - ruleStrS = sourceRule.elementsRule.splitNotBlank("&&") - } - sourceRule.elementsRule.contains("%%") -> { - elementsType = "%" - ruleStrS = sourceRule.elementsRule.splitNotBlank("%%") - } - else -> { - elementsType = "|" - ruleStrS = sourceRule.elementsRule.splitNotBlank("||") - } - } + + val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) + val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") + val results = ArrayList>() for (ruleStrX in ruleStrS) { - val temp: List? - temp = if (sourceRule.isCss) { - val lastIndex = ruleStrX.lastIndexOf('@') - getResultLast( - element!!.select(ruleStrX.substring(0, lastIndex)), - ruleStrX.substring(lastIndex + 1) - ) - } else { - getResultList(ruleStrX) - } - if (!temp.isNullOrEmpty()) { - results.add(temp) - if (results.isNotEmpty() && elementsType == "|") { - break + + val temp: List? = + if (sourceRule.isCss) { + val lastIndex = ruleStrX.lastIndexOf('@') + getResultLast( + element.select(ruleStrX.substring(0, lastIndex)), + ruleStrX.substring(lastIndex + 1) + ) + } else { + getResultList(ruleStrX) } + + if (!temp.isNullOrEmpty()) { + + results.add(temp) //!temp.isNullOrEmpty()时,results.isNotEmpty()为true + + if (ruleAnalyzes.elementsType == "||") break + } } if (results.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { @@ -139,47 +113,55 @@ class AnalyzeByJSoup { * 获取Elements */ private fun getElements(temp: Element?, rule: String): Elements { + + if (temp == null || rule.isEmpty()) return Elements() + val elements = Elements() - if (temp == null || isEmpty(rule)) { - return elements - } + val sourceRule = SourceRule(rule) - val elementsType: String - val ruleStrS: Array - when { - sourceRule.elementsRule.contains("&&") -> { - elementsType = "&" - ruleStrS = sourceRule.elementsRule.splitNotBlank("&&") - } - sourceRule.elementsRule.contains("%%") -> { - elementsType = "%" - ruleStrS = sourceRule.elementsRule.splitNotBlank("%%") - } - else -> { - elementsType = "|" - ruleStrS = sourceRule.elementsRule.splitNotBlank("||") - } - } + val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) + val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") + val elementsList = ArrayList() if (sourceRule.isCss) { for (ruleStr in ruleStrS) { val tempS = temp.select(ruleStr) elementsList.add(tempS) - if (tempS.size > 0 && elementsType == "|") { + if (tempS.size > 0 && ruleAnalyzes.elementsType == "||") { break } } } else { for (ruleStr in ruleStrS) { - val tempS = getElementsSingle(temp, ruleStr) - elementsList.add(tempS) - if (tempS.size > 0 && elementsType == "|") { + + val rsRule = RuleAnalyzer(ruleStr) + + rsRule.trim() // 修剪当前规则之前的"@"或者空白符 + + val rs = rsRule.splitRule("@") + + val el = if (rs.size > 1) { + val el = Elements() + el.add(temp) + for (rl in rs) { + val es = Elements() + for (et in el) { + es.addAll(getElements(et, rl)) + } + el.clear() + el.addAll(es) + } + el + } else ElementsSingle().getElementsSingle(temp, ruleStr) + + elementsList.add(el) + if (el.size > 0 && ruleAnalyzes.elementsType == "||") { break } } } if (elementsList.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in 0 until elementsList[0].size) { for (es in elementsList) { if (i < es.size) { @@ -196,160 +178,33 @@ class AnalyzeByJSoup { return elements } - private fun filterElements(elements: Elements, rules: Array?): Elements { - if (rules == null || rules.size < 2) return elements - val selectedEls = Elements() - for (ele in elements) { - var isOk = false - when (rules[0]) { - "class" -> isOk = ele.getElementsByClass(rules[1]).size > 0 - "id" -> isOk = ele.getElementById(rules[1]) != null - "tag" -> isOk = ele.getElementsByTag(rules[1]).size > 0 - "text" -> isOk = ele.getElementsContainingOwnText(rules[1]).size > 0 - } - if (isOk) { - selectedEls.add(ele) - } - } - return selectedEls - } - - /** - * 获取Elements按照一个规则 - */ - private fun getElementsSingle(temp: Element, rule: String): Elements { - val elements = Elements() - try { - val rs = rule.trim { it <= ' ' }.splitNotBlank("@") - if (rs.size > 1) { - elements.add(temp) - for (rl in rs) { - val es = Elements() - for (et in elements) { - es.addAll(getElements(et, rl)) - } - elements.clear() - elements.addAll(es) - } - } else { - val rulePcx = rule.splitNotBlank("!") - val rulePc = - rulePcx[0].trim { it <= ' ' }.splitNotBlank(">") - val rules = - rulePc[0].trim { it <= ' ' }.splitNotBlank(".") - var filterRules: Array? = null - var needFilterElements = rulePc.size > 1 && !isEmpty(rulePc[1].trim { it <= ' ' }) - if (needFilterElements) { - filterRules = rulePc[1].trim { it <= ' ' }.splitNotBlank(".") - filterRules[0] = filterRules[0].trim { it <= ' ' } - val validKeys = listOf("class", "id", "tag", "text") - if (filterRules.size < 2 || !validKeys.contains(filterRules[0]) || isEmpty(filterRules[1].trim { it <= ' ' })) { - needFilterElements = false - } - filterRules[1] = filterRules[1].trim { it <= ' ' } - } - when (rules[0]) { - "children" -> { - var children = temp.children() - if (needFilterElements) - children = filterElements(children, filterRules) - elements.addAll(children) - } - "class" -> { - var elementsByClass = temp.getElementsByClass(rules[1]) - if (rules.size == 3) { - val index = Integer.parseInt(rules[2]) - if (index < 0) { - elements.add(elementsByClass[elementsByClass.size + index]) - } else { - elements.add(elementsByClass[index]) - } - } else { - if (needFilterElements) - elementsByClass = filterElements(elementsByClass, filterRules) - elements.addAll(elementsByClass) - } - } - "tag" -> { - var elementsByTag = temp.getElementsByTag(rules[1]) - if (rules.size == 3) { - val index = Integer.parseInt(rules[2]) - if (index < 0) { - elements.add(elementsByTag[elementsByTag.size + index]) - } else { - elements.add(elementsByTag[index]) - } - } else { - if (needFilterElements) - elementsByTag = filterElements(elementsByTag, filterRules) - elements.addAll(elementsByTag) - } - } - "id" -> { - var elementsById = Collector.collect(Evaluator.Id(rules[1]), temp) - if (rules.size == 3) { - val index = Integer.parseInt(rules[2]) - if (index < 0) { - elements.add(elementsById[elementsById.size + index]) - } else { - elements.add(elementsById[index]) - } - } else { - if (needFilterElements) - elementsById = filterElements(elementsById, filterRules) - elements.addAll(elementsById) - } - } - "text" -> { - var elementsByText = temp.getElementsContainingOwnText(rules[1]) - if (needFilterElements) - elementsByText = filterElements(elementsByText, filterRules) - elements.addAll(elementsByText) - } - else -> elements.addAll(temp.select(rulePcx[0])) - } - if (rulePcx.size > 1) { - val rulePcs = rulePcx[1].splitNotBlank(":") - for (pc in rulePcs) { - val pcInt = Integer.parseInt(pc) - if (pcInt < 0 && elements.size + pcInt >= 0) { - elements[elements.size + pcInt] = null - } else if (Integer.parseInt(pc) < elements.size) { - elements[Integer.parseInt(pc)] = null - } - } - val es = Elements() - es.add(null) - elements.removeAll(es) - } - } - } catch (ignore: Exception) { - } - - return elements - } - /** * 获取内容列表 */ private fun getResultList(ruleStr: String): List? { - if (isEmpty(ruleStr)) { - return null - } + + if (ruleStr.isEmpty()) return null + var elements = Elements() + elements.add(element) - val rules = ruleStr.splitNotBlank("@") - for (i in 0 until rules.size - 1) { + + val rule = RuleAnalyzer(ruleStr) //创建解析 + + rule.trim() //修建前置赘余符号 + + val rules = rule.splitRule("@") // 切割成列表 + + val last = rules.size - 1 + for (i in 0 until last) { val es = Elements() for (elt in elements) { - es.addAll(getElementsSingle(elt, rules[i])) + es.addAll(ElementsSingle().getElementsSingle(elt, rules[i])) } elements.clear() elements = es } - return if (elements.isEmpty()) { - null - } else getResultLast(elements, rules[rules.size - 1]) + return if (elements.isEmpty()) null else getResultLast(elements, rules[last]) } /** @@ -367,11 +222,11 @@ class AnalyzeByJSoup { val contentEs = element.textNodes() for (item in contentEs) { val temp = item.text().trim { it <= ' ' } - if (!isEmpty(temp)) { + if (temp.isNotEmpty()) { tn.add(temp) } } - textS.add(join("\n", tn)) + textS.add(tn.joinToString("\n")) } "ownText" -> for (element in elements) { textS.add(element.ownText()) @@ -384,10 +239,12 @@ class AnalyzeByJSoup { } "all" -> textS.add(elements.outerHtml()) else -> for (element in elements) { + val url = element.attr(lastRule) - if (!isEmpty(url) && !textS.contains(url)) { - textS.add(url) - } + + if (url.isBlank() || textS.contains(url)) continue + + textS.add(url) } } } catch (e: Exception) { @@ -397,17 +254,237 @@ class AnalyzeByJSoup { return textS } - internal inner class SourceRule(ruleStr: String) { - var isCss = false - var elementsRule: String + /** + * 1.支持阅读原有写法,':'分隔索引,!或.表示筛选方式,索引可为负数 + * 例如 tag.div.-1:10:2 或 tag.div!0:3 + * + * 2. 支持与jsonPath类似的[]索引写法 + * 格式形如 [it,it,。。。] 或 [!it,it,。。。] 其中[!开头表示筛选方式为排除,it为单个索引或区间。 + * 区间格式为 start:end 或 start:end:step,其中start为0可省略,end为-1可省略。 + * 索引,区间两端及间隔都支持负数 + * 例如 tag.div[-1, 3:-2:-10, 2] + * 特殊用法 tag.div[-1:0] 可在任意地方让列表反向 + * */ + @Suppress("UNCHECKED_CAST") + data class ElementsSingle( + var split: Char = '.', + var beforeRule: String = "", + val indexDefault: MutableList = mutableListOf(), + val indexs: MutableList = mutableListOf() + ) { + /** + * 获取Elements按照一个规则 + */ + fun getElementsSingle(temp: Element, rule: String): Elements { + + findIndexSet(rule) //执行索引列表处理器 + + /** + * 获取所有元素 + * */ + var elements = + if (beforeRule.isEmpty()) temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 + else { + val rules = beforeRule.split(".") + when (rules[0]) { + "children" -> temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 + "class" -> temp.getElementsByClass(rules[1]) + "tag" -> temp.getElementsByTag(rules[1]) + "id" -> Collector.collect(Evaluator.Id(rules[1]), temp) + "text" -> temp.getElementsContainingOwnText(rules[1]) + else -> temp.select(beforeRule) + } + } + + val len = elements.size + val lastIndexs = (indexDefault.size - 1).takeIf { it != -1 } ?: indexs.size - 1 + val indexSet = mutableSetOf() + + /** + * 获取无重且不越界的索引集合 + * */ + if (indexs.isEmpty()) for (ix in lastIndexs downTo 0) { //indexs为空,表明是非[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 + + val it = indexDefault[ix] + if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 + else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 + + } else for (ix in lastIndexs downTo 0) { //indexs不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 + + if (indexs[ix] is Triple<*, *, *>) { //区间 + val (startx, endx, stepx) = indexs[ix] as Triple //还原储存时的类型 + + val start = if (startx == null) 0 //左端省略表示0 + else if (startx >= 0) if (startx < len) startx else len - 1 //右端越界,设置为最大索引 + else if (-startx <= len) len + startx /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 + + val end = if (endx == null) len - 1 //右端省略表示 len - 1 + else if (endx >= 0) if (endx < len) endx else len - 1 //右端越界,设置为最大索引 + else if (-endx <= len) len + endx /* 将负索引转正 */ else 0 //左端越界,设置为最小索引 + + if (start == end || stepx >= len) { //两端相同,区间里只有一个数。或间隔过大,区间实际上仅有首位 + + indexSet.add(start) + continue + + } + + val step = + if (stepx > 0) stepx else if (-stepx < len) stepx + len else 1 //最小正数间隔为1 + + //将区间展开到集合中,允许列表反向。 + indexSet.addAll(if (end > start) start..end step step else start downTo end step step) + + } else {//单个索引 + + val it = indexs[ix] as Int //还原储存时的类型 + + if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 + else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 + + } + + } + + /** + * 根据索引集合筛选元素 + * */ + if (split == '!') { //排除 + + for (pcInt in indexSet) elements[pcInt] = null + + elements.removeAll(listOf(null)) //测试过,这样就行 + + } else if (split == '.') { //选择 + + val es = Elements() + + for (pcInt in indexSet) es.add(elements[pcInt]) + + elements = es + + } + + return elements //返回筛选结果 + + } + + private fun findIndexSet(rule: String) { + + val rus = rule.trim { it <= ' ' } + + var len = rus.length + var curInt: Int? //当前数字 + var curMinus = false //当前数字是否为负 + val curList = mutableListOf() //当前数字区间 + var l = "" //暂存数字字符串 + + val head = rus.last() == ']' //是否为常规索引写法 + + if (head) { //常规索引写法[index...] + + len-- //跳过尾部']' + + while (len-- >= 0) { //逆向遍历,可以无前置规则 + + var rl = rus[len] + if (rl == ' ') continue //跳过空格 + + if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 + else if (rl == '-') curMinus = true + else { + + curInt = + if (l.isEmpty()) null else if (curMinus) -l.toInt() else l.toInt() //当前数字 + + when (rl) { + + ':' -> curList.add(curInt) //区间右端或区间间隔 + + else -> { + + //为保证查找顺序,区间和单个索引都添加到同一集合 + if (curList.isEmpty()) { + + if (curInt == null) break //是jsoup选择器而非索引列表,跳出 + + indexs.add(curInt) + } else { + + //列表最后压入的是区间右端,若列表有两位则最先压入的是间隔 + indexs.add( + Triple( + curInt, + curList.last(), + if (curList.size == 2) curList.first() else 1 + ) + ) + + curList.clear() //重置临时列表,避免影响到下个区间的处理 + + } + + if (rl == '!') { + split = '!' + do { + rl = rus[--len] + } while (len > 0 && rl == ' ')//跳过所有空格 + } + + if (rl == '[') { + beforeRule = rus.substring(0, len) //遇到索引边界,返回结果 + return + } + + if (rl != ',') break //非索引结构,跳出 + + } + } + + l = "" //清空 + curMinus = false //重置 + } + } + } else while (len-- >= 0) { //阅读原本写法,逆向遍历,可以无前置规则 + + val rl = rus[len] + if (rl == ' ') continue //跳过空格 + + if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 + else if (rl == '-') curMinus = true + else { + + if (rl == '!' || rl == '.' || rl == ':') { //分隔符或起始符 + + indexDefault.add(if (curMinus) -l.toInt() else l.toInt()) // 当前数字追加到列表 + + if (rl != ':') { //rl == '!' || rl == '.' + split = rl + beforeRule = rus.substring(0, len) + return + } + + } else break //非索引结构,跳出循环 + + l = "" //清空 + curMinus = false //重置 + } - init { - if (ruleStr.startsWith("@CSS:", true)) { - isCss = true - elementsRule = ruleStr.substring(5).trim { it <= ' ' } - } else { - elementsRule = ruleStr } + + split = ' ' + beforeRule = rus + } + } + + + internal inner class SourceRule(ruleStr: String) { + var isCss = false + var elementsRule: String = if (ruleStr.startsWith("@CSS:", true)) { + isCss = true + ruleStr.substring(5).trim { it <= ' ' } + } else { + ruleStr } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt index 0439713e8..a67d934f2 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt @@ -2,7 +2,6 @@ package io.legado.app.model.analyzeRule import android.text.TextUtils import androidx.annotation.Keep -import io.legado.app.utils.splitNotBlank import org.jsoup.nodes.Document import org.jsoup.nodes.Element import org.jsoup.select.Elements @@ -11,80 +10,62 @@ import org.seimicrawler.xpath.JXNode import java.util.* @Keep -class AnalyzeByXPath { - private var jxDocument: JXDocument? = null - private var jxNode: JXNode? = null +class AnalyzeByXPath(doc: Any) { + private var jxNode: Any = parse(doc) - fun parse(doc: Any): AnalyzeByXPath { - if (doc is JXNode) { - jxNode = doc - if (jxNode?.isElement == false) { - jxDocument = strToJXDocument(doc.toString()) - jxNode = null - } - } else if (doc is Document) { - jxDocument = JXDocument.create(doc) - jxNode = null - } else if (doc is Element) { - jxDocument = JXDocument.create(Elements(doc)) - jxNode = null - } else if (doc is Elements) { - jxDocument = JXDocument.create(doc) - jxNode = null - } else { - jxDocument = strToJXDocument(doc.toString()) - jxNode = null + private fun parse(doc: Any): Any { + return when (doc) { + is JXNode -> if (doc.isElement) doc else strToJXDocument(doc.toString()) + is Document -> JXDocument.create(doc) + is Element -> JXDocument.create(Elements(doc)) + is Elements -> JXDocument.create(doc) + else -> strToJXDocument(doc.toString()) } - return this } private fun strToJXDocument(html: String): JXDocument { var html1 = html if (html1.endsWith("")) { - html1 = String.format("%s", html1) + html1 = "${html1}" } if (html1.endsWith("") || html1.endsWith("")) { - html1 = String.format("%s
    ", html1) + html1 = "${html1}
    " } return JXDocument.create(html1) } - internal fun getElements(xPath: String): List? { - if (TextUtils.isEmpty(xPath)) { - return null + private fun getResult(xPath: String): List? { + val node = jxNode + return if (node is JXNode) { + node.sel(xPath) + } else { + (node as JXDocument).selN(xPath) } + } + + internal fun getElements(xPath: String): List? { + + if (xPath.isEmpty()) return null + val jxNodes = ArrayList() - val elementsType: String - val rules: Array - when { - xPath.contains("&&") -> { - rules = xPath.splitNotBlank("&&") - elementsType = "&" - } - xPath.contains("%%") -> { - rules = xPath.splitNotBlank("%%") - elementsType = "%" - } - else -> { - rules = xPath.splitNotBlank("||") - elementsType = "|" - } - } + val ruleAnalyzes = RuleAnalyzer(xPath) + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + if (rules.size == 1) { - return jxNode?.sel(rules[0]) ?: jxDocument?.selN(rules[0]) + return getResult(rules[0]) } else { val results = ArrayList>() for (rl in rules) { val temp = getElements(rl) if (temp != null && temp.isNotEmpty()) { results.add(temp) - if (temp.isNotEmpty() && elementsType == "|") { + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { @@ -103,26 +84,13 @@ class AnalyzeByXPath { } internal fun getStringList(xPath: String): List { + val result = ArrayList() - val elementsType: String - val rules: Array - when { - xPath.contains("&&") -> { - rules = xPath.splitNotBlank("&&") - elementsType = "&" - } - xPath.contains("%%") -> { - rules = xPath.splitNotBlank("%%") - elementsType = "%" - } - else -> { - rules = xPath.splitNotBlank("||") - elementsType = "|" - } - } + val ruleAnalyzes = RuleAnalyzer(xPath) + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + if (rules.size == 1) { - val jxNodes = jxNode?.sel(xPath) ?: jxDocument?.selN(xPath) - jxNodes?.map { + getResult(xPath)?.map { result.add(it.asString()) } return result @@ -132,13 +100,13 @@ class AnalyzeByXPath { val temp = getStringList(rl) if (temp.isNotEmpty()) { results.add(temp) - if (temp.isNotEmpty() && elementsType == "|") { + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { break } } } if (results.size > 0) { - if ("%" == elementsType) { + if ("%%" == ruleAnalyzes.elementsType) { for (i in results[0].indices) { for (temp in results) { if (i < temp.size) { @@ -157,19 +125,11 @@ class AnalyzeByXPath { } fun getString(rule: String): String? { - val rules: Array - val elementsType: String - if (rule.contains("&&")) { - rules = rule.splitNotBlank("&&") - elementsType = "&" - } else { - rules = rule.splitNotBlank("||") - elementsType = "|" - } + val ruleAnalyzes = RuleAnalyzer(rule) + val rules = ruleAnalyzes.splitRule("&&", "||") if (rules.size == 1) { - val jxNodes = jxNode?.sel(rule) ?: jxDocument?.selN(rule) - jxNodes?.let { - return TextUtils.join("\n", jxNodes) + getResult(rule)?.let { + return TextUtils.join("\n", it) } return null } else { @@ -178,7 +138,7 @@ class AnalyzeByXPath { val temp = getString(rl) if (!temp.isNullOrEmpty()) { textList.add(temp) - if (elementsType == "|") { + if (ruleAnalyzes.elementsType == "||") { break } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index b83a3e730..007fe1b4e 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -6,26 +6,36 @@ import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppPattern.JS_PATTERN import io.legado.app.data.entities.BaseBook import io.legado.app.data.entities.BookChapter +import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions +import io.legado.app.help.http.CookieStore import io.legado.app.utils.* +import kotlinx.coroutines.runBlocking import org.jsoup.nodes.Entities import org.mozilla.javascript.NativeObject +import java.net.URL import java.util.* import java.util.regex.Pattern import javax.script.SimpleBindings import kotlin.collections.HashMap - /** - * Created by REFGD. - * 统一解析接口 + * 解析规则获取结果 */ @Keep -@Suppress("unused") -class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { +@Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate") +class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions { + + var book = if (ruleData is BaseBook) ruleData else null + var chapter: BookChapter? = null - private var content: Any? = null - private var baseUrl: String? = null + var nextChapterUrl: String? = null + var content: Any? = null + private set + var baseUrl: String? = null + private set + var redirectUrl: URL? = null + private set private var isJSON: Boolean = false private var isRegex: Boolean = false @@ -37,35 +47,46 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { private var objectChangedJS = false private var objectChangedJP = false - @Throws(Exception::class) @JvmOverloads - fun setContent(content: Any?, baseUrl: String? = this.baseUrl): AnalyzeRule { - if (content == null) throw AssertionError("Content cannot be null") - isJSON = content.toString().isJson() + fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule { + if (content == null) throw AssertionError("内容不可空(Content cannot be null)") this.content = content - this.baseUrl = baseUrl + isJSON = content.toString().isJson() + setBaseUrl(baseUrl) objectChangedXP = true objectChangedJS = true objectChangedJP = true return this } + fun setBaseUrl(baseUrl: String?): AnalyzeRule { + baseUrl?.let { + this.baseUrl = baseUrl + } + return this + } + + fun setRedirectUrl(url: String): URL? { + kotlin.runCatching { + val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) + redirectUrl = URL(if (urlMatcher.find()) url.substring(0, urlMatcher.start()) else url) + } + return redirectUrl + } + /** * 获取XPath解析类 */ private fun getAnalyzeByXPath(o: Any): AnalyzeByXPath { return if (o != content) { - AnalyzeByXPath().parse(o) - } else getAnalyzeByXPath() - } - - private fun getAnalyzeByXPath(): AnalyzeByXPath { - if (analyzeByXPath == null || objectChangedXP) { - analyzeByXPath = AnalyzeByXPath() - analyzeByXPath?.parse(content!!) - objectChangedXP = false + AnalyzeByXPath(o) + } else { + if (analyzeByXPath == null || objectChangedXP) { + analyzeByXPath = AnalyzeByXPath(content!!) + objectChangedXP = false + } + analyzeByXPath!! } - return analyzeByXPath as AnalyzeByXPath } /** @@ -73,17 +94,14 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { */ private fun getAnalyzeByJSoup(o: Any): AnalyzeByJSoup { return if (o != content) { - AnalyzeByJSoup().parse(o) - } else getAnalyzeByJSoup() - } - - private fun getAnalyzeByJSoup(): AnalyzeByJSoup { - if (analyzeByJSoup == null || objectChangedJS) { - analyzeByJSoup = AnalyzeByJSoup() - analyzeByJSoup?.parse(content!!) - objectChangedJS = false + AnalyzeByJSoup(o) + } else { + if (analyzeByJSoup == null || objectChangedJS) { + analyzeByJSoup = AnalyzeByJSoup(content!!) + objectChangedJS = false + } + analyzeByJSoup!! } - return analyzeByJSoup as AnalyzeByJSoup } /** @@ -91,31 +109,27 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { */ private fun getAnalyzeByJSonPath(o: Any): AnalyzeByJSonPath { return if (o != content) { - AnalyzeByJSonPath().parse(o) - } else getAnalyzeByJSonPath() - } - - private fun getAnalyzeByJSonPath(): AnalyzeByJSonPath { - if (analyzeByJSonPath == null || objectChangedJP) { - analyzeByJSonPath = AnalyzeByJSonPath() - analyzeByJSonPath?.parse(content!!) - objectChangedJP = false + AnalyzeByJSonPath(o) + } else { + if (analyzeByJSonPath == null || objectChangedJP) { + analyzeByJSonPath = AnalyzeByJSonPath(content!!) + objectChangedJP = false + } + analyzeByJSonPath!! } - return analyzeByJSonPath as AnalyzeByJSonPath } /** * 获取文本列表 */ - @Throws(Exception::class) @JvmOverloads fun getStringList(rule: String?, isUrl: Boolean = false): List? { if (rule.isNullOrEmpty()) return null - val ruleList = splitSourceRule(rule) + val ruleList = splitSourceRule(rule, true) return getStringList(ruleList, isUrl) } - @Throws(Exception::class) + @JvmOverloads fun getStringList(ruleList: List, isUrl: Boolean = false): List? { var result: Any? = null val content = this.content @@ -158,8 +172,8 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { val urlList = ArrayList() if (result is List<*>) { for (url in result as List<*>) { - val absoluteURL = NetworkUtils.getAbsoluteURL(baseUrl, url.toString()) - if (!absoluteURL.isNullOrEmpty() && !urlList.contains(absoluteURL)) { + val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString()) + if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) { urlList.add(absoluteURL) } } @@ -173,22 +187,25 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { /** * 获取文本 */ - @Throws(Exception::class) - fun getString(ruleStr: String?, isUrl: Boolean = false): String { + @JvmOverloads + fun getString(ruleStr: String?, isUrl: Boolean = false, value: String? = null): String { if (TextUtils.isEmpty(ruleStr)) return "" val ruleList = splitSourceRule(ruleStr) - return getString(ruleList, isUrl) + return getString(ruleList, isUrl, value) } - @Throws(Exception::class) @JvmOverloads - fun getString(ruleList: List, isUrl: Boolean = false): String { - var result: Any? = null + fun getString( + ruleList: List, + isUrl: Boolean = false, + value: String? = null + ): String { + var result: Any? = value val content = this.content - if (content != null && ruleList.isNotEmpty()) { - result = content - if (content is NativeObject) { - result = content[ruleList[0].rule]?.toString() + if ((content != null || result != null) && ruleList.isNotEmpty()) { + if (result == null) result = content + if (result is NativeObject) { + result = result[ruleList[0].rule]?.toString() } else { for (sourceRule in ruleList) { putRule(sourceRule.putMap) @@ -215,13 +232,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } } if (result == null) result = "" - val str = try { + val str = kotlin.runCatching { Entities.unescape(result.toString()) - } catch (e: Exception) { + }.getOrElse { result.toString() } if (isUrl) { - return NetworkUtils.getAbsoluteURL(baseUrl, str) ?: "" + return if (str.isBlank()) { + baseUrl ?: "" + } else { + NetworkUtils.getAbsoluteURL(redirectUrl, str) + } } return str } @@ -229,7 +250,6 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { /** * 获取Element */ - @Throws(Exception::class) fun getElement(ruleStr: String): Any? { if (TextUtils.isEmpty(ruleStr)) return null var result: Any? = null @@ -238,6 +258,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { if (ruleList.isNotEmpty()) result = o for (sourceRule in ruleList) { putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) result?.let { result = when (sourceRule.mode) { Mode.Regex -> AnalyzeByRegex.getElement( @@ -262,10 +283,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { * 获取列表 */ @Suppress("UNCHECKED_CAST") - @Throws(Exception::class) fun getElements(ruleStr: String): List { var result: Any? = null - val ruleList = splitSourceRule(ruleStr) + val ruleList = splitSourceRule(ruleStr, true) content?.let { o -> if (ruleList.isNotEmpty()) result = o for (sourceRule in ruleList) { @@ -293,11 +313,9 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { return ArrayList() } - /** * 保存变量 */ - @Throws(Exception::class) private fun putRule(map: Map) { for ((key, value) in map) { put(key, getString(value)) @@ -307,7 +325,6 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { /** * 分离put规则 */ - @Throws(Exception::class) private fun splitPutRule(ruleStr: String, putMap: HashMap): String { var vRuleStr = ruleStr val putMatcher = putPattern.matcher(vRuleStr) @@ -323,9 +340,10 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { * 正则替换 */ private fun replaceRegex(result: String, rule: SourceRule): String { + if (rule.replaceRegex.isEmpty()) return result var vResult = result - if (rule.replaceRegex.isNotEmpty()) { - vResult = if (rule.replaceFirst) { + vResult = if (rule.replaceFirst) { + kotlin.runCatching { val pattern = Pattern.compile(rule.replaceRegex) val matcher = pattern.matcher(vResult) if (matcher.find()) { @@ -333,8 +351,14 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } else { "" } - } else { + }.getOrElse { + vResult.replaceFirst(rule.replaceRegex, rule.replacement) + } + } else { + kotlin.runCatching { vResult.replace(rule.replaceRegex.toRegex(), rule.replacement) + }.getOrElse { + vResult.replace(rule.replaceRegex, rule.replacement) } } return vResult @@ -343,61 +367,49 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { /** * 分解规则生成规则列表 */ - @Throws(Exception::class) - fun splitSourceRule(ruleStr: String?, mode: Mode = Mode.Default): List { - var vRuleStr = ruleStr + fun splitSourceRule(ruleStr: String?, isList: Boolean = false): List { + if (ruleStr.isNullOrEmpty()) return ArrayList() val ruleList = ArrayList() - if (vRuleStr.isNullOrEmpty()) return ruleList - //检测Mode - var mMode: Mode = mode - when { - vRuleStr.startsWith("@@") -> { - vRuleStr = vRuleStr.substring(2) - } - vRuleStr.startsWith("@XPath:", true) -> { - mMode = Mode.XPath - vRuleStr = vRuleStr.substring(7) - } - vRuleStr.startsWith("@Json:", true) -> { - mMode = Mode.Json - vRuleStr = vRuleStr.substring(6) - } - vRuleStr.startsWith(":") -> { - mMode = Mode.Regex - isRegex = true - vRuleStr = vRuleStr.substring(1) - } - isRegex -> mMode = Mode.Regex - isJSON -> mMode = Mode.Json - } - //拆分为规则列表 + var mMode: Mode = Mode.Default var start = 0 + //仅首字符为:时为AllInOne,其实:与伪类选择器冲突,建议改成?更合理 + if (isList && ruleStr.startsWith(":")) { + mMode = Mode.Regex + isRegex = true + start = 1 + } else if (isRegex) { + mMode = Mode.Regex + } var tmp: String - val jsMatcher = JS_PATTERN.matcher(vRuleStr) + val jsMatcher = JS_PATTERN.matcher(ruleStr) while (jsMatcher.find()) { if (jsMatcher.start() > start) { - tmp = vRuleStr.substring(start, jsMatcher.start()).trim { it <= ' ' } - if (!TextUtils.isEmpty(tmp)) { + tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' } + if (tmp.isNotEmpty()) { ruleList.add(SourceRule(tmp, mMode)) } } - ruleList.add(SourceRule(jsMatcher.group(), Mode.Js)) + ruleList.add(SourceRule(jsMatcher.group(2) ?: jsMatcher.group(1), Mode.Js)) start = jsMatcher.end() } - if (vRuleStr.length > start) { - tmp = vRuleStr.substring(start).trim { it <= ' ' } - if (!TextUtils.isEmpty(tmp)) { + + if (ruleStr.length > start) { + tmp = ruleStr.substring(start).trim { it <= ' ' } + if (tmp.isNotEmpty()) { ruleList.add(SourceRule(tmp, mMode)) } } + return ruleList } /** * 规则类 */ - inner class SourceRule internal constructor(ruleStr: String, mainMode: Mode = Mode.Default) { - internal var mode: Mode + inner class SourceRule internal constructor( + ruleStr: String, + internal var mode: Mode = Mode.Default + ) { internal var rule: String internal var replaceRegex = "" internal var replacement = "" @@ -405,108 +417,115 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { internal val putMap = HashMap() private val ruleParam = ArrayList() private val ruleType = ArrayList() + private val getRuleType = -2 + private val jsRuleType = -1 + private val defaultRuleType = 0 init { - this.mode = mainMode - if (mode == Mode.Js) { - rule = if (ruleStr.startsWith("")) { - ruleStr.substring(4, ruleStr.lastIndexOf("<")) - } else { - ruleStr.substring(4) + rule = when { + mode == Mode.Js || mode == Mode.Regex -> ruleStr + ruleStr.startsWith("@CSS:", true) -> { + mode = Mode.Default + ruleStr } - } else { - when { - ruleStr.startsWith("@CSS:", true) -> { - mode = Mode.Default - rule = ruleStr - } - ruleStr.startsWith("@@") -> { - mode = Mode.Default - rule = ruleStr.substring(2) - } - ruleStr.startsWith("@XPath:", true) -> { - mode = Mode.XPath - rule = ruleStr.substring(7) - } - ruleStr.startsWith("//") -> {//XPath特征很明显,无需配置单独的识别标头 - mode = Mode.XPath - rule = ruleStr - } - ruleStr.startsWith("@Json:", true) -> { - mode = Mode.Json - rule = ruleStr.substring(6) - } - ruleStr.startsWith("$.") -> { - mode = Mode.Json - rule = ruleStr - } - else -> rule = ruleStr + ruleStr.startsWith("@@") -> { + mode = Mode.Default + ruleStr.substring(2) + } + ruleStr.startsWith("@XPath:", true) -> { + mode = Mode.XPath + ruleStr.substring(7) + } + ruleStr.startsWith("@Json:", true) -> { + mode = Mode.Json + ruleStr.substring(6) + } + isJSON || ruleStr.startsWith("$.") || ruleStr.startsWith("$[") -> { + mode = Mode.Json + ruleStr } + ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头 + mode = Mode.XPath + ruleStr + } + else -> ruleStr } //分离put rule = splitPutRule(rule, putMap) - //分离正则表达式 - val index = rule.indexOf("}}") - var rule1 = "" - var rule2 = rule - if (index > 0) { - rule1 = rule.substring(0, index) - rule2 = rule.substring(index) - } - val ruleStrS = rule2.trim { it <= ' ' }.split("##") - rule = rule1 + ruleStrS[0] - if (ruleStrS.size > 1) { - replaceRegex = ruleStrS[1] - } - if (ruleStrS.size > 2) { - replacement = ruleStrS[2] - } - if (ruleStrS.size > 3) { - replaceFirst = true - } - //@get,{{ }},$1, 拆分 + //@get,{{ }}, 拆分 var start = 0 var tmp: String val evalMatcher = evalPattern.matcher(rule) - while (evalMatcher.find()) { - if (mode != Mode.Js) { + + if (evalMatcher.find()) { + tmp = rule.substring(start, evalMatcher.start()) + if (mode != Mode.Js && mode != Mode.Regex && + (evalMatcher.start() == 0 || !tmp.contains("##")) + ) { mode = Mode.Regex } - if (evalMatcher.start() > start) { - tmp = rule.substring(start, evalMatcher.start()) - ruleType.add(0) - ruleParam.add(tmp) - } - tmp = evalMatcher.group() - when { - tmp.startsWith("$") -> { - ruleType.add(tmp.substring(1).toInt()) - ruleParam.add(tmp) - } - tmp.startsWith("@get:", true) -> { - ruleType.add(-2) - ruleParam.add(tmp.substring(6, tmp.lastIndex)) + do { + if (evalMatcher.start() > start) { + tmp = rule.substring(start, evalMatcher.start()) + splitRegex(tmp) } - tmp.startsWith("{{") -> { - ruleType.add(-1) - ruleParam.add(tmp.substring(2, tmp.length - 2)) - } - else -> { - ruleType.add(0) - ruleParam.add(tmp) + tmp = evalMatcher.group() + when { + tmp.startsWith("@get:", true) -> { + ruleType.add(getRuleType) + ruleParam.add(tmp.substring(6, tmp.lastIndex)) + } + tmp.startsWith("{{") -> { + ruleType.add(jsRuleType) + ruleParam.add(tmp.substring(2, tmp.length - 2)) + } + else -> { + splitRegex(tmp) + } } - } - start = evalMatcher.end() + start = evalMatcher.end() + } while (evalMatcher.find()) } if (rule.length > start) { tmp = rule.substring(start) - ruleType.add(0) + splitRegex(tmp) + } + } + + /** + * 拆分\$\d{1,2} + */ + private fun splitRegex(ruleStr: String) { + var start = 0 + var tmp: String + val ruleStrArray = ruleStr.split("##") + val regexMatcher = regexPattern.matcher(ruleStrArray[0]) + + if (regexMatcher.find()) { + if (mode != Mode.Js && mode != Mode.Regex) { + mode = Mode.Regex + } + do { + if (regexMatcher.start() > start) { + tmp = ruleStr.substring(start, regexMatcher.start()) + ruleType.add(defaultRuleType) + ruleParam.add(tmp) + } + tmp = regexMatcher.group() + ruleType.add(tmp.substring(1).toInt()) + ruleParam.add(tmp) + start = regexMatcher.end() + } while (regexMatcher.find()) + } + if (ruleStr.length > start) { + tmp = ruleStr.substring(start) + ruleType.add(defaultRuleType) ruleParam.add(tmp) } } /** - * 替换@get,{{ }},$1, + * 替换@get,{{ }} */ fun makeUpRule(result: Any?) { val infoVal = StringBuilder() @@ -515,20 +534,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { while (index-- > 0) { val regType = ruleType[index] when { - regType > 0 -> { + regType > defaultRuleType -> { @Suppress("UNCHECKED_CAST") - val resultList = result as? List - if (resultList != null) { - if (resultList.size > regType) { - resultList[regType]?.let { - infoVal.insert(0, resultList[regType]) + (result as? List)?.run { + if (this.size > regType) { + this[regType]?.let { + infoVal.insert(0, it) } } - } else { - infoVal.insert(0, ruleParam[index]) - } + } ?: infoVal.insert(0, ruleParam[index]) } - regType == -1 -> { + regType == jsRuleType -> { if (isRule(ruleParam[index])) { getString(arrayListOf(SourceRule(ruleParam[index]))).let { infoVal.insert(0, it) @@ -546,7 +562,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } } } - regType == -2 -> { + regType == getRuleType -> { infoVal.insert(0, get(ruleParam[index])) } else -> infoVal.insert(0, ruleParam[index]) @@ -554,18 +570,25 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } rule = infoVal.toString() } + //分离正则表达式 + val ruleStrS = rule.split("##") + rule = ruleStrS[0].trim() + if (ruleStrS.size > 1) { + replaceRegex = ruleStrS[1] + } + if (ruleStrS.size > 2) { + replacement = ruleStrS[2] + } + if (ruleStrS.size > 3) { + replaceFirst = true + } } private fun isRule(ruleStr: String): Boolean { - return when { - ruleStr.startsWith("$.") -> true - ruleStr.startsWith("@Json:", true) -> true - ruleStr.startsWith("//") -> true - ruleStr.startsWith("@XPath:", true) -> true - ruleStr.startsWith("@CSS:", true) -> true - ruleStr.startsWith("@@") -> true - else -> false - } + return ruleStr.startsWith('@') //js首个字符不可能是@,除非是装饰器,所以@开头规定为规则 + || ruleStr.startsWith("$.") + || ruleStr.startsWith("$[") + || ruleStr.startsWith("//") } } @@ -576,43 +599,56 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { fun put(key: String, value: String): String { chapter?.putVariable(key, value) ?: book?.putVariable(key, value) + ?: ruleData.putVariable(key, value) return value } fun get(key: String): String { + when (key) { + "bookName" -> book?.let { + return it.name + } + "title" -> chapter?.let { + return it.title + } + } return chapter?.variableMap?.get(key) ?: book?.variableMap?.get(key) + ?: ruleData.variableMap[key] ?: "" } /** * 执行JS */ - private fun evalJS(jsStr: String, result: Any?): Any? { - try { - val bindings = SimpleBindings() - bindings["java"] = this - bindings["book"] = book - bindings["result"] = result - bindings["baseUrl"] = baseUrl - return SCRIPT_ENGINE.eval(jsStr, bindings) - } catch (e: Exception) { - e.printStackTrace() - throw e - } + fun evalJS(jsStr: String, result: Any?): Any? { + val bindings = SimpleBindings() + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + bindings["book"] = book + bindings["result"] = result + bindings["baseUrl"] = baseUrl + bindings["chapter"] = chapter + bindings["title"] = chapter?.title + bindings["src"] = content + bindings["nextChapterUrl"] = nextChapterUrl + return SCRIPT_ENGINE.eval(jsStr, bindings) } /** * js实现跨域访问,不能删 */ override fun ajax(urlStr: String): String? { - return try { - val analyzeUrl = AnalyzeUrl(urlStr, baseUrl = baseUrl, book = book) - val call = analyzeUrl.getResponse(urlStr) - val response = call.execute() - response.body() - } catch (e: Exception) { - e.localizedMessage + return runBlocking { + kotlin.runCatching { + val analyzeUrl = AnalyzeUrl(urlStr, book = book) + analyzeUrl.getStrResponse(urlStr).body + }.onFailure { + it.printStackTrace() + }.getOrElse { + it.msg + } } } @@ -620,25 +656,20 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { * 章节数转数字 */ fun toNumChapter(s: String?): String? { - if (s == null) { - return null - } - val pattern = Pattern.compile("(第)(.+?)(章)") - val matcher = pattern.matcher(s) - return if (matcher.find()) { - matcher.group(1)!! + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3) - } else { - s + s ?: return null + val matcher = titleNumPattern.matcher(s) + if (matcher.find()) { + return "${matcher.group(1)}${StringUtils.stringToInt(matcher.group(2))}${matcher.group(3)}" } + return s } companion object { private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) - private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE) - private val evalPattern = Pattern.compile( - "@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}|\\$\\d{1,2}", - Pattern.CASE_INSENSITIVE - ) + private val evalPattern = + Pattern.compile("@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE) + private val regexPattern = Pattern.compile("\\$\\d{1,2}") + private val titleNumPattern = Pattern.compile("(第)(.+?)(章)") } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 26069af22..d37fda70f 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -1,25 +1,19 @@ package io.legado.app.model.analyzeRule import android.annotation.SuppressLint -import android.text.TextUtils import androidx.annotation.Keep import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.AppConst.UA_NAME -import io.legado.app.constant.AppConst.userAgent -import io.legado.app.constant.AppPattern.EXP_PATTERN import io.legado.app.constant.AppPattern.JS_PATTERN import io.legado.app.data.entities.BaseBook +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.AppConfig +import io.legado.app.help.CacheManager import io.legado.app.help.JsExtensions import io.legado.app.help.http.* -import io.legado.app.help.http.api.HttpGetApi -import io.legado.app.help.http.api.HttpPostApi import io.legado.app.utils.* -import okhttp3.FormBody -import okhttp3.MediaType -import okhttp3.RequestBody -import retrofit2.Call import java.net.URLEncoder import java.util.* import java.util.regex.Pattern @@ -33,38 +27,37 @@ import javax.script.SimpleBindings @SuppressLint("DefaultLocale") class AnalyzeUrl( var ruleUrl: String, - key: String? = null, - page: Int? = null, - speakText: String? = null, - speakSpeed: Int? = null, - headerMapF: Map? = null, - baseUrl: String? = null, - val book: BaseBook? = null, + val key: String? = null, + val page: Int? = null, + val speakText: String? = null, + val speakSpeed: Int? = null, + var baseUrl: String = "", var useWebView: Boolean = false, + val book: BaseBook? = null, + val chapter: BookChapter? = null, + private val ruleData: RuleDataInterface? = null, + headerMapF: Map? = null ) : JsExtensions { companion object { + val paramPattern: Pattern = Pattern.compile("\\s*,\\s*(?=\\{)") private val pagePattern = Pattern.compile("<(.*?)>") - private val jsonType = MediaType.parse("application/json; charset=utf-8") } - private var baseUrl: String = "" - lateinit var url: String - private set - private lateinit var urlHasQuery: String + var url: String = "" val headerMap = HashMap() + var body: String? = null + var type: String? = null + private lateinit var urlHasQuery: String private var queryStr: String? = null private val fieldMap = LinkedHashMap() private var charset: String? = null - private var body: String? = null - private var requestBody: RequestBody? = null private var method = RequestMethod.GET - private val splitUrlRegex = Regex(",\\s*(?=\\{)") private var proxy: String? = null + private var retry: Int = 0 init { - baseUrl?.let { - this.baseUrl = it.split(splitUrlRegex, 1)[0] - } + val urlMatcher = paramPattern.matcher(baseUrl) + if (urlMatcher.find()) baseUrl = baseUrl.substring(0, urlMatcher.start()) headerMapF?.let { headerMap.putAll(it) if (it.containsKey("proxy")) { @@ -73,54 +66,31 @@ class AnalyzeUrl( } } //替换参数 - analyzeJs(key, page, speakText, speakSpeed, book) - replaceKeyPageJs(key, page, speakText, speakSpeed, book) + analyzeJs() + replaceKeyPageJs() //处理URL initUrl() } - private fun analyzeJs( - key: String?, - page: Int?, - speakText: String?, - speakSpeed: Int?, - book: BaseBook?, - ) { - val ruleList = arrayListOf() + private fun analyzeJs() { var start = 0 var tmp: String val jsMatcher = JS_PATTERN.matcher(ruleUrl) while (jsMatcher.find()) { if (jsMatcher.start() > start) { tmp = - ruleUrl.substring(start, jsMatcher.start()).replace("\n", "").trim { it <= ' ' } - if (!TextUtils.isEmpty(tmp)) { - ruleList.add(tmp) + ruleUrl.substring(start, jsMatcher.start()).trim { it <= ' ' } + if (tmp.isNotEmpty()) { + ruleUrl = tmp.replace("@result", ruleUrl) } } - ruleList.add(jsMatcher.group()) + ruleUrl = evalJS(jsMatcher.group(2) ?: jsMatcher.group(1), ruleUrl) as String start = jsMatcher.end() } if (ruleUrl.length > start) { - tmp = ruleUrl.substring(start).replace("\n", "").trim { it <= ' ' } - if (!TextUtils.isEmpty(tmp)) { - ruleList.add(tmp) - } - } - for (rule in ruleList) { - var ruleStr = rule - when { - ruleStr.startsWith("") -> { - ruleStr = ruleStr.substring(4, ruleStr.lastIndexOf("<")) - ruleUrl = - evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String - } - ruleStr.startsWith("@js", true) -> { - ruleStr = ruleStr.substring(4) - ruleUrl = - evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String - } - else -> ruleUrl = ruleStr.replace("@result", ruleUrl) + tmp = ruleUrl.substring(start).trim { it <= ' ' } + if (tmp.isNotEmpty()) { + ruleUrl = tmp.replace("@result", ruleUrl) } } } @@ -128,111 +98,108 @@ class AnalyzeUrl( /** * 替换关键字,页数,JS */ - private fun replaceKeyPageJs( - key: String?, - page: Int?, - speakText: String?, - speakSpeed: Int?, - book: BaseBook?, - ) { + private fun replaceKeyPageJs() { //先替换内嵌规则再替换页数规则,避免内嵌规则中存在大于小于号时,规则被切错 + //js + if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { + + val analyze = RuleAnalyzer(ruleUrl) //创建解析 + + val bindings = SimpleBindings() + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + bindings["baseUrl"] = baseUrl + bindings["page"] = page + bindings["key"] = key + bindings["speakText"] = speakText + bindings["speakSpeed"] = speakSpeed + bindings["book"] = book + + //替换所有内嵌{{js}} + val url = analyze.innerRule("{{", "}}") { + val jsEval = SCRIPT_ENGINE.eval(it, bindings) ?: "" + when { + jsEval is String -> jsEval + jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval) + else -> jsEval.toString() + } + } + if (url.isNotEmpty()) ruleUrl = url + } //page page?.let { val matcher = pagePattern.matcher(ruleUrl) while (matcher.find()) { val pages = matcher.group(1)!!.split(",") - ruleUrl = if (page <= pages.size) { + ruleUrl = if (page < pages.size) { //pages[pages.size - 1]等同于pages.last() ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' }) } else { ruleUrl.replace(matcher.group(), pages.last().trim { it <= ' ' }) } } } - //js - if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { - var jsEval: Any - val sb = StringBuffer(ruleUrl.length) - val simpleBindings = SimpleBindings() - simpleBindings["java"] = this - simpleBindings["baseUrl"] = baseUrl - simpleBindings["page"] = page - simpleBindings["key"] = key - simpleBindings["speakText"] = speakText - simpleBindings["speakSpeed"] = speakSpeed - simpleBindings["book"] = book - val expMatcher = EXP_PATTERN.matcher(ruleUrl) - while (expMatcher.find()) { - jsEval = expMatcher.group(1)?.let { - SCRIPT_ENGINE.eval(it, simpleBindings) - } ?: "" - if (jsEval is String) { - expMatcher.appendReplacement(sb, jsEval) - } else if (jsEval is Double && jsEval % 1.0 == 0.0) { - expMatcher.appendReplacement(sb, String.format("%.0f", jsEval)) - } else { - expMatcher.appendReplacement(sb, jsEval.toString()) - } - } - expMatcher.appendTail(sb) - ruleUrl = sb.toString() - } } /** * 处理URL */ - private fun initUrl() { - var urlArray = ruleUrl.split(splitUrlRegex, 2) - url = urlArray[0] - urlHasQuery = urlArray[0] + private fun initUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。 + val urlMatcher = paramPattern.matcher(ruleUrl) + urlHasQuery = if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl + url = NetworkUtils.getAbsoluteURL(baseUrl, urlHasQuery) NetworkUtils.getBaseUrl(url)?.let { baseUrl = it } - if (urlArray.size > 1) { - val option = GSON.fromJsonObject(urlArray[1]) - option?.let { _ -> - option.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST } + if (urlHasQuery.length != ruleUrl.length) { + GSON.fromJsonObject(ruleUrl.substring(urlMatcher.end()))?.let { option -> + option.method?.let { + if (it.equals("POST", true)) method = RequestMethod.POST + } + option.type?.let { type = it } option.headers?.let { headers -> - (headers as? Map<*, *>)?.forEach { key, value -> - headerMap[key.toString()] = value.toString() - } - if (headers is String) { + if (headers is Map<*, *>) { + headers.forEach { entry -> + headerMap[entry.key.toString()] = entry.value.toString() + } + } else if (headers is String) { GSON.fromJsonObject>(headers) ?.let { headerMap.putAll(it) } } } - headerMap[UA_NAME] = headerMap[UA_NAME] ?: userAgent - charset = option.charset - body = if (option.body is String) { - option.body - } else { - GSON.toJson(option.body) + option.charset?.let { charset = it } + option.body?.let { + body = if (it is String) it else GSON.toJson(it) } option.webView?.let { if (it.toString().isNotEmpty()) { useWebView = true } } + option.js?.let { + evalJS(it) + } + retry = option.retry } } + + headerMap[UA_NAME] ?: let { + headerMap[UA_NAME] = AppConfig.userAgent + } when (method) { RequestMethod.GET -> { if (!useWebView) { - urlArray = url.split("?") - url = urlArray[0] - if (urlArray.size > 1) { - analyzeFields(urlArray[1]) + val pos = url.indexOf('?') + if (pos != -1) { + analyzeFields(url.substring(pos + 1)) + url = url.substring(0, pos) } } } RequestMethod.POST -> { body?.let { - if (it.isJson()) { - requestBody = RequestBody.create(jsonType, it) - } else { + if (!it.isJson()) { analyzeFields(it) } - } ?: let { - requestBody = FormBody.Builder().build() } } } @@ -247,7 +214,7 @@ class AnalyzeUrl( for (query in queryS) { val queryM = query.splitNotBlank("=") val value = if (queryM.size > 1) queryM[1] else "" - if (TextUtils.isEmpty(charset)) { + if (charset.isNullOrEmpty()) { if (NetworkUtils.hasUrlEncoded(value)) { fieldMap[queryM[0]] = value } else { @@ -264,17 +231,11 @@ class AnalyzeUrl( /** * 执行JS */ - private fun evalJS( - jsStr: String, - result: Any?, - page: Int?, - key: String?, - speakText: String?, - speakSpeed: Int?, - book: BaseBook?, - ): Any { + private fun evalJS(jsStr: String, result: Any? = null): Any? { val bindings = SimpleBindings() bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager bindings["page"] = page bindings["key"] = key bindings["speakText"] = speakText @@ -286,45 +247,36 @@ class AnalyzeUrl( } fun put(key: String, value: String): String { - book?.putVariable(key, value) + chapter?.putVariable(key, value) + ?: book?.putVariable(key, value) + ?: ruleData?.putVariable(key, value) return value } fun get(key: String): String { - return book?.variableMap?.get(key) ?: "" - } - - fun getResponse(tag: String): Call { - val cookie = CookieStore.getCookie(tag) - if (cookie.isNotEmpty()) { - headerMap["Cookie"] = cookie - } - return when { - method == RequestMethod.POST -> { - if (fieldMap.isNotEmpty()) { - HttpHelper - .getApiService(baseUrl, charset) - .postMap(url, fieldMap, headerMap) - } else { - HttpHelper - .getApiService(baseUrl, charset) - .postBody(url, requestBody!!, headerMap) - } + when (key) { + "bookName" -> book?.let { + return it.name + } + "title" -> chapter?.let { + return it.title } - fieldMap.isEmpty() -> HttpHelper - .getApiService(baseUrl, charset) - .get(url, headerMap) - else -> HttpHelper - .getApiService(baseUrl, charset) - .getMap(url, fieldMap, headerMap) } + return chapter?.variableMap?.get(key) + ?: book?.variableMap?.get(key) + ?: ruleData?.variableMap?.get(key) + ?: "" } - suspend fun getResponseAwait( + suspend fun getStrResponse( tag: String, jsStr: String? = null, sourceRegex: String? = null, - ): Res { + ): StrResponse { + if (type != null) { + return StrResponse(url, StringUtils.byteToHexString(getByteArray(tag))) + } + setCookie(tag) if (useWebView) { val params = AjaxWebView.AjaxParams(url) params.headerMap = headerMap @@ -333,104 +285,58 @@ class AnalyzeUrl( params.sourceRegex = sourceRegex params.postData = body?.toByteArray() params.tag = tag - return HttpHelper.ajax(params) - } - val cookie = CookieStore.getCookie(tag) - if (cookie.isNotEmpty()) { - headerMap["Cookie"] = cookie + return getWebViewSrc(params) } - val res = when { - method == RequestMethod.POST -> { - if (fieldMap.isNotEmpty()) { - if (proxy == null) { - HttpHelper - .getApiService(baseUrl, charset) - .postMapAsync(url, fieldMap, headerMap) + return getProxyClient(proxy).newCallStrResponse(retry) { + removeHeader(UA_NAME) + addHeaders(headerMap) + when (method) { + RequestMethod.POST -> { + url(url) + if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { + postForm(fieldMap, true) } else { - HttpHelper - .getApiServiceWithProxy(baseUrl, charset, proxy) - .postMapAsync(url, fieldMap, headerMap) - } - } else { - if (proxy == null) { - HttpHelper - .getApiService(baseUrl, charset) - .postBodyAsync(url, requestBody!!, headerMap) - } else { - HttpHelper - .getApiServiceWithProxy(baseUrl, charset, proxy) - .postBodyAsync(url, requestBody!!, headerMap) + postJson(body) } } - } - fieldMap.isEmpty() -> { - if (proxy == null) { - HttpHelper - .getApiService(baseUrl, charset) - .getAsync(url, headerMap) - - } else { - HttpHelper - .getApiServiceWithProxy(baseUrl, charset, proxy) - .getAsync(url, headerMap) - } - - } - else -> { - if (proxy == null) { - HttpHelper - .getApiService(baseUrl, charset) - .getMapAsync(url, fieldMap, headerMap) - } else { - HttpHelper - .getApiServiceWithProxy(baseUrl, charset, proxy) - .getMapAsync(url, fieldMap, headerMap) - } - + else -> get(url, fieldMap, true) } } - return Res(NetworkUtils.getUrl(res), res.body()) } - fun getImageBytes(tag: String): ByteArray? { - val cookie = CookieStore.getCookie(tag) - if (cookie.isNotEmpty()) { - headerMap["Cookie"] += cookie - } - return if (fieldMap.isEmpty()) { - HttpHelper.getBytes(url, mapOf(), headerMap) - } else { - HttpHelper.getBytes(url, fieldMap, headerMap) - } + suspend fun getByteArray(tag: String? = null): ByteArray { + setCookie(tag) + @Suppress("BlockingMethodInNonBlockingContext") + return getProxyClient(proxy).newCall(retry) { + removeHeader(UA_NAME) + addHeaders(headerMap) + when (method) { + RequestMethod.POST -> { + url(url) + if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { + postForm(fieldMap, true) + } else { + postJson(body) + } + } + else -> get(url, fieldMap, true) + } + }.bytes() } - suspend fun getResponseBytes(tag: String? = null): ByteArray? { + private fun setCookie(tag: String?) { if (tag != null) { val cookie = CookieStore.getCookie(tag) if (cookie.isNotEmpty()) { - headerMap["Cookie"] = cookie - } - } - val response = when { - method == RequestMethod.POST -> { - if (fieldMap.isNotEmpty()) { - HttpHelper - .getBytesApiService(baseUrl) - .postMapByteAsync(url, fieldMap, headerMap) - } else { - HttpHelper - .getBytesApiService(baseUrl) - .postBodyByteAsync(url, requestBody!!, headerMap) + val cookieMap = CookieStore.cookieToMap(cookie) + val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "") + cookieMap.putAll(customCookieMap) + val newCookie = CookieStore.mapToCookie(cookieMap) + newCookie?.let { + headerMap.put("Cookie", it) } } - fieldMap.isEmpty() -> HttpHelper - .getBytesApiService(baseUrl) - .getByteAsync(url, headerMap) - else -> HttpHelper - .getBytesApiService(baseUrl) - .getMapByteAsync(url, fieldMap, headerMap) } - return response.body() } fun getGlideUrl(): GlideUrl { @@ -447,6 +353,9 @@ class AnalyzeUrl( val webView: Any?, val headers: Any?, val body: Any?, + val type: String?, + val js: String?, + val retry: Int = 0 ) } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java b/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java new file mode 100644 index 000000000..7fb875627 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java @@ -0,0 +1,604 @@ +package io.legado.app.model.analyzeRule; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@SuppressWarnings({"FieldCanBeLocal", "StatementWithEmptyBody", "unused"}) +public class QueryTTF { + private static class Header { + public int majorVersion; + public int minorVersion; + public int numOfTables; + public int searchRange; + public int entrySelector; + public int rangeShift; + } + + private static class Directory { + public String tag; // table name + public int checkSum; // Check sum + public int offset; // Offset from beginning of file + public int length; // length of the table in bytes + } + + private static class NameLayout { + public int format; + public int count; + public int stringOffset; + public List records = new LinkedList<>(); + } + + private static class NameRecord { + public int platformID; // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom> + public int encodingID; // 编码标识符 + public int languageID; // 语言标识符 + public int nameID; // 名称标识符 + public int length; // 名称字符串的长度 + public int offset; // 名称字符串相对于stringOffset的字节偏移量 + } + + private static class HeadLayout { + public int majorVersion; + public int minorVersion; + public int fontRevision; + public int checkSumAdjustment; + public int magicNumber; + public int flags; + public int unitsPerEm; + public long created; + public long modified; + public short xMin; + public short yMin; + public short xMax; + public short yMax; + public int macStyle; + public int lowestRecPPEM; + public short fontDirectionHint; + public short indexToLocFormat; // <0:loca是2字节数组, 1:loca是4字节数组> + public short glyphDataFormat; + } + + private static class MaxpLayout { + public int majorVersion; + public int minorVersion; + public int numGlyphs; // 字体中的字形数量 + public int maxPoints; + public int maxContours; + public int maxCompositePoints; + public int maxCompositeContours; + public int maxZones; + public int maxTwilightPoints; + public int maxStorage; + public int maxFunctionDefs; + public int maxInstructionDefs; + public int maxStackElements; + public int maxSizeOfInstructions; + public int maxComponentElements; + public int maxComponentDepth; + } + + private static class CmapLayout { + public int version; + public int numTables; + public List records = new LinkedList<>(); + public Map tables = new HashMap<>(); + } + + private static class CmapRecord { + public int platformID; + public int encodingID; + public int offset; + } + + private static class CmapFormat { + public int format; + public int length; + public int language; + public byte[] glyphIdArray; + } + + private static class CmapFormat4 extends CmapFormat { + public int segCountX2; + public int searchRange; + public int entrySelector; + public int rangeShift; + public int[] endCode; + public int reservedPad; + public int[] startCode; + public short[] idDelta; + public int[] idRangeOffset; + public int[] glyphIdArray; + } + + private static class CmapFormat6 extends CmapFormat { + public int firstCode; + public int entryCount; + public int[] glyphIdArray; + } + + private static class CmapFormat12 extends CmapFormat { + public int reserved; + public int length; + public int language; + public int numGroups; + public List> groups; + } + + private static class GlyfLayout { + public short numberOfContours; // 非负值为简单字型,负值为符合字型 + public short xMin; + public short yMin; + public short xMax; + public short yMax; + public int[] endPtsOfContours; // length=numberOfContours + public int instructionLength; + public byte[] instructions; // length=instructionLength + public byte[] flags; + public short[] xCoordinates; // length = flags.length + public short[] yCoordinates; // length = flags.length + } + + private static class ByteArrayReader { + public int index; + public byte[] buffer; + + public ByteArrayReader(byte[] buffer, int index) { + this.buffer = buffer; + this.index = index; + } + + public long ReadUIntX(long len) { + long result = 0; + for (long i = 0; i < len; ++i) { + result <<= 8; + result |= buffer[index++] & 0xFF; + } + return result; + } + + public long ReadUInt64() { + return ReadUIntX(8); + } + + public int ReadUInt32() { + return (int) ReadUIntX(4); + } + + public int ReadUInt16() { + return (int) ReadUIntX(2); + } + + public short ReadInt16() { + return (short) ReadUIntX(2); + } + + public short ReadUInt8() { + return (short) ReadUIntX(1); + } + + + public String ReadStrings(int len, Charset charset) { + byte[] result = len > 0 ? new byte[len] : null; + for (int i = 0; i < len; ++i) result[i] = buffer[index++]; + return new String(result, charset); + } + + public byte GetByte() { + return buffer[index++]; + } + + public byte[] GetBytes(int len) { + byte[] result = len > 0 ? new byte[len] : null; + for (int i = 0; i < len; ++i) result[i] = buffer[index++]; + return result; + } + + public int[] GetUInt16Array(int len) { + int[] result = len > 0 ? new int[len] : null; + for (int i = 0; i < len; ++i) result[i] = ReadUInt16(); + return result; + } + + public short[] GetInt16Array(int len) { + short[] result = len > 0 ? new short[len] : null; + for (int i = 0; i < len; ++i) result[i] = ReadInt16(); + return result; + } + } + + private final ByteArrayReader fontReader; + private final Header fileHeader = new Header(); + private final List directorys = new LinkedList<>(); + private final NameLayout name = new NameLayout(); + private final HeadLayout head = new HeadLayout(); + private final MaxpLayout maxp = new MaxpLayout(); + private final List loca = new LinkedList<>(); + private final CmapLayout Cmap = new CmapLayout(); + private final List glyf = new LinkedList<>(); + @SuppressWarnings("unchecked") + private final Pair[] pps = new Pair[]{ + Pair.of(3, 10), + Pair.of(0, 4), + Pair.of(3, 1), + Pair.of(1, 0), + Pair.of(0, 3), + Pair.of(0, 1) + }; + + public final Map codeToGlyph = new HashMap<>(); + public final Map glyphToCode = new HashMap<>(); + private int limitMix = 0; + private int limitMax = 0; + + /** + * 构造函数 + * + * @param buffer 传入TTF字体二进制数组 + */ + public QueryTTF(byte[] buffer) { + fontReader = new ByteArrayReader(buffer, 0); + // 获取文件头 + fileHeader.majorVersion = fontReader.ReadUInt16(); + fileHeader.minorVersion = fontReader.ReadUInt16(); + fileHeader.numOfTables = fontReader.ReadUInt16(); + fileHeader.searchRange = fontReader.ReadUInt16(); + fileHeader.entrySelector = fontReader.ReadUInt16(); + fileHeader.rangeShift = fontReader.ReadUInt16(); + // 获取目录 + for (int i = 0; i < fileHeader.numOfTables; ++i) { + Directory d = new Directory(); + d.tag = fontReader.ReadStrings(4, StandardCharsets.US_ASCII); + d.checkSum = fontReader.ReadUInt32(); + d.offset = fontReader.ReadUInt32(); + d.length = fontReader.ReadUInt32(); + directorys.add(d); + } + // 解析表 name (字体信息,包含版权、名称、作者等...) + for (Directory Temp : directorys) { + if (Temp.tag.equals("name")) { + fontReader.index = Temp.offset; + name.format = fontReader.ReadUInt16(); + name.count = fontReader.ReadUInt16(); + name.stringOffset = fontReader.ReadUInt16(); + for (int i = 0; i < name.count; ++i) { + NameRecord record = new NameRecord(); + record.platformID = fontReader.ReadUInt16(); + record.encodingID = fontReader.ReadUInt16(); + record.languageID = fontReader.ReadUInt16(); + record.nameID = fontReader.ReadUInt16(); + record.length = fontReader.ReadUInt16(); + record.offset = fontReader.ReadUInt16(); + name.records.add(record); + } + } + } + // 解析表 head (获取 head.indexToLocFormat) + for (Directory Temp : directorys) { + if (Temp.tag.equals("head")) { + fontReader.index = Temp.offset; + head.majorVersion = fontReader.ReadUInt16(); + head.minorVersion = fontReader.ReadUInt16(); + head.fontRevision = fontReader.ReadUInt32(); + head.checkSumAdjustment = fontReader.ReadUInt32(); + head.magicNumber = fontReader.ReadUInt32(); + head.flags = fontReader.ReadUInt16(); + head.unitsPerEm = fontReader.ReadUInt16(); + head.created = fontReader.ReadUInt64(); + head.modified = fontReader.ReadUInt64(); + head.xMin = fontReader.ReadInt16(); + head.yMin = fontReader.ReadInt16(); + head.xMax = fontReader.ReadInt16(); + head.yMax = fontReader.ReadInt16(); + head.macStyle = fontReader.ReadUInt16(); + head.lowestRecPPEM = fontReader.ReadUInt16(); + head.fontDirectionHint = fontReader.ReadInt16(); + head.indexToLocFormat = fontReader.ReadInt16(); + head.glyphDataFormat = fontReader.ReadInt16(); + } + } + // 解析表 maxp (获取 maxp.numGlyphs) + for (Directory Temp : directorys) { + if (Temp.tag.equals("maxp")) { + fontReader.index = Temp.offset; + maxp.majorVersion = fontReader.ReadUInt16(); + maxp.minorVersion = fontReader.ReadUInt16(); + maxp.numGlyphs = fontReader.ReadUInt16(); + maxp.maxPoints = fontReader.ReadUInt16(); + maxp.maxContours = fontReader.ReadUInt16(); + maxp.maxCompositePoints = fontReader.ReadUInt16(); + maxp.maxCompositeContours = fontReader.ReadUInt16(); + maxp.maxZones = fontReader.ReadUInt16(); + maxp.maxTwilightPoints = fontReader.ReadUInt16(); + maxp.maxStorage = fontReader.ReadUInt16(); + maxp.maxFunctionDefs = fontReader.ReadUInt16(); + maxp.maxInstructionDefs = fontReader.ReadUInt16(); + maxp.maxStackElements = fontReader.ReadUInt16(); + maxp.maxSizeOfInstructions = fontReader.ReadUInt16(); + maxp.maxComponentElements = fontReader.ReadUInt16(); + maxp.maxComponentDepth = fontReader.ReadUInt16(); + } + } + // 解析表 loca (轮廓数据偏移地址表) + for (Directory Temp : directorys) { + if (Temp.tag.equals("loca")) { + fontReader.index = Temp.offset; + int offset = head.indexToLocFormat == 0 ? 2 : 4; + for (long i = 0; i < Temp.length; i += offset) { + loca.add(offset == 2 ? fontReader.ReadUInt16() << 1 : fontReader.ReadUInt32()); + } + } + } + // 解析表 cmap (Unicode编码轮廓索引对照表) + for (Directory Temp : directorys) { + if (Temp.tag.equals("cmap")) { + fontReader.index = Temp.offset; + Cmap.version = fontReader.ReadUInt16(); + Cmap.numTables = fontReader.ReadUInt16(); + + for (int i = 0; i < Cmap.numTables; ++i) { + CmapRecord record = new CmapRecord(); + record.platformID = fontReader.ReadUInt16(); + record.encodingID = fontReader.ReadUInt16(); + record.offset = fontReader.ReadUInt32(); + Cmap.records.add(record); + } + for (int i = 0; i < Cmap.numTables; ++i) { + int fmtOffset = Cmap.records.get(i).offset; + fontReader.index = Temp.offset + fmtOffset; + int EndIndex = fontReader.index; + + int format = fontReader.ReadUInt16(); + if (Cmap.tables.containsKey(fmtOffset)) continue; + if (format == 0) { + CmapFormat f = new CmapFormat(); + f.format = format; + f.length = fontReader.ReadUInt16(); + f.language = fontReader.ReadUInt16(); + f.glyphIdArray = fontReader.GetBytes(f.length - 6); + Cmap.tables.put(fmtOffset, f); + } else if (format == 4) { + CmapFormat4 f = new CmapFormat4(); + f.format = format; + f.length = fontReader.ReadUInt16(); + f.language = fontReader.ReadUInt16(); + f.segCountX2 = fontReader.ReadUInt16(); + int segCount = f.segCountX2 >> 1; + f.searchRange = fontReader.ReadUInt16(); + f.entrySelector = fontReader.ReadUInt16(); + f.rangeShift = fontReader.ReadUInt16(); + f.endCode = fontReader.GetUInt16Array(segCount); + f.reservedPad = fontReader.ReadUInt16(); + f.startCode = fontReader.GetUInt16Array(segCount); + f.idDelta = fontReader.GetInt16Array(segCount); + f.idRangeOffset = fontReader.GetUInt16Array(segCount); + f.glyphIdArray = fontReader.GetUInt16Array((EndIndex + f.length - fontReader.index) >> 1); + Cmap.tables.put(fmtOffset, f); + } else if (format == 6) { + CmapFormat6 f = new CmapFormat6(); + f.format = format; + f.length = fontReader.ReadUInt16(); + f.language = fontReader.ReadUInt16(); + f.firstCode = fontReader.ReadUInt16(); + f.entryCount = fontReader.ReadUInt16(); + f.glyphIdArray = fontReader.GetUInt16Array(f.entryCount); + Cmap.tables.put(fmtOffset, f); + } else if (format == 12) { + CmapFormat12 f = new CmapFormat12(); + f.format = format; + f.reserved = fontReader.ReadUInt16(); + f.length = fontReader.ReadUInt32(); + f.language = fontReader.ReadUInt32(); + f.numGroups = fontReader.ReadUInt32(); + f.groups = new ArrayList<>(f.numGroups); + for (int n = 0; n < f.numGroups; ++n) { + f.groups.add(Triple.of(fontReader.ReadUInt32(), fontReader.ReadUInt32(), fontReader.ReadUInt32())); + } + Cmap.tables.put(fmtOffset, f); + } + } + } + } + // 解析表 glyf (字体轮廓数据表) + for (Directory Temp : directorys) { + if (Temp.tag.equals("glyf")) { + fontReader.index = Temp.offset; + for (int i = 0; i < maxp.numGlyphs; ++i) { + fontReader.index = Temp.offset + loca.get(i); + + short numberOfContours = fontReader.ReadInt16(); + if (numberOfContours > 0) { + GlyfLayout g = new GlyfLayout(); + g.numberOfContours = numberOfContours; + g.xMin = fontReader.ReadInt16(); + g.yMin = fontReader.ReadInt16(); + g.xMax = fontReader.ReadInt16(); + g.yMax = fontReader.ReadInt16(); + g.endPtsOfContours = fontReader.GetUInt16Array(numberOfContours); + g.instructionLength = fontReader.ReadUInt16(); + g.instructions = fontReader.GetBytes(g.instructionLength); + int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1; + // 获取轮廓点描述标志 + g.flags = new byte[flagLength]; + for (int n = 0; n < flagLength; ++n) { + g.flags[n] = fontReader.GetByte(); + if ((g.flags[n] & 0x08) != 0x00) { + for (int m = fontReader.ReadUInt8(); m > 0; --m) { + g.flags[++n] = g.flags[n - 1]; + } + } + } + // 获取轮廓点描述x轴相对值 + g.xCoordinates = new short[flagLength]; + for (int n = 0; n < flagLength; ++n) { + short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1); + if ((g.flags[n] & 0x02) != 0) { + g.xCoordinates[n] = (short) (same * fontReader.ReadUInt8()); + } else { + g.xCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16(); + } + } + // 获取轮廓点描述y轴相对值 + g.yCoordinates = new short[flagLength]; + for (int n = 0; n < flagLength; ++n) { + short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1); + if ((g.flags[n] & 0x04) != 0) { + g.yCoordinates[n] = (short) (same * fontReader.ReadUInt8()); + } else { + g.yCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16(); + } + } + // 相对坐标转绝对坐标 +// for (int n = 1; n < flagLength; ++n) { +// xCoordinates[n] += xCoordinates[n - 1]; +// yCoordinates[n] += yCoordinates[n - 1]; +// } + + glyf.add(g); + } else { + // 复合字体暂未使用 + } + } + } + } + + // 建立Unicode&Glyph双向表 + for (int key = 0; key < 130000; ++key) { + if (key == 0xFF) key = 0x3400; + int gid = getGlyfIndex(key); + if (gid == 0) continue; + StringBuilder sb = new StringBuilder(); + // 字型数据转String,方便存HashMap + for (short b : glyf.get(gid).xCoordinates) sb.append(b); + for (short b : glyf.get(gid).yCoordinates) sb.append(b); + String val = sb.toString(); + if (limitMix == 0) limitMix = key; + limitMax = key; + codeToGlyph.put(key, val); + if (glyphToCode.containsKey(val)) continue; + glyphToCode.put(val, key); + } + } + + /** + * 获取字体信息 (1=字体名称) + * + * @param nameId 传入十进制字体信息索引 + * @return 返回查询结果字符串 + */ + public String getNameById(int nameId) { + for (Directory Temp : directorys) { + if (!Temp.tag.equals("name")) continue; + fontReader.index = Temp.offset; + break; + } + for (NameRecord record : name.records) { + if (record.nameID != nameId) continue; + fontReader.index += name.stringOffset + record.offset; + return fontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE); + } + return "error"; + } + + /** + * 使用Unicode值查找轮廓索引 + * + * @param code 传入Unicode十进制值 + * @return 返回十进制轮廓索引 + */ + private int getGlyfIndex(int code) { + if (code == 0) return 0; + int fmtKey = 0; + for (Pair item : pps) { + for (CmapRecord record : Cmap.records) { + if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) { + fmtKey = record.offset; + break; + } + } + if (fmtKey > 0) break; + } + if (fmtKey == 0) return 0; + + int glyfID = 0; + CmapFormat table = Cmap.tables.get(fmtKey); + assert table != null; + int fmt = table.format; + if (fmt == 0) { + if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF; + } else if (fmt == 4) { + CmapFormat4 tab = (CmapFormat4) table; + if (code > tab.endCode[tab.endCode.length - 1]) return 0; + // 二分法查找数值索引 + int start = 0, middle, end = tab.endCode.length - 1; + while (start + 1 < end) { + middle = (start + end) / 2; + if (tab.endCode[middle] <= code) start = middle; + else end = middle; + } + if (tab.endCode[start] < code) ++start; + if (code < tab.startCode[start]) return 0; + if (tab.idRangeOffset[start] != 0) { + glyfID = tab.glyphIdArray[code - tab.startCode[start] + (tab.idRangeOffset[start] >> 1) - (tab.idRangeOffset.length - start)]; + } else glyfID = code + tab.idDelta[start]; + glyfID &= 0xFFFF; + } else if (fmt == 6) { + CmapFormat6 tab = (CmapFormat6) table; + int index = code - tab.firstCode; + if (index < 0 || index >= tab.glyphIdArray.length) glyfID = 0; + else glyfID = tab.glyphIdArray[index]; + } else if (fmt == 12) { + CmapFormat12 tab = (CmapFormat12) table; + if (code > tab.groups.get(tab.numGroups - 1).getMiddle()) return 0; + // 二分法查找数值索引 + int start = 0, middle, end = tab.numGroups - 1; + while (start + 1 < end) { + middle = (start + end) / 2; + if (tab.groups.get(middle).getLeft() <= code) start = middle; + else end = middle; + } + if (tab.groups.get(start).getLeft() <= code && code <= tab.groups.get(start).getMiddle()) { + glyfID = tab.groups.get(start).getRight() + code - tab.groups.get(start).getLeft(); + } + } + return glyfID; + } + + /** + * 判断Unicode值是否在字体范围内 + * + * @param code 传入Unicode十进制值 + * @return 返回bool查询结果 + */ + public boolean inLimit(char code) { + return (limitMix <= code) && (code < limitMax); + } + + /** + * 使用Unicode值获取轮廓数据 + * + * @param key 传入Unicode十进制值 + * @return 返回轮廓数组的String值 + */ + public String getGlyfByCode(int key) { + return codeToGlyph.getOrDefault(key, ""); + } + + /** + * 使用轮廓数据获取Unicode值 + * + * @param val 传入轮廓数组的String值 + * @return 返回Unicode十进制值 + */ + public int getCodeByGlyf(String val) { + //noinspection ConstantConditions + return glyphToCode.getOrDefault(val, 0); + } +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt new file mode 100644 index 000000000..93749dc22 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt @@ -0,0 +1,381 @@ +package io.legado.app.model.analyzeRule + +//通用的规则切分处理 +class RuleAnalyzer(data: String, code: Boolean = false) { + + private var queue: String = data //被处理字符串 + private var pos = 0 //当前处理到的位置 + private var start = 0 //当前处理字段的开始 + private var startX = 0 //当前规则的开始 + + private var rule = ArrayList() //分割出的规则列表 + private var step: Int = 0 //分割字符的长度 + var elementsType = "" //当前分割字符串 + var innerType = true //是否为内嵌{{}} + + fun trim() { // 修剪当前规则之前的"@"或者空白符 + if (queue[pos] == '@' || queue[pos] < '!') { //在while里重复设置start和startX会拖慢执行速度,所以先来个判断是否存在需要修剪的字段,最后再一次性设置start和startX + pos++ + while (queue[pos] == '@' || queue[pos] < '!') pos++ + start = pos //开始点推移 + startX = pos //规则起始点推移 + } + } + + //将pos重置为0,方便复用 + fun reSetPos() { + pos = 0 + startX = 0 + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列 + * @param seq 查找的字符串 **区分大小写** + * @return 是否找到相应字段。 + */ + fun consumeTo(seq: String): Boolean { + start = pos //将处理到的位置设置为规则起点 + val offset = queue.indexOf(seq, pos) + return if (offset != -1) { + pos = offset + true + } else false + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 + * @param seq 匹配字符串序列 + * @return 成功返回true并设置间隔,失败则直接返回fasle + */ + fun consumeToAny(vararg seq: String): Boolean { + + var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 + + while (pos != queue.length) { + + for (s in seq) { + if (queue.regionMatches(pos, s, 0, s.length)) { + step = s.length //间隔数 + this.pos = pos //匹配成功, 同步处理位置到类 + return true //匹配就返回 true + } + } + + pos++ //逐个试探 + } + return false + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 + * @param seq 匹配字符序列 + * @return 返回匹配位置 + */ + private fun findToAny(vararg seq: Char): Int { + + var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 + + while (pos != queue.length) { + + for (s in seq) if (queue[pos] == s) return pos //匹配则返回位置 + + pos++ //逐个试探 + + } + + return -1 + } + + /** + * 拉出一个非内嵌代码平衡组,存在转义文本 + */ + fun chompCodeBalanced(open: Char, close: Char): Boolean { + + var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos + + var depth = 0 //嵌套深度 + var otherDepth = 0 //其他对称符合嵌套深度 + + var inSingleQuote = false //单引号 + var inDoubleQuote = false //双引号 + + do { + if (pos == queue.length) break + val c = queue[pos++] + if (c != ESC) { //非转义字符 + if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 + else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 + + if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 + + if (c == '[') depth++ //开始嵌套一层 + else if (c == ']') depth-- //闭合一层嵌套 + else if (depth == 0) { + //处于默认嵌套中的非默认字符不需要平衡,仅depth为0时默认嵌套全部闭合,此字符才进行嵌套 + if (c == open) otherDepth++ + else if (c == close) otherDepth-- + } + + } else pos++ + + } while (depth > 0 || otherDepth > 0) //拉出一个平衡字串 + + return if (depth > 0 || otherDepth > 0) false else { + this.pos = pos //同步位置 + true + } + } + + /** + * 拉出一个规则平衡组,经过仔细测试xpath和jsoup中,引号内转义字符无效。 + */ + fun chompRuleBalanced(open: Char, close: Char): Boolean { + + var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos + var depth = 0 //嵌套深度 + var inSingleQuote = false //单引号 + var inDoubleQuote = false //双引号 + + do { + if (pos == queue.length) break + val c = queue[pos++] + if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 + else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 + + if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 + else if (c == '\\') { //不在引号中的转义字符才将下个字符转义 + pos++ + continue + } + + if (c == open) depth++ //开始嵌套一层 + else if (c == close) depth-- //闭合一层嵌套 + + } while (depth > 0) //拉出一个平衡字串 + + return if (depth > 0) false else { + this.pos = pos //同步位置 + true + } + } + + /** + * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 + * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 + */ + tailrec fun splitRule(vararg split: String): ArrayList { //首段匹配,elementsType为空 + + if (split.size == 1) { + elementsType = split[0] //设置分割字串 + return if (!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + } else { + step = elementsType.length //设置分隔符长度 + splitRule() + } //递归匹配 + } else if (!consumeToAny(* split)) { //未找到分隔符 + rule += queue.substring(startX) + return rule + } + + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 + + do { + val st = findToAny('[', '(') //查找筛选器位置 + + if (st == -1) { + + rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + rule += queue.substring(pos) //将剩余字段压入数组末尾 + + return rule + } + + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + + rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + return if (pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } + + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring(0, start) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 + + } while (end > pos) + + start = pos //设置开始查找筛选器位置的起始位置 + + return splitRule(* split) //递归调用首段匹配 + } + + @JvmName("splitRuleNext") + private tailrec fun splitRule(): ArrayList { //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 + + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 + + do { + val st = findToAny('[', '(') //查找筛选器位置 + + if (st == -1) { + + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + rule += queue.substring(pos) //将剩余字段压入数组末尾 + + return rule + } + + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + + rule += arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + return if (pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } + + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring(0, start) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 + + } while (end > pos) + + start = pos //设置开始查找筛选器位置的起始位置 + + return if (!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + } else splitRule() //递归匹配 + + } + + /** + * 替换内嵌规则 + * @param inner 起始标志,如{$. + * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 + * @param endStep 不属于规则部分的后置字符长度 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ + fun innerRule( + inner: String, + startStep: Int = 1, + endStep: Int = 1, + fr: (String) -> String? + ): String { + val st = StringBuilder() + + while (consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + val posPre = pos //记录consumeTo匹配位置 + if (chompCodeBalanced('{', '}')) { + val frv = fr(queue.substring(posPre + startStep, pos - endStep)) + if (!frv.isNullOrEmpty()) { + st.append(queue.substring(startX, posPre) + frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + startX = pos //记录下次规则起点 + continue //获取内容成功,继续选择下个内嵌规则 + } + } + pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 + } + + return if (startX == 0) "" else st.apply { + append(queue.substring(startX)) + }.toString() + } + + /** + * 替换内嵌规则 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ + fun innerRule( + startStr: String, + endStr: String, + fr: (String) -> String? + ): String { + + val st = StringBuilder() + while (consumeTo(startStr)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + pos += startStr.length //跳过开始字符串 + val posPre = pos //记录consumeTo匹配位置 + if (consumeTo(endStr)) { + val frv = fr(queue.substring(posPre, pos)) + st.append( + queue.substring( + startX, + posPre - startStr.length + ) + frv + ) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + pos += endStr.length //跳过结束字符串 + startX = pos //记录下次规则起点 + } + } + + return if (startX == 0) queue else st.apply { + append(queue.substring(startX)) + }.toString() + } + + val ruleTypeList = ArrayList() + + //设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced + val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced + + companion object { + + /** + * 转义字符 + */ + private const val ESC = '\\' + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt new file mode 100644 index 000000000..b26e43715 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt @@ -0,0 +1,13 @@ +package io.legado.app.model.analyzeRule + +class RuleData : RuleDataInterface { + + override val variableMap by lazy { + hashMapOf() + } + + override fun putVariable(key: String, value: String) { + variableMap[key] = value + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt new file mode 100644 index 000000000..80bf658f6 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt @@ -0,0 +1,9 @@ +package io.legado.app.model.analyzeRule + +interface RuleDataInterface { + + val variableMap: HashMap + + fun putVariable(key: String, value: String) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt b/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt deleted file mode 100644 index 067837f17..000000000 --- a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt +++ /dev/null @@ -1,185 +0,0 @@ -package io.legado.app.model.localBook - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import android.text.TextUtils -import io.legado.app.App -import io.legado.app.data.entities.BookChapter -import io.legado.app.utils.* -import nl.siegmann.epublib.domain.Book -import nl.siegmann.epublib.domain.TOCReference -import nl.siegmann.epublib.epub.EpubReader -import org.jsoup.Jsoup -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.nio.charset.Charset -import java.util.* - -class EPUBFile(val book: io.legado.app.data.entities.Book) { - - companion object { - private var eFile: EPUBFile? = null - - @Synchronized - private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile { - if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) { - eFile = EPUBFile(book) - return eFile!! - } - return eFile!! - } - - @Synchronized - fun getChapterList(book: io.legado.app.data.entities.Book): ArrayList { - return getEFile(book).getChapterList() - } - - @Synchronized - fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? { - return getEFile(book).getContent(chapter) - } - - @Synchronized - fun getImage( - book: io.legado.app.data.entities.Book, - href: String - ): InputStream? { - return getEFile(book).getImage(href) - } - } - - private var epubBook: Book? = null - private var mCharset: Charset = Charset.defaultCharset() - - init { - try { - val epubReader = EpubReader() - val inputStream = if (book.bookUrl.isContentPath()) { - val uri = Uri.parse(book.bookUrl) - App.INSTANCE.contentResolver.openInputStream(uri) - } else { - File(book.bookUrl).inputStream() - } - epubBook = epubReader.readEpub(inputStream) - if (book.coverUrl.isNullOrEmpty()) { - book.coverUrl = FileUtils.getPath( - App.INSTANCE.externalFilesDir, - "covers", - "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" - ) - } - if (!File(book.coverUrl!!).exists()) { - epubBook!!.coverImage?.inputStream?.use { - val cover = BitmapFactory.decodeStream(it) - val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) - cover.compress(Bitmap.CompressFormat.JPEG, 90, out) - out.flush() - out.close() - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun getContent(chapter: BookChapter): String? { - epubBook?.let { eBook -> - val resource = eBook.resources.getByHref(chapter.url) - val doc = Jsoup.parse(String(resource.data, mCharset)) - val elements = doc.body().children() - elements.select("script").remove() - elements.select("style").remove() - return elements.outerHtml().htmlFormat() - } - return null - } - - private fun getImage(href: String): InputStream? { - val abHref = href.replace("../", "") - return epubBook?.resources?.getByHref(abHref)?.inputStream - } - - private fun getChapterList(): ArrayList { - val chapterList = ArrayList() - epubBook?.let { eBook -> - val metadata = eBook.metadata - book.name = metadata.firstTitle - if (metadata.authors.size > 0) { - val author = - metadata.authors[0].toString().replace("^, |, $".toRegex(), "") - book.author = author - } - if (metadata.descriptions.size > 0) { - book.intro = Jsoup.parse(metadata.descriptions[0]).text() - } - - val refs = eBook.tableOfContents.tocReferences - if (refs == null || refs.isEmpty()) { - val spineReferences = eBook.spine.spineReferences - var i = 0 - val size = spineReferences.size - while (i < size) { - val resource = - spineReferences[i].resource - var title = resource.title - if (TextUtils.isEmpty(title)) { - try { - val doc = - Jsoup.parse(String(resource.data, mCharset)) - val elements = doc.getElementsByTag("title") - if (elements.size > 0) { - title = elements[0].text() - } - } catch (e: IOException) { - e.printStackTrace() - } - } - val chapter = BookChapter() - chapter.index = i - chapter.bookUrl = book.bookUrl - chapter.url = resource.href - if (i == 0 && title.isEmpty()) { - chapter.title = "封面" - } else { - chapter.title = title - } - chapterList.add(chapter) - i++ - } - } else { - parseMenu(chapterList, refs, 0) - for (i in chapterList.indices) { - chapterList[i].index = i - } - } - } - book.latestChapterTitle = chapterList.lastOrNull()?.title - book.totalChapterNum = chapterList.size - return chapterList - } - - private fun parseMenu( - chapterList: ArrayList, - refs: List?, - level: Int - ) { - if (refs == null) return - for (ref in refs) { - if (ref.resource != null) { - val chapter = BookChapter() - chapter.bookUrl = book.bookUrl - chapter.title = ref.title - chapter.url = ref.completeHref - chapterList.add(chapter) - } - if (ref.children != null && ref.children.isNotEmpty()) { - parseMenu(chapterList, ref.children, level + 1) - } - } - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt new file mode 100644 index 000000000..c4be8584e --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt @@ -0,0 +1,230 @@ +package io.legado.app.model.localBook + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.text.TextUtils +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.BookHelp +import io.legado.app.utils.FileUtils +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import me.ag2s.epublib.domain.EpubBook +import me.ag2s.epublib.epub.EpubReader +import org.jsoup.Jsoup +import splitties.init.appCtx +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.charset.Charset +import java.util.* +import java.util.zip.ZipFile + +class EpubFile(var book: Book) { + + companion object { + private var eFile: EpubFile? = null + + @Synchronized + private fun getEFile(book: Book): EpubFile { + BookHelp.getEpubFile(book) + + if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) { + eFile = EpubFile(book) + //对于Epub文件默认不启用替换 + book.setUseReplaceRule(false) + return eFile!! + } + eFile?.book = book + return eFile!! + } + + @Synchronized + fun getChapterList(book: Book): ArrayList { + return getEFile(book).getChapterList() + } + + @Synchronized + fun getContent(book: Book, chapter: BookChapter): String? { + return getEFile(book).getContent(chapter) + } + + @Synchronized + fun getImage( + book: Book, + href: String + ): InputStream? { + return getEFile(book).getImage(href) + } + + @Synchronized + fun upBookInfo(book: Book) { + return getEFile(book).upBookInfo() + } + } + + private var mCharset: Charset = Charset.defaultCharset() + private var epubBook: EpubBook? = null + get() { + if (field != null) { + return field + } + field = readEpub() + return field + } + + init { + try { + epubBook?.let { + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = FileUtils.getPath( + appCtx.externalFiles, + "covers", + "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" + ) + } + if (!File(book.coverUrl!!).exists()) { + /*部分书籍DRM处理后,封面获取异常,待优化*/ + it.coverImage?.inputStream?.use { input -> + val cover = BitmapFactory.decodeStream(input) + val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) + cover.compress(Bitmap.CompressFormat.JPEG, 90, out) + out.flush() + out.close() + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + /*重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误*/ + private fun readEpub(): EpubBook? { + try { + + val file = BookHelp.getEpubFile(book) + //通过懒加载读取epub + return EpubReader().readEpubLazy(ZipFile(file), "utf-8") + + + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + private fun getContent(chapter: BookChapter): String? { + /*获取当前章节文本*/ + return getChildChapter(chapter, chapter.url) + } + + private fun getChildChapter(chapter: BookChapter, href: String): String? { + epubBook?.let { + val body = Jsoup.parse(String(it.resources.getByHref(href).data, mCharset)).body() + + if (chapter.url == href) { + val startFragmentId = chapter.startFragmentId + val endFragmentId = chapter.endFragmentId + /*一些书籍依靠href索引的resource会包含多个章节,需要依靠fragmentId来截取到当前章节的内容*/ + /*注:这里较大增加了内容加载的时间,所以首次获取内容后可存储到本地cache,减少重复加载*/ + if (!startFragmentId.isNullOrBlank()) + body.getElementById(startFragmentId)?.previousElementSiblings()?.remove() + if (!endFragmentId.isNullOrBlank() && endFragmentId != startFragmentId) + body.getElementById(endFragmentId)?.nextElementSiblings()?.remove() + } + + /*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/ + var tag = Book.hTag + if (book.getDelTag(tag)) { + body.getElementsByTag("h1").remove() + body.getElementsByTag("h2").remove() + body.getElementsByTag("h3").remove() + body.getElementsByTag("h4").remove() + body.getElementsByTag("h5").remove() + body.getElementsByTag("h6").remove() + //body.getElementsMatchingOwnText(chapter.title)?.remove() + } + + /*选择去除正文中的img标签,目前图片支持效果待优化*/ + tag = Book.imgTag + if (book.getDelTag(tag)) { + body.getElementsByTag("img").remove() + } + + val elements = body.children() + elements.select("script").remove() + elements.select("style").remove() + /*选择去除正文中的ruby标签,目前注释支持效果待优化*/ + tag = Book.rubyTag + var html = elements.outerHtml() + if (book.getDelTag(tag)) { + html = html.replace("\\s?([\\u4e00-\\u9fa5])\\s?.*?".toRegex(), "$1") + } + return HtmlFormatter.formatKeepImg(html) + } + return null + } + + private fun getImage(href: String): InputStream? { + val abHref = href.replace("../", "") + return epubBook?.resources?.getByHref(abHref)?.inputStream + } + + private fun upBookInfo() { + if (epubBook == null) { + eFile = null + book.intro = "书籍导入异常" + } else { + val metadata = epubBook!!.metadata + book.name = metadata.firstTitle + if (book.name.isEmpty()) { + book.name = book.originName.replace(".epub", "") + } + + if (metadata.authors.size > 0) { + val author = + metadata.authors[0].toString().replace("^, |, $".toRegex(), "") + book.author = author + } + if (metadata.descriptions.size > 0) { + book.intro = Jsoup.parse(metadata.descriptions[0]).text() + } + } + } + + private fun getChapterList(): ArrayList { + val chapterList = ArrayList() + epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource -> + var title = resource.title + if (TextUtils.isEmpty(title)) { + try { + val doc = + Jsoup.parse(String(resource.data, mCharset)) + val elements = doc.getElementsByTag("title") + if (elements.size > 0) { + title = elements[0].text() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + val chapter = BookChapter() + chapter.index = index + chapter.bookUrl = book.bookUrl + chapter.url = resource.href + if (index == 0 && title.isEmpty()) { + chapter.title = "封面" + } else { + chapter.title = title + } + chapterList.add(chapter) + } + book.latestChapterTitle = chapterList.lastOrNull()?.title + book.totalChapterNum = chapterList.size + return chapterList + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 1af6f027e..5bf94834a 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -2,89 +2,166 @@ package io.legado.app.model.localBook import android.net.Uri import androidx.documentfile.provider.DocumentFile -import io.legado.app.App +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp -import io.legado.app.utils.FileUtils -import io.legado.app.utils.MD5Utils -import io.legado.app.utils.externalFilesDir -import io.legado.app.utils.isContentPath +import io.legado.app.utils.* +import splitties.init.appCtx import java.io.File - +import java.util.regex.Matcher +import java.util.regex.Pattern +import javax.script.SimpleBindings object LocalBook { + private const val folderName = "bookTxt" + val cacheFolder: File by lazy { + FileUtils.createFolderIfNotExist(appCtx.externalFiles, folderName) + } fun getChapterList(book: Book): ArrayList { - return if (book.isEpub()) { - EPUBFile.getChapterList(book) - } else { - AnalyzeTxtFile().analyze(book) + return when { + book.isEpub() -> { + EpubFile.getChapterList(book) + } + book.isUmd() -> { + UmdFile.getChapterList(book) + } + else -> { + TextFile().analyze(book) + } } } fun getContext(book: Book, chapter: BookChapter): String? { - return if (book.isEpub()) { - EPUBFile.getContent(book, chapter) - } else { - AnalyzeTxtFile.getContent(book, chapter) + return when { + book.isEpub() -> { + EpubFile.getContent(book, chapter) + } + book.isUmd() -> { + UmdFile.getContent(book, chapter) + } + else -> { + TextFile.getContent(book, chapter) + } } } - fun importFile(path: String): Book { - val fileName = if (path.isContentPath()) { - val doc = DocumentFile.fromSingleUri(App.INSTANCE, Uri.parse(path)) - doc?.name ?: "" + fun importFile(uri: Uri): Book { + val path: String + //这个变量不要修改,否则会导致读取不到缓存 + val fileName = (if (uri.isContentScheme()) { + path = uri.toString() + val doc = DocumentFile.fromSingleUri(appCtx, uri) + doc?.let { + val bookFile = FileUtils.getFile(cacheFolder, it.name!!) + if (!bookFile.exists()) { + bookFile.createNewFile() + doc.readBytes(appCtx)?.let { bytes -> + bookFile.writeBytes(bytes) + } + } + } + doc?.name!! } else { + path = uri.path!! File(path).name - } - val str = fileName.substringBeforeLast(".") - val authorIndex = str.indexOf("作者") - var name: String - var author: String - if (authorIndex == -1) { - name = str - author = "" - } else { - name = str.substring(0, authorIndex) - author = str.substring(authorIndex) - author = BookHelp.formatBookAuthor(author) - } - val smhStart = name.indexOf("《") - val smhEnd = name.indexOf("》") - if (smhStart != -1 && smhEnd != -1) { - name = (name.substring(smhStart + 1, smhEnd)) - } + }) + + val nameAuthor = analyzeNameAuthor(fileName) + val book = Book( bookUrl = path, - name = name, - author = author, + name = nameAuthor.first, + author = nameAuthor.second, originName = fileName, coverUrl = FileUtils.getPath( - App.INSTANCE.externalFilesDir, + appCtx.externalFiles, "covers", "${MD5Utils.md5Encode16(path)}.jpg" ) ) - App.db.bookDao().insert(book) + if (book.isEpub()) EpubFile.upBookInfo(book) + if (book.isUmd()) UmdFile.upBookInfo(book) + appDb.bookDao.insert(book) return book } + fun analyzeNameAuthor(fileName: String): Pair { + val tempFileName = fileName.substringBeforeLast(".") + val name: String + val author: String + + //匹配(知轩藏书常用格式) 《书名》其它信息作者:作者名.txt + val m1 = Pattern + .compile("(.*?)《([^《》]+)》(.*)") + .matcher(tempFileName) + + //匹配 书名 by 作者名.txt + val m2 = Pattern + .compile("(^)(.+) by (.+)$") + .matcher(tempFileName) + + (m1.takeIf { m1.find() } ?: m2.takeIf { m2.find() }).run { + + if (this is Matcher) { + + //按默认格式将文件名分解成书名、作者名 + name = group(2)!! + author = BookHelp.formatBookAuthor((group(1) ?: "") + (group(3) ?: "")) + + } else if (!AppConfig.bookImportFileName.isNullOrBlank()) { + + //在脚本中定义如何分解文件名成书名、作者名 + val jsonStr = AppConst.SCRIPT_ENGINE.eval( + + //在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获 + AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})", + + //将文件名注入到脚步的src变量中 + SimpleBindings().also { it["src"] = tempFileName } + ).toString() + val bookMess = GSON.fromJsonObject>(jsonStr) ?: HashMap() + name = bookMess["name"] ?: tempFileName + author = bookMess["author"]?.takeIf { it.length != tempFileName.length } ?: "" + + } else { + + name = tempFileName.replace(AppPattern.nameRegex, "") + author = tempFileName.replace(AppPattern.authorRegex, "") + .takeIf { it.length != tempFileName.length } ?: "" + + } + + } + return Pair(name, author) + } + fun deleteBook(book: Book, deleteOriginal: Boolean) { kotlin.runCatching { - if (book.isLocalTxt()) { - val bookFile = FileUtils.getFile(AnalyzeTxtFile.cacheFolder, book.originName) + if (book.isLocalTxt() || book.isUmd()) { + val bookFile = FileUtils.getFile(cacheFolder, book.originName) bookFile.delete() } + if (book.isEpub()) { + val bookFile = BookHelp.getEpubFile(book).parentFile + if (bookFile != null && bookFile.exists()) { + FileUtils.delete(bookFile, true) + } + + } if (deleteOriginal) { - if (book.bookUrl.isContentPath()) { + if (book.bookUrl.isContentScheme()) { val uri = Uri.parse(book.bookUrl) - DocumentFile.fromSingleUri(App.INSTANCE, uri)?.delete() + DocumentFile.fromSingleUri(appCtx, uri)?.delete() } else { FileUtils.deleteFile(book.bookUrl) } } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt b/app/src/main/java/io/legado/app/model/localBook/TextFile.kt similarity index 90% rename from app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt rename to app/src/main/java/io/legado/app/model/localBook/TextFile.kt index b7f1b015e..cec006ac0 100644 --- a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/TextFile.kt @@ -1,18 +1,20 @@ package io.legado.app.model.localBook import android.net.Uri -import io.legado.app.App +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.DefaultData import io.legado.app.utils.* +import splitties.init.appCtx import java.io.File import java.io.RandomAccessFile import java.nio.charset.Charset import java.util.regex.Matcher import java.util.regex.Pattern -class AnalyzeTxtFile { +class TextFile { private val tocRules = arrayListOf() private lateinit var charset: Charset @@ -238,7 +240,7 @@ class AnalyzeTxtFile { } companion object { - private const val folderName = "bookTxt" + private const val BLANK: Byte = 0x0a //默认从文件中获取数据的长度 @@ -246,12 +248,6 @@ class AnalyzeTxtFile { //没有标题的时候,每个章节的最大长度 private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024 - val cacheFolder: File by lazy { - val rootFile = App.INSTANCE.getExternalFilesDir(null) - ?: App.INSTANCE.externalCacheDir - ?: App.INSTANCE.cacheDir - FileUtils.createFolderIfNotExist(rootFile, folderName) - } fun getContent(book: Book, bookChapter: BookChapter): String { val bookFile = getBookFile(book) @@ -261,15 +257,17 @@ class AnalyzeTxtFile { bookStream.seek(bookChapter.start!!) bookStream.read(content) return String(content, book.fileCharset()) + .substringAfter(bookChapter.title) + .replace("^[\\n\\s]+".toRegex(), "  ") } private fun getBookFile(book: Book): File { - if (book.bookUrl.isContentPath()) { + if (book.bookUrl.isContentScheme()) { val uri = Uri.parse(book.bookUrl) - val bookFile = FileUtils.getFile(cacheFolder, book.originName) + val bookFile = FileUtils.getFile(LocalBook.cacheFolder, book.originName) if (!bookFile.exists()) { bookFile.createNewFile() - DocumentUtils.readBytes(App.INSTANCE, uri)?.let { + DocumentUtils.readBytes(appCtx, uri)?.let { bookFile.writeBytes(it) } } @@ -279,24 +277,17 @@ class AnalyzeTxtFile { } private fun getTocRules(): List { - val rules = App.db.txtTocRule().enabled + var rules = appDb.txtTocRuleDao.enabled if (rules.isEmpty()) { - return getDefaultEnabledRules() + rules = DefaultData.txtTocRules.apply { + appDb.txtTocRuleDao.insert(*this.toTypedArray()) + }.filter { + it.enable + } } return rules } - fun getDefaultEnabledRules(): List { - App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> - GSON.fromJsonArray(String(byteArray))?.let { txtTocRules -> - App.db.txtTocRule().insert(*txtTocRules.toTypedArray()) - return txtTocRules.filter { - it.enable - } - } - } - return emptyList() - } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt b/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt new file mode 100644 index 000000000..107ab1cab --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt @@ -0,0 +1,137 @@ +package io.legado.app.model.localBook + +import android.net.Uri +import android.util.Log +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.isContentScheme +import me.ag2s.umdlib.domain.UmdBook +import me.ag2s.umdlib.umd.UmdReader +import splitties.init.appCtx +import java.io.File +import java.io.InputStream +import java.util.* + +class UmdFile(var book: Book) { + companion object { + private var uFile: UmdFile? = null + + @Synchronized + private fun getUFile(book: Book): UmdFile { + + if (uFile == null || uFile?.book?.bookUrl != book.bookUrl) { + uFile = UmdFile(book) + return uFile!! + } + uFile?.book = book + return uFile!! + } + + @Synchronized + fun getChapterList(book: Book): ArrayList { + return getUFile(book).getChapterList() + } + + @Synchronized + fun getContent(book: Book, chapter: BookChapter): String? { + return getUFile(book).getContent(chapter) + } + + @Synchronized + fun getImage( + book: Book, + href: String + ): InputStream? { + return getUFile(book).getImage(href) + } + + + @Synchronized + fun upBookInfo(book: Book) { + return getUFile(book).upBookInfo() + } + } + + + private var umdBook: UmdBook? = null + get() { + if (field != null) { + return field + } + field = readUmd() + return field + } + + + init { + try { + umdBook?.let { + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = FileUtils.getPath( + appCtx.externalFiles, + "covers", + "${MD5Utils.md5Encode16(book.bookUrl)}.jpg" + ) + } + if (!File(book.coverUrl!!).exists()) { + FileUtils.writeBytes(book.coverUrl!!, it.cover.coverData) + + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun readUmd(): UmdBook? { + val input = if (book.bookUrl.isContentScheme()) { + val uri = Uri.parse(book.bookUrl) + appCtx.contentResolver.openInputStream(uri) + } else { + File(book.bookUrl).inputStream() + } + return UmdReader().read(input) + } + + private fun upBookInfo() { + if (umdBook == null) { + uFile = null + book.intro = "书籍导入异常" + } else { + val hd = umdBook!!.header + book.name = hd.title + book.author = hd.author + book.kind = hd.bookType + } + } + + private fun getContent(chapter: BookChapter): String? { + return umdBook?.chapters?.getContentString(chapter.index) + } + + private fun getChapterList(): ArrayList { + val chapterList = ArrayList() + umdBook?.chapters?.titles?.forEachIndexed { index, _ -> + val title = umdBook!!.chapters.getTitle(index) + val chapter = BookChapter() + chapter.title = title + chapter.index = index + chapter.bookUrl = book.bookUrl + chapter.url = index.toString() + Log.d("UMD", chapter.url) + chapterList.add(chapter) + } + book.latestChapterTitle = chapterList.lastOrNull()?.title + book.totalChapterNum = chapterList.size + return chapterList + } + + private fun getImage(href: String): InputStream? { + TODO("Not yet implemented") + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/rss/Result.kt b/app/src/main/java/io/legado/app/model/rss/Result.kt deleted file mode 100644 index 4d7f67c0a..000000000 --- a/app/src/main/java/io/legado/app/model/rss/Result.kt +++ /dev/null @@ -1,5 +0,0 @@ -package io.legado.app.model.rss - -import io.legado.app.data.entities.RssArticle - -data class Result(val articles: MutableList, val nextPageUrl: String?) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/rss/Rss.kt b/app/src/main/java/io/legado/app/model/rss/Rss.kt index f9ea78195..d86095390 100644 --- a/app/src/main/java/io/legado/app/model/rss/Rss.kt +++ b/app/src/main/java/io/legado/app/model/rss/Rss.kt @@ -3,10 +3,10 @@ package io.legado.app.model.rss import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.model.rss.Result -import io.legado.app.model.rss.RssParserByRule +import io.legado.app.model.analyzeRule.RuleData import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -15,42 +15,46 @@ import kotlin.coroutines.CoroutineContext object Rss { fun getArticles( + scope: CoroutineScope, sortName: String, sortUrl: String, rssSource: RssSource, page: Int, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO - ): Coroutine { + ): Coroutine { return Coroutine.async(scope, context) { + val ruleData = RuleData() val analyzeUrl = AnalyzeUrl( sortUrl, page = page, + ruleData = ruleData, headerMapF = rssSource.getHeaderMap() ) - val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body - RssParserByRule.parseXML(sortName, sortUrl, body, rssSource) + val body = analyzeUrl.getStrResponse(rssSource.sourceUrl).body + RssParserByRule.parseXML(sortName, sortUrl, body, rssSource, ruleData) } } fun getContent( + scope: CoroutineScope, rssArticle: RssArticle, ruleContent: String, - rssSource: RssSource?, - scope: CoroutineScope = Coroutine.DEFAULT, + rssSource: RssSource, context: CoroutineContext = Dispatchers.IO ): Coroutine { return Coroutine.async(scope, context) { - val body = AnalyzeUrl( - rssArticle.link, baseUrl = rssArticle.origin, - headerMapF = rssSource?.getHeaderMap() - ).getResponseAwait(rssArticle.origin) - .body - val analyzeRule = AnalyzeRule() - analyzeRule.setContent( - body, - NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link) + val analyzeUrl = AnalyzeUrl( + rssArticle.link, + baseUrl = rssArticle.origin, + ruleData = rssArticle, + headerMapF = rssSource.getHeaderMap() ) + val body = analyzeUrl.getStrResponse(rssArticle.origin).body + Debug.log(rssSource.sourceUrl, "≡获取成功:${rssSource.sourceUrl}") + Debug.log(rssSource.sourceUrl, body, state = 20) + val analyzeRule = AnalyzeRule(rssArticle) + analyzeRule.setContent(body) + .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)) analyzeRule.getString(ruleContent) } } diff --git a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt index 047d39459..1f544425c 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt @@ -1,39 +1,46 @@ package io.legado.app.model.rss import androidx.annotation.Keep -import io.legado.app.App import io.legado.app.R import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.GSON import io.legado.app.utils.NetworkUtils +import splitties.init.appCtx import java.util.* @Keep object RssParserByRule { @Throws(Exception::class) - fun parseXML(sortName: String, sortUrl: String, body: String?, rssSource: RssSource): Result { + fun parseXML( + sortName: String, + sortUrl: String, + body: String?, + rssSource: RssSource, + ruleData: RuleDataInterface + ): RssResult { val sourceUrl = rssSource.sourceUrl var nextUrl: String? = null if (body.isNullOrBlank()) { throw Exception( - App.INSTANCE.getString( - R.string.error_get_web_content, - rssSource.sourceUrl - ) + appCtx.getString(R.string.error_get_web_content, rssSource.sourceUrl) ) } Debug.log(sourceUrl, "≡获取成功:$sourceUrl") + Debug.log(sourceUrl, body, state = 10) var ruleArticles = rssSource.ruleArticles if (ruleArticles.isNullOrBlank()) { Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析") - return RssParser.parseXML(sortName, body, sourceUrl) + return RssParserDefault.parseXML(sortName, body, sourceUrl) } else { val articleList = mutableListOf() - val analyzeRule = AnalyzeRule() - analyzeRule.setContent(body, sortUrl) + val analyzeRule = AnalyzeRule(ruleData) + analyzeRule.setContent(body).setBaseUrl(sortUrl) + analyzeRule.setRedirectUrl(sortUrl) var reverse = false if (ruleArticles.startsWith("-")) { reverse = true @@ -44,7 +51,7 @@ object RssParserByRule { Debug.log(sourceUrl, "└列表大小:${collections.size}") if (!rssSource.ruleNextPage.isNullOrEmpty()) { Debug.log(sourceUrl, "┌获取下一页链接") - if (rssSource.ruleNextPage!!.toUpperCase(Locale.getDefault()) == "PAGE") { + if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == "PAGE") { nextUrl = sortUrl } else { nextUrl = analyzeRule.getString(rssSource.ruleNextPage) @@ -72,7 +79,7 @@ object RssParserByRule { if (reverse) { articleList.reverse() } - return Result(articleList, nextUrl) + return RssResult(articleList, nextUrl) } } @@ -107,8 +114,9 @@ object RssParserByRule { rssArticle.image = analyzeRule.getString(ruleImage, true) Debug.log(sourceUrl, "└${rssArticle.image}", log) Debug.log(sourceUrl, "┌获取文章链接", log) - rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink))!! + rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink)) Debug.log(sourceUrl, "└${rssArticle.link}", log) + rssArticle.variable = GSON.toJson(analyzeRule.ruleData.variableMap) if (rssArticle.title.isBlank()) { return null } diff --git a/app/src/main/java/io/legado/app/model/rss/RssParser.kt b/app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt similarity index 96% rename from app/src/main/java/io/legado/app/model/rss/RssParser.kt rename to app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt index 09cd756bb..2cb3d6c17 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParser.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt @@ -8,10 +8,11 @@ import org.xmlpull.v1.XmlPullParserFactory import java.io.IOException import java.io.StringReader -object RssParser { +@Suppress("unused") +object RssParserDefault { @Throws(XmlPullParserException::class, IOException::class) - fun parseXML(sortName: String, xml: String, sourceUrl: String): Result { + fun parseXML(sortName: String, xml: String, sourceUrl: String): RssResult { val articleList = mutableListOf() var currentArticle = RssArticle() @@ -105,7 +106,7 @@ object RssParser { Debug.log(sourceUrl, "┌获取文章链接") Debug.log(sourceUrl, "└${it.link}") } - return Result(articleList, null) + return RssResult(articleList, null) } /** @@ -117,11 +118,11 @@ object RssParser { private fun getImageUrl(input: String): String? { var url: String? = null - val patternImg = "()".toPattern() + val patternImg = "(]*>)".toPattern() val matcherImg = patternImg.matcher(input) if (matcherImg.find()) { val imgTag = matcherImg.group(1) - val patternLink = "src\\s*=\\s*\"(.+?)\"".toPattern() + val patternLink = "src\\s*=\\s*\"([^\"]+)\"".toPattern() val matcherLink = patternLink.matcher(imgTag!!) if (matcherLink.find()) { url = matcherLink.group(1)!!.trim() diff --git a/app/src/main/java/io/legado/app/model/rss/RssResult.kt b/app/src/main/java/io/legado/app/model/rss/RssResult.kt new file mode 100644 index 000000000..4074871ff --- /dev/null +++ b/app/src/main/java/io/legado/app/model/rss/RssResult.kt @@ -0,0 +1,5 @@ +package io.legado.app.model.rss + +import io.legado.app.data.entities.RssArticle + +data class RssResult(val articles: MutableList, val nextPageUrl: String?) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt index 78a139345..8b1a56803 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt @@ -1,192 +1,118 @@ package io.legado.app.model.webBook import android.text.TextUtils -import io.legado.app.App import io.legado.app.R import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.TocRule -import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext + +import splitties.init.appCtx object BookChapterList { suspend fun analyzeChapterList( - coroutineScope: CoroutineScope, + scope: CoroutineScope, book: Book, body: String?, bookSource: BookSource, - baseUrl: String - ): List = suspendCancellableCoroutine { block -> - try { - val chapterList = ArrayList() - body ?: throw Exception( - App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) + baseUrl: String, + redirectUrl: String + ): List { + val chapterList = ArrayList() + body ?: throw Exception( + appCtx.getString(R.string.error_get_web_content, baseUrl) + ) + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 30) + val tocRule = bookSource.getTocRule() + val nextUrlList = arrayListOf(baseUrl) + var reverse = false + var listRule = tocRule.chapterList ?: "" + if (listRule.startsWith("-")) { + reverse = true + listRule = listRule.substring(1) + } + if (listRule.startsWith("+")) { + listRule = listRule.substring(1) + } + var chapterData = + analyzeChapterList( + scope, book, baseUrl, redirectUrl, body, + tocRule, listRule, bookSource, log = true ) - Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") - - val tocRule = bookSource.getTocRule() - val nextUrlList = arrayListOf(baseUrl) - var reverse = false - var listRule = tocRule.chapterList ?: "" - if (listRule.startsWith("-")) { - reverse = true - listRule = listRule.substring(1) - } - if (listRule.startsWith("+")) { - listRule = listRule.substring(1) - } - var chapterData = - analyzeChapterList( - book, baseUrl, body, tocRule, listRule, bookSource, log = true - ) - chapterData.chapterList?.let { - chapterList.addAll(it) - } - when (chapterData.nextUrl.size) { - 0 -> { - block.resume(finish(book, chapterList, reverse)) - } - 1 -> { - Coroutine.async(scope = coroutineScope) { - var nextUrl = chapterData.nextUrl[0] - while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { - nextUrlList.add(nextUrl) - AnalyzeUrl( - ruleUrl = nextUrl, - book = book, - headerMapF = bookSource.getHeaderMap() - ).getResponseAwait(bookSource.bookSourceUrl) - .body?.let { nextBody -> - chapterData = analyzeChapterList( - book, nextUrl, nextBody, tocRule, listRule, bookSource - ) - nextUrl = if (chapterData.nextUrl.isNotEmpty()) { - chapterData.nextUrl[0] - } else "" - chapterData.chapterList?.let { - chapterList.addAll(it) - } - } + chapterData.chapterList?.let { + chapterList.addAll(it) + } + when (chapterData.nextUrl.size) { + 0 -> Unit + 1 -> { + var nextUrl = chapterData.nextUrl[0] + while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { + nextUrlList.add(nextUrl) + AnalyzeUrl( + ruleUrl = nextUrl, + book = book, + headerMapF = bookSource.getHeaderMap() + ).getStrResponse(bookSource.bookSourceUrl).body?.let { nextBody -> + chapterData = analyzeChapterList( + scope, book, nextUrl, nextUrl, + nextBody, tocRule, listRule, bookSource + ) + nextUrl = chapterData.nextUrl.firstOrNull() ?: "" + chapterData.chapterList?.let { + chapterList.addAll(it) } - Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") - block.resume(finish(book, chapterList, reverse)) - }.onError { - block.resumeWithException(it) } } - else -> { - val chapterDataList = arrayListOf>() - for (item in chapterData.nextUrl) { - if (!nextUrlList.contains(item)) { - val data = ChapterData(nextUrl = item) - chapterDataList.add(data) - nextUrlList.add(item) + Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") + } + else -> { + Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${chapterData.nextUrl.size}") + withContext(IO) { + val asyncArray = Array(chapterData.nextUrl.size) { + async(IO) { + val urlStr = chapterData.nextUrl[it] + val analyzeUrl = AnalyzeUrl( + ruleUrl = urlStr, + book = book, + headerMapF = bookSource.getHeaderMap() + ) + val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl) + analyzeChapterList( + this, book, urlStr, res.url, + res.body!!, tocRule, listRule, bookSource, false + ).chapterList } } - Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") - for (item in chapterDataList) { - downloadToc( - coroutineScope, - item, - book, - bookSource, - tocRule, - listRule, - chapterList, - chapterDataList, - { - block.resume(finish(book, chapterList, reverse)) - }, { - block.cancel(it) - }) - } - } - } - } catch (e: Exception) { - block.resumeWithException(e) - } - } - - private fun downloadToc( - coroutineScope: CoroutineScope, - chapterData: ChapterData, - book: Book, - bookSource: BookSource, - tocRule: TocRule, - listRule: String, - chapterList: ArrayList, - chapterDataList: ArrayList>, - onFinish: () -> Unit, - onError: (e: Throwable) -> Unit - ) { - Coroutine.async(scope = coroutineScope) { - val nextBody = AnalyzeUrl( - ruleUrl = chapterData.nextUrl, - book = book, - headerMapF = bookSource.getHeaderMap() - ).getResponseAwait(bookSource.bookSourceUrl).body - ?: throw Exception("${chapterData.nextUrl}, 下载失败") - val nextChapterData = analyzeChapterList( - book, chapterData.nextUrl, nextBody, tocRule, listRule, bookSource, - false - ) - synchronized(chapterDataList) { - val isFinished = addChapterListIsFinish( - chapterDataList, - chapterData, - nextChapterData.chapterList - ) - if (isFinished) { - chapterDataList.forEach { item -> - item.chapterList?.let { + asyncArray.forEach { coroutine -> + coroutine.await()?.let { chapterList.addAll(it) } } - onFinish() } } - }.onError { - onError(it) } - } - - private fun addChapterListIsFinish( - chapterDataList: ArrayList>, - chapterData: ChapterData, - chapterList: List? - ): Boolean { - chapterData.chapterList = chapterList - chapterDataList.forEach { - if (it.chapterList == null) { - return false - } - } - return true - } - - private fun finish( - book: Book, - chapterList: ArrayList, - reverse: Boolean - ): ArrayList { //去重 if (!reverse) { chapterList.reverse() } val lh = LinkedHashSet(chapterList) val list = ArrayList(lh) - list.reverse() + if (!book.getReverseToc()) { + list.reverse() + } Debug.log(book.origin, "◇目录总数:${list.size}") - for ((index, item) in list.withIndex()) { - item.index = index + list.forEachIndexed { index, bookChapter -> + bookChapter.index = index } book.latestChapterTitle = list.last().title book.durChapterTitle = @@ -194,14 +120,17 @@ object BookChapterList { if (book.totalChapterNum < list.size) { book.lastCheckCount = list.size - book.totalChapterNum book.latestChapterTime = System.currentTimeMillis() + book.lastCheckTime = System.currentTimeMillis() } book.totalChapterNum = list.size return list } private fun analyzeChapterList( + scope: CoroutineScope, book: Book, baseUrl: String, + redirectUrl: String, body: String, tocRule: TocRule, listRule: String, @@ -210,8 +139,14 @@ object BookChapterList { log: Boolean = false ): ChapterData> { val analyzeRule = AnalyzeRule(book) - analyzeRule.setContent(body, baseUrl) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(redirectUrl) + //获取目录列表 val chapterList = arrayListOf() + Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) + val elements = analyzeRule.getElements(listRule) + Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) + //获取下一页链接 val nextUrlList = arrayListOf() val nextTocRule = tocRule.nextTocUrl if (getNextUrl && !nextTocRule.isNullOrEmpty()) { @@ -229,25 +164,26 @@ object BookChapterList { log ) } - Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) - val elements = analyzeRule.getElements(listRule) - Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) + scope.ensureActive() if (elements.isNotEmpty()) { - Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) + Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log) val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) val update = analyzeRule.splitSourceRule(tocRule.updateTime) var isVip: String? for (item in elements) { + scope.ensureActive() analyzeRule.setContent(item) - val bookChapter = BookChapter(bookUrl = book.bookUrl) + val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = baseUrl) analyzeRule.chapter = bookChapter bookChapter.title = analyzeRule.getString(nameRule) - bookChapter.url = analyzeRule.getString(urlRule, true) + bookChapter.url = analyzeRule.getString(urlRule) bookChapter.tag = analyzeRule.getString(update) isVip = analyzeRule.getString(vipRule) - if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl + if (bookChapter.url.isEmpty()) { + bookChapter.url = baseUrl + } if (bookChapter.title.isNotEmpty()) { if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") { bookChapter.title = "\uD83D\uDD12" + bookChapter.title @@ -255,6 +191,8 @@ object BookChapterList { chapterList.add(bookChapter) } } + Debug.log(bookSource.bookSourceUrl, "└目录列表解析完成", log) + Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].title}", log) Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log) Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].url}", log) diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt index b17c5e4ff..1d71effc6 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -1,110 +1,119 @@ package io.legado.app.model.webBook -import io.legado.app.App import io.legado.app.R +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.ContentRule +import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.NetworkUtils -import io.legado.app.utils.htmlFormat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext +import splitties.init.appCtx object BookContent { @Throws(Exception::class) suspend fun analyzeContent( - coroutineScope: CoroutineScope, + scope: CoroutineScope, body: String?, book: Book, bookChapter: BookChapter, bookSource: BookSource, baseUrl: String, - nextChapterUrlF: String? = null + redirectUrl: String, + nextChapterUrl: String? = null ): String { body ?: throw Exception( - App.INSTANCE.getString( - R.string.error_get_web_content, - baseUrl - ) + appCtx.getString(R.string.error_get_web_content, baseUrl) ) Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 40) + val mNextChapterUrl = if (!nextChapterUrl.isNullOrEmpty()) { + nextChapterUrl + } else { + appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url + } val content = StringBuilder() val nextUrlList = arrayListOf(baseUrl) val contentRule = bookSource.getContentRule() + val analyzeRule = AnalyzeRule(book).setContent(body, baseUrl) + analyzeRule.setRedirectUrl(baseUrl) + analyzeRule.nextChapterUrl = mNextChapterUrl + scope.ensureActive() var contentData = analyzeContent( - book, baseUrl, body, contentRule, bookChapter, bookSource + book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl ) - content.append(contentData.content).append("\n") - + content.append(contentData.content) if (contentData.nextUrl.size == 1) { var nextUrl = contentData.nextUrl[0] - val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty()) - nextChapterUrlF - else - App.db.bookChapterDao().getChapter(book.bookUrl, bookChapter.index + 1)?.url while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { - if (!nextChapterUrl.isNullOrEmpty() + if (!mNextChapterUrl.isNullOrEmpty() && NetworkUtils.getAbsoluteURL(baseUrl, nextUrl) - == NetworkUtils.getAbsoluteURL(baseUrl, nextChapterUrl) + == NetworkUtils.getAbsoluteURL(baseUrl, mNextChapterUrl) ) break nextUrlList.add(nextUrl) - AnalyzeUrl( + scope.ensureActive() + val res = AnalyzeUrl( ruleUrl = nextUrl, book = book, headerMapF = bookSource.getHeaderMap() - ).getResponseAwait(bookSource.bookSourceUrl) - .body?.let { nextBody -> - contentData = - analyzeContent( - book, nextUrl, nextBody, contentRule, bookChapter, bookSource, false - ) + ).getStrResponse(bookSource.bookSourceUrl) + res.body?.let { nextBody -> + contentData = analyzeContent( + book, nextUrl, res.url, nextBody, contentRule, + bookChapter, bookSource, mNextChapterUrl, false + ) nextUrl = if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" - content.append(contentData.content).append("\n") + content.append("\n").append(contentData.content) } } Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") } else if (contentData.nextUrl.size > 1) { - val contentDataList = arrayListOf>() - for (item in contentData.nextUrl) { - if (!nextUrlList.contains(item)) - contentDataList.add(ContentData(nextUrl = item)) - } - for (item in contentDataList) { - withContext(coroutineScope.coroutineContext) { - AnalyzeUrl( - ruleUrl = item.nextUrl, - book = book, - headerMapF = bookSource.getHeaderMap() - ).getResponseAwait(bookSource.bookSourceUrl) - .body?.let { - contentData = - analyzeContent( - book, item.nextUrl, it, contentRule, bookChapter, bookSource, false - ) - item.content = contentData.content - } + Debug.log(bookSource.bookSourceUrl, "◇并发解析目录,总页数:${contentData.nextUrl.size}") + withContext(IO) { + val asyncArray = Array(contentData.nextUrl.size) { + async(IO) { + val urlStr = contentData.nextUrl[it] + val analyzeUrl = AnalyzeUrl( + ruleUrl = urlStr, + book = book, + headerMapF = bookSource.getHeaderMap() + ) + val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl) + analyzeContent( + book, urlStr, res.url, res.body!!, contentRule, + bookChapter, bookSource, mNextChapterUrl, false + ).content + } + } + asyncArray.forEach { coroutine -> + scope.ensureActive() + content.append("\n").append(coroutine.await()) } - } - for (item in contentDataList) { - content.append(item.content).append("\n") } } - content.deleteCharAt(content.length - 1) - var contentStr = content.toString().htmlFormat() - val replaceRegex = bookSource.ruleContent?.replaceRegex?.trim() + var contentStr = content.toString() + val replaceRegex = contentRule.replaceRegex if (!replaceRegex.isNullOrEmpty()) { - contentStr = AnalyzeRule(book).setContent(contentStr).getString(replaceRegex) + contentStr = analyzeRule.getString(replaceRegex, value = contentStr) } Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") Debug.log(bookSource.bookSourceUrl, "└\n$contentStr") + if (contentStr.isNotBlank()) { + BookHelp.saveContent(book, bookChapter, contentStr) + } return contentStr } @@ -112,16 +121,24 @@ object BookContent { private fun analyzeContent( book: Book, baseUrl: String, + redirectUrl: String, body: String, contentRule: ContentRule, chapter: BookChapter, bookSource: BookSource, + nextChapterUrl: String?, printLog: Boolean = true ): ContentData> { val analyzeRule = AnalyzeRule(book) analyzeRule.setContent(body, baseUrl) + val rUrl = analyzeRule.setRedirectUrl(redirectUrl) + analyzeRule.nextChapterUrl = nextChapterUrl val nextUrlList = arrayListOf() analyzeRule.chapter = chapter + //获取正文 + var content = analyzeRule.getString(contentRule.content) + content = HtmlFormatter.formatKeepImg(content, rUrl) + //获取下一页链接 val nextUrlRule = contentRule.nextContentUrl if (!nextUrlRule.isNullOrEmpty()) { Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog) @@ -130,7 +147,6 @@ object BookContent { } Debug.log(bookSource.bookSourceUrl, "└" + nextUrlList.joinToString(","), printLog) } - val content = analyzeRule.getString(contentRule.content) return ContentData(content, nextUrlList) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt index 7d6c3a94c..df12185fc 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -1,81 +1,129 @@ package io.legado.app.model.webBook -import io.legado.app.App import io.legado.app.R import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils.wordCountFormat -import io.legado.app.utils.htmlFormat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ensureActive +import splitties.init.appCtx object BookInfo { @Throws(Exception::class) fun analyzeBookInfo( + scope: CoroutineScope, book: Book, body: String?, bookSource: BookSource, baseUrl: String, + redirectUrl: String, canReName: Boolean, ) { body ?: throw Exception( - App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) + appCtx.getString(R.string.error_get_web_content, baseUrl) ) Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") - val infoRule = bookSource.getBookInfoRule() + Debug.log(bookSource.bookSourceUrl, body, state = 20) val analyzeRule = AnalyzeRule(book) - analyzeRule.setContent(body, baseUrl) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(redirectUrl) + analyzeBookInfo(scope, book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName) + } + + fun analyzeBookInfo( + scope: CoroutineScope, + book: Book, + body: String, + analyzeRule: AnalyzeRule, + bookSource: BookSource, + baseUrl: String, + redirectUrl: String, + canReName: Boolean, + ) { + val infoRule = bookSource.getBookInfoRule() infoRule.init?.let { - if (it.isNotEmpty()) { + if (it.isNotBlank()) { + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") analyzeRule.setContent(analyzeRule.getElement(it)) } } + val mCanReName = canReName && !infoRule.canReName.isNullOrBlank() + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取书名") BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let { - if (it.isNotEmpty() && (canReName || book.name.isEmpty())) { + if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) { book.name = it } + Debug.log(bookSource.bookSourceUrl, "└${it}") } - Debug.log(bookSource.bookSourceUrl, "└${book.name}") + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取作者") BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let { - if (it.isNotEmpty() && (canReName || book.name.isEmpty())) { + if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) { book.author = it } + Debug.log(bookSource.bookSourceUrl, "└${it}") } - Debug.log(bookSource.bookSourceUrl, "└${book.author}") + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取分类") - analyzeRule.getStringList(infoRule.kind) - ?.joinToString(",") - ?.let { - if (it.isNotEmpty()) book.kind = it - } - Debug.log(bookSource.bookSourceUrl, "└${book.kind}") + try { + analyzeRule.getStringList(infoRule.kind) + ?.joinToString(",") + ?.let { + if (it.isNotEmpty()) book.kind = it + } + Debug.log(bookSource.bookSourceUrl, "└${book.kind}") + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + } + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取字数") - wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let { - if (it.isNotEmpty()) book.wordCount = it + try { + wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let { + if (it.isNotEmpty()) book.wordCount = it + } + Debug.log(bookSource.bookSourceUrl, "└${book.wordCount}") + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } - Debug.log(bookSource.bookSourceUrl, "└${book.wordCount}") + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") - analyzeRule.getString(infoRule.lastChapter).let { - if (it.isNotEmpty()) book.latestChapterTitle = it + try { + analyzeRule.getString(infoRule.lastChapter).let { + if (it.isNotEmpty()) book.latestChapterTitle = it + } + Debug.log(bookSource.bookSourceUrl, "└${book.latestChapterTitle}") + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } - Debug.log(bookSource.bookSourceUrl, "└${book.latestChapterTitle}") + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取简介") - analyzeRule.getString(infoRule.intro).let { - if (it.isNotEmpty()) book.intro = it.htmlFormat() + try { + analyzeRule.getString(infoRule.intro).let { + if (it.isNotEmpty()) book.intro = HtmlFormatter.format(it) + } + Debug.log(bookSource.bookSourceUrl, "└${book.intro}") + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } - Debug.log(bookSource.bookSourceUrl, "└${book.intro}", isHtml = true) - + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") - analyzeRule.getString(infoRule.coverUrl).let { - if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) + try { + analyzeRule.getString(infoRule.coverUrl).let { + if (it.isNotEmpty()) book.coverUrl = NetworkUtils.getAbsoluteURL(redirectUrl, it) + } + Debug.log(bookSource.bookSourceUrl, "└${book.coverUrl}") + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") } - Debug.log(bookSource.bookSourceUrl, "└${book.coverUrl}") + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true) if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl diff --git a/app/src/main/java/io/legado/app/model/webBook/BookList.kt b/app/src/main/java/io/legado/app/model/webBook/BookList.kt index 05570b6ff..2c1faab41 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookList.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookList.kt @@ -1,7 +1,7 @@ package io.legado.app.model.webBook -import io.legado.app.App import io.legado.app.R +import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.rule.BookListRule @@ -9,12 +9,12 @@ import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils.wordCountFormat -import io.legado.app.utils.htmlFormat -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.isActive +import kotlinx.coroutines.ensureActive +import splitties.init.appCtx object BookList { @@ -30,19 +30,21 @@ object BookList { ): ArrayList { val bookList = ArrayList() body ?: throw Exception( - App.INSTANCE.getString( + appCtx.getString( R.string.error_get_web_content, analyzeUrl.ruleUrl ) ) Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") - if (!scope.isActive) throw CancellationException() + Debug.log(bookSource.bookSourceUrl, body, state = 10) val analyzeRule = AnalyzeRule(variableBook) - analyzeRule.setContent(body, baseUrl) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(baseUrl) bookSource.bookUrlPattern?.let { + scope.ensureActive() if (baseUrl.matches(it.toRegex())) { Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") - getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) + getInfoItem(scope, body, analyzeRule, bookSource, baseUrl, variableBook.variable) ?.let { searchBook -> searchBook.infoHtml = body bookList.add(searchBook) @@ -67,9 +69,10 @@ object BookList { } Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表") collections = analyzeRule.getElements(ruleList) + scope.ensureActive() if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") - getInfoItem(scope, analyzeRule, bookSource, baseUrl, variableBook.variable) + getInfoItem(scope, body, analyzeRule, bookSource, baseUrl, variableBook.variable) ?.let { searchBook -> searchBook.infoHtml = body bookList.add(searchBook) @@ -85,7 +88,6 @@ object BookList { val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") for ((index, item) in collections.withIndex()) { - if (!scope.isActive) throw CancellationException() getSearchItem( scope, item, @@ -119,57 +121,31 @@ object BookList { @Throws(Exception::class) private fun getInfoItem( scope: CoroutineScope, + body: String, analyzeRule: AnalyzeRule, bookSource: BookSource, baseUrl: String, variable: String? ): SearchBook? { - val searchBook = SearchBook(variable = variable) - searchBook.bookUrl = baseUrl - searchBook.origin = bookSource.bookSourceUrl - searchBook.originName = bookSource.bookSourceName - searchBook.originOrder = bookSource.customOrder - searchBook.type = bookSource.bookSourceType - analyzeRule.book = searchBook - with(bookSource.getBookInfoRule()) { - init?.let { - if (it.isNotEmpty()) { - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") - analyzeRule.setContent(analyzeRule.getElement(it)) - } - } - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取书名") - searchBook.name = BookHelp.formatBookName(analyzeRule.getString(name)) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.name}") - if (searchBook.name.isNotEmpty()) { - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取作者") - searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(author)) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}") - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取分类") - searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") - Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind}") - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取字数") - searchBook.wordCount = wordCountFormat(analyzeRule.getString(wordCount)) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}") - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") - searchBook.latestChapterTitle = analyzeRule.getString(lastChapter) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}") - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取简介") - searchBook.intro = analyzeRule.getString(intro).htmlFormat() - Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", true) - if (!scope.isActive) throw CancellationException() - Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") - searchBook.coverUrl = analyzeRule.getString(coverUrl, true) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl}") - return searchBook - } + val book = Book(variable = variable) + book.bookUrl = baseUrl + book.origin = bookSource.bookSourceUrl + book.originName = bookSource.bookSourceName + book.originOrder = bookSource.customOrder + book.type = bookSource.bookSourceType + analyzeRule.book = book + BookInfo.analyzeBookInfo( + scope, + book, + body, + analyzeRule, + bookSource, + baseUrl, + baseUrl, + false + ) + if (book.name.isNotBlank()) { + return book.toSearchBook() } return null } @@ -199,38 +175,59 @@ object BookList { searchBook.originOrder = bookSource.customOrder analyzeRule.book = searchBook analyzeRule.setContent(item) - if (!scope.isActive) throw CancellationException() + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName)) Debug.log(bookSource.bookSourceUrl, "└${searchBook.name}", log) if (searchBook.name.isNotEmpty()) { - if (!scope.isActive) throw CancellationException() + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor)) Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}", log) - if (!scope.isActive) throw CancellationException() + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) - searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") - Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind}", log) - if (!scope.isActive) throw CancellationException() + try { + searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") + Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind}", log) + } catch (e: Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) - searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount)) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}", log) - if (!scope.isActive) throw CancellationException() + try { + searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}", log) + } catch (e: java.lang.Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) - searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) - Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}", log) - if (!scope.isActive) throw CancellationException() + try { + searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}", log) + } catch (e: java.lang.Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) - searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat() - Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", log, true) - if (!scope.isActive) throw CancellationException() + try { + searchBook.intro = HtmlFormatter.format(analyzeRule.getString(ruleIntro)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", log) + } catch (e: java.lang.Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) - analyzeRule.getString(ruleCoverUrl).let { - if (it.isNotEmpty()) searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) + try { + analyzeRule.getString(ruleCoverUrl).let { + if (it.isNotEmpty()) searchBook.coverUrl = + NetworkUtils.getAbsoluteURL(baseUrl, it) + } + Debug.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl}", log) + } catch (e: java.lang.Exception) { + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) } - Debug.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl}", log) - if (!scope.isActive) throw CancellationException() + scope.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, true) if (searchBook.bookUrl.isEmpty()) { diff --git a/app/src/main/java/io/legado/app/model/webBook/PreciseSearch.kt b/app/src/main/java/io/legado/app/model/webBook/PreciseSearch.kt new file mode 100644 index 000000000..31d08314c --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/PreciseSearch.kt @@ -0,0 +1,38 @@ +package io.legado.app.model.webBook + +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.isActive + +/** + * 精准搜索 + */ +object PreciseSearch { + + suspend fun searchFirstBook( + scope: CoroutineScope, + bookSources: List, + name: String, + author: String + ): Book? { + bookSources.forEach { bookSource -> + val webBook = WebBook(bookSource) + kotlin.runCatching { + if (!scope.isActive) return null + webBook.searchBookAwait(scope, name).firstOrNull { + it.name == name && it.author == author + }?.let { + return if (it.tocUrl.isBlank()) { + if (!scope.isActive) return null + webBook.getBookInfoAwait(scope, it.toBook()) + } else { + it.toBook() + } + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt b/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt index 9c257a7db..c15134453 100644 --- a/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt +++ b/app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt @@ -1,15 +1,15 @@ package io.legado.app.model.webBook -import io.legado.app.App +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.utils.getPrefString import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher +import splitties.init.appCtx import java.util.concurrent.Executors import kotlin.math.min @@ -21,13 +21,12 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C private var searchKey: String = "" private var tasks = CompositeCoroutine() private var bookSourceList = arrayListOf() - private val variableBookMap = hashMapOf() @Volatile private var searchIndex = -1 private fun initSearchPool() { - searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() } fun search(searchId: Long, key: String) { @@ -45,12 +44,12 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C initSearchPool() mSearchId = searchId searchPage = 1 - val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: "" + val searchGroup = appCtx.getPrefString("searchGroup") ?: "" bookSourceList.clear() if (searchGroup.isBlank()) { - bookSourceList.addAll(App.db.bookSourceDao().allEnabled) + bookSourceList.addAll(appDb.bookSourceDao.allEnabled) } else { - bookSourceList.addAll(App.db.bookSourceDao().getEnabledByGroup(searchGroup)) + bookSourceList.addAll(appDb.bookSourceDao.getEnabledByGroup(searchGroup)) } } else { searchPage++ @@ -61,15 +60,6 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C } } - private fun getVariableBook(sourceUrl: String): SearchBook { - var vBook = variableBookMap[sourceUrl] - if (vBook == null) { - vBook = SearchBook() - variableBookMap[sourceUrl] = vBook - } - return vBook - } - private fun search(searchId: Long) { synchronized(this) { if (searchIndex >= bookSourceList.lastIndex) { @@ -77,28 +67,26 @@ class SearchBookModel(private val scope: CoroutineScope, private val callBack: C } searchIndex++ val source = bookSourceList[searchIndex] - val variableBook = getVariableBook(source.bookSourceUrl) val task = WebBook(source).searchBook( + scope, searchKey, searchPage, - variableBook, - scope = scope, context = searchPool!! ).timeout(30000L) - .onSuccess(IO) { + .onSuccess(searchPool) { if (searchId == mSearchId) { callBack.onSearchSuccess(it) } } - .onFinally { + .onFinally(searchPool) { synchronized(this) { if (searchIndex < bookSourceList.lastIndex) { search(searchId) } else { searchIndex++ } - if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, - threadCount) + if (searchIndex >= bookSourceList.lastIndex + + min(bookSourceList.size, threadCount) ) { callBack.onSearchFinish() } diff --git a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt index bcc087839..75def476a 100644 --- a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext +@Suppress("MemberVisibilityCanBePrivate") class WebBook(val bookSource: BookSource) { val sourceUrl: String @@ -20,23 +21,22 @@ class WebBook(val bookSource: BookSource) { * 搜索 */ fun searchBook( + scope: CoroutineScope, key: String, page: Int? = 1, - variableBook: SearchBook, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO, ): Coroutine> { return Coroutine.async(scope, context) { - searchBookSuspend(scope, key, page, variableBook) + searchBookAwait(scope, key, page) } } - suspend fun searchBookSuspend( + suspend fun searchBookAwait( scope: CoroutineScope, key: String, page: Int? = 1, - variableBook: SearchBook, ): ArrayList { + val variableBook = SearchBook() bookSource.searchUrl?.let { searchUrl -> val analyzeUrl = AnalyzeUrl( ruleUrl = searchUrl, @@ -46,7 +46,7 @@ class WebBook(val bookSource: BookSource) { headerMapF = bookSource.getHeaderMap(), book = variableBook ) - val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) + val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl) return BookList.analyzeBookList( scope, res.body, @@ -64,82 +64,134 @@ class WebBook(val bookSource: BookSource) { * 发现 */ fun exploreBook( + scope: CoroutineScope, url: String, page: Int? = 1, - variableBook: SearchBook, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO, ): Coroutine> { return Coroutine.async(scope, context) { - val analyzeUrl = AnalyzeUrl( - ruleUrl = url, - page = page, - baseUrl = sourceUrl, - headerMapF = bookSource.getHeaderMap() - ) - val res = analyzeUrl.getResponseAwait(bookSource.bookSourceUrl) - BookList.analyzeBookList( - scope, - res.body, - bookSource, - analyzeUrl, - res.url, - variableBook, - false - ) + exploreBookAwait(scope, url, page) } } + suspend fun exploreBookAwait( + scope: CoroutineScope, + url: String, + page: Int? = 1, + ): ArrayList { + val variableBook = SearchBook() + val analyzeUrl = AnalyzeUrl( + ruleUrl = url, + page = page, + baseUrl = sourceUrl, + book = variableBook, + headerMapF = bookSource.getHeaderMap() + ) + val res = analyzeUrl.getStrResponse(bookSource.bookSourceUrl) + return BookList.analyzeBookList( + scope, + res.body, + bookSource, + analyzeUrl, + res.url, + variableBook, + false + ) + } + /** * 书籍信息 */ fun getBookInfo( + scope: CoroutineScope, book: Book, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO, canReName: Boolean = true, ): Coroutine { return Coroutine.async(scope, context) { - book.type = bookSource.bookSourceType - val body = - if (!book.infoHtml.isNullOrEmpty()) { - book.infoHtml - } else { - val analyzeUrl = AnalyzeUrl( - book = book, - ruleUrl = book.bookUrl, - baseUrl = sourceUrl, - headerMapF = bookSource.getHeaderMap() - ) - analyzeUrl.getResponseAwait(bookSource.bookSourceUrl).body - } - BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl, canReName) - book + getBookInfoAwait(scope, book, canReName) } } + suspend fun getBookInfoAwait( + scope: CoroutineScope, + book: Book, + canReName: Boolean = true, + ): Book { + book.type = bookSource.bookSourceType + if (!book.infoHtml.isNullOrEmpty()) { + book.infoHtml + BookInfo.analyzeBookInfo( + scope, + book, + book.infoHtml, + bookSource, + book.bookUrl, + book.bookUrl, + canReName + ) + } else { + val res = AnalyzeUrl( + ruleUrl = book.bookUrl, + baseUrl = sourceUrl, + headerMapF = bookSource.getHeaderMap(), + book = book + ).getStrResponse(bookSource.bookSourceUrl) + BookInfo.analyzeBookInfo( + scope, + book, + res.body, + bookSource, + book.bookUrl, + res.url, + canReName + ) + } + return book + } + /** * 目录 */ fun getChapterList( + scope: CoroutineScope, book: Book, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO ): Coroutine> { return Coroutine.async(scope, context) { - book.type = bookSource.bookSourceType - val body = - if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { - book.tocHtml - } else { - AnalyzeUrl( - book = book, - ruleUrl = book.tocUrl, - baseUrl = book.bookUrl, - headerMapF = bookSource.getHeaderMap() - ).getResponseAwait(bookSource.bookSourceUrl).body - } - BookChapterList.analyzeChapterList(this, book, body, bookSource, book.tocUrl) + getChapterListAwait(scope, book) + } + } + + suspend fun getChapterListAwait( + scope: CoroutineScope, + book: Book, + ): List { + book.type = bookSource.bookSourceType + return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { + BookChapterList.analyzeChapterList( + scope, + book, + book.tocHtml, + bookSource, + book.tocUrl, + book.tocUrl + ) + } else { + val res = AnalyzeUrl( + book = book, + ruleUrl = book.tocUrl, + baseUrl = book.bookUrl, + headerMapF = bookSource.getHeaderMap() + ).getStrResponse(bookSource.bookSourceUrl) + BookChapterList.analyzeChapterList( + scope, + book, + res.body, + bookSource, + book.tocUrl, + res.url + ) } } @@ -147,57 +199,60 @@ class WebBook(val bookSource: BookSource) { * 章节内容 */ fun getContent( + scope: CoroutineScope, book: Book, bookChapter: BookChapter, nextChapterUrl: String? = null, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO ): Coroutine { return Coroutine.async(scope, context) { - getContentSuspend( - book, bookChapter, nextChapterUrl, scope - ) + getContentAwait(scope, book, bookChapter, nextChapterUrl) } } - /** - * 章节内容 - */ - suspend fun getContentSuspend( + suspend fun getContentAwait( + scope: CoroutineScope, book: Book, bookChapter: BookChapter, nextChapterUrl: String? = null, - scope: CoroutineScope = Coroutine.DEFAULT ): String { if (bookSource.getContentRule().content.isNullOrEmpty()) { Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") return bookChapter.url } - val body = - if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { - book.tocHtml - } else { - val analyzeUrl = - AnalyzeUrl( - book = book, - ruleUrl = bookChapter.url, - baseUrl = book.tocUrl, - headerMapF = bookSource.getHeaderMap() - ) - analyzeUrl.getResponseAwait( - bookSource.bookSourceUrl, - jsStr = bookSource.getContentRule().webJs, - sourceRegex = bookSource.getContentRule().sourceRegex - ).body - } - return BookContent.analyzeContent( - scope, - body, - book, - bookChapter, - bookSource, - bookChapter.url, - nextChapterUrl - ) + return if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { + BookContent.analyzeContent( + scope, + book.tocHtml, + book, + bookChapter, + bookSource, + bookChapter.getAbsoluteURL(), + bookChapter.getAbsoluteURL(), + nextChapterUrl + ) + } else { + val res = AnalyzeUrl( + ruleUrl = bookChapter.getAbsoluteURL(), + baseUrl = book.tocUrl, + headerMapF = bookSource.getHeaderMap(), + book = book, + chapter = bookChapter + ).getStrResponse( + bookSource.bookSourceUrl, + jsStr = bookSource.getContentRule().webJs, + sourceRegex = bookSource.getContentRule().sourceRegex + ) + BookContent.analyzeContent( + scope, + res.body, + book, + bookChapter, + bookSource, + bookChapter.getAbsoluteURL(), + res.url, + nextChapterUrl + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt index fb1f5df59..79d53779e 100644 --- a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt @@ -4,47 +4,48 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.view.KeyEvent -import io.legado.app.App import io.legado.app.constant.EventBus -import io.legado.app.data.entities.Book -import io.legado.app.help.ActivityHelp +import io.legado.app.data.appDb +import io.legado.app.help.AppConfig +import io.legado.app.help.LifecycleHelp import io.legado.app.service.AudioPlayService import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.ReadAloud -import io.legado.app.ui.audio.AudioPlayActivity +import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.main.MainActivity -import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext /** * Created by GKF on 2018/1/6. * 监听耳机键 */ - class MediaButtonReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (handleIntent(context, intent) && isOrderedBroadcast) { + abortBroadcast() + } + } + companion object { fun handleIntent(context: Context, intent: Intent): Boolean { val intentAction = intent.action if (Intent.ACTION_MEDIA_BUTTON == intentAction) { - val keyEvent = - intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) ?: return false + val keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + ?: return false val keycode: Int = keyEvent.keyCode val action: Int = keyEvent.action if (action == KeyEvent.ACTION_DOWN) { when (keycode) { KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + ReadAloud.prevParagraph(context) } KeyEvent.KEYCODE_MEDIA_NEXT -> { + ReadAloud.nextParagraph(context) } else -> readAloud(context) } @@ -53,7 +54,7 @@ class MediaButtonReceiver : BroadcastReceiver() { return true } - private fun readAloud(context: Context) { + fun readAloud(context: Context, isMediaKey: Boolean = true) { when { BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) { ReadAloud.pause(context) @@ -67,38 +68,27 @@ class MediaButtonReceiver : BroadcastReceiver() { } else { AudioPlay.pause(context) } - ActivityHelp.isExist(AudioPlayActivity::class.java) -> + LifecycleHelp.isExistActivity(ReadBookActivity::class.java) -> postEvent(EventBus.MEDIA_BUTTON, true) - ActivityHelp.isExist(ReadBookActivity::class.java) -> + LifecycleHelp.isExistActivity(AudioPlayActivity::class.java) -> postEvent(EventBus.MEDIA_BUTTON, true) - else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) { - GlobalScope.launch(Main) { - val lastBook: Book? = withContext(IO) { - App.db.bookDao().lastReadBook - } - lastBook?.let { - if (!ActivityHelp.isExist(MainActivity::class.java)) { - Intent(context, MainActivity::class.java).let { - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(it) - } - } - Intent(context, ReadBookActivity::class.java).let { + else -> if (AppConfig.mediaButtonOnExit || !isMediaKey) { + appDb.bookDao.lastReadBook?.let { + if (!LifecycleHelp.isExistActivity(MainActivity::class.java)) { + Intent(context, MainActivity::class.java).let { it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - it.putExtra("readAloud", true) context.startActivity(it) } } + Intent(context, ReadBookActivity::class.java).let { + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + it.putExtra("readAloud", true) + context.startActivity(it) + } } } } } } - override fun onReceive(context: Context, intent: Intent) { - if (handleIntent(context, intent) && isOrderedBroadcast) { - abortBroadcast() - } - } - } diff --git a/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt b/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt index e6b78f0f5..c5867c899 100644 --- a/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt +++ b/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt @@ -6,7 +6,8 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.main.MainActivity -import org.jetbrains.anko.startActivity +import io.legado.app.utils.startActivity +import splitties.init.appCtx class SharedReceiverActivity : AppCompatActivity() { @@ -19,27 +20,28 @@ class SharedReceiverActivity : AppCompatActivity() { } private fun initIntent() { - if (Intent.ACTION_SEND == intent.action && intent.type == receivingType) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - if (openUrl(it)) { - startActivity(Pair("key", it)) + when { + intent.action == Intent.ACTION_SEND && intent.type == receivingType -> { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + dispose(it) } } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && Intent.ACTION_PROCESS_TEXT == intent.action - && intent.type == receivingType - ) { - intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let { - if (openUrl(it)) { - startActivity(Pair("key", it)) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && intent.action == Intent.ACTION_PROCESS_TEXT + && intent.type == receivingType -> { + intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let { + dispose(it) } } + intent.getStringExtra("action") == "readAloud" -> { + MediaButtonReceiver.readAloud(appCtx, false) + } } } - private fun openUrl(text: String): Boolean { + private fun dispose(text: String) { if (text.isBlank()) { - return false + return } val urls = text.split("\\s".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val result = StringBuilder() @@ -47,11 +49,10 @@ class SharedReceiverActivity : AppCompatActivity() { if (url.matches("http.+".toRegex())) result.append("\n").append(url.trim { it <= ' ' }) } - return if (result.length > 1) { + if (result.length > 1) { startActivity() - false } else { - true + SearchActivity.start(this, text) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt b/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt index ce2e04fbe..73737aa79 100644 --- a/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt @@ -25,15 +25,13 @@ class TimeBatteryReceiver : BroadcastReceiver() { } override fun onReceive(context: Context?, intent: Intent?) { - intent?.action?.let { - when (it) { - Intent.ACTION_TIME_TICK -> { - postEvent(EventBus.TIME_CHANGED, "") - } - Intent.ACTION_BATTERY_CHANGED -> { - val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - postEvent(EventBus.BATTERY_CHANGED, level) - } + when (intent?.action) { + Intent.ACTION_TIME_TICK -> { + postEvent(EventBus.TIME_CHANGED, "") + } + Intent.ACTION_BATTERY_CHANGED -> { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + postEvent(EventBus.BATTERY_CHANGED, level) } } } diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt index 1817da185..4c9d247ce 100644 --- a/app/src/main/java/io/legado/app/service/AudioPlayService.kt +++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt @@ -6,36 +6,36 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.BitmapFactory -import android.media.AudioFocusRequest import android.media.AudioManager import android.media.MediaPlayer import android.net.Uri import android.os.Build import android.os.Handler +import android.os.Looper import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat -import io.legado.app.App +import androidx.media.AudioFocusRequestCompat import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.AppConst import io.legado.app.constant.EventBus import io.legado.app.constant.IntentAction import io.legado.app.constant.Status +import io.legado.app.data.appDb import io.legado.app.data.entities.BookChapter -import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp import io.legado.app.help.MediaHelp import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.service.help.AudioPlay -import io.legado.app.ui.audio.AudioPlayActivity +import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.utils.postEvent -import kotlinx.coroutines.Dispatchers.IO +import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jetbrains.anko.toast +import splitties.init.appCtx class AudioPlayService : BaseService(), @@ -50,9 +50,9 @@ class AudioPlayService : BaseService(), var timeMinute: Int = 0 } - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private lateinit var audioManager: AudioManager - private var mFocusRequest: AudioFocusRequest? = null + private var mFocusRequest: AudioFocusRequestCompat? = null private var title: String = "" private var subtitle: String = "" private val mediaPlayer = MediaPlayer() @@ -62,12 +62,12 @@ class AudioPlayService : BaseService(), private var position = 0 private val dsRunnable: Runnable = Runnable { doDs() } private var mpRunnable: Runnable = Runnable { upPlayProgress() } - private var bookChapter: BookChapter? = null private var playSpeed: Float = 1f override fun onCreate() { super.onCreate() isRun = true + upNotification() audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager mFocusRequest = MediaHelp.getFocusRequest(this) mediaPlayer.setOnErrorListener(this) @@ -75,7 +75,6 @@ class AudioPlayService : BaseService(), mediaPlayer.setOnCompletionListener(this) initMediaSession() initBroadcastReceiver() - upNotification() upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) } @@ -85,14 +84,15 @@ class AudioPlayService : BaseService(), IntentAction.play -> { AudioPlay.book?.let { title = it.name + subtitle = AudioPlay.durChapter?.title ?: "" position = it.durChapterPos - loadContent(it.durChapterIndex) + loadContent() } } IntentAction.pause -> pause(true) IntentAction.resume -> resume() - IntentAction.prev -> moveToPrev() - IntentAction.next -> moveToNext() + IntentAction.prev -> AudioPlay.prev(this) + IntentAction.next -> AudioPlay.next(this) IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) IntentAction.addTimer -> addTimer() IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) @@ -121,18 +121,20 @@ class AudioPlayService : BaseService(), private fun play() { upNotification() if (requestFocus()) { - try { - AudioPlay.status = Status.PLAY - postEvent(EventBus.AUDIO_STATE, Status.PLAY) + kotlin.runCatching { + AudioPlay.status = Status.STOP + postEvent(EventBus.AUDIO_STATE, Status.STOP) mediaPlayer.reset() val analyzeUrl = AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true) val uri = Uri.parse(analyzeUrl.url) mediaPlayer.setDataSource(this, uri, analyzeUrl.headerMap) mediaPlayer.prepareAsync() - } catch (e: Exception) { + handler.removeCallbacks(mpRunnable) + }.onFailure { + it.printStackTrace() launch { - toast("$url ${e.localizedMessage}") + toastOnUi("$url ${it.localizedMessage}") stopSelf() } } @@ -147,7 +149,7 @@ class AudioPlayService : BaseService(), AudioPlayService.pause = pause handler.removeCallbacks(mpRunnable) position = mediaPlayer.currentPosition - mediaPlayer.pause() + if (mediaPlayer.isPlaying) mediaPlayer.pause() upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) AudioPlay.status = Status.PAUSE postEvent(EventBus.AUDIO_STATE, Status.PAUSE) @@ -160,8 +162,10 @@ class AudioPlayService : BaseService(), private fun resume() { pause = false - mediaPlayer.start() - mediaPlayer.seekTo(position) + if (!mediaPlayer.isPlaying) { + mediaPlayer.start() + mediaPlayer.seekTo(position) + } handler.removeCallbacks(mpRunnable) handler.postDelayed(mpRunnable, 1000) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) @@ -194,30 +198,29 @@ class AudioPlayService : BaseService(), /** * 加载完成 */ - override fun onPrepared(mp: MediaPlayer?) { - if (pause) return + override fun onPrepared(mp: MediaPlayer) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mediaPlayer.playbackParams = mediaPlayer.playbackParams.apply { speed = playSpeed } } else { mediaPlayer.start() } mediaPlayer.seekTo(position) + AudioPlay.status = Status.PLAY + postEvent(EventBus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration) - bookChapter?.let { - it.end = mediaPlayer.duration.toLong() - } handler.removeCallbacks(mpRunnable) handler.post(mpRunnable) + AudioPlay.saveDurChapter(mediaPlayer.duration.toLong()) } /** * 播放出错 */ - override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { + override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { if (!mediaPlayer.isPlaying) { AudioPlay.status = Status.STOP postEvent(EventBus.AUDIO_STATE, Status.STOP) - launch { toast("error: $what $extra $url") } + launch { toastOnUi("error: $what $extra $url") } } return true } @@ -225,9 +228,9 @@ class AudioPlayService : BaseService(), /** * 播放结束 */ - override fun onCompletion(mp: MediaPlayer?) { + override fun onCompletion(mp: MediaPlayer) { handler.removeCallbacks(mpRunnable) - moveToNext() + AudioPlay.next(this) } private fun setTimer(minute: Int) { @@ -262,44 +265,31 @@ class AudioPlayService : BaseService(), handler.postDelayed(mpRunnable, 1000) } - - private fun loadContent(index: Int) { - AudioPlay.book?.let { book -> - if (addLoading(index)) { - launch(IO) { - App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> - if (index == AudioPlay.durChapterIndex) { - bookChapter = chapter - subtitle = chapter.title - postEvent(EventBus.AUDIO_SUB_TITLE, subtitle) - postEvent(EventBus.AUDIO_SIZE, chapter.end?.toInt() ?: 0) - postEvent(EventBus.AUDIO_PROGRESS, position) - } - loadContent(chapter) - } ?: removeLoading(index) - } - } - } - } - - private fun loadContent(chapter: BookChapter) { - AudioPlay.book?.let { book -> - AudioPlay.webBook?.getContent(book, chapter, scope = this) - ?.onSuccess(IO) { content -> - if (content.isEmpty()) { - withContext(Main) { - toast("未获取到资源链接") + private fun loadContent() = with(AudioPlay) { + durChapter?.let { chapter -> + if (addLoading(durChapterIndex)) { + val book = AudioPlay.book + val webBook = AudioPlay.webBook + if (book != null && webBook != null) { + webBook.getContent(this@AudioPlayService, book, chapter) + .onSuccess { content -> + if (content.isEmpty()) { + withContext(Main) { + toastOnUi("未获取到资源链接") + } + } else { + contentLoadFinish(chapter, content) + } + }.onError { + contentLoadFinish(chapter, it.localizedMessage ?: toString()) + }.onFinally { + removeLoading(chapter.index) } - removeLoading(chapter.index) - } else { - BookHelp.saveContent(book, chapter, content) - contentLoadFinish(chapter, content) - removeLoading(chapter.index) - } - }?.onError { - contentLoadFinish(chapter, it.localizedMessage ?: toString()) + } else { removeLoading(chapter.index) + toastOnUi("book or source is null") } + } } } @@ -328,49 +318,11 @@ class AudioPlayService : BaseService(), } } - private fun moveToPrev() { - if (AudioPlay.durChapterIndex > 0) { - mediaPlayer.pause() - AudioPlay.durChapterIndex-- - AudioPlay.durPageIndex = 0 - AudioPlay.book?.durChapterIndex = AudioPlay.durChapterIndex - saveRead() - position = 0 - loadContent(AudioPlay.durChapterIndex) - } - } - - private fun moveToNext() { - if (AudioPlay.durChapterIndex < AudioPlay.chapterSize - 1) { - mediaPlayer.pause() - AudioPlay.durChapterIndex++ - AudioPlay.durPageIndex = 0 - AudioPlay.book?.durChapterIndex = AudioPlay.durChapterIndex - saveRead() - position = 0 - loadContent(AudioPlay.durChapterIndex) - } else { - stopSelf() - } - } - - private fun saveRead() { - launch(IO) { - AudioPlay.book?.let { book -> - book.lastCheckCount = 0 - book.durChapterTime = System.currentTimeMillis() - book.durChapterIndex = AudioPlay.durChapterIndex - book.durChapterPos = AudioPlay.durPageIndex - book.durChapterTitle = subtitle - App.db.bookDao().update(book) - } - } - } - private fun saveProgress() { - launch(IO) { + execute { AudioPlay.book?.let { - App.db.bookDao().upProgress(it.bookUrl, AudioPlay.durPageIndex) + AudioPlay.durChapterPos = mediaPlayer.currentPosition + appDb.bookDao.upProgress(it.bookUrl, AudioPlay.durChapterPos) } } } @@ -415,12 +367,11 @@ class AudioPlayService : BaseService(), }) mediaSessionCompat?.setMediaButtonReceiver( PendingIntent.getBroadcast( - this, - 0, + this, 0, Intent( Intent.ACTION_MEDIA_BUTTON, null, - App.INSTANCE, + appCtx, MediaButtonReceiver::class.java ), PendingIntent.FLAG_CANCEL_CURRENT @@ -473,7 +424,7 @@ class AudioPlayService : BaseService(), var nTitle: String = when { pause -> getString(R.string.audio_pause) timeMinute in 1..60 -> getString( - R.string.read_aloud_timer, + R.string.playing_timer, timeMinute ) else -> getString(R.string.audio_play_t) @@ -528,7 +479,7 @@ class AudioPlayService : BaseService(), * @return 音频焦点 */ private fun requestFocus(): Boolean { - return MediaHelp.requestFocus(audioManager, this, mFocusRequest) + return MediaHelp.requestFocus(audioManager, mFocusRequest) } private fun thisPendingIntent(action: String): PendingIntent? { diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index 2fd69506e..a6c35c7ae 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -6,14 +6,14 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.BitmapFactory -import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Handler +import android.os.Looper import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import androidx.annotation.CallSuper import androidx.core.app.NotificationCompat -import io.legado.app.App +import androidx.media.AudioFocusRequestCompat import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.* @@ -26,6 +26,8 @@ import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx abstract class BaseReadAloudService : BaseService(), AudioManager.OnAudioFocusChangeListener { @@ -40,9 +42,9 @@ abstract class BaseReadAloudService : BaseService(), } } - internal val handler = Handler() + internal val handler = Handler(Looper.getMainLooper()) private lateinit var audioManager: AudioManager - private var mFocusRequest: AudioFocusRequest? = null + private var mFocusRequest: AudioFocusRequestCompat? = null private var broadcastReceiver: BroadcastReceiver? = null private lateinit var mediaSessionCompat: MediaSessionCompat private var title: String = "" @@ -74,6 +76,7 @@ abstract class BaseReadAloudService : BaseService(), postEvent(EventBus.ALOUD_STATE, Status.STOP) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) mediaSessionCompat.release() + ReadBook.uploadProgress() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -139,6 +142,7 @@ abstract class BaseReadAloudService : BaseService(), upNotification() upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) postEvent(EventBus.ALOUD_STATE, Status.PAUSE) + ReadBook.uploadProgress() } @CallSuper @@ -199,7 +203,11 @@ abstract class BaseReadAloudService : BaseService(), * @return 音频焦点 */ fun requestFocus(): Boolean { - return MediaHelp.requestFocus(audioManager, this, mFocusRequest) + val requestFocus = MediaHelp.requestFocus(audioManager, mFocusRequest) + if (!requestFocus) { + toastOnUi("未获取到音频焦点") + } + return requestFocus } /** @@ -230,7 +238,7 @@ abstract class BaseReadAloudService : BaseService(), Intent( Intent.ACTION_MEDIA_BUTTON, null, - App.INSTANCE, + appCtx, MediaButtonReceiver::class.java ), PendingIntent.FLAG_CANCEL_CURRENT diff --git a/app/src/main/java/io/legado/app/service/CacheBookService.kt b/app/src/main/java/io/legado/app/service/CacheBookService.kt new file mode 100644 index 000000000..7e592f545 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/CacheBookService.kt @@ -0,0 +1,295 @@ +package io.legado.app.service + +import android.content.Intent +import android.os.Handler +import android.os.Looper +import androidx.core.app.NotificationCompat +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.AppConfig +import io.legado.app.help.BookHelp +import io.legado.app.help.IntentHelp +import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.help.CacheBook +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.isActive +import splitties.init.appCtx +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet +import java.util.concurrent.Executors +import kotlin.math.min + +class CacheBookService : BaseService() { + private val threadCount = AppConfig.threadCount + private var cachePool = + Executors.newFixedThreadPool(min(threadCount, 8)).asCoroutineDispatcher() + private var tasks = CompositeCoroutine() + private val handler = Handler(Looper.getMainLooper()) + private var runnable: Runnable = Runnable { upDownload() } + private val bookMap = ConcurrentHashMap() + private val webBookMap = ConcurrentHashMap() + private val downloadMap = ConcurrentHashMap>() + private val downloadCount = ConcurrentHashMap() + private val finalMap = ConcurrentHashMap>() + private val downloadingList = CopyOnWriteArraySet() + + @Volatile + private var downloadingCount = 0 + private var notificationContent = appCtx.getString(R.string.starting_download) + + private val notificationBuilder by lazy { + val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(true) + .setContentTitle(getString(R.string.offline_cache)) + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent(this, IntentAction.stop) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } + + override fun onCreate() { + super.onCreate() + upNotification() + handler.postDelayed(runnable, 1000) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + IntentAction.start -> addDownloadData( + intent.getStringExtra("bookUrl"), + intent.getIntExtra("start", 0), + intent.getIntExtra("end", 0) + ) + IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl")) + IntentAction.stop -> stopDownload() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + tasks.clear() + cachePool.close() + handler.removeCallbacks(runnable) + downloadMap.clear() + finalMap.clear() + super.onDestroy() + postEvent(EventBus.UP_DOWNLOAD, downloadMap) + } + + private fun getBook(bookUrl: String): Book? { + var book = bookMap[bookUrl] + if (book == null) { + synchronized(this) { + book = bookMap[bookUrl] + if (book == null) { + book = appDb.bookDao.getBook(bookUrl) + if (book == null) { + removeDownload(bookUrl) + } + } + } + } + return book + } + + private fun getWebBook(bookUrl: String, origin: String): WebBook? { + var webBook = webBookMap[origin] + if (webBook == null) { + synchronized(this) { + webBook = webBookMap[origin] + if (webBook == null) { + appDb.bookSourceDao.getBookSource(origin)?.let { + webBook = WebBook(it) + } + if (webBook == null) { + removeDownload(bookUrl) + } + } + } + } + return webBook + } + + private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { + bookUrl ?: return + if (downloadMap.containsKey(bookUrl)) { + notificationContent = getString(R.string.already_in_download) + upNotification() + toastOnUi(notificationContent) + return + } + downloadCount[bookUrl] = DownloadCount() + execute(context = cachePool) { + appDb.bookChapterDao.getChapterList(bookUrl, start, end).let { + if (it.isNotEmpty()) { + val chapters = CopyOnWriteArraySet() + chapters.addAll(it) + downloadMap[bookUrl] = chapters + } else { + CacheBook.addLog("${getBook(bookUrl)?.name} is empty") + } + } + for (i in 0 until threadCount) { + if (downloadingCount < threadCount) { + download() + } + } + } + } + + private fun removeDownload(bookUrl: String?) { + downloadMap.remove(bookUrl) + finalMap.remove(bookUrl) + } + + private fun download() { + downloadingCount += 1 + val task = Coroutine.async(this, context = cachePool) { + if (!isActive) return@async + val bookChapter: BookChapter? = synchronized(this@CacheBookService) { + downloadMap.forEach { + it.value.forEach { chapter -> + if (!downloadingList.contains(chapter.url)) { + downloadingList.add(chapter.url) + return@synchronized chapter + } + } + } + return@synchronized null + } + if (bookChapter == null) { + postDownloading(false) + } else { + val book = getBook(bookChapter.bookUrl) + if (book == null) { + postDownloading(true) + return@async + } + val webBook = getWebBook(bookChapter.bookUrl, book.origin) + if (webBook == null) { + postDownloading(true) + return@async + } + if (!BookHelp.hasImageContent(book, bookChapter)) { + webBook.getContent(this, book, bookChapter, context = cachePool) + .timeout(60000L) + .onError(cachePool) { + synchronized(this) { + downloadingList.remove(bookChapter.url) + } + notificationContent = "getContentError${it.localizedMessage}" + upNotification() + } + .onSuccess(cachePool) { + synchronized(this@CacheBookService) { + downloadCount[book.bookUrl]?.increaseSuccess() + downloadCount[book.bookUrl]?.increaseFinished() + downloadCount[book.bookUrl]?.let { + upNotification( + it, + downloadMap[book.bookUrl]?.size, + bookChapter.title + ) + } + val chapterMap = + finalMap[book.bookUrl] + ?: CopyOnWriteArraySet().apply { + finalMap[book.bookUrl] = this + } + chapterMap.add(bookChapter) + if (chapterMap.size == downloadMap[book.bookUrl]?.size) { + downloadMap.remove(book.bookUrl) + finalMap.remove(book.bookUrl) + downloadCount.remove(book.bookUrl) + } + } + }.onFinally(cachePool) { + postDownloading(true) + } + } else { + //无需下载的,设置为增加成功 + downloadCount[book.bookUrl]?.increaseSuccess() + downloadCount[book.bookUrl]?.increaseFinished() + postDownloading(true) + } + } + }.onError(cachePool) { + notificationContent = "ERROR:${it.localizedMessage}" + CacheBook.addLog(notificationContent) + upNotification() + } + tasks.add(task) + } + + private fun postDownloading(hasChapter: Boolean) { + downloadingCount -= 1 + if (hasChapter) { + download() + } else { + if (downloadingCount < 1) { + stopDownload() + } + } + } + + private fun stopDownload() { + tasks.clear() + stopSelf() + } + + private fun upDownload() { + upNotification() + postEvent(EventBus.UP_DOWNLOAD, downloadMap) + handler.removeCallbacks(runnable) + handler.postDelayed(runnable, 1000) + } + + private fun upNotification( + downloadCount: DownloadCount, + totalCount: Int?, + content: String + ) { + notificationContent = + "进度:" + downloadCount.downloadFinishedCount + "/" + totalCount + ",成功:" + downloadCount.successCount + "," + content + } + + /** + * 更新通知 + */ + private fun upNotification() { + notificationBuilder.setContentText(notificationContent) + val notification = notificationBuilder.build() + startForeground(AppConst.notificationIdDownload, notification) + } + + class DownloadCount { + @Volatile + var downloadFinishedCount = 0 // 下载完成的条目数量 + + @Volatile + var successCount = 0 //下载成功的条目数量 + + fun increaseSuccess() { + ++successCount + } + + fun increaseFinished() { + ++downloadFinishedCount + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/CheckSourceService.kt b/app/src/main/java/io/legado/app/service/CheckSourceService.kt index e6cbb5966..0c19e7445 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -2,32 +2,53 @@ package io.legado.app.service import android.content.Intent import androidx.core.app.NotificationCompat -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.AppConst +import io.legado.app.constant.EventBus import io.legado.app.constant.IntentAction +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.CheckSource import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi import kotlinx.coroutines.asCoroutineDispatcher -import org.jetbrains.anko.toast import java.util.concurrent.Executors import kotlin.math.min class CheckSourceService : BaseService() { private var threadCount = AppConfig.threadCount - private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + private var searchCoroutine = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() private var tasks = CompositeCoroutine() private val allIds = ArrayList() private val checkedIds = ArrayList() private var processIndex = 0 + private var notificationMsg = "" + private val notificationBuilder by lazy { + NotificationCompat.Builder(this, AppConst.channelIdReadAloud) + .setSmallIcon(R.drawable.ic_network_check) + .setOngoing(true) + .setContentTitle(getString(R.string.check_book_source)) + .setContentIntent( + IntentHelp.activityPendingIntent(this, "activity") + ) + .addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent(this, IntentAction.stop) + ) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } override fun onCreate() { super.onCreate() - updateNotification(0, getString(R.string.start)) + notificationMsg = getString(R.string.start) + upNotification() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -43,12 +64,13 @@ class CheckSourceService : BaseService() { override fun onDestroy() { super.onDestroy() tasks.clear() - searchPool.close() + searchCoroutine.close() + postEvent(EventBus.CHECK_SOURCE_DONE, 0) } private fun check(ids: List) { if (allIds.isNotEmpty()) { - toast("已有书源在校验,等完成后再试") + toastOnUi("已有书源在校验,等完成后再试") return } tasks.clear() @@ -57,7 +79,8 @@ class CheckSourceService : BaseService() { allIds.addAll(ids) processIndex = 0 threadCount = min(allIds.size, threadCount) - updateNotification(0, getString(R.string.progress_show, 0, allIds.size)) + notificationMsg = getString(R.string.progress_show, "", 0, allIds.size) + upNotification() for (i in 0 until threadCount) { check() } @@ -71,30 +94,68 @@ class CheckSourceService : BaseService() { synchronized(this) { processIndex++ } - execute { + execute(context = searchCoroutine) { if (index < allIds.size) { val sourceUrl = allIds[index] - App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> - if (source.searchUrl.isNullOrEmpty()) { - onNext(sourceUrl) - } else { - CheckSource(source).check(this, searchPool) { - onNext(it) - } - } - } ?: onNext(sourceUrl) + appDb.bookSourceDao.getBookSource(sourceUrl)?.let { source -> + check(source) + } ?: onNext(sourceUrl, "") } } } - private fun onNext(sourceUrl: String) { + fun check(source: BookSource) { + execute(context = searchCoroutine) { + val webBook = WebBook(source) + var books = webBook.searchBookAwait(this, CheckSource.keyword) + if (books.isEmpty()) { + val exs = source.exploreKinds + if (exs.isEmpty()) { + throw Exception("搜索内容为空并且没有发现") + } + var url: String? = null + for (ex in exs) { + url = ex.url + if (!url.isNullOrBlank()) { + break + } + } + books = webBook.exploreBookAwait(this, url!!) + } + val book = webBook.getBookInfoAwait(this, books.first().toBook()) + val toc = webBook.getChapterListAwait(this, book) + val content = webBook.getContentAwait(this, book, toc.first()) + if (content.isBlank()) { + throw Exception("正文内容为空") + } + }.timeout(180000L) + .onError(searchCoroutine) { + source.addGroup("失效") + source.bookSourceComment = """ + "error:${it.localizedMessage} + ${source.bookSourceComment}" + """.trimIndent() + appDb.bookSourceDao.update(source) + }.onSuccess(searchCoroutine) { + source.removeGroup("失效") + source.bookSourceComment = source.bookSourceComment + ?.split("\n") + ?.filterNot { + it.startsWith("error:") + }?.joinToString("\n") + appDb.bookSourceDao.update(source) + }.onFinally(searchCoroutine) { + onNext(source.bookSourceUrl, source.bookSourceName) + } + } + + private fun onNext(sourceUrl: String, sourceName: String) { synchronized(this) { check() checkedIds.add(sourceUrl) - updateNotification( - checkedIds.size, - getString(R.string.progress_show, checkedIds.size, allIds.size) - ) + notificationMsg = + getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size) + upNotification() if (processIndex >= allIds.size + threadCount - 1) { stopSelf() } @@ -104,24 +165,11 @@ class CheckSourceService : BaseService() { /** * 更新通知 */ - private fun updateNotification(state: Int, msg: String) { - val builder = NotificationCompat.Builder(this, AppConst.channelIdReadAloud) - .setSmallIcon(R.drawable.ic_network_check) - .setOngoing(true) - .setContentTitle(getString(R.string.check_book_source)) - .setContentText(msg) - .setContentIntent( - IntentHelp.activityPendingIntent(this, "activity") - ) - .addAction( - R.drawable.ic_stop_black_24dp, - getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, IntentAction.stop) - ) - builder.setProgress(allIds.size, state, false) - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - val notification = builder.build() - startForeground(112202, notification) + private fun upNotification() { + notificationBuilder.setContentText(notificationMsg) + notificationBuilder.setProgress(allIds.size, checkedIds.size, false) + postEvent(EventBus.CHECK_SOURCE, notificationMsg) + startForeground(112202, notificationBuilder.build()) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt index 047e8d643..d3c7af5ef 100644 --- a/app/src/main/java/io/legado/app/service/DownloadService.kt +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -1,296 +1,178 @@ package io.legado.app.service +import android.app.DownloadManager +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Build import android.os.Handler +import android.os.Looper import androidx.core.app.NotificationCompat -import io.legado.app.App +import androidx.core.content.FileProvider import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.AppConst -import io.legado.app.constant.EventBus import io.legado.app.constant.IntentAction -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.help.AppConfig -import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp -import io.legado.app.help.coroutine.CompositeCoroutine -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.webBook.WebBook -import io.legado.app.service.help.Download -import io.legado.app.utils.postEvent -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.isActive -import org.jetbrains.anko.toast -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArraySet -import java.util.concurrent.Executors +import io.legado.app.utils.RealPathUtil +import io.legado.app.utils.msg +import io.legado.app.utils.toastOnUi +import splitties.systemservices.downloadManager +import java.io.File + class DownloadService : BaseService() { - private val threadCount = AppConfig.threadCount - private var searchPool = - Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() - private var tasks = CompositeCoroutine() - private val handler = Handler() - private var runnable: Runnable = Runnable { upDownload() } - private val bookMap = ConcurrentHashMap() - private val webBookMap = ConcurrentHashMap() - private val downloadMap = ConcurrentHashMap>() - private val downloadCount = ConcurrentHashMap() - private val finalMap = ConcurrentHashMap>() - private val downloadingList = CopyOnWriteArraySet() - @Volatile - private var downloadingCount = 0 - private var notificationContent = App.INSTANCE.getString(R.string.starting_download) + private val downloads = hashMapOf() + private val completeDownloads = hashSetOf() + private val handler = Handler(Looper.getMainLooper()) + private val runnable = Runnable { + checkDownloadState() + } - private val notificationBuilder by lazy { - val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) - .setSmallIcon(R.drawable.ic_download) - .setOngoing(true) - .setContentTitle(getString(R.string.download_offline)) - builder.addAction( - R.drawable.ic_stop_black_24dp, - getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, IntentAction.stop) - ) - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + private val downloadReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + queryState() + } } override fun onCreate() { super.onCreate() - updateNotification(notificationContent) - handler.postDelayed(runnable, 1000) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - intent?.action?.let { action -> - when (action) { - IntentAction.start -> addDownloadData( - intent.getStringExtra("bookUrl"), - intent.getIntExtra("start", 0), - intent.getIntExtra("end", 0) - ) - IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl")) - IntentAction.stop -> stopDownload() - } - } - return super.onStartCommand(intent, flags, startId) + registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) } override fun onDestroy() { - tasks.clear() - searchPool.close() - handler.removeCallbacks(runnable) - downloadMap.clear() - finalMap.clear() super.onDestroy() - postEvent(EventBus.UP_DOWNLOAD, downloadMap) + unregisterReceiver(downloadReceiver) } - private fun getBook(bookUrl: String): Book? { - var book = bookMap[bookUrl] - if (book == null) { - synchronized(this) { - book = bookMap[bookUrl] - if (book == null) { - book = App.db.bookDao().getBook(bookUrl) - if (book == null) { - removeDownload(bookUrl) - } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.start -> startDownload( + intent.getLongExtra("downloadId", 0), + intent.getStringExtra("fileName") ?: "未知文件" + ) + IntentAction.play -> { + val id = intent.getLongExtra("downloadId", 0) + if (downloads[id]?.endsWith(".apk") == true) { + installApk(id) } } - } - return book - } - - private fun getWebBook(bookUrl: String, origin: String): WebBook? { - var webBook = webBookMap[origin] - if (webBook == null) { - synchronized(this) { - webBook = webBookMap[origin] - if (webBook == null) { - App.db.bookSourceDao().getBookSource(origin)?.let { - webBook = WebBook(it) - } - if (webBook == null) { - removeDownload(bookUrl) - } - } + IntentAction.stop -> { + val downloadId = intent.getLongExtra("downloadId", 0) + downloads.remove(downloadId) + stopSelf() } } - return webBook + return super.onStartCommand(intent, flags, startId) } - private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { - bookUrl ?: return - if (downloadMap.containsKey(bookUrl)) { - updateNotification(getString(R.string.already_in_download)) - toast(R.string.already_in_download) - return - } - downloadCount[bookUrl] = DownloadCount() - execute { - App.db.bookChapterDao().getChapterList(bookUrl, start, end).let { - if (it.isNotEmpty()) { - val chapters = CopyOnWriteArraySet() - chapters.addAll(it) - downloadMap[bookUrl] = chapters - } else { - Download.addLog("${getBook(bookUrl)?.name} is empty") - } - } - for (i in 0 until threadCount) { - if (downloadingCount < threadCount) { - download() - } - } + private fun startDownload(downloadId: Long, fileName: String) { + if (downloadId > 0) { + downloads[downloadId] = fileName + queryState() + checkDownloadState() } } - private fun removeDownload(bookUrl: String?) { - downloadMap.remove(bookUrl) - finalMap.remove(bookUrl) + private fun checkDownloadState() { + handler.removeCallbacks(runnable) + queryState() + handler.postDelayed(runnable, 1000) } - private fun download() { - downloadingCount += 1 - val task = Coroutine.async(this, context = searchPool) { - if (!isActive) return@async - val bookChapter: BookChapter? = synchronized(this@DownloadService) { - downloadMap.forEach { - it.value.forEach { chapter -> - if (!downloadingList.contains(chapter.url)) { - downloadingList.add(chapter.url) - return@synchronized chapter - } - } - } - return@synchronized null - } - if (bookChapter == null) { - postDownloading(false) - } else { - val book = getBook(bookChapter.bookUrl) - if (book == null) { - postDownloading(true) - return@async - } - val webBook = getWebBook(bookChapter.bookUrl, book.origin) - if (webBook == null) { - postDownloading(true) - return@async - } - if (!BookHelp.hasContent(book, bookChapter)) { - webBook.getContent( - book, - bookChapter, - scope = this, - context = searchPool - ).timeout(60000L) - .onError { - synchronized(this) { - downloadingList.remove(bookChapter.url) + //查询下载进度 + private fun queryState() { + val ids = downloads.keys + val query = DownloadManager.Query() + query.setFilterById(*ids.toLongArray()) + downloadManager.query(query).use { cursor -> + if (!cursor.moveToFirst()) return + val id = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID)) + val progress: Int = + cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + val max: Int = + cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) + val status = + when (cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))) { + DownloadManager.STATUS_PAUSED -> "暂停" + DownloadManager.STATUS_PENDING -> "待下载" + DownloadManager.STATUS_RUNNING -> "下载中" + DownloadManager.STATUS_SUCCESSFUL -> { + if (!completeDownloads.contains(id)) { + completeDownloads.add(id) + if (downloads[id]?.endsWith(".apk") == true) { + installApk(id) } - Download.addLog("getContentError${it.localizedMessage}") - updateNotification("getContentError${it.localizedMessage}") } - .onSuccess(IO) { content -> - BookHelp.saveContent(book, bookChapter, content) - synchronized(this@DownloadService) { - downloadCount[book.bookUrl]?.increaseSuccess() - downloadCount[book.bookUrl]?.increaseFinished() - downloadCount[book.bookUrl]?.let { - updateNotification( - it, - downloadMap[book.bookUrl]?.size, - bookChapter.title - ) - } - val chapterMap = - finalMap[book.bookUrl] - ?: CopyOnWriteArraySet().apply { - finalMap[book.bookUrl] = this - } - chapterMap.add(bookChapter) - if (chapterMap.size == downloadMap[book.bookUrl]?.size) { - downloadMap.remove(book.bookUrl) - finalMap.remove(book.bookUrl) - downloadCount.remove(book.bookUrl) - } - } - }.onFinally(IO) { - postDownloading(true) - } - } else { - //无需下载的,设置为增加成功 - downloadCount[book.bookUrl]?.increaseSuccess() - downloadCount[book.bookUrl]?.increaseFinished() - postDownloading(true) + "下载完成" + } + DownloadManager.STATUS_FAILED -> "下载失败" + else -> "未知状态" } - } - }.onError { - Download.addLog("ERROR:${it.localizedMessage}") - updateNotification("ERROR:${it.localizedMessage}") + updateNotification(id, "${downloads[id]} $status", max, progress) } - tasks.add(task) } - private fun postDownloading(hasChapter: Boolean) { - downloadingCount -= 1 - if (hasChapter) { - download() - } else { - if (downloadingCount < 1) { - stopDownload() + private fun installApk(downloadId: Long) { + downloadManager.getUriForDownloadedFile(downloadId)?.let { + val filePath = RealPathUtil.getPath(this, it) ?: return + val file = File(filePath) + //调用系统安装apk + val intent = Intent() + intent.action = Intent.ACTION_VIEW + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0版本以上 + val contentUrl = FileProvider.getUriForFile(this, AppConst.authority, file) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.setDataAndType(contentUrl, "application/vnd.android.package-archive") + } else { + val uri: Uri = Uri.fromFile(file) + intent.setDataAndType(uri, "application/vnd.android.package-archive") } - } - } - private fun stopDownload() { - tasks.clear() - stopSelf() - } - - private fun upDownload() { - updateNotification(notificationContent) - postEvent(EventBus.UP_DOWNLOAD, downloadMap) - handler.removeCallbacks(runnable) - handler.postDelayed(runnable, 1000) - } - - private fun updateNotification( - downloadCount: DownloadCount, - totalCount: Int?, - content: String - ) { - notificationContent = - "进度:" + downloadCount.downloadFinishedCount + "/" + totalCount + ",成功:" + downloadCount.successCount + "," + content + try { + startActivity(intent) + } catch (e: Exception) { + toastOnUi(e.msg) + } + } } /** * 更新通知 */ - private fun updateNotification(content: String) { + private fun updateNotification(downloadId: Long, content: String, max: Int, progress: Int) { + val notificationBuilder = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(true) + .setContentTitle(getString(R.string.action_download)) + notificationBuilder.setContentIntent( + IntentHelp.servicePendingIntent(this, IntentAction.play) { + putExtra("downloadId", downloadId) + } + ) + notificationBuilder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + IntentHelp.servicePendingIntent(this, IntentAction.stop) { + putExtra("downloadId", downloadId) + } + ) + notificationBuilder.setDeleteIntent( + IntentHelp.servicePendingIntent(this, IntentAction.stop) { + putExtra("downloadId", downloadId) + } + ) + notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notificationBuilder.setContentText(content) + notificationBuilder.setProgress(max, progress, false) + notificationBuilder.setAutoCancel(true) val notification = notificationBuilder.build() - startForeground(AppConst.notificationIdDownload, notification) + startForeground(downloadId.toInt(), notification) } - class DownloadCount { - @Volatile - var downloadFinishedCount = 0 // 下载完成的条目数量 - - @Volatile - var successCount = 0 //下载成功的条目数量 - - fun increaseSuccess() { - ++successCount - } - - fun increaseFinished() { - ++downloadFinishedCount - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 4fe576dcf..7d60c76d9 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -1,7 +1,6 @@ package io.legado.app.service import android.app.PendingIntent -import android.content.Intent import android.media.MediaPlayer import io.legado.app.constant.EventBus import io.legado.app.help.AppConfig @@ -10,20 +9,23 @@ import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook -import io.legado.app.utils.FileUtils -import io.legado.app.utils.LogUtils -import io.legado.app.utils.postEvent +import io.legado.app.utils.* +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.isActive import java.io.File import java.io.FileDescriptor import java.io.FileInputStream +import java.io.IOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.util.* class HttpReadAloudService : BaseReadAloudService(), MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { - private val mediaPlayer = MediaPlayer() + private val player by lazy { MediaPlayer() } private lateinit var ttsFolder: String private var task: Coroutine<*>? = null private var playingIndex = -1 @@ -31,36 +33,37 @@ class HttpReadAloudService : BaseReadAloudService(), override fun onCreate() { super.onCreate() ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS" - mediaPlayer.setOnErrorListener(this) - mediaPlayer.setOnPreparedListener(this) - mediaPlayer.setOnCompletionListener(this) - } - - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - stopSelf() + player.setOnErrorListener(this) + player.setOnPreparedListener(this) + player.setOnCompletionListener(this) } override fun onDestroy() { super.onDestroy() task?.cancel() - mediaPlayer.release() + player.release() } override fun newReadAloud(dataKey: String?, play: Boolean) { - mediaPlayer.reset() + player.reset() playingIndex = -1 super.newReadAloud(dataKey, play) } override fun play() { if (contentList.isEmpty()) return - if (nowSpeak == 0) { - downloadAudio() - } else { - val file = getSpeakFile(nowSpeak) - if (file.exists()) { - playAudio(FileInputStream(file).fd) + ReadAloud.httpTTS?.let { + val fileName = + md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), contentList[nowSpeak]) + if (nowSpeak == 0) { + downloadAudio() + } else { + val file = getSpeakFileAsMd5(fileName) + if (file.exists()) { + playAudio(FileInputStream(file).fd) + } else { + downloadAudio() + } } } } @@ -68,28 +71,63 @@ class HttpReadAloudService : BaseReadAloudService(), private fun downloadAudio() { task?.cancel() task = execute { - FileUtils.deleteFile(ttsFolder) - for (index in 0 until contentList.size) { - if (isActive) { - ReadAloud.httpTTS?.let { - AnalyzeUrl( - it.url, - speakText = contentList[index], - speakSpeed = AppConfig.ttsSpeechRate - ).getResponseBytes()?.let { bytes -> - if (isActive) { - val file = getSpeakFile(index) - file.writeBytes(bytes) - if (index == nowSpeak) { - @Suppress("BlockingMethodInNonBlockingContext") + removeCacheFile() + ReadAloud.httpTTS?.let { + contentList.forEachIndexed { index, item -> + if (isActive) { + val fileName = + md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item) + + if (hasSpeakFile(fileName)) { //已经下载好的语音缓存 + if (index == nowSpeak) { + val file = getSpeakFileAsMd5(fileName) + + @Suppress("BlockingMethodInNonBlockingContext") + val fis = FileInputStream(file) + playAudio(fis.fd) + } + } else if (hasSpeakCacheFile(fileName)) { //缓存文件还在,可能还没下载完 + return@let + } else { //没有下载并且没有缓存文件 + try { + createSpeakCacheFile(fileName) + AnalyzeUrl( + it.url, + speakText = item, + speakSpeed = AppConfig.ttsSpeechRate + ).getByteArray().let { bytes -> + ensureActive() + + val file = getSpeakFileAsMd5IfNotExist(fileName) + //val file = getSpeakFile(index) + file.writeBytes(bytes) + removeSpeakCacheFile(fileName) + val fis = FileInputStream(file) - playAudio(fis.fd) + + if (index == nowSpeak) { + @Suppress("BlockingMethodInNonBlockingContext") + playAudio(fis.fd) + } } + } catch (e: SocketTimeoutException) { + removeSpeakCacheFile(fileName) + toastOnUi("tts接口超时,尝试重新获取") + downloadAudio() + } catch (e: ConnectException) { + removeSpeakCacheFile(fileName) + toastOnUi("网络错误") + } catch (e: IOException) { + val file = getSpeakFileAsMd5(fileName) + if (file.exists()) { + FileUtils.deleteFile(file.absolutePath) + } + toastOnUi("tts文件解析错误") + } catch (e: Exception) { + removeSpeakCacheFile(fileName) } } } - } else { - break } } } @@ -99,9 +137,9 @@ class HttpReadAloudService : BaseReadAloudService(), private fun playAudio(fd: FileDescriptor) { if (playingIndex != nowSpeak && requestFocus()) { try { - mediaPlayer.reset() - mediaPlayer.setDataSource(fd) - mediaPlayer.prepareAsync() + player.reset() + player.setDataSource(fd) + player.prepareAsync() playingIndex = nowSpeak postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } catch (e: Exception) { @@ -110,13 +148,53 @@ class HttpReadAloudService : BaseReadAloudService(), } } - private fun getSpeakFile(index: Int = nowSpeak): File { - return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") + private fun speakFilePath() = ttsFolder + File.separator + private fun md5SpeakFileName(url: String, ttsConfig: String, content: String): String { + return MD5Utils.md5Encode16(textChapter!!.title) + "_" + MD5Utils.md5Encode16("$url-|-$ttsConfig-|-$content") + } + + private fun hasSpeakFile(name: String) = + FileUtils.exist("${speakFilePath()}$name.mp3") + + private fun hasSpeakCacheFile(name: String) = + FileUtils.exist("${speakFilePath()}$name.mp3.cache") + + private fun createSpeakCacheFile(name: String): File = + FileUtils.createFileWithReplace("${speakFilePath()}$name.mp3.cache") + + private fun removeSpeakCacheFile(name: String) { + FileUtils.delete("${speakFilePath()}$name.mp3.cache") } + private fun getSpeakFileAsMd5(name: String): File = + FileUtils.getFile(File(speakFilePath()), "$name.mp3") + + private fun getSpeakFileAsMd5IfNotExist(name: String): File = + FileUtils.createFileIfNotExist("${speakFilePath()}$name.mp3") + + private fun removeCacheFile() { + FileUtils.listDirsAndFiles(speakFilePath())?.forEach { + if (it == null) { + return@forEach + } + if (Regex(""".+\.mp3$""").matches(it.name)) { //mp3缓存文件 + val reg = + """^${MD5Utils.md5Encode16(textChapter!!.title)}_[a-z0-9]{16}\.mp3$""".toRegex() + if (!reg.matches(it.name)) { + FileUtils.deleteFile(it.absolutePath) + } + } else { + if (Date().time - it.lastModified() > 30000) { + FileUtils.deleteFile(it.absolutePath) + } + } + } + } + + override fun pauseReadAloud(pause: Boolean) { super.pauseReadAloud(pause) - mediaPlayer.pause() + player.pause() } override fun resumeReadAloud() { @@ -124,7 +202,7 @@ class HttpReadAloudService : BaseReadAloudService(), if (playingIndex == -1) { play() } else { - mediaPlayer.start() + player.start() } } @@ -133,7 +211,7 @@ class HttpReadAloudService : BaseReadAloudService(), */ override fun upSpeechRate(reset: Boolean) { task?.cancel() - mediaPlayer.stop() + player.stop() playingIndex = -1 downloadAudio() } @@ -143,7 +221,7 @@ class HttpReadAloudService : BaseReadAloudService(), */ override fun prevP() { if (nowSpeak > 0) { - mediaPlayer.stop() + player.stop() nowSpeak-- readAloudNumber -= contentList[nowSpeak].length.minus(1) play() @@ -155,7 +233,7 @@ class HttpReadAloudService : BaseReadAloudService(), */ override fun nextP() { if (nowSpeak < contentList.size - 1) { - mediaPlayer.stop() + player.stop() readAloudNumber += contentList[nowSpeak].length.plus(1) nowSpeak++ play() diff --git a/app/src/main/java/io/legado/app/service/README.md b/app/src/main/java/io/legado/app/service/README.md index 64c4ca44c..2d40d9375 100644 --- a/app/src/main/java/io/legado/app/service/README.md +++ b/app/src/main/java/io/legado/app/service/README.md @@ -1,4 +1,4 @@ -## android服务 +# android服务 * AudioPlayService 音频播放服务 * CheckSourceService 书源检测服务 * DownloadService 缓存服务 diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt index 981d4bc68..16ad9c52a 100644 --- a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -1,7 +1,6 @@ package io.legado.app.service import android.app.PendingIntent -import android.content.Intent import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import io.legado.app.R @@ -13,26 +12,13 @@ import io.legado.app.help.MediaHelp import io.legado.app.service.help.ReadBook import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent -import kotlinx.coroutines.launch -import org.jetbrains.anko.toast +import io.legado.app.utils.toastOnUi import java.util.* class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener { - companion object { - private var textToSpeech: TextToSpeech? = null - private var ttsInitFinish = false - - fun clearTTS() { - textToSpeech?.let { - it.stop() - it.shutdown() - } - textToSpeech = null - ttsInitFinish = false - } - } - + private var textToSpeech: TextToSpeech? = null + private var ttsInitFinish = false private val ttsUtteranceListener = TTSUtteranceListener() override fun onCreate() { @@ -41,20 +27,23 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener upSpeechRate() } - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) + override fun onDestroy() { + super.onDestroy() clearTTS() - stopSelf() } + @Synchronized private fun initTts() { ttsInitFinish = false textToSpeech = TextToSpeech(this, this) } - override fun onDestroy() { - super.onDestroy() - clearTTS() + @Synchronized + fun clearTTS() { + textToSpeech?.stop() + textToSpeech?.shutdown() + textToSpeech = null + ttsInitFinish = false } override fun onInit(status: Int) { @@ -66,9 +55,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener play() } } else { - launch { - toast(R.string.tts_init_failed) - } + toastOnUi(R.string.tts_init_failed) } } @@ -78,15 +65,11 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener super.play() execute { MediaHelp.playSilentSound(this@TTSReadAloudService) + }.onFinally { textToSpeech?.let { it.speak("", TextToSpeech.QUEUE_FLUSH, null, null) for (i in nowSpeak until contentList.size) { - it.speak( - contentList[i], - TextToSpeech.QUEUE_ADD, - null, - AppConst.APP_TAG + i - ) + it.speak(contentList[i], TextToSpeech.QUEUE_ADD, null, AppConst.APP_TAG + i) } } } diff --git a/app/src/main/java/io/legado/app/service/WebService.kt b/app/src/main/java/io/legado/app/service/WebService.kt index bbf317066..c11024ac8 100644 --- a/app/src/main/java/io/legado/app/service/WebService.kt +++ b/app/src/main/java/io/legado/app/service/WebService.kt @@ -10,41 +10,38 @@ import io.legado.app.constant.EventBus import io.legado.app.constant.IntentAction import io.legado.app.constant.PreferKey import io.legado.app.help.IntentHelp -import io.legado.app.utils.NetworkUtils -import io.legado.app.utils.getPrefInt -import io.legado.app.utils.postEvent +import io.legado.app.ui.main.MainActivity +import io.legado.app.utils.* import io.legado.app.web.HttpServer import io.legado.app.web.WebSocketServer -import kotlinx.coroutines.launch -import org.jetbrains.anko.startService -import org.jetbrains.anko.toast import java.io.IOException class WebService : BaseService() { companion object { var isRun = false + var hostAddress = "" fun start(context: Context) { context.startService() } fun stop(context: Context) { - if (isRun) { - val intent = Intent(context, WebService::class.java) - intent.action = IntentAction.stop - context.startService(intent) - } + context.stopService() } + } private var httpServer: HttpServer? = null private var webSocketServer: WebSocketServer? = null + private var notificationContent = "" override fun onCreate() { super.onCreate() isRun = true - updateNotification(getString(R.string.service_starting)) + notificationContent = getString(R.string.service_starting) + upNotification() + WebTileService.setState(this, true) } override fun onDestroy() { @@ -56,7 +53,8 @@ class WebService : BaseService() { if (webSocketServer?.isAlive == true) { webSocketServer?.stop() } - postEvent(EventBus.WEB_SERVICE_STOP, true) + postEvent(EventBus.WEB_SERVICE, "") + WebTileService.setState(this, false) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -74,21 +72,23 @@ class WebService : BaseService() { if (webSocketServer?.isAlive == true) { webSocketServer?.stop() } - val port = getPort() - httpServer = HttpServer(port) - webSocketServer = WebSocketServer(port + 1) val address = NetworkUtils.getLocalIPAddress() if (address != null) { + val port = getPort() + httpServer = HttpServer(port) + webSocketServer = WebSocketServer(port + 1) try { httpServer?.start() webSocketServer?.start(1000 * 30) // 通信超时设置 + hostAddress = getString(R.string.http_ip, address.hostAddress, port) isRun = true - updateNotification(getString(R.string.http_ip, address.hostAddress, port)) + postEvent(EventBus.WEB_SERVICE, hostAddress) + notificationContent = hostAddress + upNotification() } catch (e: IOException) { - launch { - toast(e.localizedMessage ?: "") - stopSelf() - } + toastOnUi(e.localizedMessage ?: "") + e.printStackTrace() + stopSelf() } } else { stopSelf() @@ -106,12 +106,15 @@ class WebService : BaseService() { /** * 更新通知 */ - private fun updateNotification(content: String) { + private fun upNotification() { val builder = NotificationCompat.Builder(this, AppConst.channelIdWeb) .setSmallIcon(R.drawable.ic_web_service_noti) .setOngoing(true) .setContentTitle(getString(R.string.web_service)) - .setContentText(content) + .setContentText(notificationContent) + .setContentIntent( + IntentHelp.activityPendingIntent(this, "webService") + ) builder.addAction( R.drawable.ic_stop_black_24dp, getString(R.string.cancel), diff --git a/app/src/main/java/io/legado/app/service/WebTileService.kt b/app/src/main/java/io/legado/app/service/WebTileService.kt new file mode 100644 index 000000000..9049db46f --- /dev/null +++ b/app/src/main/java/io/legado/app/service/WebTileService.kt @@ -0,0 +1,72 @@ +package io.legado.app.service + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import androidx.annotation.RequiresApi +import io.legado.app.constant.IntentAction +import io.legado.app.utils.startService + +/** + * web服务快捷开关 + */ +@RequiresApi(Build.VERSION_CODES.N) +class WebTileService : TileService() { + + companion object { + + fun setState(context: Context, active: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + context.startService { + action = if (active) { + IntentAction.start + } else { + IntentAction.stop + } + } + } + } + + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + try { + when (intent?.action) { + IntentAction.start -> { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } + IntentAction.stop -> { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onStartListening() { + super.onStartListening() + if (WebService.isRun) { + qsTile.state = Tile.STATE_ACTIVE + qsTile.updateTile() + } else { + qsTile.state = Tile.STATE_INACTIVE + qsTile.updateTile() + } + } + + override fun onClick() { + super.onClick() + if (WebService.isRun) { + WebService.stop(this) + } else { + WebService.start(this) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt index 06bb6b5a9..2bc8f006a 100644 --- a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt +++ b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt @@ -3,21 +3,27 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent import androidx.lifecycle.MutableLiveData +import io.legado.app.constant.EventBus import io.legado.app.constant.IntentAction import io.legado.app.constant.Status +import io.legado.app.data.appDb import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.webBook.WebBook import io.legado.app.service.AudioPlayService +import io.legado.app.utils.postEvent +import io.legado.app.utils.startService object AudioPlay { var titleData = MutableLiveData() var coverData = MutableLiveData() var status = Status.STOP var book: Book? = null + var durChapter: BookChapter? = null var inBookshelf = false - var chapterSize = 0 var durChapterIndex = 0 - var durPageIndex = 0 + var durChapterPos = 0 var webBook: WebBook? = null val loadingChapters = arrayListOf() @@ -26,67 +32,132 @@ object AudioPlay { } fun play(context: Context) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.play - context.startService(intent) + book?.let { + if (durChapter == null) { + upDurChapter(it) + } + durChapter?.let { + val intent = Intent(context, AudioPlayService::class.java) + intent.action = IntentAction.play + context.startService(intent) + } + } + } + + fun upDurChapter(book: Book) { + durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex) + postEvent(EventBus.AUDIO_SUB_TITLE, durChapter?.title ?: "") + postEvent(EventBus.AUDIO_SIZE, durChapter?.end?.toInt() ?: 0) + postEvent(EventBus.AUDIO_PROGRESS, durChapterPos) } fun pause(context: Context) { if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.pause - context.startService(intent) + context.startService { + action = IntentAction.pause + } } } fun resume(context: Context) { if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.resume - context.startService(intent) + context.startService { + action = IntentAction.resume + } } } fun stop(context: Context) { if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.stop - context.startService(intent) + context.startService { + action = IntentAction.stop + } } } fun adjustSpeed(context: Context, adjust: Float) { if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.adjustSpeed - intent.putExtra("adjust", adjust) - context.startService(intent) + context.startService { + action = IntentAction.adjustSpeed + putExtra("adjust", adjust) + } } } fun adjustProgress(context: Context, position: Int) { if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.adjustProgress - intent.putExtra("position", position) - context.startService(intent) + context.startService { + action = IntentAction.adjustProgress + putExtra("position", position) + } + } + } + + fun skipTo(context: Context, index: Int) { + Coroutine.async { + book?.let { book -> + durChapterIndex = index + durChapterPos = 0 + durChapter = null + saveRead(book) + play(context) + } } } fun prev(context: Context) { - if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.prev - context.startService(intent) + Coroutine.async { + book?.let { book -> + if (book.durChapterIndex <= 0) { + return@let + } + durChapterIndex-- + durChapterPos = 0 + durChapter = null + saveRead(book) + play(context) + } } } fun next(context: Context) { - if (AudioPlayService.isRun) { - val intent = Intent(context, AudioPlayService::class.java) - intent.action = IntentAction.next - context.startService(intent) + book?.let { book -> + if (book.durChapterIndex >= book.totalChapterNum) { + return@let + } + durChapterIndex++ + durChapterPos = 0 + durChapter = null + saveRead(book) + play(context) } } + fun addTimer(context: Context) { + val intent = Intent(context, AudioPlayService::class.java) + intent.action = IntentAction.addTimer + context.startService(intent) + } + + fun saveRead(book: Book) { + book.lastCheckCount = 0 + book.durChapterTime = System.currentTimeMillis() + book.durChapterIndex = durChapterIndex + book.durChapterPos = durChapterPos + Coroutine.async { + appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let { + book.durChapterTitle = it.title + } + book.save() + } + } + + fun saveDurChapter(audioSize: Long) { + Coroutine.async { + durChapter?.let { + it.end = audioSize + appDb.bookChapterDao.insert(it) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/CacheBook.kt b/app/src/main/java/io/legado/app/service/help/CacheBook.kt new file mode 100644 index 000000000..520497c7e --- /dev/null +++ b/app/src/main/java/io/legado/app/service/help/CacheBook.kt @@ -0,0 +1,103 @@ +package io.legado.app.service.help + +import android.content.Context +import io.legado.app.R +import io.legado.app.constant.IntentAction +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.CacheBookService +import io.legado.app.utils.msg +import io.legado.app.utils.startService +import kotlinx.coroutines.CoroutineScope +import splitties.init.appCtx +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet + +object CacheBook { + val logs = arrayListOf() + private val downloadMap = ConcurrentHashMap>() + + fun addLog(log: String?) { + log ?: return + synchronized(this) { + if (logs.size > 1000) { + logs.removeAt(0) + } + logs.add(log) + } + } + + fun start(context: Context, bookUrl: String, start: Int, end: Int) { + context.startService { + action = IntentAction.start + putExtra("bookUrl", bookUrl) + putExtra("start", start) + putExtra("end", end) + } + } + + fun remove(context: Context, bookUrl: String) { + context.startService { + action = IntentAction.remove + putExtra("bookUrl", bookUrl) + } + } + + fun stop(context: Context) { + context.startService { + action = IntentAction.stop + } + } + + fun downloadCount(): Int { + var count = 0 + downloadMap.forEach { + count += it.value.size + } + return count + } + + fun download( + scope: CoroutineScope, + webBook: WebBook, + book: Book, + chapter: BookChapter, + resetPageOffset: Boolean = false + ) { + if (downloadMap[book.bookUrl]?.contains(chapter.index) == true) { + return + } + if (downloadMap[book.bookUrl] == null) { + downloadMap[book.bookUrl] = CopyOnWriteArraySet() + } + downloadMap[book.bookUrl]?.add(chapter.index) + webBook.getContent(scope, book, chapter) + .onSuccess { content -> + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.contentLoadFinish( + book, + chapter, + content.ifBlank { appCtx.getString(R.string.content_empty) }, + resetPageOffset = resetPageOffset + ) + } + }.onError { + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.contentLoadFinish( + book, + chapter, + it.msg, + resetPageOffset = resetPageOffset + ) + } + }.onFinally { + downloadMap[book.bookUrl]?.remove(chapter.index) + if (downloadMap[book.bookUrl].isNullOrEmpty()) { + downloadMap.remove(book.bookUrl) + } + ReadBook.removeLoading(chapter.index) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/CheckSource.kt b/app/src/main/java/io/legado/app/service/help/CheckSource.kt index 352c4b799..1c00d5f77 100644 --- a/app/src/main/java/io/legado/app/service/help/CheckSource.kt +++ b/app/src/main/java/io/legado/app/service/help/CheckSource.kt @@ -1,67 +1,34 @@ package io.legado.app.service.help import android.content.Context -import android.content.Intent -import io.legado.app.App import io.legado.app.R import io.legado.app.constant.IntentAction import io.legado.app.data.entities.BookSource -import io.legado.app.data.entities.SearchBook -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.webBook.WebBook import io.legado.app.service.CheckSourceService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import org.jetbrains.anko.toast -import kotlin.coroutines.CoroutineContext +import io.legado.app.utils.startService +import io.legado.app.utils.toastOnUi -class CheckSource(val source: BookSource) { +object CheckSource { + var keyword = "我的" - companion object { - var keyword = "我的" - - fun start(context: Context, sources: List) { - if (sources.isEmpty()) { - context.toast(R.string.non_select) - return - } - val selectedIds: ArrayList = arrayListOf() - sources.map { - selectedIds.add(it.bookSourceUrl) - } - Intent(context, CheckSourceService::class.java).let { - it.action = IntentAction.start - it.putExtra("selectIds", selectedIds) - context.startService(it) - } + fun start(context: Context, sources: List) { + if (sources.isEmpty()) { + context.toastOnUi(R.string.non_select) + return } - - fun stop(context: Context) { - Intent(context, CheckSourceService::class.java).let { - it.action = IntentAction.stop - context.startService(it) - } + val selectedIds: ArrayList = arrayListOf() + sources.map { + selectedIds.add(it.bookSourceUrl) + } + context.startService { + action = IntentAction.start + putExtra("selectIds", selectedIds) } } - fun check( - scope: CoroutineScope, - context: CoroutineContext, - onNext: (sourceUrl: String) -> Unit - ): Coroutine<*> { - val webBook = WebBook(source) - val variableBook = SearchBook(origin = source.bookSourceUrl) - return webBook - .searchBook(keyword, scope = scope, context = context, variableBook = variableBook) - .timeout(60000L) - .onError(Dispatchers.IO) { - source.addGroup("失效") - App.db.bookSourceDao().update(source) - }.onSuccess(Dispatchers.IO) { - source.removeGroup("失效") - App.db.bookSourceDao().update(source) - }.onFinally(Dispatchers.IO) { - onNext(source.bookSourceUrl) - } + fun stop(context: Context) { + context.startService { + action = IntentAction.stop + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/help/Download.kt b/app/src/main/java/io/legado/app/service/help/Download.kt index 5ba31004a..1a5fcd657 100644 --- a/app/src/main/java/io/legado/app/service/help/Download.kt +++ b/app/src/main/java/io/legado/app/service/help/Download.kt @@ -1,46 +1,23 @@ package io.legado.app.service.help import android.content.Context -import android.content.Intent import io.legado.app.constant.IntentAction import io.legado.app.service.DownloadService +import io.legado.app.utils.startService object Download { - val logs = arrayListOf() - - fun addLog(log: String?) { - log ?: return - synchronized(this) { - if (logs.size > 1000) { - logs.removeAt(0) - } - logs.add(log) - } - } - - fun start(context: Context, bookUrl: String, start: Int, end: Int) { - Intent(context, DownloadService::class.java).let { - it.action = IntentAction.start - it.putExtra("bookUrl", bookUrl) - it.putExtra("start", start) - it.putExtra("end", end) - context.startService(it) - } - } - - fun remove(context: Context, bookUrl: String) { - Intent(context, DownloadService::class.java).let { - it.action = IntentAction.remove - it.putExtra("bookUrl", bookUrl) - context.startService(it) + fun start(context: Context, downloadId: Long, fileName: String) { + context.startService { + action = IntentAction.start + putExtra("downloadId", downloadId) + putExtra("fileName", fileName) } } fun stop(context: Context) { - Intent(context, DownloadService::class.java).let { - it.action = IntentAction.stop - context.startService(it) + context.startService { + action = IntentAction.stop } } diff --git a/app/src/main/java/io/legado/app/service/help/ReadAloud.kt b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt index b25c270c3..fb0b71804 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadAloud.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt @@ -2,23 +2,23 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent -import io.legado.app.App import io.legado.app.constant.IntentAction import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.HttpTTS import io.legado.app.service.BaseReadAloudService import io.legado.app.service.HttpReadAloudService import io.legado.app.service.TTSReadAloudService import io.legado.app.utils.getPrefLong +import splitties.init.appCtx object ReadAloud { private var aloudClass: Class<*> = getReadAloudClass() var httpTTS: HttpTTS? = null private fun getReadAloudClass(): Class<*> { - val spId = App.INSTANCE.getPrefLong(PreferKey.speakEngine) - httpTTS = App.db.httpTTSDao().get(spId) - stop(App.INSTANCE) + val spId = appCtx.getPrefLong(PreferKey.speakEngine) + httpTTS = appDb.httpTTSDao.get(spId) return if (httpTTS != null) { HttpReadAloudService::class.java } else { @@ -27,6 +27,7 @@ object ReadAloud { } fun upReadAloudClass() { + stop(appCtx) aloudClass = getReadAloudClass() } diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index e8485261c..35f668f48 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -1,38 +1,36 @@ package io.legado.app.service.help import androidx.lifecycle.MutableLiveData -import com.hankcs.hanlp.HanLP -import io.legado.app.App -import io.legado.app.R +import com.github.liuyueyi.quick.transfer.ChineseUtils import io.legado.app.constant.BookType -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.BookSource -import io.legado.app.data.entities.ReadRecord -import io.legado.app.help.AppConfig -import io.legado.app.help.BookHelp -import io.legado.app.help.IntentDataHelp +import io.legado.app.data.appDb +import io.legado.app.data.entities.* +import io.legado.app.help.* import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.storage.BookWebDav import io.legado.app.model.webBook.WebBook import io.legado.app.service.BaseReadAloudService import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ImageProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import io.legado.app.utils.msg +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.jetbrains.anko.getStackTraceString -import org.jetbrains.anko.toast +import splitties.init.appCtx +import kotlin.math.max +import kotlin.math.min +@Suppress("MemberVisibilityCanBePrivate") object ReadBook { var titleDate = MutableLiveData() var book: Book? = null var inBookshelf = false var chapterSize = 0 var durChapterIndex = 0 - var durPageIndex = 0 + var durChapterPos = 0 var isLocalBook = true var callBack: CallBack? = null var prevTextChapter: TextChapter? = null @@ -48,17 +46,19 @@ object ReadBook { fun resetData(book: Book) { this.book = book readRecord.bookName = book.name - readRecord.readTime = App.db.readRecordDao().getReadTime(book.name) ?: 0 + readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0 durChapterIndex = book.durChapterIndex - durPageIndex = book.durChapterPos + durChapterPos = book.durChapterPos isLocalBook = book.origin == BookType.local - chapterSize = 0 - prevTextChapter = null - curTextChapter = null - nextTextChapter = null + chapterSize = book.totalChapterNum + clearTextChapter() titleDate.postValue(book.name) + callBack?.upPageAnim() upWebBook(book) ImageProvider.clearAllCache() + synchronized(this) { + loadingChapters.clear() + } } fun upWebBook(book: Book) { @@ -66,9 +66,12 @@ object ReadBook { bookSource = null webBook = null } else { - App.db.bookSourceDao().getBookSource(book.origin)?.let { + appDb.bookSourceDao.getBookSource(book.origin)?.let { bookSource = it webBook = WebBook(it) + if (book.getImageStyle().isNullOrBlank()) { + book.setImageStyle(it.getContentRule().imageStyle) + } } ?: let { bookSource = null webBook = null @@ -76,11 +79,32 @@ object ReadBook { } } + fun setProgress(progress: BookProgress) { + durChapterIndex = progress.durChapterIndex + durChapterPos = progress.durChapterPos + clearTextChapter() + loadContent(resetPageOffset = true) + } + + fun clearTextChapter() { + prevTextChapter = null + curTextChapter = null + nextTextChapter = null + } + + fun uploadProgress(syncBookProgress: Boolean = AppConfig.syncBookProgress) { + if (syncBookProgress) { + book?.let { + BookWebDav.uploadBookProgress(it) + } + } + } + fun upReadStartTime() { Coroutine.async { readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime readStartTime = System.currentTimeMillis() - App.db.readRecordDao().insert(readRecord) + appDb.readRecordDao.insert(readRecord) } } @@ -92,14 +116,14 @@ object ReadBook { } fun moveToNextPage() { - durPageIndex++ + durChapterPos = curTextChapter?.getNextPageLength(durChapterPos) ?: durChapterPos callBack?.upContent() saveRead() } fun moveToNextChapter(upContent: Boolean): Boolean { if (durChapterIndex < chapterSize - 1) { - durPageIndex = 0 + durChapterPos = 0 durChapterIndex++ prevTextChapter = curTextChapter curTextChapter = nextTextChapter @@ -111,10 +135,12 @@ object ReadBook { callBack?.upContent() } loadContent(durChapterIndex.plus(1), upContent, false) - GlobalScope.launch(Dispatchers.IO) { - for (i in 2..10) { - delay(100) - download(durChapterIndex + i) + Coroutine.async { + val maxChapterIndex = + min(chapterSize - 1, durChapterIndex + AppConfig.preDownloadNum) + for (i in durChapterIndex.plus(2)..maxChapterIndex) { + delay(1000) + download(i) } } } @@ -127,9 +153,12 @@ object ReadBook { } } - fun moveToPrevChapter(upContent: Boolean, toLast: Boolean = true): Boolean { + fun moveToPrevChapter( + upContent: Boolean, + toLast: Boolean = true + ): Boolean { if (durChapterIndex > 0) { - durPageIndex = if (toLast) prevTextChapter?.lastIndex ?: 0 else 0 + durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: 0 else 0 durChapterIndex-- nextTextChapter = curTextChapter curTextChapter = prevTextChapter @@ -141,10 +170,11 @@ object ReadBook { callBack?.upContent() } loadContent(durChapterIndex.minus(1), upContent, false) - GlobalScope.launch(Dispatchers.IO) { - for (i in -5..-2) { - delay(100) - download(durChapterIndex + i) + Coroutine.async { + val minChapterIndex = max(0, durChapterIndex - 5) + for (i in durChapterIndex.minus(2) downTo minChapterIndex) { + delay(1000) + download(i) } } } @@ -157,15 +187,17 @@ object ReadBook { } } - fun skipToPage(page: Int) { - durPageIndex = page - callBack?.upContent() + fun skipToPage(index: Int, success: (() -> Unit)? = null) { + durChapterPos = curTextChapter?.getReadLength(index) ?: index + callBack?.upContent { + success?.invoke() + } curPageChanged() saveRead() } - fun setPageIndex(pageIndex: Int) { - durPageIndex = pageIndex + fun setPageIndex(index: Int) { + durChapterPos = curTextChapter?.getReadLength(index) ?: index saveRead() curPageChanged() } @@ -187,24 +219,16 @@ object ReadBook { if (book != null && textChapter != null) { val key = IntentDataHelp.putData(textChapter) ReadAloud.play( - App.INSTANCE, - book.name, - textChapter.title, - durPageIndex, - key, - play + appCtx, book.name, textChapter.title, durPageIndex(), key, play ) } } - fun durChapterPos(): Int { + fun durPageIndex(): Int { curTextChapter?.let { - if (durPageIndex < it.pageSize) { - return durPageIndex - } - return it.pageSize - 1 + return it.getPageIndexByCharIndex(durChapterPos) } - return durPageIndex + return durChapterPos } /** @@ -222,21 +246,30 @@ object ReadBook { /** * 加载章节内容 */ - fun loadContent(resetPageOffset: Boolean) { - loadContent(durChapterIndex, resetPageOffset = resetPageOffset) + fun loadContent(resetPageOffset: Boolean, success: (() -> Unit)? = null) { + loadContent(durChapterIndex, resetPageOffset = resetPageOffset) { + success?.invoke() + } loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset) loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) } - fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) { + fun loadContent( + index: Int, + upContent: Boolean = true, + resetPageOffset: Boolean, + success: (() -> Unit)? = null + ) { book?.let { book -> if (addLoading(index)) { Coroutine.async { - App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> + appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter -> BookHelp.getContent(book, chapter)?.let { - contentLoadFinish(book, chapter, it, upContent, resetPageOffset) + contentLoadFinish(book, chapter, it, upContent, resetPageOffset) { + success?.invoke() + } removeLoading(chapter.index) - } ?: download(chapter, resetPageOffset = resetPageOffset) + } ?: download(this, chapter, resetPageOffset = resetPageOffset) } ?: removeLoading(index) }.onError { removeLoading(index) @@ -250,11 +283,11 @@ object ReadBook { if (book.isLocalBook()) return if (addLoading(index)) { Coroutine.async { - App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> + appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter -> if (BookHelp.hasContent(book, chapter)) { removeLoading(chapter.index) } else { - download(chapter, false) + download(this, chapter, false) } } ?: removeLoading(index) }.onError { @@ -264,46 +297,22 @@ object ReadBook { } } - private fun download(chapter: BookChapter, resetPageOffset: Boolean) { + private fun download( + scope: CoroutineScope, + chapter: BookChapter, + resetPageOffset: Boolean, + success: (() -> Unit)? = null + ) { val book = book val webBook = webBook if (book != null && webBook != null) { - webBook.getContent(book, chapter) - .onSuccess(Dispatchers.IO) { content -> - if (content.isEmpty()) { - contentLoadFinish( - book, - chapter, - App.INSTANCE.getString(R.string.content_empty), - resetPageOffset = resetPageOffset - ) - removeLoading(chapter.index) - } else { - BookHelp.saveContent(book, chapter, content) - contentLoadFinish( - book, - chapter, - content, - resetPageOffset = resetPageOffset - ) - removeLoading(chapter.index) - } - }.onError { - contentLoadFinish( - book, - chapter, - it.localizedMessage ?: "未知错误", - resetPageOffset = resetPageOffset - ) - removeLoading(chapter.index) - } + CacheBook.download(scope, webBook, book, chapter) } else if (book != null) { contentLoadFinish( - book, - chapter, - "没有书源", - resetPageOffset = resetPageOffset - ) + book, chapter, "没有书源", resetPageOffset = resetPageOffset + ) { + success?.invoke() + } removeLoading(chapter.index) } else { removeLoading(chapter.index) @@ -318,83 +327,130 @@ object ReadBook { } } - private fun removeLoading(index: Int) { + fun removeLoading(index: Int) { synchronized(this) { loadingChapters.remove(index) } } + fun searchResultPositions( + pages: List, + indexWithinChapter: Int, + query: String + ): Array { + // calculate search result's pageIndex + var content = "" + pages.map { + content += it.text + } + var count = 1 + var index = content.indexOf(query) + while (count != indexWithinChapter) { + index = content.indexOf(query, index + 1) + count += 1 + } + val contentPosition = index + var pageIndex = 0 + var length = pages[pageIndex].text.length + while (length < contentPosition) { + pageIndex += 1 + if (pageIndex > pages.size) { + pageIndex = pages.size + break + } + length += pages[pageIndex].text.length + } + + // calculate search result's lineIndex + val currentPage = pages[pageIndex] + var lineIndex = 0 + length = length - currentPage.text.length + currentPage.textLines[lineIndex].text.length + while (length < contentPosition) { + lineIndex += 1 + if (lineIndex > currentPage.textLines.size) { + lineIndex = currentPage.textLines.size + break + } + length += currentPage.textLines[lineIndex].text.length + } + + // charIndex + val currentLine = currentPage.textLines[lineIndex] + length -= currentLine.text.length + val charIndex = contentPosition - length + var addLine = 0 + var charIndex2 = 0 + // change line + if ((charIndex + query.length) > currentLine.text.length) { + addLine = 1 + charIndex2 = charIndex + query.length - currentLine.text.length - 1 + } + // changePage + if ((lineIndex + addLine + 1) > currentPage.textLines.size) { + addLine = -1 + charIndex2 = charIndex + query.length - currentLine.text.length - 1 + } + return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2) + } + /** * 内容加载完成 */ - private fun contentLoadFinish( + fun contentLoadFinish( book: Book, chapter: BookChapter, content: String, upContent: Boolean = true, - resetPageOffset: Boolean + resetPageOffset: Boolean, + success: (() -> Unit)? = null ) { Coroutine.async { + ImageProvider.clearOut(durChapterIndex) if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) { chapter.title = when (AppConfig.chineseConverterType) { - 1 -> HanLP.convertToSimplifiedChinese(chapter.title) - 2 -> HanLP.convertToTraditionalChinese(chapter.title) + 1 -> ChineseUtils.t2s(chapter.title) + 2 -> ChineseUtils.s2t(chapter.title) else -> chapter.title } - val contents = BookHelp.disposeContent( - chapter.title, - book.name, - webBook?.bookSource?.bookSourceUrl, - content, - book.useReplaceRule - ) - when (chapter.index) { - durChapterIndex -> { - curTextChapter = - ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle - ) - if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) + val contents = ContentProcessor.get(book.name, book.origin) + .getContent(book, chapter.title, content) + val textChapter = ChapterProvider + .getTextChapter(book, chapter, contents, chapterSize) + when (val offset = chapter.index - durChapterIndex) { + 0 -> { + curTextChapter = textChapter + if (upContent) callBack?.upContent(offset, resetPageOffset) callBack?.upView() curPageChanged() callBack?.contentLoadFinish() - ImageProvider.clearOut(durChapterIndex) } - durChapterIndex - 1 -> { - prevTextChapter = - ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle - ) - if (upContent) callBack?.upContent(-1, resetPageOffset) + -1 -> { + prevTextChapter = textChapter + if (upContent) callBack?.upContent(offset, resetPageOffset) } - durChapterIndex + 1 -> { - nextTextChapter = - ChapterProvider.getTextChapter( - book, - chapter, - contents, - chapterSize, - imageStyle - ) - if (upContent) callBack?.upContent(1, resetPageOffset) + 1 -> { + nextTextChapter = textChapter + if (upContent) callBack?.upContent(offset, resetPageOffset) } } } }.onError { it.printStackTrace() - App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}") + appCtx.toastOnUi("ChapterProvider ERROR:\n${it.msg}") + }.onSuccess { + success?.invoke() } } - private val imageStyle get() = webBook?.bookSource?.ruleContent?.imageStyle + fun pageAnim(): Int { + book?.let { + return if (it.getPageAnim() < 0) + ReadBookConfig.pageAnim + else + it.getPageAnim() + } + return ReadBookConfig.pageAnim + } fun setCharset(charset: String) { book?.let { @@ -410,20 +466,31 @@ object ReadBook { book.lastCheckCount = 0 book.durChapterTime = System.currentTimeMillis() book.durChapterIndex = durChapterIndex - book.durChapterPos = durPageIndex - App.db.bookChapterDao().getChapter(book.bookUrl, durChapterIndex)?.let { + book.durChapterPos = durChapterPos + appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let { book.durChapterTitle = it.title } - App.db.bookDao().update(book) + appDb.bookDao.update(book) } } } interface CallBack { fun loadChapterList(book: Book) - fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) + + fun upContent( + relativePosition: Int = 0, + resetPageOffset: Boolean = true, + success: (() -> Unit)? = null + ) + fun upView() + fun pageChanged() + fun contentLoadFinish() + + fun upPageAnim() } -} \ No newline at end of file + +} diff --git a/app/src/main/java/io/legado/app/ui/README.md b/app/src/main/java/io/legado/app/ui/README.md index ef44bed69..7c5ab259a 100644 --- a/app/src/main/java/io/legado/app/ui/README.md +++ b/app/src/main/java/io/legado/app/ui/README.md @@ -1,7 +1,8 @@ -## 放置与界面有关的类 +# 放置与界面有关的类 * about 关于界面 -* audio 音频播放界面 +* association 导入书源界面 +* book\audio 音频播放界面 * book\arrange 书架整理界面 * book\info 书籍信息查看 * book\read 书籍阅读界面 @@ -9,11 +10,11 @@ * book\source 书源界面 * book\changeCover 封面换源界面 * book\changeSource 换源界面 -* book\chapterList 目录界面 +* book\toc 目录界面 * book\download 下载界面 * book\explore 发现界面 * book\local 书籍导入界面 -* fileChooser 文件选择界面 +* document 文件选择界面 * config 配置界面 * main 主界面 * qrCode 二维码扫描界面 diff --git a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt index 271190561..f7462712d 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt @@ -8,36 +8,37 @@ import android.view.Menu import android.view.MenuItem import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.databinding.ActivityAboutBinding import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor import io.legado.app.utils.openUrl -import kotlinx.android.synthetic.main.activity_about.* -import org.jetbrains.anko.share +import io.legado.app.utils.share +import io.legado.app.utils.viewbindingdelegate.viewBinding -class AboutActivity : BaseActivity(R.layout.activity_about) { +class AboutActivity : BaseActivity() { + + override val binding by viewBinding(ActivityAboutBinding::inflate) override fun onActivityCreated(savedInstanceState: Bundle?) { - ll_about.background = ATH.getDialogBackground() + binding.llAbout.background = ATH.getDialogBackground() val fTag = "aboutFragment" var aboutFragment = supportFragmentManager.findFragmentByTag(fTag) if (aboutFragment == null) aboutFragment = AboutFragment() supportFragmentManager.beginTransaction() .replace(R.id.fl_fragment, aboutFragment, fTag) .commit() - tv_app_summary.post { - try { + binding.tvAppSummary.post { + kotlin.runCatching { val span = ForegroundColorSpan(accentColor) - val spannableString = SpannableString(tv_app_summary.text) + val spannableString = SpannableString(binding.tvAppSummary.text) val gzh = getString(R.string.legado_gzh) val start = spannableString.indexOf(gzh) spannableString.setSpan( span, start, start + gzh.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) - tv_app_summary.text = spannableString - } catch (e: Exception) { - e.printStackTrace() + binding.tvAppSummary.text = spannableString } } } diff --git a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt index 44a85a7fe..fdf797777 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt @@ -7,35 +7,44 @@ import android.view.View import androidx.annotation.StringRes import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import io.legado.app.App import io.legado.app.R +import io.legado.app.constant.AppConst.appInfo +import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector import io.legado.app.ui.widget.dialog.TextDialog -import io.legado.app.utils.applyTint -import io.legado.app.utils.openUrl -import io.legado.app.utils.sendToClip -import io.legado.app.utils.toast +import io.legado.app.utils.* class AboutFragment : PreferenceFragmentCompat() { private val licenseUrl = "https://github.com/gedoor/legado/blob/master/LICENSE" private val disclaimerUrl = "https://gedoor.github.io/MyBookshelf/disclaimer.html" private val qqGroups = linkedMapOf( + Pair("(QQ群VIP中转)1017837876", "0d9-zpmqbYfK3i_wt8uCvQoB2lmXadrg"), Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"), Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"), Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"), + Pair("(QQ群VIP4)682555679", "VF2UwvUCuaqlo6pddWTe_kw__a1_Fr8O"), + Pair("(QQ群VIP5)161622578", "S81xdnhJ5EBC389LTUvoyiyM-wr71pvJ"), Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"), Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"), Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"), Pair("(QQ群4)256929088", "czEJPLDnT4Pd9SKQ6RoRVzKhDxLchZrO"), Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX"), - Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n") + Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n"), + Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC"), + Pair("(QQ群8)1079926194", "gg2qFH8q9IPFaCHV3H7CqCN-YljvazE1"), + Pair("(QQ群9)892108780", "Ci_O3aysKjEBfplOWeCud-rxl71TjU2Q"), + Pair("(QQ群10)812720266", "oW9ksY0sAWUEq0hfM5irN5aOdvKVgMEE") ) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.about) - findPreference("check_update")?.summary = - "${getString(R.string.version)} ${App.versionName}" + findPreference("update_log")?.summary = + "${getString(R.string.version)} ${appInfo.versionName}" + if (AppConfig.isGooglePlay) { + preferenceScreen.removePreferenceRecursively("check_update") + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -48,7 +57,7 @@ class AboutFragment : PreferenceFragmentCompat() { "contributors" -> openUrl(R.string.contributors_url) "update_log" -> showUpdateLog() "check_update" -> openUrl(R.string.latest_release_url) - "mail" -> sendMail() + "mail" -> requireContext().sendMail("kunfei.ge@gmail.com") "sourceRuleSummary" -> openUrl(R.string.source_rule_url) "git" -> openUrl(R.string.this_github_url) "home_page" -> openUrl(R.string.home_page_url) @@ -56,6 +65,9 @@ class AboutFragment : PreferenceFragmentCompat() { "disclaimer" -> requireContext().openUrl(disclaimerUrl) "qq" -> showQqGroups() "gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh)) + "crashLog" -> showCrashLogs() + "tg" -> openUrl(R.string.tg_url) + "discord" -> openUrl(R.string.discord_url) } return super.onPreferenceTreeClick(preference) } @@ -65,23 +77,13 @@ class AboutFragment : PreferenceFragmentCompat() { requireContext().openUrl(getString(addressID)) } - private fun sendMail() { - try { - val intent = Intent(Intent.ACTION_SENDTO) - intent.data = Uri.parse("mailto:kunfei.ge@gmail.com") - startActivity(intent) - } catch (e: Exception) { - toast(e.localizedMessage ?: "Error") - } - } - private fun showUpdateLog() { val log = String(requireContext().assets.open("updateLog.md").readBytes()) TextDialog.show(childFragmentManager, log, TextDialog.MD) } private fun showQqGroups() { - alert(title = R.string.join_qq_group) { + alert(titleResource = R.string.join_qq_group) { val names = arrayListOf() qqGroups.forEach { names.add(it.key) @@ -93,7 +95,7 @@ class AboutFragment : PreferenceFragmentCompat() { } } } - }.show().applyTint() + }.show() } private fun joinQQGroup(key: String): Boolean { @@ -102,11 +104,28 @@ class AboutFragment : PreferenceFragmentCompat() { Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D$key") // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面 // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - return try { + kotlin.runCatching { startActivity(intent) - true - } catch (e: java.lang.Exception) { - false + return true + }.onFailure { + toastOnUi("添加失败,请手动添加") + } + return false + } + + private fun showCrashLogs() { + context?.externalCacheDir?.let { exCacheDir -> + val crashDir = FileUtils.getFile(exCacheDir, "crash") + val crashLogs = crashDir.listFiles() + val crashLogNames = arrayListOf() + crashLogs?.forEach { + crashLogNames.add(it.name) + } + context?.selector(R.string.crash_log, crashLogNames) { _, select -> + crashLogs?.getOrNull(select)?.let { logFile -> + TextDialog.show(childFragmentManager, logFile.readText()) + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt index c29d47fcb..831d25535 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt @@ -4,13 +4,17 @@ package io.legado.app.ui.about import android.os.Bundle import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.databinding.ActivityDonateBinding +import io.legado.app.utils.viewbindingdelegate.viewBinding /** * Created by GKF on 2018/1/13. * 捐赠页面 */ -class DonateActivity : BaseActivity(R.layout.activity_donate) { +class DonateActivity : BaseActivity() { + + override val binding by viewBinding(ActivityDonateBinding::inflate) override fun onActivityCreated(savedInstanceState: Bundle?) { val fTag = "donateFragment" diff --git a/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt b/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt index b716c8f05..29da08d54 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt @@ -8,9 +8,9 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.R import io.legado.app.utils.ACache +import io.legado.app.utils.longToastOnUi import io.legado.app.utils.openUrl import io.legado.app.utils.sendToClip -import org.jetbrains.anko.longToast class DonateFragment : PreferenceFragmentCompat() { @@ -42,7 +42,7 @@ class DonateFragment : PreferenceFragmentCompat() { private fun getZfbHb(context: Context) { requireContext().sendToClip("537954522") - context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包") + context.longToastOnUi("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包") try { val packageManager = context.applicationContext.packageManager val intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone")!! diff --git a/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt index f171433e4..8f49e6cd7 100644 --- a/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt @@ -2,58 +2,90 @@ package io.legado.app.ui.about import android.content.Context import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App +import android.view.Menu +import android.view.MenuItem +import android.view.ViewGroup import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb import io.legado.app.data.entities.ReadRecordShow +import io.legado.app.databinding.ActivityReadRecordBinding +import io.legado.app.databinding.ItemReadRecordBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.utils.applyTint -import kotlinx.android.synthetic.main.activity_read_record.* -import kotlinx.android.synthetic.main.item_read_record.* -import kotlinx.android.synthetic.main.item_read_record.view.* +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.utils.cnCompare +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* -class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { +class ReadRecordActivity : BaseActivity() { lateinit var adapter: RecordAdapter + private var sortMode = 0 + + override val binding by viewBinding(ActivityReadRecordBinding::inflate) override fun onActivityCreated(savedInstanceState: Bundle?) { initView() initData() } - private fun initView() { - tv_book_name.setText(R.string.all_read_time) - recycler_view.layoutManager = LinearLayoutManager(this) - adapter = RecordAdapter(this) - recycler_view.adapter = adapter - iv_remove.onClick { + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_read_record, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_sort_name -> { + sortMode = 0 + initData() + } + R.id.menu_sort_time -> { + sortMode = 1 + initData() + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() = binding.run { + readRecord.tvBookName.setText(R.string.all_read_time) + adapter = RecordAdapter(this@ReadRecordActivity) + recyclerView.adapter = adapter + readRecord.tvRemove.setOnClickListener { alert(R.string.delete, R.string.sure_del) { okButton { - App.db.readRecordDao().clear() + appDb.readRecordDao.clear() initData() } noButton() - }.show().applyTint() + }.show() } } private fun initData() { launch(IO) { - val allTime = App.db.readRecordDao().allTime + val allTime = appDb.readRecordDao.allTime withContext(Main) { - tv_read_time.text = formatDuring(allTime) + binding.readRecord.tvReadTime.text = formatDuring(allTime) + } + var readRecords = appDb.readRecordDao.allShow + readRecords = when (sortMode) { + 1 -> readRecords.sortedBy { it.readTime } + else -> { + readRecords.sortedWith { o1, o2 -> + o1.bookName.cnCompare(o2.bookName) + } + } } - val readRecords = App.db.readRecordDao().allShow withContext(Main) { adapter.setItems(readRecords) } @@ -61,35 +93,60 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { } inner class RecordAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_read_record) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemReadRecordBinding { + return ItemReadRecordBinding.inflate(inflater, parent, false) + } override fun convert( holder: ItemViewHolder, + binding: ItemReadRecordBinding, item: ReadRecordShow, payloads: MutableList ) { - holder.itemView.apply { - tv_book_name.text = item.bookName - tv_read_time.text = formatDuring(item.readTime) + binding.apply { + tvBookName.text = item.bookName + tvReadTime.text = formatDuring(item.readTime) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - iv_remove.onClick { - alert(R.string.delete, R.string.sure_del) { - okButton { - getItem(holder.layoutPosition)?.let { - App.db.readRecordDao().deleteByName(it.bookName) - initData() + override fun registerListener(holder: ItemViewHolder, binding: ItemReadRecordBinding) { + binding.apply { + root.setOnClickListener { + val item = getItem(holder.layoutPosition) ?: return@setOnClickListener + launch { + val book = withContext(IO) { + appDb.bookDao.findByName(item.bookName).firstOrNull() + } + if (book == null) { + SearchActivity.start(this@ReadRecordActivity, item.bookName) + } else { + startActivity { + putExtra("bookUrl", book.bookUrl) } } - noButton() - }.show().applyTint() + } + } + tvRemove.setOnClickListener { + getItem(holder.layoutPosition)?.let { item -> + sureDelAlert(item) + } } } } + private fun sureDelAlert(item: ReadRecordShow) { + alert(R.string.delete) { + setMessage(getString(R.string.sure_del_any, item.bookName)) + okButton { + appDb.readRecordDao.deleteByName(item.bookName) + initData() + } + noButton() + }.show() + } + } fun formatDuring(mss: Long): String { @@ -101,7 +158,11 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) { val h = if (hours > 0) "${hours}小时" else "" val m = if (minutes > 0) "${minutes}分钟" else "" val s = if (seconds > 0) "${seconds}秒" else "" - return "$d$h$m$s" + var time = "$d$h$m$s" + if (time.isBlank()) { + time = "0秒" + } + return time } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt index 6c5741414..e58f2ade2 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt @@ -1,34 +1,47 @@ package io.legado.app.ui.association -import android.content.Intent import android.os.Bundle -import io.legado.app.R +import androidx.activity.viewModels import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Theme -import io.legado.app.ui.main.MainActivity -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_translucence.* -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.toast +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +class FileAssociationActivity : + VMBaseActivity() { -class FileAssociationActivity : VMBaseActivity( - R.layout.activity_translucence, - theme = Theme.Transparent -) { + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) - override val viewModel: FileAssociationViewModel - get() = getViewModel(FileAssociationViewModel::class.java) + override val viewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { - rotate_loading.show() + binding.rotateLoading.show() + viewModel.onLineImportLive.observe(this) { + startActivity { + data = it + } + finish() + } + viewModel.importBookSourceLive.observe(this) { + binding.rotateLoading.hide() + ImportBookSourceDialog.start(supportFragmentManager, it, true) + } + viewModel.importRssSourceLive.observe(this) { + binding.rotateLoading.hide() + ImportRssSourceDialog.start(supportFragmentManager, it, true) + } + viewModel.importReplaceRuleLive.observe(this) { + binding.rotateLoading.hide() + ImportReplaceRuleDialog.start(supportFragmentManager, it, true) + } viewModel.errorLiveData.observe(this, { - rotate_loading.hide() - toast(it) + binding.rotateLoading.hide() + toastOnUi(it) finish() }) viewModel.successLiveData.observe(this, { - rotate_loading.hide() + binding.rotateLoading.hide() startActivity(it) finish() }) @@ -37,14 +50,4 @@ class FileAssociationActivity : VMBaseActivity( } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - finish() - //返回后直接跳转到主页面 - gotoMainActivity() - } - - private fun gotoMainActivity() { - startActivity() - } } diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt index ad7700d33..4d009981b 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt @@ -3,85 +3,63 @@ package io.legado.app.ui.association import android.app.Application import android.content.Intent import android.net.Uri -import android.text.TextUtils import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData import io.legado.app.base.BaseViewModel import io.legado.app.model.localBook.LocalBook import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.utils.isJsonArray -import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isJson import io.legado.app.utils.readText import java.io.File class FileAssociationViewModel(application: Application) : BaseViewModel(application) { - + val onLineImportLive = MutableLiveData() + val importBookSourceLive = MutableLiveData() + val importRssSourceLive = MutableLiveData() + val importReplaceRuleLive = MutableLiveData() val successLiveData = MutableLiveData() val errorLiveData = MutableLiveData() + @Suppress("BlockingMethodInNonBlockingContext") fun dispatchIndent(uri: Uri) { execute { - val url: String //如果是普通的url,需要根据返回的内容判断是什么 if (uri.scheme == "file" || uri.scheme == "content") { - var scheme = "" val content = if (uri.scheme == "file") { - val file = File(uri.path.toString()) - if (file.exists()) { - file.readText() - } else { - null - } + File(uri.path.toString()).readText() } else { DocumentFile.fromSingleUri(context, uri)?.readText(context) } - if (content != null) { - if (content.isJsonObject() || content.isJsonArray()) { + content?.let { + if (it.isJson()) { //暂时根据文件内容判断属于什么 when { content.contains("bookSourceUrl") -> { - scheme = "booksource" + importBookSourceLive.postValue(it) + return@execute } content.contains("sourceUrl") -> { - scheme = "rsssource" + importRssSourceLive.postValue(it) + return@execute } content.contains("pattern") -> { - scheme = "replace" + importReplaceRuleLive.postValue(it) + return@execute } } } - if (TextUtils.isEmpty(scheme)) { - val book = if (uri.scheme == "content") { - LocalBook.importFile(uri.toString()) - } else { - LocalBook.importFile(uri.path.toString()) - } - val intent = Intent(context, ReadBookActivity::class.java) - intent.putExtra("bookUrl", book.bookUrl) - successLiveData.postValue(intent) - return@execute + val book = if (uri.scheme == "content") { + LocalBook.importFile(uri) + } else { + LocalBook.importFile(uri) } - } else { - errorLiveData.postValue("文件不存在") - return@execute - } - // content模式下,需要传递完整的路径,方便后续解析 - url = if (uri.scheme == "content") { - "yuedu://${scheme}/importonline?src=$uri" - } else { - "yuedu://${scheme}/importonline?src=${uri.path}" - } - - } else if (uri.scheme == "yuedu") { - url = uri.toString() + val intent = Intent(context, ReadBookActivity::class.java) + intent.putExtra("bookUrl", book.bookUrl) + successLiveData.postValue(intent) + } ?: throw Exception("文件不存在") } else { - url = "yuedu://booksource/importonline?src=${uri.path}" + onLineImportLive.postValue(uri) } - val data = Uri.parse(url) - val newIndent = Intent(Intent.ACTION_VIEW) - newIndent.data = data - successLiveData.postValue(newIndent) - return@execute }.onError { it.printStackTrace() errorLiveData.postValue(it.localizedMessage) diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt deleted file mode 100644 index 07b6451da..000000000 --- a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt +++ /dev/null @@ -1,236 +0,0 @@ -package io.legado.app.ui.association - -import android.content.Context -import android.content.DialogInterface -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.Toolbar -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.R -import io.legado.app.base.BaseDialogFragment -import io.legado.app.base.VMBaseActivity -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.Theme -import io.legado.app.data.entities.BookSource -import io.legado.app.help.IntentDataHelp -import io.legado.app.help.SourceHelp -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.okButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.activity_translucence.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_source_import.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.toast - -class ImportBookSourceActivity : VMBaseActivity( - R.layout.activity_translucence, - theme = Theme.Transparent -) { - - override val viewModel: ImportBookSourceViewModel - get() = getViewModel(ImportBookSourceViewModel::class.java) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - rotate_loading.show() - viewModel.errorLiveData.observe(this, { - rotate_loading.hide() - errorDialog(it) - }) - viewModel.successLiveData.observe(this, { - rotate_loading.hide() - if (it > 0) { - successDialog() - } else { - errorDialog(getString(R.string.wrong_format)) - } - }) - initData() - } - - private fun initData() { - intent.getStringExtra("dataKey")?.let { - IntentDataHelp.getData(it)?.let { source -> - viewModel.importSource(source) - return - } - } - intent.getStringExtra("source")?.let { - viewModel.importSource(it) - return - } - intent.getStringExtra("filePath")?.let { - viewModel.importSourceFromFilePath(it) - return - } - intent.data?.let { - when (it.path) { - "/importonline" -> it.getQueryParameter("src")?.let { url -> - if (url.startsWith("http", false)) { - viewModel.importSource(url) - } else { - viewModel.importSourceFromFilePath(url) - } - } - else -> { - rotate_loading.hide() - toast(R.string.wrong_format) - finish() - } - } - } - } - - private fun errorDialog(msg: String) { - alert(getString(R.string.error), msg) { - okButton { } - }.show().applyTint().setOnDismissListener { - finish() - } - } - - private fun successDialog() { - val bundle = Bundle() - val allSourceKey = IntentDataHelp.putData(viewModel.allSources, "source") - bundle.putString("allSourceKey", allSourceKey) - val checkStatusKey = IntentDataHelp.putData(viewModel.sourceCheckState, "check") - bundle.putString("checkStatusKey", checkStatusKey) - val selectStatusKey = IntentDataHelp.putData(viewModel.selectStatus, "select") - bundle.putString("selectStatusKey", selectStatusKey) - SourcesDialog().apply { - arguments = bundle - }.show(supportFragmentManager, "SourceDialog") - } - - class SourcesDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { - - lateinit var adapter: SourcesAdapter - - override fun onStart() { - super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) - dialog?.window?.setLayout( - (dm.widthPixels * 0.9).toInt(), - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.dialog_recycler_view, container) - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setTitle(R.string.import_book_source) - initMenu() - arguments?.let { - adapter = SourcesAdapter(requireContext()) - val allSources = - IntentDataHelp.getData>(it.getString("allSourceKey")) - adapter.sourceCheckState = - IntentDataHelp.getData>(it.getString("checkStatusKey"))!! - adapter.selectStatus = - IntentDataHelp.getData>(it.getString("selectStatusKey"))!! - - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.adapter = adapter - adapter.setItems(allSources) - tv_cancel.visible() - tv_cancel.onClick { - dismiss() - } - tv_ok.visible() - tv_ok.onClick { - importSelect() - dismiss() - } - } - } - - private fun initMenu() { - tool_bar.setOnMenuItemClickListener(this) - tool_bar.inflateMenu(R.menu.import_source) - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_select_all -> { - adapter.selectStatus.forEachIndexed { index, b -> - if (!b) { - adapter.selectStatus[index] = true - } - } - adapter.notifyDataSetChanged() - } - R.id.menu_un_select_all -> { - adapter.selectStatus.forEachIndexed { index, b -> - if (b) { - adapter.selectStatus[index] = false - } - } - adapter.notifyDataSetChanged() - } - } - return false - } - - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - activity?.finish() - } - - private fun importSelect() { - val selectSource = arrayListOf() - adapter.selectStatus.forEachIndexed { index, b -> - if (b) { - selectSource.add(adapter.getItem(index)!!) - } - } - SourceHelp.insertBookSource(*selectSource.toTypedArray()) - } - - } - - class SourcesAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_source_import) { - - lateinit var sourceCheckState: ArrayList - lateinit var selectStatus: ArrayList - - override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList) { - holder.itemView.apply { - cb_source_name.isChecked = selectStatus[holder.layoutPosition] - cb_source_name.text = item.bookSourceName - tv_source_state.text = if (sourceCheckState[holder.layoutPosition]) { - "已存在" - } else { - "新书源" - } - - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - cb_source_name.setOnCheckedChangeListener { buttonView, isChecked -> - if (buttonView.isPressed) { - selectStatus[holder.layoutPosition] = isChecked - } - } - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt new file mode 100644 index 000000000..7775cbc0b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt @@ -0,0 +1,241 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppPattern +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.help.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.dp +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible + + +/** + * 导入书源弹出窗口 + */ +class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + + companion object { + + fun start( + fragmentManager: FragmentManager, + source: String, + finishOnDismiss: Boolean = false + ) { + ImportBookSourceDialog().apply { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + }.show(fragmentManager, "importBookSource") + } + + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private lateinit var adapter: SourcesAdapter + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_recycler_view, container) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_book_source) + binding.rotateLoading.show() + initMenu() + adapter = SourcesAdapter(requireContext()) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll() + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this, { + binding.rotateLoading.hide() + binding.tvMsg.apply { + text = it + visible() + } + }) + viewModel.successLiveData.observe(this, { + binding.rotateLoading.hide() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + }) + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll()) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount(), + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount(), + viewModel.allSources.size + ) + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.import_source) + binding.toolBar.menu.findItem(R.id.menu_Keep_original_name) + ?.isChecked = AppConfig.importKeepName + } + + @SuppressLint("InflateParams") + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_new_group -> { + alert(R.string.diy_edit_source_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + val groups = linkedSetOf() + appDb.bookSourceDao.allGroup.forEach { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp + } + customView { + alertBinding.root + } + okButton { + alertBinding.editView.text?.toString()?.let { group -> + viewModel.groupName = group + item.title = getString(R.string.diy_edit_source_group_title, group) + } + } + noButton() + }.show() + } + R.id.menu_Keep_original_name -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepName, item.isChecked) + } + } + return false + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: BookSource, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.bookSourceName + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新书源" + item.lastUpdateTime > localSource.lastUpdateTime -> "更新" + else -> "已存在" + } + } + + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt index 3fbad78f2..0659487ad 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt @@ -1,73 +1,98 @@ package io.legado.app.ui.association import android.app.Application -import android.net.Uri -import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData import com.jayway.jsonpath.JsonPath -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource -import io.legado.app.help.http.HttpHelper +import io.legado.app.help.AppConfig +import io.legado.app.help.ContentProcessor +import io.legado.app.help.SourceHelp +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text import io.legado.app.help.storage.OldRule import io.legado.app.help.storage.Restore -import io.legado.app.utils.* -import java.io.File +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { - + var groupName: String? = null val errorLiveData = MutableLiveData() val successLiveData = MutableLiveData() val allSources = arrayListOf() - val sourceCheckState = arrayListOf() + val checkSources = arrayListOf() val selectStatus = arrayListOf() - fun importSourceFromFilePath(path: String) { + fun isSelectAll(): Boolean { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + fun selectCount(): Int { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { execute { - val content = if (path.isContentPath()) { - //在前面被解码了,如果不进行编码,中文会无法识别 - val newPath = Uri.encode(path, ":/.") - DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) - } else { - val file = File(path) - if (file.exists()) { - file.readText() - } else { - null + val keepName = AppConfig.importKeepName + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val source = allSources[index] + if (keepName) { + checkSources[index]?.let { + source.bookSourceName = it.bookSourceName + source.bookSourceGroup = it.bookSourceGroup + source.customOrder = it.customOrder + } + } + if (groupName != null) { + source.bookSourceGroup = groupName + } + selectSource.add(source) } } - if (content != null) { - importSource(content) - } else { - errorLiveData.postValue(context.getString(R.string.error_read_file)) - } - }.onError { - it.printStackTrace() - errorLiveData.postValue(context.getString(R.string.error_read_file)) + SourceHelp.insertBookSource(*selectSource.toTypedArray()) + ContentProcessor.upReplaceRules() + }.onFinally { + finally.invoke() } } fun importSource(text: String) { execute { - val text1 = text.trim() + val mText = text.trim() when { - text1.isJsonObject() -> { - val json = JsonPath.parse(text1) + mText.isJsonObject() -> { + val json = JsonPath.parse(mText) val urls = json.read>("$.sourceUrls") if (!urls.isNullOrEmpty()) { urls.forEach { importSourceUrl(it) } } else { - OldRule.jsonToBookSource(text1)?.let { + OldRule.jsonToBookSource(mText)?.let { allSources.add(it) } } } - text1.isJsonArray() -> { - val items: List> = Restore.jsonPath.parse(text1).read("$") + mText.isJsonArray() -> { + val items: List> = Restore.jsonPath.parse(mText).read("$") for (item in items) { val jsonItem = Restore.jsonPath.parse(item) OldRule.jsonToBookSource(jsonItem.jsonString())?.let { @@ -75,8 +100,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { } } } - text1.isAbsUrl() -> { - importSourceUrl(text1) + mText.isAbsUrl() -> { + importSourceUrl(mText) } else -> throw Exception(context.getString(R.string.wrong_format)) } @@ -88,11 +113,10 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { } } - private fun importSourceUrl(url: String) { - HttpHelper.simpleGet(url, "UTF-8").let { body -> - if (body == null) { - throw Exception(context.getString(R.string.error_get_data)) - } + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCall { + url(url) + }.text("utf-8").let { body -> val items: List> = Restore.jsonPath.parse(body).read("$") for (item in items) { val jsonItem = Restore.jsonPath.parse(item) @@ -106,9 +130,9 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { private fun comparisonSource() { execute { allSources.forEach { - val has = App.db.bookSourceDao().getBookSource(it.bookSourceUrl) != null - sourceCheckState.add(has) - selectStatus.add(!has) + val source = appDb.bookSourceDao.getBookSource(it.bookSourceUrl) + checkSources.add(source) + selectStatus.add(source == null || source.lastUpdateTime < it.lastUpdateTime) } successLiveData.postValue(allSources.size) } diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt deleted file mode 100644 index 12ae447d1..000000000 --- a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt +++ /dev/null @@ -1,96 +0,0 @@ -package io.legado.app.ui.association - -import android.os.Bundle -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Theme -import io.legado.app.data.entities.ReplaceRule -import io.legado.app.help.IntentDataHelp -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_translucence.* -import org.jetbrains.anko.toast - -class ImportReplaceRuleActivity : VMBaseActivity( - R.layout.activity_translucence, - theme = Theme.Transparent -) { - - override val viewModel: ImportReplaceRuleViewModel - get() = getViewModel(ImportReplaceRuleViewModel::class.java) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - rotate_loading.show() - viewModel.errorLiveData.observe(this, { - rotate_loading.hide() - errorDialog(it) - }) - viewModel.successLiveData.observe(this, { - rotate_loading.hide() - if (it.size > 0) { - successDialog(it) - } else { - errorDialog("格式不对") - } - }) - initData() - } - - private fun initData() { - intent.getStringExtra("dataKey")?.let { - IntentDataHelp.getData(it)?.let { source -> - viewModel.import(source) - return - } - } - intent.getStringExtra("source")?.let { - viewModel.import(it) - return - } - intent.getStringExtra("filePath")?.let { - viewModel.importFromFilePath(it) - return - } - intent.data?.let { - when (it.path) { - "/importonline" -> it.getQueryParameter("src")?.let { url -> - if (url.startsWith("http", false)) { - viewModel.import(url) - } else { - viewModel.importFromFilePath(url) - } - } - else -> { - rotate_loading.hide() - toast("格式不对") - finish() - } - } - } - } - - private fun errorDialog(msg: String) { - alert("导入出错", msg) { - okButton { } - }.show().applyTint().setOnDismissListener { - finish() - } - } - - private fun successDialog(allSource: ArrayList) { - alert("解析结果", "共${allSource.size}个替换规则,是否确认导入?") { - okButton { - App.db.replaceRuleDao().insert(*allSource.toTypedArray()) - } - noButton { - - } - }.show().applyTint().setOnDismissListener { - finish() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt new file mode 100644 index 000000000..0cf97bb6a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt @@ -0,0 +1,176 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible + +class ImportReplaceRuleDialog : BaseDialogFragment() { + + companion object { + fun start( + fragmentManager: FragmentManager, + source: String, + finishOnDismiss: Boolean = false + ) { + ImportReplaceRuleDialog().apply { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + }.show(fragmentManager, "importReplaceRule") + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + lateinit var adapter: SourcesAdapter + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_recycler_view, container) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_replace_rule) + binding.rotateLoading.show() + adapter = SourcesAdapter(requireContext()) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll() + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this, { + binding.rotateLoading.hide() + binding.tvMsg.apply { + text = it + visible() + } + }) + viewModel.successLiveData.observe(this, { + binding.rotateLoading.hide() + if (it > 0) { + adapter.setItems(viewModel.allRules) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + }) + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.import(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll()) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount(), + viewModel.allRules.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount(), + viewModel.allRules.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: ReplaceRule, + payloads: MutableList + ) { + binding.run { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.name + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.run { + cbSourceName.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt index 960bafa9b..0fc20ca89 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt @@ -1,54 +1,50 @@ package io.legado.app.ui.association import android.app.Application -import android.net.Uri -import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData -import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.ReplaceRule -import io.legado.app.help.http.HttpHelper +import io.legado.app.help.AppConfig +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text import io.legado.app.help.storage.OldReplace import io.legado.app.utils.isAbsUrl -import io.legado.app.utils.isContentPath -import io.legado.app.utils.readText -import java.io.File class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { val errorLiveData = MutableLiveData() - val successLiveData = MutableLiveData>() + val successLiveData = MutableLiveData() - private val allRules = arrayListOf() + val allRules = arrayListOf() + val checkRules = arrayListOf() + val selectStatus = arrayListOf() - fun importFromFilePath(path: String) { - execute { - val content = if (path.isContentPath()) { - //在前面被解码了,如果不进行编码,中文会无法识别 - val newPath = Uri.encode(path, ":/.") - DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) - } else { - val file = File(path) - if (file.exists()) { - file.readText() - } else { - null - } + fun isSelectAll(): Boolean { + selectStatus.forEach { + if (!it) { + return false } - if (content != null) { - import(content) - } else { - errorLiveData.postValue(context.getString(R.string.error_read_file)) + } + return true + } + + fun selectCount(): Int { + var count = 0 + selectStatus.forEach { + if (it) { + count++ } - }.onError { - it.printStackTrace() - errorLiveData.postValue(context.getString(R.string.error_read_file)) } + return count } fun import(text: String) { execute { if (text.isAbsUrl()) { - HttpHelper.simpleGet(text)?.let { + okHttpClient.newCall { + url(text) + }.text("utf-8").let { val rules = OldReplace.jsonToReplaceRules(it) allRules.addAll(rules) } @@ -59,7 +55,34 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { }.onError { errorLiveData.postValue(it.localizedMessage ?: "ERROR") }.onSuccess { - successLiveData.postValue(allRules) + comparisonSource() + } + } + + fun importSelect(finally: () -> Unit) { + execute { + val keepName = AppConfig.importKeepName + val selectRules = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val rule = allRules[index] + selectRules.add(rule) + } + } + appDb.replaceRuleDao.insert(*selectRules.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + private fun comparisonSource() { + execute { + allRules.forEach { + checkRules.add(null) + selectStatus.add(false) + } + }.onSuccess { + successLiveData.postValue(allRules.size) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt deleted file mode 100644 index 31413a879..000000000 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt +++ /dev/null @@ -1,236 +0,0 @@ -package io.legado.app.ui.association - -import android.content.Context -import android.content.DialogInterface -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.Toolbar -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.R -import io.legado.app.base.BaseDialogFragment -import io.legado.app.base.VMBaseActivity -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.Theme -import io.legado.app.data.entities.RssSource -import io.legado.app.help.IntentDataHelp -import io.legado.app.help.SourceHelp -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.okButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.activity_translucence.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_source_import.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.toast - -class ImportRssSourceActivity : VMBaseActivity( - R.layout.activity_translucence, - theme = Theme.Transparent -) { - - override val viewModel: ImportRssSourceViewModel - get() = getViewModel(ImportRssSourceViewModel::class.java) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - rotate_loading.show() - viewModel.errorLiveData.observe(this, { - rotate_loading.hide() - errorDialog(it) - }) - viewModel.successLiveData.observe(this, { - rotate_loading.hide() - if (it > 0) { - successDialog() - } else { - errorDialog(getString(R.string.wrong_format)) - } - }) - initData() - } - - private fun initData() { - intent.getStringExtra("dataKey")?.let { - IntentDataHelp.getData(it)?.let { source -> - viewModel.importSource(source) - return - } - } - intent.getStringExtra("source")?.let { - viewModel.importSource(it) - return - } - intent.getStringExtra("filePath")?.let { - viewModel.importSourceFromFilePath(it) - return - } - intent.data?.let { - when (it.path) { - "/importonline" -> it.getQueryParameter("src")?.let { url -> - if (url.startsWith("http", false)) { - viewModel.importSource(url) - } else { - viewModel.importSourceFromFilePath(url) - } - } - else -> { - rotate_loading.hide() - toast(R.string.wrong_format) - finish() - } - } - } - } - - private fun errorDialog(msg: String) { - alert(getString(R.string.error), msg) { - okButton { } - }.show().applyTint().setOnDismissListener { - finish() - } - } - - private fun successDialog() { - val bundle = Bundle() - val allSourceKey = IntentDataHelp.putData(viewModel.allSources, "source") - bundle.putString("allSourceKey", allSourceKey) - val checkStatusKey = IntentDataHelp.putData(viewModel.sourceCheckState, "check") - bundle.putString("checkStatusKey", checkStatusKey) - val selectStatusKey = IntentDataHelp.putData(viewModel.selectStatus, "select") - bundle.putString("selectStatusKey", selectStatusKey) - SourcesDialog().apply { - arguments = bundle - }.show(supportFragmentManager, "SourceDialog") - } - - class SourcesDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { - - lateinit var adapter: SourcesAdapter - - override fun onStart() { - super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) - dialog?.window?.setLayout( - (dm.widthPixels * 0.9).toInt(), - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.dialog_recycler_view, container) - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.title = getString(R.string.import_rss_source) - initMenu() - arguments?.let { - adapter = SourcesAdapter(requireContext()) - val allSources = - IntentDataHelp.getData>(it.getString("allSourceKey")) - adapter.sourceCheckState = - IntentDataHelp.getData>(it.getString("checkStatusKey"))!! - adapter.selectStatus = - IntentDataHelp.getData>(it.getString("selectStatusKey"))!! - - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.adapter = adapter - adapter.setItems(allSources) - tv_cancel.visible() - tv_cancel.onClick { - dismiss() - } - tv_ok.visible() - tv_ok.onClick { - importSelect() - dismiss() - } - } - } - - private fun initMenu() { - tool_bar.setOnMenuItemClickListener(this) - tool_bar.inflateMenu(R.menu.import_source) - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_select_all -> { - adapter.selectStatus.forEachIndexed { index, b -> - if (!b) { - adapter.selectStatus[index] = true - } - } - adapter.notifyDataSetChanged() - } - R.id.menu_un_select_all -> { - adapter.selectStatus.forEachIndexed { index, b -> - if (b) { - adapter.selectStatus[index] = false - } - } - adapter.notifyDataSetChanged() - } - } - return false - } - - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - activity?.finish() - } - - private fun importSelect() { - val selectSource = arrayListOf() - adapter.selectStatus.forEachIndexed { index, b -> - if (b) { - selectSource.add(adapter.getItem(index)!!) - } - } - SourceHelp.insertRssSource(*selectSource.toTypedArray()) - } - - } - - class SourcesAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_source_import) { - - lateinit var sourceCheckState: ArrayList - lateinit var selectStatus: ArrayList - - override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { - holder.itemView.apply { - cb_source_name.isChecked = selectStatus[holder.layoutPosition] - cb_source_name.text = item.sourceName - tv_source_state.text = if (sourceCheckState[holder.layoutPosition]) { - "已存在" - } else { - "新订阅源" - } - - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - cb_source_name.setOnCheckedChangeListener { buttonView, isChecked -> - if (buttonView.isPressed) { - selectStatus[holder.layoutPosition] = isChecked - } - } - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt new file mode 100644 index 000000000..b139beb90 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt @@ -0,0 +1,234 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppPattern +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.help.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.dp +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible + +/** + * 导入rss源弹出窗口 + */ +class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + + companion object { + fun start( + fragmentManager: FragmentManager, + source: String, + finishOnDismiss: Boolean = false + ) { + ImportRssSourceDialog().apply { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + }.show(fragmentManager, "importRssSource") + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + lateinit var adapter: SourcesAdapter + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_recycler_view, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_rss_source) + binding.rotateLoading.show() + initMenu() + adapter = SourcesAdapter(requireContext()) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll() + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this, { + binding.rotateLoading.hide() + binding.tvMsg.apply { + text = it + visible() + } + }) + viewModel.successLiveData.observe(this, { + binding.rotateLoading.hide() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + }) + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll()) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount(), + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount(), + viewModel.allSources.size + ) + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.import_source) + binding.toolBar.menu.findItem(R.id.menu_Keep_original_name)?.isChecked = + AppConfig.importKeepName + } + + @SuppressLint("InflateParams") + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_new_group -> { + alert(R.string.diy_edit_source_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + val groups = linkedSetOf() + appDb.rssSourceDao.allGroup.forEach { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp + } + customView { + alertBinding.root + } + okButton { + alertBinding.editView.text?.toString()?.let { group -> + viewModel.groupName = group + item.title = getString(R.string.diy_edit_source_group_title, group) + } + } + noButton() + }.show() + } + R.id.menu_Keep_original_name -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepName, item.isChecked) + } + } + return false + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: RssSource, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.sourceName + tvSourceState.text = if (viewModel.checkSources[holder.layoutPosition] != null) { + "已存在" + } else { + "新订阅源" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt index 13b752a79..911eb3ad0 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt @@ -1,72 +1,93 @@ package io.legado.app.ui.association import android.app.Application -import android.net.Uri -import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData import com.jayway.jsonpath.JsonPath -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.RssSource -import io.legado.app.help.http.HttpHelper +import io.legado.app.help.AppConfig +import io.legado.app.help.SourceHelp +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text import io.legado.app.help.storage.Restore import io.legado.app.utils.* -import java.io.File class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { - + var groupName: String? = null val errorLiveData = MutableLiveData() val successLiveData = MutableLiveData() val allSources = arrayListOf() - val sourceCheckState = arrayListOf() + val checkSources = arrayListOf() val selectStatus = arrayListOf() + fun isSelectAll(): Boolean { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } - fun importSourceFromFilePath(path: String) { - execute { - val content = if (path.isContentPath()) { - //在前面被解码了,如果不进行编码,中文会无法识别 - val newPath = Uri.encode(path, ":/.") - DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) - } else { - val file = File(path) - if (file.exists()) { - file.readText() - } else { - null - } + fun selectCount(): Int { + var count = 0 + selectStatus.forEach { + if (it) { + count++ } - if (null != content) { - GSON.fromJsonArray(content)?.let { - allSources.addAll(it) + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val keepName = AppConfig.importKeepName + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val source = allSources[index] + if (keepName) { + checkSources[index]?.let { + source.sourceName = it.sourceName + source.sourceGroup = it.sourceGroup + source.customOrder = it.customOrder + } + } + if (groupName != null) { + source.sourceGroup = groupName + } + selectSource.add(source) } } - }.onSuccess { - comparisonSource() + SourceHelp.insertRssSource(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() } } fun importSource(text: String) { execute { - val text1 = text.trim() + val mText = text.trim() when { - text1.isJsonObject() -> { - val json = JsonPath.parse(text1) + mText.isJsonObject() -> { + val json = JsonPath.parse(mText) val urls = json.read>("$.sourceUrls") if (!urls.isNullOrEmpty()) { urls.forEach { importSourceUrl(it) } } else { - GSON.fromJsonArray(text1)?.let { + GSON.fromJsonArray(mText)?.let { allSources.addAll(it) } } } - text1.isJsonArray() -> { - val items: List> = Restore.jsonPath.parse(text1).read("$") + mText.isJsonArray() -> { + val items: List> = Restore.jsonPath.parse(mText).read("$") for (item in items) { val jsonItem = Restore.jsonPath.parse(item) GSON.fromJsonObject(jsonItem.jsonString())?.let { @@ -74,8 +95,8 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { } } } - text1.isAbsUrl() -> { - importSourceUrl(text1) + mText.isAbsUrl() -> { + importSourceUrl(mText) } else -> throw Exception(context.getString(R.string.wrong_format)) } @@ -86,8 +107,10 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { } } - private fun importSourceUrl(url: String) { - HttpHelper.simpleGet(url, "UTF-8")?.let { body -> + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCall { + url(url) + }.text("utf-8").let { body -> val items: List> = Restore.jsonPath.parse(body).read("$") for (item in items) { val jsonItem = Restore.jsonPath.parse(item) @@ -101,9 +124,9 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { private fun comparisonSource() { execute { allSources.forEach { - val has = App.db.rssSourceDao().getByKey(it.sourceUrl) != null - sourceCheckState.add(has) - selectStatus.add(!has) + val has = appDb.rssSourceDao.getByKey(it.sourceUrl) + checkSources.add(has) + selectStatus.add(has == null) } successLiveData.postValue(allSources.size) } diff --git a/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt new file mode 100644 index 000000000..2aec56703 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt @@ -0,0 +1,80 @@ +package io.legado.app.ui.association + +import android.os.Bundle +import androidx.activity.viewModels +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 网络一键导入 + * 格式: legado://import/{path}?src={url} + */ +class OnLineImportActivity : + VMBaseActivity() { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + override val viewModel by viewModels() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.successLive.observe(this) { + when (it.first) { + "bookSource" -> ImportBookSourceDialog + .start(supportFragmentManager, it.second, true) + "rssSource" -> ImportRssSourceDialog + .start(supportFragmentManager, it.second, true) + "replaceRule" -> ImportReplaceRuleDialog + .start(supportFragmentManager, it.second, true) + } + } + viewModel.errorLive.observe(this) { + finallyDialog(getString(R.string.error), it) + } + intent.data?.let { + val url = it.getQueryParameter("src") + if (url.isNullOrBlank()) { + finish() + return + } + when (it.path) { + "/bookSource" -> ImportBookSourceDialog.start(supportFragmentManager, url, true) + "/rssSource" -> ImportRssSourceDialog.start(supportFragmentManager, url, true) + "/replaceRule" -> ImportReplaceRuleDialog.start(supportFragmentManager, url, true) + "/textTocRule" -> viewModel.getText(url) { json -> + viewModel.importTextTocRule(json, this::finallyDialog) + } + "/httpTTS" -> viewModel.getText(url) { json -> + viewModel.importHttpTTS(json, this::finallyDialog) + } + "/theme" -> viewModel.getText(url) { json -> + viewModel.importTheme(json, this::finallyDialog) + } + "/readConfig" -> viewModel.getBytes(url) { bytes -> + viewModel.importReadConfig(bytes, this::finallyDialog) + } + "/importonline" -> when (it.host) { + "booksource" -> ImportBookSourceDialog.start(supportFragmentManager, url, true) + "rsssource" -> ImportRssSourceDialog.start(supportFragmentManager, url, true) + "replace" -> ImportReplaceRuleDialog.start(supportFragmentManager, url, true) + else -> { + toastOnUi("url error") + finish() + } + } + } + } + } + + private fun finallyDialog(title: String, msg: String) { + alert(title, msg) { + okButton() + onDismiss { + finish() + } + }.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt b/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt new file mode 100644 index 000000000..5dd4fbea3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt @@ -0,0 +1,172 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.ReadBookConfig +import io.legado.app.help.ThemeConfig +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isJsonArray +import okhttp3.MediaType.Companion.toMediaType + +class OnLineImportViewModel(app: Application) : BaseViewModel(app) { + val successLive = MutableLiveData>() + val errorLive = MutableLiveData() + + fun getText(url: String, success: (text: String) -> Unit) { + execute { + okHttpClient.newCall { + url(url) + }.text("utf-8") + }.onSuccess { + success.invoke(it) + }.onError { + errorLive.postValue( + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun getBytes(url: String, success: (bytes: ByteArray) -> Unit) { + execute { + @Suppress("BlockingMethodInNonBlockingContext") + okHttpClient.newCall { + url(url) + }.bytes() + }.onSuccess { + success.invoke(it) + }.onError { + errorLive.postValue( + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) { + execute { + if (json.isJsonArray()) { + GSON.fromJsonArray(json)?.let { + appDb.txtTocRuleDao.insert(*it.toTypedArray()) + } ?: throw Exception("格式不对") + } else { + GSON.fromJsonObject(json)?.let { + appDb.txtTocRuleDao.insert(it) + } ?: throw Exception("格式不对") + } + }.onSuccess { + finally.invoke(context.getString(R.string.success), "导入Txt规则成功") + }.onError { + finally.invoke( + context.getString(R.string.error), + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun importHttpTTS(json: String, finally: (title: String, msg: String) -> Unit) { + execute { + if (json.isJsonArray()) { + GSON.fromJsonArray(json)?.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) + return@execute it.size + } ?: throw Exception("格式不对") + } else { + GSON.fromJsonObject(json)?.let { + appDb.httpTTSDao.insert(it) + return@execute 1 + } ?: throw Exception("格式不对") + } + }.onSuccess { + finally.invoke(context.getString(R.string.success), "导入${it}朗读引擎") + }.onError { + finally.invoke( + context.getString(R.string.error), + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) { + execute { + if (json.isJsonArray()) { + GSON.fromJsonArray(json)?.forEach { + ThemeConfig.addConfig(it) + } ?: throw Exception("格式不对") + } else { + GSON.fromJsonObject(json)?.let { + ThemeConfig.addConfig(it) + } ?: throw Exception("格式不对") + } + }.onSuccess { + finally.invoke(context.getString(R.string.success), "导入主题成功") + }.onError { + finally.invoke( + context.getString(R.string.error), + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun importReadConfig(bytes: ByteArray, finally: (title: String, msg: String) -> Unit) { + execute { + val config = ReadBookConfig.import(bytes) + ReadBookConfig.configList.forEachIndexed { index, c -> + if (c.name == config.name) { + ReadBookConfig.configList[index] = config + return@execute config.name + } + ReadBookConfig.configList.add(config) + return@execute config.name + } + }.onSuccess { + finally.invoke(context.getString(R.string.success), "导入排版成功") + }.onError { + finally.invoke( + context.getString(R.string.error), + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun determineType(url: String, finally: (title: String, msg: String) -> Unit) { + execute { + val rs = okHttpClient.newCall { + url(url) + } + when (rs.contentType()) { + "application/zip".toMediaType(), + "application/octet-stream".toMediaType() -> { + @Suppress("BlockingMethodInNonBlockingContext") + importReadConfig(rs.bytes(), finally) + } + else -> { + val json = rs.text("utf-8") + when { + json.contains("bookSourceUrl") -> + successLive.postValue(Pair("bookSource", json)) + json.contains("sourceUrl") -> + successLive.postValue(Pair("rssSource", json)) + json.contains("replacement") -> + successLive.postValue(Pair("replaceRule", json)) + json.contains("themeName") -> + importTextTocRule(json, finally) + json.contains("name") && json.contains("rule") -> + importTextTocRule(json, finally) + json.contains("name") && json.contains("url") -> + importTextTocRule(json, finally) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt b/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt deleted file mode 100644 index 4c1a5a85b..000000000 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt +++ /dev/null @@ -1,149 +0,0 @@ -package io.legado.app.ui.audio - -import android.app.Application -import android.content.Intent -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.BaseViewModel -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.help.BookHelp -import io.legado.app.model.webBook.WebBook -import io.legado.app.service.help.AudioPlay -import kotlinx.coroutines.Dispatchers - -class AudioPlayViewModel(application: Application) : BaseViewModel(application) { - - fun initData(intent: Intent) { - execute { - val bookUrl = intent.getStringExtra("bookUrl") - if (AudioPlay.book?.bookUrl != bookUrl) { - AudioPlay.stop(context) - AudioPlay.inBookshelf = intent.getBooleanExtra("inBookshelf", true) - AudioPlay.book = if (!bookUrl.isNullOrEmpty()) { - App.db.bookDao().getBook(bookUrl) - } else { - App.db.bookDao().lastReadBook - } - AudioPlay.book?.let { book -> - AudioPlay.titleData.postValue(book.name) - AudioPlay.coverData.postValue(book.getDisplayCover()) - AudioPlay.durChapterIndex = book.durChapterIndex - AudioPlay.durPageIndex = book.durChapterPos - App.db.bookSourceDao().getBookSource(book.origin)?.let { - AudioPlay.webBook = WebBook(it) - } - val count = App.db.bookChapterDao().getChapterCount(book.bookUrl) - if (count == 0) { - if (book.tocUrl.isEmpty()) { - loadBookInfo(book) - } else { - loadChapterList(book) - } - } else { - if (AudioPlay.durChapterIndex > count - 1) { - AudioPlay.durChapterIndex = count - 1 - } - AudioPlay.chapterSize = count - } - } - saveRead() - } - } - } - - private fun loadBookInfo( - book: Book, - changeDruChapterIndex: ((chapters: List) -> Unit)? = null - ) { - execute { - AudioPlay.webBook?.getBookInfo(book, this) - ?.onSuccess { - loadChapterList(book, changeDruChapterIndex) - } - } - } - - private fun loadChapterList( - book: Book, - changeDruChapterIndex: ((chapters: List) -> Unit)? = null - ) { - execute { - AudioPlay.webBook?.getChapterList(book, this) - ?.onSuccess(Dispatchers.IO) { cList -> - if (cList.isNotEmpty()) { - if (changeDruChapterIndex == null) { - App.db.bookChapterDao().insert(*cList.toTypedArray()) - AudioPlay.chapterSize = cList.size - } else { - changeDruChapterIndex(cList) - } - } else { - toast(R.string.error_load_toc) - } - }?.onError { - toast(R.string.error_load_toc) - } - } - } - - fun changeTo(book1: Book) { - execute { - AudioPlay.book?.let { - book1.order = it.order - App.db.bookDao().delete(it) - } - App.db.bookDao().insert(book1) - AudioPlay.book = book1 - App.db.bookSourceDao().getBookSource(book1.origin)?.let { - AudioPlay.webBook = WebBook(it) - } - if (book1.tocUrl.isEmpty()) { - loadBookInfo(book1) { upChangeDurChapterIndex(book1, it) } - } else { - loadChapterList(book1) { upChangeDurChapterIndex(book1, it) } - } - } - } - - private fun upChangeDurChapterIndex(book: Book, chapters: List) { - execute { - AudioPlay.durChapterIndex = BookHelp.getDurChapterIndexByChapterTitle( - book.durChapterTitle, - book.durChapterIndex, - chapters - ) - book.durChapterIndex = AudioPlay.durChapterIndex - book.durChapterTitle = chapters[AudioPlay.durChapterIndex].title - App.db.bookDao().update(book) - App.db.bookChapterDao().insert(*chapters.toTypedArray()) - AudioPlay.chapterSize = chapters.size - } - } - - fun saveRead() { - execute { - AudioPlay.book?.let { book -> - book.lastCheckCount = 0 - book.durChapterTime = System.currentTimeMillis() - book.durChapterIndex = AudioPlay.durChapterIndex - book.durChapterPos = AudioPlay.durPageIndex - App.db.bookChapterDao().getChapter(book.bookUrl, book.durChapterIndex)?.let { - book.durChapterTitle = it.title - } - App.db.bookDao().update(book) - } - } - } - - fun removeFromBookshelf(success: (() -> Unit)?) { - execute { - AudioPlay.book?.let { - App.db.bookDao().delete(it) - } - }.onSuccess { - success?.invoke() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt index f23a2008c..44fec7475 100644 --- a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt @@ -3,49 +3,60 @@ package io.legado.app.ui.book.arrange import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.widget.PopupMenu -import androidx.lifecycle.LiveData import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ActivityArrangeBookBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.theme.ATH import io.legado.app.ui.book.group.GroupManageDialog import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.applyTint +import io.legado.app.utils.cnCompare import io.legado.app.utils.getPrefInt -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_arrange_book.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class ArrangeBookActivity : VMBaseActivity(R.layout.activity_arrange_book), +class ArrangeBookActivity : VMBaseActivity(), PopupMenu.OnMenuItemClickListener, - ArrangeBookAdapter.CallBack, GroupSelectDialog.CallBack { - override val viewModel: ArrangeBookViewModel - get() = getViewModel(ArrangeBookViewModel::class.java) + SelectActionBar.CallBack, + ArrangeBookAdapter.CallBack, + GroupSelectDialog.CallBack { + + override val binding by viewBinding(ActivityArrangeBookBinding::inflate) + override val viewModel by viewModels() override val groupList: ArrayList = arrayListOf() private val groupRequestCode = 22 private val addToGroupRequestCode = 34 private lateinit var adapter: ArrangeBookAdapter - private var groupLiveData: LiveData>? = null - private var booksLiveData: LiveData>? = null + private var booksFlowJob: Job? = null private var menu: Menu? = null - private var groupId: Int = -1 + private var groupId: Long = -1 override fun onActivityCreated(savedInstanceState: Bundle?) { - groupId = intent.getIntExtra("groupId", -1) - title_bar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all) + groupId = intent.getLongExtra("groupId", -1) + launch { + binding.titleBar.subtitle = withContext(IO) { + appDb.bookGroupDao.getByID(groupId)?.groupName + ?: getString(R.string.no_group) + } + } initView() initGroupData() initBookData() @@ -62,94 +73,80 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi return super.onPrepareOptionsMenu(menu) } + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + selectGroup(groupRequestCode, 0) + } + private fun initView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) + ATH.applyEdgeEffectColor(binding.recyclerView) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) adapter = ArrangeBookAdapter(this, this) - recycler_view.adapter = adapter - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) itemTouchCallback.isCanDrag = getPrefInt(PreferKey.bookshelfSort) == 3 - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) - select_action_bar.setMainActionText(R.string.move_to_group) - select_action_bar.inflateMenu(R.menu.arrange_book_sel) - select_action_bar.setOnMenuItemClickListener(this) - select_action_bar.setCallBack(object : SelectActionBar.CallBack { - override fun selectAll(selectAll: Boolean) { - adapter.selectAll(selectAll) - } - - override fun revertSelection() { - adapter.revertSelection() - } - - override fun onClickMainAction() { - selectGroup(0, groupRequestCode) - } - }) + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + binding.selectActionBar.setMainActionText(R.string.move_to_group) + binding.selectActionBar.inflateMenu(R.menu.arrange_book_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) } private fun initGroupData() { - groupLiveData?.removeObservers(this) - groupLiveData = App.db.bookGroupDao().liveDataAll() - groupLiveData?.observe(this, { - groupList.clear() - groupList.addAll(it) - adapter.notifyDataSetChanged() - upMenu() - }) + launch { + appDb.bookGroupDao.flowAll().collect { + groupList.clear() + groupList.addAll(it) + adapter.notifyDataSetChanged() + upMenu() + } + } } private fun initBookData() { - booksLiveData?.removeObservers(this) - booksLiveData = + booksFlowJob?.cancel() + booksFlowJob = launch { when (groupId) { - AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll() - AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal() - AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio() - AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() - else -> App.db.bookDao().observeByGroup(groupId) - } - booksLiveData?.observe(this, { list -> - val books = when (getPrefInt(PreferKey.bookshelfSort)) { - 1 -> list.sortedByDescending { it.latestChapterTime } - 2 -> list.sortedBy { it.name } - 3 -> list.sortedBy { it.order } - else -> list.sortedByDescending { it.durChapterTime } + AppConst.bookGroupAllId -> appDb.bookDao.flowAll() + AppConst.bookGroupLocalId -> appDb.bookDao.flowLocal() + AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() + AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup() + else -> appDb.bookDao.flowByGroup(groupId) + }.collect { list -> + val books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> list.sortedByDescending { it.latestChapterTime } + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + 3 -> list.sortedBy { it.order } + else -> list.sortedByDescending { it.durChapterTime } + } + adapter.setItems(books) } - adapter.setItems(books) - upSelectCount() - }) + } } override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_group_manage -> GroupManageDialog() .show(supportFragmentManager, "groupManage") - R.id.menu_no_group -> { - title_bar.subtitle = getString(R.string.no_group) - groupId = AppConst.bookGroupNone.groupId - initBookData() - } - R.id.menu_all -> { - title_bar.subtitle = item.title - groupId = AppConst.bookGroupAll.groupId - initBookData() - } - R.id.menu_local -> { - title_bar.subtitle = item.title - groupId = AppConst.bookGroupLocal.groupId - initBookData() - } - R.id.menu_audio -> { - title_bar.subtitle = item.title - groupId = AppConst.bookGroupAudio.groupId - initBookData() - } else -> if (item.groupId == R.id.menu_group) { - title_bar.subtitle = item.title - groupId = item.itemId + binding.titleBar.subtitle = item.title + groupId = appDb.bookGroupDao.getByName(item.title.toString())?.groupId ?: 0 initBookData() } } @@ -161,13 +158,13 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi R.id.menu_del_selection -> alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { okButton { viewModel.deleteBook(*adapter.selectedBooks()) } - noButton { } - }.show().applyTint() + noButton() + }.show() R.id.menu_update_enable -> viewModel.upCanUpdate(adapter.selectedBooks(), true) R.id.menu_update_disable -> viewModel.upCanUpdate(adapter.selectedBooks(), false) - R.id.menu_add_to_group -> selectGroup(0, addToGroupRequestCode) + R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0) } return false } @@ -176,16 +173,16 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> subMenu.removeGroup(R.id.menu_group) groupList.forEach { bookGroup -> - subMenu.add(R.id.menu_group, bookGroup.groupId, Menu.NONE, bookGroup.groupName) + subMenu.add(R.id.menu_group, bookGroup.order, Menu.NONE, bookGroup.groupName) } } } - override fun selectGroup(groupId: Int, requestCode: Int) { + override fun selectGroup(requestCode: Int, groupId: Long) { GroupSelectDialog.show(supportFragmentManager, groupId, requestCode) } - override fun upGroup(requestCode: Int, groupId: Int) { + override fun upGroup(requestCode: Int, groupId: Long) { when (requestCode) { groupRequestCode -> { val books = arrayListOf() @@ -210,7 +207,7 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi } override fun upSelectCount() { - select_action_bar.upCountView(adapter.selectedBooks().size, adapter.getItems().size) + binding.selectActionBar.upCountView(adapter.selectedBooks().size, adapter.getItems().size) } override fun updateBook(vararg book: Book) { @@ -222,7 +219,7 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi okButton { viewModel.deleteBook(book) } - }.show().applyTint() + }.show() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt index 84e070259..d35d23542 100644 --- a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt @@ -1,88 +1,70 @@ package io.legado.app.ui.book.arrange +import android.annotation.SuppressLint import android.content.Context import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemArrangeBookBinding import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback -import kotlinx.android.synthetic.main.item_arrange_book.view.* -import org.jetbrains.anko.backgroundColor -import org.jetbrains.anko.sdk27.listeners.onClick import java.util.* class ArrangeBookAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_arrange_book), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + + ItemTouchCallback.Callback { val groupRequestCode = 12 private val selectedBooks: HashSet = hashSetOf() var actionItem: Book? = null - fun selectAll(selectAll: Boolean) { - if (selectAll) { - getItems().forEach { - selectedBooks.add(it) - } - } else { - selectedBooks.clear() - } - notifyDataSetChanged() - callBack.upSelectCount() + override fun getViewBinding(parent: ViewGroup): ItemArrangeBookBinding { + return ItemArrangeBookBinding.inflate(inflater, parent, false) } - fun revertSelection() { - getItems().forEach { - if (selectedBooks.contains(it)) { - selectedBooks.remove(it) - } else { - selectedBooks.add(it) - } - } - notifyDataSetChanged() + override fun onCurrentListChanged() { callBack.upSelectCount() } - fun selectedBooks(): Array { - val books = arrayListOf() - selectedBooks.forEach { - if (getItems().contains(it)) { - books.add(it) - } - } - return books.toTypedArray() - } - - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { - with(holder.itemView) { - backgroundColor = context.backgroundColor - tv_name.text = item.name - tv_author.text = item.author - tv_author.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE - tv_group_s.text = getGroupName(item.group) + override fun convert( + holder: ItemViewHolder, + binding: ItemArrangeBookBinding, + item: Book, + payloads: MutableList + ) { + binding.apply { + root.setBackgroundColor(context.backgroundColor) + tvName.text = item.name + tvAuthor.text = item.author + tvAuthor.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE + tvGroupS.text = getGroupName(item.group) checkbox.isChecked = selectedBooks.contains(item) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { + override fun registerListener(holder: ItemViewHolder, binding: ItemArrangeBookBinding) { + binding.apply { checkbox.setOnCheckedChangeListener { buttonView, isChecked -> - getItem(holder.layoutPosition)?.let { - if (buttonView.isPressed) { - if (isChecked) { - selectedBooks.add(it) - } else { - selectedBooks.remove(it) + if (buttonView.isPressed) { + getItem(holder.layoutPosition)?.let { + if (buttonView.isPressed) { + if (isChecked) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + callBack.upSelectCount() } - callBack.upSelectCount() } - } } - onClick { + root.setOnClickListener { getItem(holder.layoutPosition)?.let { checkbox.isChecked = !checkbox.isChecked if (checkbox.isChecked) { @@ -93,31 +75,67 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) : callBack.upSelectCount() } } - tv_delete.onClick { + tvDelete.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.deleteBook(it) } } - tv_group.onClick { + tvGroup.setOnClickListener { getItem(holder.layoutPosition)?.let { actionItem = it - callBack.selectGroup(it.group, groupRequestCode) + callBack.selectGroup(groupRequestCode, it.group) } } } } - private fun getGroupList(groupId: Int): List { + @SuppressLint("NotifyDataSetChanged") + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + selectedBooks.add(it) + } + } else { + selectedBooks.clear() + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + @SuppressLint("NotifyDataSetChanged") + fun revertSelection() { + getItems().forEach { + if (selectedBooks.contains(it)) { + selectedBooks.remove(it) + } else { + selectedBooks.add(it) + } + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + fun selectedBooks(): Array { + val books = arrayListOf() + selectedBooks.forEach { + if (getItems().contains(it)) { + books.add(it) + } + } + return books.toTypedArray() + } + + private fun getGroupList(groupId: Long): List { val groupNames = arrayListOf() callBack.groupList.forEach { - if (it.groupId and groupId > 0) { + if (it.groupId > 0 && it.groupId and groupId > 0) { groupNames.add(it.groupName) } } return groupNames } - private fun getGroupName(groupId: Int): String { + private fun getGroupName(groupId: Long): String { val groupNames = getGroupList(groupId) if (groupNames.isEmpty()) { return "" @@ -127,11 +145,9 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) : private var isMoved = false - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { val srcItem = getItem(srcPosition) val targetItem = getItem(targetPosition) - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) if (srcItem != null && targetItem != null) { if (srcItem.order == targetItem.order) { for ((index, item) in getItems().withIndex()) { @@ -143,6 +159,7 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) : targetItem.order = pos } } + swapItem(srcPosition, targetPosition) isMoved = true return true } @@ -154,11 +171,36 @@ class ArrangeBookAdapter(context: Context, val callBack: CallBack) : isMoved = false } + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selectedBooks + } + + override fun getItemId(position: Int): Book { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upSelectCount() + return true + } + return false + } + } + interface CallBack { val groupList: List fun upSelectCount() fun updateBook(vararg book: Book) fun deleteBook(book: Book) - fun selectGroup(groupId: Int, requestCode: Int) + fun selectGroup(requestCode: Int, groupId: Long) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt index 82ec712d2..c6d389997 100644 --- a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt @@ -1,8 +1,8 @@ package io.legado.app.ui.book.arrange import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.Book @@ -13,19 +13,19 @@ class ArrangeBookViewModel(application: Application) : BaseViewModel(application books.forEach { it.canUpdate = canUpdate } - App.db.bookDao().update(*books) + appDb.bookDao.update(*books) } } fun updateBook(vararg book: Book) { execute { - App.db.bookDao().update(*book) + appDb.bookDao.update(*book) } } fun deleteBook(vararg book: Book) { execute { - App.db.bookDao().delete(*book) + appDb.bookDao.delete(*book) } } diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt similarity index 53% rename from app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt rename to app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt index f1d2883fb..d13b395ca 100644 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt @@ -1,13 +1,14 @@ -package io.legado.app.ui.audio +package io.legado.app.ui.book.audio import android.app.Activity -import android.content.Intent import android.graphics.drawable.Drawable +import android.icu.text.SimpleDateFormat import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.widget.SeekBar +import androidx.activity.viewModels import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions.bitmapTransform @@ -17,38 +18,54 @@ import io.legado.app.constant.EventBus import io.legado.app.constant.Status import io.legado.app.constant.Theme import io.legado.app.data.entities.Book +import io.legado.app.databinding.ActivityAudioPlayBinding import io.legado.app.help.BlurTransformation import io.legado.app.help.ImageLoader import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.service.AudioPlayService import io.legado.app.service.help.AudioPlay import io.legado.app.ui.book.changesource.ChangeSourceDialog -import io.legado.app.ui.book.chapterlist.ChapterListActivity +import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.widget.image.CoverImageView +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_audio_play.* -import org.apache.commons.lang3.time.DateFormatUtils -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick -import org.jetbrains.anko.startActivityForResult - +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onLongClick +import java.util.* +/** + * 音频播放 + */ class AudioPlayActivity : - VMBaseActivity(R.layout.activity_audio_play, toolBarTheme = Theme.Dark), + VMBaseActivity(toolBarTheme = Theme.Dark), ChangeSourceDialog.CallBack { - override val viewModel: AudioPlayViewModel - get() = getViewModel(AudioPlayViewModel::class.java) + override val binding by viewBinding(ActivityAudioPlayBinding::inflate) + override val viewModel by viewModels() - private var requestCodeChapter = 8461 private var adjustProgress = false + private val progressTimeFormat by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + SimpleDateFormat("mm:ss", Locale.getDefault()) + } else { + java.text.SimpleDateFormat("mm:ss", Locale.getDefault()) + } + } + private val tocActivityResult = registerForActivityResult(TocActivityResult()) { + it?.let { + if (it.first != AudioPlay.durChapterIndex) { + AudioPlay.skipTo(this, it.first) + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { - title_bar.transparent() - AudioPlay.titleData.observe(this, { title_bar.title = it }) - AudioPlay.coverData.observe(this, { upCover(it) }) + binding.titleBar.transparent() + AudioPlay.titleData.observe(this) { + binding.titleBar.title = it + } + AudioPlay.coverData.observe(this) { + upCover(it) + } viewModel.initData(intent) initView() } @@ -68,50 +85,49 @@ class AudioPlayActivity : } private fun initView() { - fab_play_stop.onClick { + binding.fabPlayStop.setOnClickListener { playButton() } - fab_play_stop.onLongClick { - AudioPlay.stop(this) - true + binding.fabPlayStop.onLongClick { + AudioPlay.stop(this@AudioPlayActivity) } - iv_skip_next.onClick { - AudioPlay.next(this) + binding.ivSkipNext.setOnClickListener { + AudioPlay.next(this@AudioPlayActivity) } - iv_skip_previous.onClick { - AudioPlay.prev(this) + binding.ivSkipPrevious.setOnClickListener { + AudioPlay.prev(this@AudioPlayActivity) } - player_progress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - tv_dur_time.text = DateFormatUtils.format(progress.toLong(), "mm:ss") + binding.playerProgress.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + binding.tvDurTime.text = progressTimeFormat.format(progress.toLong()) } - override fun onStartTrackingTouch(seekBar: SeekBar?) { + override fun onStartTrackingTouch(seekBar: SeekBar) { adjustProgress = true } - override fun onStopTrackingTouch(seekBar: SeekBar?) { + override fun onStopTrackingTouch(seekBar: SeekBar) { adjustProgress = false - AudioPlay.adjustProgress(this@AudioPlayActivity, player_progress.progress) + AudioPlay.adjustProgress(this@AudioPlayActivity, seekBar.progress) } }) - iv_chapter.onClick { + binding.ivChapter.setOnClickListener { AudioPlay.book?.let { - startActivityForResult( - requestCodeChapter, - Pair("bookUrl", it.bookUrl) - ) + tocActivityResult.launch(it.bookUrl) } } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - iv_fast_rewind.invisible() - iv_fast_forward.invisible() + binding.ivFastRewind.invisible() + binding.ivFastForward.invisible() + } + binding.ivFastForward.setOnClickListener { + AudioPlay.adjustSpeed(this@AudioPlayActivity, 0.1f) } - iv_fast_forward.onClick { - AudioPlay.adjustSpeed(this, 0.1f) + binding.ivFastRewind.setOnClickListener { + AudioPlay.adjustSpeed(this@AudioPlayActivity, -0.1f) } - iv_fast_rewind.onClick { - AudioPlay.adjustSpeed(this, -0.1f) + binding.ivTimer.setOnClickListener { + AudioPlay.addTimer(this@AudioPlayActivity) } } @@ -119,12 +135,12 @@ class AudioPlayActivity : ImageLoader.load(this, path) .placeholder(CoverImageView.defaultDrawable) .error(CoverImageView.defaultDrawable) - .into(iv_cover) + .into(binding.ivCover) ImageLoader.load(this, path) .transition(DrawableTransitionOptions.withCrossFade(1500)) .thumbnail(defaultCover()) .apply(bitmapTransform(BlurTransformation(this, 25))) - .into(iv_bg) + .into(binding.ivBg) } private fun defaultCover(): RequestBuilder { @@ -150,14 +166,14 @@ class AudioPlayActivity : override fun finish() { AudioPlay.book?.let { if (!AudioPlay.inBookshelf) { - this.alert(title = getString(R.string.add_to_shelf)) { - message = getString(R.string.check_add_bookshelf, it.name) + alert(title = getString(R.string.add_to_shelf)) { + setMessage(getString(R.string.check_add_bookshelf, it.name)) okButton { AudioPlay.inBookshelf = true setResult(Activity.RESULT_OK) } noButton { viewModel.removeFromBookshelf { super.finish() } } - }.show().applyTint() + }.show() } else { super.finish() } @@ -171,28 +187,6 @@ class AudioPlayActivity : } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - requestCodeChapter -> data?.getIntExtra("index", AudioPlay.durChapterIndex)?.let { - if (it != AudioPlay.durChapterIndex) { - val isPlay = !AudioPlayService.pause - AudioPlay.pause(this) - AudioPlay.status = Status.STOP - AudioPlay.durChapterIndex = it - AudioPlay.durPageIndex = 0 - AudioPlay.book?.durChapterIndex = AudioPlay.durChapterIndex - viewModel.saveRead() - if (isPlay) { - AudioPlay.play(this) - } - } - } - } - } - } - override fun observeLiveBus() { observeEvent(EventBus.MEDIA_BUTTON) { if (it) { @@ -202,26 +196,26 @@ class AudioPlayActivity : observeEventSticky(EventBus.AUDIO_STATE) { AudioPlay.status = it if (it == Status.PLAY) { - fab_play_stop.setImageResource(R.drawable.ic_pause_24dp) + binding.fabPlayStop.setImageResource(R.drawable.ic_pause_24dp) } else { - fab_play_stop.setImageResource(R.drawable.ic_play_24dp) + binding.fabPlayStop.setImageResource(R.drawable.ic_play_24dp) } } observeEventSticky(EventBus.AUDIO_SUB_TITLE) { - tv_sub_title.text = it + binding.tvSubTitle.text = it } observeEventSticky(EventBus.AUDIO_SIZE) { - player_progress.max = it - tv_all_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") + binding.playerProgress.max = it + binding.tvAllTime.text = progressTimeFormat.format(it.toLong()) } observeEventSticky(EventBus.AUDIO_PROGRESS) { - AudioPlay.durPageIndex = it - if (!adjustProgress) player_progress.progress = it - tv_dur_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") + AudioPlay.durChapterPos = it + if (!adjustProgress) binding.playerProgress.progress = it + binding.tvDurTime.text = progressTimeFormat.format(it.toLong()) } observeEventSticky(EventBus.AUDIO_SPEED) { - tv_speed.text = String.format("%.1fX", it) - tv_speed.visible() + binding.tvSpeed.text = String.format("%.1fX", it) + binding.tvSpeed.visible() } } diff --git a/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt new file mode 100644 index 000000000..8770dd494 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt @@ -0,0 +1,133 @@ +package io.legado.app.ui.book.audio + +import android.app.Application +import android.content.Intent +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.BookHelp +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.help.AudioPlay +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers + +class AudioPlayViewModel(application: Application) : BaseViewModel(application) { + + fun initData(intent: Intent) = AudioPlay.apply { + execute { + val bookUrl = intent.getStringExtra("bookUrl") + if (bookUrl != null && bookUrl != book?.bookUrl) { + stop(context) + inBookshelf = intent.getBooleanExtra("inBookshelf", true) + book = appDb.bookDao.getBook(bookUrl) + book?.let { book -> + titleData.postValue(book.name) + coverData.postValue(book.getDisplayCover()) + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex) + upDurChapter(book) + appDb.bookSourceDao.getBookSource(book.origin)?.let { + webBook = WebBook(it) + } + if (durChapter == null) { + if (book.tocUrl.isEmpty()) { + loadBookInfo(book) + } else { + loadChapterList(book) + } + } + saveRead(book) + } + } + } + } + + private fun loadBookInfo( + book: Book, + changeDruChapterIndex: ((chapters: List) -> Unit)? = null + ) { + execute { + AudioPlay.webBook?.getBookInfo(this, book) + ?.onSuccess { + loadChapterList(book, changeDruChapterIndex) + } + } + } + + private fun loadChapterList( + book: Book, + changeDruChapterIndex: ((chapters: List) -> Unit)? = null + ) { + execute { + AudioPlay.webBook?.getChapterList(this, book) + ?.onSuccess(Dispatchers.IO) { cList -> + if (cList.isNotEmpty()) { + if (changeDruChapterIndex == null) { + appDb.bookChapterDao.insert(*cList.toTypedArray()) + } else { + changeDruChapterIndex(cList) + } + AudioPlay.upDurChapter(book) + } else { + context.toastOnUi(R.string.error_load_toc) + } + }?.onError { + context.toastOnUi(R.string.error_load_toc) + } + } + } + + fun changeTo(book1: Book) { + execute { + var oldTocSize: Int = book1.totalChapterNum + AudioPlay.book?.let { + oldTocSize = it.totalChapterNum + book1.order = it.order + appDb.bookDao.delete(it) + } + appDb.bookDao.insert(book1) + AudioPlay.book = book1 + appDb.bookSourceDao.getBookSource(book1.origin)?.let { + AudioPlay.webBook = WebBook(it) + } + if (book1.tocUrl.isEmpty()) { + loadBookInfo(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) } + } else { + loadChapterList(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) } + } + } + } + + private fun upChangeDurChapterIndex( + book: Book, + oldTocSize: Int, + chapters: List + ) { + execute { + AudioPlay.durChapterIndex = BookHelp.getDurChapter( + book.durChapterIndex, + oldTocSize, + book.durChapterTitle, + chapters + ) + book.durChapterIndex = AudioPlay.durChapterIndex + book.durChapterTitle = chapters[AudioPlay.durChapterIndex].title + appDb.bookDao.update(book) + appDb.bookChapterDao.insert(*chapters.toTypedArray()) + } + } + + fun removeFromBookshelf(success: (() -> Unit)?) { + execute { + AudioPlay.book?.let { + appDb.bookDao.delete(it) + } + }.onSuccess { + success?.invoke() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt new file mode 100644 index 000000000..55b482f72 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt @@ -0,0 +1,331 @@ +package io.legado.app.ui.book.cache + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.charsets +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ActivityCacheBookBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.AppConfig +import io.legado.app.help.BookHelp +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.service.help.CacheBook +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.widget.dialog.TextListDialog +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArraySet + +class CacheActivity : VMBaseActivity(), + CacheAdapter.CallBack { + + override val binding by viewBinding(ActivityCacheBookBinding::inflate) + override val viewModel by viewModels() + + private val exportBookPathKey = "exportBookPath" + lateinit var adapter: CacheAdapter + private var booksFlowJob: Job? = null + private var menu: Menu? = null + private var exportPosition = -1 + private val groupList: ArrayList = arrayListOf() + private var groupId: Long = -1 + + private val exportDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + ACache.get(this@CacheActivity).put(exportBookPathKey, uri.toString()) + startExport(uri.toString()) + } else { + uri.path?.let { path -> + ACache.get(this@CacheActivity).put(exportBookPathKey, path) + startExport(path) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + groupId = intent.getLongExtra("groupId", -1) + launch { + binding.titleBar.subtitle = withContext(IO) { + appDb.bookGroupDao.getByID(groupId)?.groupName + ?: getString(R.string.no_group) + } + } + initRecyclerView() + initGroupData() + initBookData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_cache, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace + menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav + return super.onMenuOpened(featureId, menu) + } + + private fun upMenu() { + menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group) + groupList.forEach { bookGroup -> + subMenu.add(R.id.menu_group, bookGroup.order, Menu.NONE, bookGroup.groupName) + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_download -> { + if (adapter.downloadMap.isNullOrEmpty()) { + adapter.getItems().forEach { book -> + CacheBook.start( + this@CacheActivity, + book.bookUrl, + book.durChapterIndex, + book.totalChapterNum + ) + } + } else { + CacheBook.stop(this@CacheActivity) + } + } + R.id.menu_export_all -> exportAll() + R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked + R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked + R.id.menu_export_folder -> { + exportPosition = -1 + selectExportFolder() + } + R.id.menu_export_file_name -> alertExportFileName() + R.id.menu_export_type -> showExportTypeConfig() + R.id.menu_export_charset -> showCharsetConfig() + R.id.menu_log -> + TextListDialog.show(supportFragmentManager, getString(R.string.log), CacheBook.logs) + else -> if (item.groupId == R.id.menu_group) { + binding.titleBar.subtitle = item.title + groupId = appDb.bookGroupDao.getByName(item.title.toString())?.groupId ?: 0 + initBookData() + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initRecyclerView() { + binding.recyclerView.layoutManager = LinearLayoutManager(this) + adapter = CacheAdapter(this, this) + binding.recyclerView.adapter = adapter + } + + private fun initBookData() { + booksFlowJob?.cancel() + booksFlowJob = launch { + when (groupId) { + AppConst.bookGroupAllId -> appDb.bookDao.flowAll() + AppConst.bookGroupLocalId -> appDb.bookDao.flowLocal() + AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() + AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup() + else -> appDb.bookDao.flowByGroup(groupId) + }.collect { list -> + val booksDownload = list.filter { + it.isOnLineTxt() + } + val books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> booksDownload.sortedByDescending { it.latestChapterTime } + 2 -> booksDownload.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + 3 -> booksDownload.sortedBy { it.order } + else -> booksDownload.sortedByDescending { it.durChapterTime } + } + adapter.setItems(books) + initCacheSize(books) + } + } + } + + private fun initGroupData() { + launch { + appDb.bookGroupDao.flowAll().collect { + groupList.clear() + groupList.addAll(it) + adapter.notifyDataSetChanged() + upMenu() + } + } + } + + private fun initCacheSize(books: List) { + launch(IO) { + books.forEach { book -> + val chapterCaches = hashSetOf() + val cacheNames = BookHelp.getChapterFiles(book) + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + if (cacheNames.contains(chapter.getFileName())) { + chapterCaches.add(chapter.url) + } + } + adapter.cacheChapters[book.bookUrl] = chapterCaches + withContext(Dispatchers.Main) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + } + } + + override fun observeLiveBus() { + observeEvent>>(EventBus.UP_DOWNLOAD) { + if (it.isEmpty()) { + menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_play_24dp) + menu?.applyTint(this) + } else { + menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp) + menu?.applyTint(this) + } + adapter.downloadMap = it + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + observeEvent(EventBus.SAVE_CONTENT) { + adapter.cacheChapters[it.bookUrl]?.add(it.url) + } + } + + override fun export(position: Int) { + exportPosition = position + val path = ACache.get(this@CacheActivity).getAsString(exportBookPathKey) + if (path.isNullOrEmpty()) { + selectExportFolder() + } else { + startExport(path) + } + } + + private fun exportAll() { + exportPosition = -10 + val path = ACache.get(this@CacheActivity).getAsString(exportBookPathKey) + if (path.isNullOrEmpty()) { + selectExportFolder() + } else { + startExport(path) + } + } + + private fun selectExportFolder() { + val default = arrayListOf() + val path = ACache.get(this@CacheActivity).getAsString(exportBookPathKey) + if (!path.isNullOrEmpty()) { + default.add(path) + } + exportDir.launch( + FilePickerParam( + otherActions = default.toTypedArray() + ) + ) + } + + private fun startExport(path: String) { + if (exportPosition == -10) { + if (adapter.getItems().isNotEmpty()) { + Snackbar.make(binding.titleBar, R.string.exporting, Snackbar.LENGTH_INDEFINITE) + .show() + var exportSize = adapter.getItems().size + adapter.getItems().forEach { book -> + when (AppConfig.exportType) { + 1 -> viewModel.exportEPUB(path, book) { + exportSize-- + toastOnUi(it) + if (exportSize <= 0) { + binding.titleBar.snackbar(R.string.complete) + } + } + else -> viewModel.export(path, book) { + exportSize-- + toastOnUi(it) + if (exportSize <= 0) { + binding.titleBar.snackbar(R.string.complete) + } + } + } + } + } else { + toastOnUi(R.string.no_book) + } + } else if (exportPosition >= 0) { + adapter.getItem(exportPosition)?.let { book -> + Snackbar.make(binding.titleBar, R.string.exporting, Snackbar.LENGTH_INDEFINITE) + .show() + when (AppConfig.exportType) { + 1 -> viewModel.exportEPUB(path, book) { + binding.titleBar.snackbar(it) + } + else -> viewModel.export(path, book) { + binding.titleBar.snackbar(it) + } + } + } + } + } + + private fun alertExportFileName() { + alert(R.string.export_file_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setText(AppConfig.bookExportFileName) + } + customView { alertBinding.root } + okButton { + AppConfig.bookExportFileName = alertBinding.editView.text?.toString() + } + cancelButton() + }.show() + } + + private fun showExportTypeConfig() { + selector(R.string.export_type, arrayListOf("txt", "epub")) { _, i -> + AppConfig.exportType = i + } + } + + private fun showCharsetConfig() { + alert(R.string.set_charset) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(charsets) + editView.setText(AppConfig.exportCharset) + } + customView { alertBinding.root } + okButton { + AppConfig.exportCharset = alertBinding.editView.text?.toString() ?: "UTF-8" + } + cancelButton() + }.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/download/DownloadAdapter.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt similarity index 55% rename from app/src/main/java/io/legado/app/ui/book/download/DownloadAdapter.kt rename to app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt index 3da043450..b1fddc3e8 100644 --- a/app/src/main/java/io/legado/app/ui/book/download/DownloadAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt @@ -1,59 +1,69 @@ -package io.legado.app.ui.book.download +package io.legado.app.ui.book.cache import android.content.Context +import android.view.ViewGroup import android.widget.ImageView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.service.help.Download -import kotlinx.android.synthetic.main.item_download.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.databinding.ItemDownloadBinding +import io.legado.app.service.help.CacheBook + import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet -class DownloadAdapter(context: Context, private val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_download) { +class CacheAdapter(context: Context, private val callBack: CallBack) : + RecyclerAdapter(context) { val cacheChapters = hashMapOf>() var downloadMap: ConcurrentHashMap>? = null - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { - with(holder.itemView) { + override fun getViewBinding(parent: ViewGroup): ItemDownloadBinding { + return ItemDownloadBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemDownloadBinding, + item: Book, + payloads: MutableList + ) { + binding.run { if (payloads.isEmpty()) { - tv_name.text = item.name - tv_author.text = context.getString(R.string.author_show, item.getRealAuthor()) + tvName.text = item.name + tvAuthor.text = context.getString(R.string.author_show, item.getRealAuthor()) val cs = cacheChapters[item.bookUrl] if (cs == null) { - tv_download.setText(R.string.loading) + tvDownload.setText(R.string.loading) } else { - tv_download.text = + tvDownload.text = context.getString(R.string.download_count, cs.size, item.totalChapterNum) } - upDownloadIv(iv_download, item) + upDownloadIv(ivDownload, item) } else { val cacheSize = cacheChapters[item.bookUrl]?.size ?: 0 - tv_download.text = + tvDownload.text = context.getString(R.string.download_count, cacheSize, item.totalChapterNum) - upDownloadIv(iv_download, item) + upDownloadIv(ivDownload, item) } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - iv_download.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemDownloadBinding) { + binding.run { + ivDownload.setOnClickListener { getItem(holder.layoutPosition)?.let { if (downloadMap?.containsKey(it.bookUrl) == true) { - Download.remove(context, it.bookUrl) + CacheBook.remove(context, it.bookUrl) } else { - Download.start(context, it.bookUrl, 0, it.totalChapterNum) + CacheBook.start(context, it.bookUrl, 0, it.totalChapterNum) } } } - tv_export.onClick { + tvExport.setOnClickListener { callBack.export(holder.layoutPosition) } } diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt new file mode 100644 index 000000000..8e6019c1c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt @@ -0,0 +1,451 @@ +package io.legado.app.ui.book.cache + +import android.app.Application +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.AppConfig +import io.legado.app.help.BookHelp +import io.legado.app.help.ContentProcessor +import io.legado.app.help.storage.BookWebDav +import io.legado.app.utils.* +import me.ag2s.epublib.domain.* +import me.ag2s.epublib.epub.EpubWriter +import me.ag2s.epublib.util.ResourceUtil +import splitties.init.appCtx +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.nio.charset.Charset +import javax.script.SimpleBindings + + +class CacheViewModel(application: Application) : BaseViewModel(application) { + + fun getExportFileName(book: Book): String { + val jsStr = AppConfig.bookExportFileName + if (jsStr.isNullOrBlank()) { + return "${book.name} 作者:${book.getRealAuthor()}" + } + val bindings = SimpleBindings() + bindings["name"] = book.name + bindings["author"] = book.getRealAuthor() + return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString() + } + + fun export(path: String, book: Book, finally: (msg: String) -> Unit) { + execute { + if (path.isContentScheme()) { + val uri = Uri.parse(path) + DocumentFile.fromTreeUri(context, uri)?.let { + export(it, book) + } + } else { + export(FileUtils.createFolderIfNotExist(path), book) + } + }.onError { + finally(it.localizedMessage ?: "ERROR") + }.onSuccess { + finally(context.getString(R.string.success)) + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + private suspend fun export(doc: DocumentFile, book: Book) { + val filename = "${getExportFileName(book)}.txt" + DocumentUtils.delete(doc, filename) + DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> + val stringBuilder = StringBuilder() + context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> + getAllContents(book) { + bookOs.write(it.toByteArray(Charset.forName(AppConfig.exportCharset))) + stringBuilder.append(it) + } + } + if (AppConfig.exportToWebDav) { + // 导出到webdav + val byteArray = + stringBuilder.toString().toByteArray(Charset.forName(AppConfig.exportCharset)) + BookWebDav.exportWebDav(byteArray, filename) + } + } + getSrcList(book).forEach { + val vFile = BookHelp.getImage(book, it.third) + if (vFile.exists()) { + DocumentUtils.createFileIfNotExist( + doc, + "${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg", + subDirs = arrayOf("${book.name}_${book.author}", "images", it.first) + )?.writeBytes(context, vFile.readBytes()) + } + } + } + + private suspend fun export(file: File, book: Book) { + val filename = "${getExportFileName(book)}.txt" + val bookPath = FileUtils.getPath(file, filename) + val bookFile = FileUtils.createFileWithReplace(bookPath) + val stringBuilder = StringBuilder() + getAllContents(book) { + bookFile.appendText(it, Charset.forName(AppConfig.exportCharset)) + stringBuilder.append(it) + } + if (AppConfig.exportToWebDav) { + val byteArray = + stringBuilder.toString().toByteArray(Charset.forName(AppConfig.exportCharset)) + BookWebDav.exportWebDav(byteArray, filename) // 导出到webdav + } + getSrcList(book).forEach { + val vFile = BookHelp.getImage(book, it.third) + if (vFile.exists()) { + FileUtils.createFileIfNotExist( + file, + "${book.name}_${book.author}", + "images", + it.first, + "${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg" + ).writeBytes(vFile.readBytes()) + } + } + } + + private fun getAllContents(book: Book, append: (text: String) -> Unit) { + val useReplace = AppConfig.exportUseReplace + val contentProcessor = ContentProcessor.get(book.name, book.origin) + append( + "${book.name}\n${ + context.getString( + R.string.author_show, + book.getRealAuthor() + ) + }\n${ + context.getString( + R.string.intro_show, + "\n" + HtmlFormatter.format(book.getDisplayIntro()) + ) + }" + ) + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + BookHelp.getContent(book, chapter).let { content -> + val content1 = contentProcessor + .getContent( + book, + chapter.title.replace("\\r?\\n".toRegex(), " "), + content ?: "null", + false, + useReplace + ) + .joinToString("\n") + append.invoke("\n\n$content1") + } + } + } + + private fun getSrcList(book: Book): ArrayList> { + val srcList = arrayListOf>() + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + BookHelp.getContent(book, chapter)?.let { content -> + content.split("\n").forEachIndexed { index, text -> + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + srcList.add(Triple(chapter.title, index, src)) + } + } + } + } + } + return srcList + } + //////////////////Start EPUB + /** + * 导出Epub + */ + fun exportEPUB(path: String, book: Book, finally: (msg: String) -> Unit) { + execute { + if (path.isContentScheme()) { + val uri = Uri.parse(path) + DocumentFile.fromTreeUri(context, uri)?.let { + exportEpub(it, book) + } + } else { + exportEpub(FileUtils.createFolderIfNotExist(path), book) + } + }.onError { + finally(it.localizedMessage ?: "ERROR") + }.onSuccess { + finally(context.getString(R.string.success)) + } + } + + @Suppress("BlockingMethodInNonBlockingContext") + private fun exportEpub(doc: DocumentFile, book: Book) { + val filename = "${getExportFileName(book)}.epub" + DocumentUtils.delete(doc, filename) + val epubBook = EpubBook() + epubBook.version = "2.0" + //set metadata + setEpubMetadata(book, epubBook) + //set cover + setCover(book, epubBook) + //set css + val contentModel = setAssets(doc, book, epubBook) + + //设置正文 + setEpubContent(contentModel, book, epubBook) + DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc -> + context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> + EpubWriter().write(epubBook, bookOs) + } + + } + } + + + private fun exportEpub(file: File, book: Book) { + val filename = "${getExportFileName(book)}.epub" + val epubBook = EpubBook() + epubBook.version = "2.0" + //set metadata + setEpubMetadata(book, epubBook) + //set cover + setCover(book, epubBook) + //set css + val contentModel = setAssets(book, epubBook) + + val bookPath = FileUtils.getPath(file, filename) + val bookFile = FileUtils.createFileWithReplace(bookPath) + //设置正文 + setEpubContent(contentModel, book, epubBook) + EpubWriter().write(epubBook, FileOutputStream(bookFile)) + } + + private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String { + if (!(AppConfig.isGooglePlay || appCtx.packageName.contains( + "debug", + true + )) + ) return setAssets(book, epubBook) + + var contentModel = "" + DocumentUtils.getDirDocument(doc, "Asset").let { customPath -> + if (customPath == null) {//使用内置模板 + contentModel = setAssets(book, epubBook) + } else {//外部模板 + customPath.listFiles().forEach { folder -> + if (folder.isDirectory && folder.name == "Text") { + folder.listFiles().sortedWith { o1, o2 -> + val name1 = o1.name ?: "" + val name2 = o2.name ?: "" + name1.cnCompare(name2) + }.forEach { file -> + if (file.isFile) { + when { + //正文模板 + file.name.equals( + "chapter.html", + true + ) || file.name.equals("chapter.xhtml", true) -> { + contentModel = file.readText(context) ?: "" + } + //封面等其他模板 + true == file.name?.endsWith("html", true) -> { + epubBook.addSection( + FileUtils.getNameExcludeExtension( + file.name ?: "Cover.html" + ), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + file.readText(context) ?: "", + "${folder.name}/${file.name}" + ) + ) + } + else -> { + //其他格式文件当做资源文件 + folder.listFiles().forEach { + if (it.isFile) + epubBook.resources.add( + Resource( + it.readBytes(context), + "${folder.name}/${it.name}" + ) + ) + } + } + } + } + } + } else if (folder.isDirectory) { + //资源文件 + folder.listFiles().forEach { + if (it.isFile) + epubBook.resources.add( + Resource( + it.readBytes(context), + "${folder.name}/${it.name}" + ) + ) + } + } else {//Asset下面的资源文件 + epubBook.resources.add( + Resource( + folder.readBytes(context), + "${folder.name}" + ) + ) + } + } + } + } + + return contentModel + } + + private fun setAssets(book: Book, epubBook: EpubBook): String { + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/fonts.css").readBytes(), + "Styles/fonts.css" + ) + ) + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/main.css").readBytes(), + "Styles/main.css" + ) + ) + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/logo.png").readBytes(), + "Images/logo.png" + ) + ) + epubBook.addSection( + context.getString(R.string.img_cover), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + String(appCtx.assets.open("epub/cover.html").readBytes()), + "Text/cover.html" + ) + ) + epubBook.addSection( + context.getString(R.string.book_intro), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + String(appCtx.assets.open("epub/intro.html").readBytes()), + "Text/intro.html" + ) + ) + return String(appCtx.assets.open("epub/chapter.html").readBytes()) + } + + private fun setCover(book: Book, epubBook: EpubBook) { + Glide.with(context) + .asBitmap() + .load(book.getDisplayCover()) + .into(object : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + val stream = ByteArrayOutputStream() + resource.compress(Bitmap.CompressFormat.JPEG, 100, stream) + val byteArray: ByteArray = stream.toByteArray() + resource.recycle() + stream.close() + epubBook.coverImage = Resource(byteArray, "Images/cover.jpg") + } + + override fun onLoadCleared(placeholder: Drawable?) { + } + }) + } + + private fun setEpubContent(contentModel: String, book: Book, epubBook: EpubBook) { + //正文 + val useReplace = AppConfig.exportUseReplace + val contentProcessor = ContentProcessor.get(book.name, book.origin) + appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> + BookHelp.getContent(book, chapter).let { content -> + var content1 = fixPic(epubBook, book, content ?: "null", chapter) + content1 = contentProcessor + .getContent(book, "", content1, false, useReplace) + .joinToString("\n") + epubBook.addSection( + chapter.title, + ResourceUtil.createChapterResource( + chapter.title.replace("\uD83D\uDD12", ""), + content1, + contentModel, + "Text/chapter_${index}.html" + ) + ) + } + } + } + + private fun fixPic( + epubBook: EpubBook, + book: Book, + content: String, + chapter: BookChapter + ): String { + val data = StringBuilder("") + content.split("\n").forEach { text -> + var text1 = text + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + val originalHref = "${MD5Utils.md5Encode16(src)}${BookHelp.getImageSuffix(src)}" + val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" + val vFile = BookHelp.getImage(book, src) + val fp = FileResourceProvider(vFile.parent) + if (vFile.exists()) { + val img = LazyResource(fp, href, originalHref) + epubBook.resources.add(img) + } + text1 = text1.replace(src, "../${href}") + } + } + data.append(text1).append("\n") + } + return data.toString() + } + + private fun setEpubMetadata(book: Book, epubBook: EpubBook) { + val metadata = Metadata() + metadata.titles.add(book.name)//书籍的名称 + metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者 + metadata.language = "zh"//数据的语言 + metadata.dates.add(Date())//数据的创建日期 + metadata.publishers.add("Legado")//数据的创建者 + metadata.descriptions.add(book.getDisplayIntro())//书籍的简介 + //metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍 + epubBook.metadata = metadata + } + + //////end of EPUB +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt index 4a898fed4..5558da7fc 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt @@ -1,22 +1,22 @@ package io.legado.app.ui.book.changecover import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.DiffUtil +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment -import io.legado.app.constant.Theme +import io.legado.app.databinding.DialogChangeCoverBinding import io.legado.app.lib.theme.primaryColor import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.dialog_change_source.* +import io.legado.app.utils.getSize + +import io.legado.app.utils.viewbindingdelegate.viewBinding class ChangeCoverDialog : BaseDialogFragment(), @@ -38,14 +38,14 @@ class ChangeCoverDialog : BaseDialogFragment(), } } + private val binding by viewBinding(DialogChangeCoverBinding::bind) private var callBack: CallBack? = null - private lateinit var viewModel: ChangeCoverViewModel + private val viewModel: ChangeCoverViewModel by viewModels() lateinit var adapter: CoverAdapter override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -55,46 +55,43 @@ class ChangeCoverDialog : BaseDialogFragment(), savedInstanceState: Bundle? ): View? { callBack = activity as? CallBack - viewModel = getViewModel(ChangeCoverViewModel::class.java) return inflater.inflate(R.layout.dialog_change_cover, container) } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.setTitle(R.string.change_cover_source) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.change_cover_source) viewModel.initData(arguments) initMenu() initView() } private fun initMenu() { - tool_bar.inflateMenu(R.menu.change_cover) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.change_cover) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) } private fun initView() { - recycler_view.layoutManager = GridLayoutManager(requireContext(), 3) + binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 3) adapter = CoverAdapter(requireContext(), this) - recycler_view.adapter = adapter + binding.recyclerView.adapter = adapter viewModel.loadDbSearchBook() } override fun observeLiveBus() { super.observeLiveBus() viewModel.searchStateData.observe(viewLifecycleOwner, { - refresh_progress_bar.isAutoLoading = it + binding.refreshProgressBar.isAutoLoading = it if (it) { stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp) } else { stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp) } - tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + binding.toolBar.menu.applyTint(requireContext()) }) viewModel.searchBooksLiveData.observe(viewLifecycleOwner, { - val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItems(it) - diffResult.dispatchUpdatesTo(adapter) }) } @@ -106,11 +103,11 @@ class ChangeCoverDialog : BaseDialogFragment(), } private val stopMenuItem: MenuItem? - get() = tool_bar.menu.findItem(R.id.menu_stop) + get() = binding.toolBar.menu.findItem(R.id.menu_stop) override fun changeTo(coverUrl: String) { callBack?.coverChangeTo(coverUrl) - dismiss() + dismissAllowingStateLoss() } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt index fc8f6adf5..b73644267 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt @@ -2,31 +2,37 @@ package io.legado.app.ui.book.changecover import android.app.Application import android.os.Bundle +import android.os.Handler +import android.os.Looper import androidx.lifecycle.MutableLiveData -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.model.webBook.WebBook -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.Executors import kotlin.math.min class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { private val threadCount = AppConfig.threadCount private var searchPool: ExecutorCoroutineDispatcher? = null + val handler = Handler(Looper.getMainLooper()) var name: String = "" var author: String = "" private var tasks = CompositeCoroutine() private var bookSourceList = arrayListOf() val searchStateData = MutableLiveData() val searchBooksLiveData = MutableLiveData>() - private val searchBooks = ArrayList() + private val searchBooks = CopyOnWriteArraySet() + private val sendRunnable = Runnable { upAdapter() } + private var postTime = 0L @Volatile private var searchIndex = -1 @@ -43,28 +49,39 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application } private fun initSearchPool() { - searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() searchIndex = -1 } fun loadDbSearchBook() { execute { - App.db.searchBookDao().getEnableHasCover(name, author).let { + appDb.searchBookDao.getEnableHasCover(name, author).let { searchBooks.addAll(it) + searchBooksLiveData.postValue(searchBooks.toList()) if (it.size <= 1) { - searchBooksLiveData.postValue(searchBooks) startSearch() - } else { - searchBooksLiveData.postValue(searchBooks) } } } } + @Synchronized + private fun upAdapter() { + if (System.currentTimeMillis() >= postTime + 500) { + handler.removeCallbacks(sendRunnable) + postTime = System.currentTimeMillis() + val books = searchBooks.toList() + searchBooksLiveData.postValue(books.sortedBy { it.originOrder }) + } else { + handler.removeCallbacks(sendRunnable) + handler.postDelayed(sendRunnable, 500) + } + } + private fun startSearch() { execute { bookSourceList.clear() - bookSourceList.addAll(App.db.bookSourceDao().allEnabled) + bookSourceList.addAll(appDb.bookSourceDao.allEnabled) searchStateData.postValue(true) initSearchPool() for (i in 0 until threadCount) { @@ -73,46 +90,53 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application } } + @Synchronized private fun search() { - synchronized(this) { - if (searchIndex >= bookSourceList.lastIndex) { - return - } - searchIndex++ - val source = bookSourceList[searchIndex] - val variableBook = SearchBook(origin = source.bookSourceUrl) - val task = WebBook(source) - .searchBook(name, scope = this, context = searchPool!!, variableBook = variableBook) - .timeout(60000L) - .onSuccess(Dispatchers.IO) { - if (it.isNotEmpty()) { - val searchBook = it[0] - if (searchBook.name == name && searchBook.author == author - && !searchBook.coverUrl.isNullOrEmpty() - ) { - App.db.searchBookDao().insert(searchBook) - if (!searchBooks.contains(searchBook)) { - searchBooks.add(searchBook) - searchBooksLiveData.postValue(searchBooks) - } - } - } - } - .onFinally { - synchronized(this) { - if (searchIndex < bookSourceList.lastIndex) { - search() - } else { - searchIndex++ - } - if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, - threadCount) - ) { - searchStateData.postValue(false) + if (searchIndex >= bookSourceList.lastIndex) { + return + } + searchIndex++ + val source = bookSourceList[searchIndex] + if (source.getSearchRule().coverUrl.isNullOrBlank()) { + searchNext() + return + } + val task = WebBook(source) + .searchBook(viewModelScope, name, context = searchPool!!) + .timeout(60000L) + .onSuccess(searchPool) { + if (it.isNotEmpty()) { + val searchBook = it[0] + if (searchBook.name == name && searchBook.author == author + && !searchBook.coverUrl.isNullOrEmpty() + ) { + appDb.searchBookDao.insert(searchBook) + if (!searchBooks.contains(searchBook)) { + searchBooks.add(searchBook) + upAdapter() } } } - tasks.add(task) + } + .onFinally(searchPool) { + searchNext() + } + tasks.add(task) + } + + @Synchronized + private fun searchNext() { + if (searchIndex < bookSourceList.lastIndex) { + search() + } else { + searchIndex++ + } + if (searchIndex >= bookSourceList.lastIndex + min( + bookSourceList.size, + threadCount + ) + ) { + searchStateData.postValue(false) } } @@ -121,6 +145,7 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application startSearch() } else { tasks.clear() + searchStateData.postValue(false) } } diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt index 55a841fcd..323dbfc05 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt @@ -1,26 +1,47 @@ package io.legado.app.ui.book.changecover import android.content.Context -import io.legado.app.R +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchBook -import kotlinx.android.synthetic.main.item_cover.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.databinding.ItemCoverBinding + class CoverAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_cover) { + DiffRecyclerAdapter(context) { + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.originName == newItem.originName + && oldItem.coverUrl == newItem.coverUrl + } - override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { - with(holder.itemView) { - iv_cover.load(item.coverUrl, item.name, item.author) - tv_source.text = item.originName } + + override fun getViewBinding(parent: ViewGroup): ItemCoverBinding { + return ItemCoverBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemCoverBinding, + item: SearchBook, + payloads: MutableList + ) = binding.run { + ivCover.load(item.coverUrl, item.name, item.author) + tvSource.text = item.originName } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemCoverBinding) { holder.itemView.apply { - onClick { + setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.changeTo(it.coverUrl ?: "") } diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt index 12e63a907..d34f7a441 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt @@ -3,52 +3,79 @@ package io.legado.app.ui.book.changesource import android.content.Context import android.os.Bundle import android.view.View +import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.DiffUtil import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemChangeSourceBinding import io.legado.app.utils.invisible import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_change_source.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick +import splitties.views.onLongClick -class ChangeSourceAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_change_source) { +class ChangeSourceAdapter( + context: Context, + val viewModel: ChangeSourceViewModel, + val callBack: CallBack +) : + DiffRecyclerAdapter(context) { - override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.originName == newItem.originName + && oldItem.getDisplayLastChapterTitle() == newItem.getDisplayLastChapterTitle() + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemChangeSourceBinding { + return ItemChangeSourceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChangeSourceBinding, + item: SearchBook, + payloads: MutableList + ) { val bundle = payloads.getOrNull(0) as? Bundle - holder.itemView.apply { + binding.apply { if (bundle == null) { - tv_origin.text = item.originName - tv_last.text = item.getDisplayLastChapterTitle() + tvOrigin.text = item.originName + tvAuthor.text = item.author + tvLast.text = item.getDisplayLastChapterTitle() if (callBack.bookUrl == item.bookUrl) { - iv_checked.visible() + ivChecked.visible() } else { - iv_checked.invisible() + ivChecked.invisible() } } else { bundle.keySet().map { when (it) { - "name" -> tv_origin.text = item.originName - "latest" -> tv_last.text = item.getDisplayLastChapterTitle() + "name" -> tvOrigin.text = item.originName + "latest" -> tvLast.text = item.getDisplayLastChapterTitle() } } } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.changeTo(it) } } holder.itemView.onLongClick { showMenu(holder.itemView, getItem(holder.layoutPosition)) - true } } @@ -61,6 +88,16 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) : R.id.menu_disable_book_source -> { callBack.disableSource(searchBook) } + R.id.menu_top_source -> { + callBack.topSource(searchBook) + } + R.id.menu_bottom_source -> { + callBack.bottomSource(searchBook) + } + R.id.menu_delete_book_source -> { + callBack.deleteSource(searchBook) + updateItems(0, itemCount, listOf()) + } } true } @@ -71,5 +108,8 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) : val bookUrl: String? fun changeTo(searchBook: SearchBook) fun disableSource(searchBook: SearchBook) + fun topSource(searchBook: SearchBook) + fun bottomSource(searchBook: SearchBook) + fun deleteSource(searchBook: SearchBook) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt index bbd627466..acd00f18d 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt @@ -1,29 +1,29 @@ package io.legado.app.ui.book.changesource import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.DiffUtil +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.DialogChangeSourceBinding +import io.legado.app.help.AppConfig import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.applyTint -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getViewModel -import io.legado.app.utils.putPrefBoolean -import kotlinx.android.synthetic.main.dialog_change_source.* +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch class ChangeSourceDialog : BaseDialogFragment(), @@ -45,14 +45,15 @@ class ChangeSourceDialog : BaseDialogFragment(), } } + private val binding by viewBinding(DialogChangeSourceBinding::bind) + private val groups = linkedSetOf() private var callBack: CallBack? = null - private lateinit var viewModel: ChangeSourceViewModel + private val viewModel: ChangeSourceViewModel by viewModels() lateinit var adapter: ChangeSourceAdapter override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -62,12 +63,11 @@ class ChangeSourceDialog : BaseDialogFragment(), savedInstanceState: Bundle? ): View? { callBack = activity as? CallBack - viewModel = getViewModel(ChangeSourceViewModel::class.java) return inflater.inflate(R.layout.dialog_change_source, container) } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) + binding.toolBar.setBackgroundColor(primaryColor) viewModel.initData(arguments) showTitle() initMenu() @@ -78,47 +78,51 @@ class ChangeSourceDialog : BaseDialogFragment(), } private fun showTitle() { - tool_bar.title = viewModel.name - tool_bar.subtitle = getString(R.string.author_show, viewModel.author) + binding.toolBar.title = viewModel.name + binding.toolBar.subtitle = viewModel.author } private fun initMenu() { - tool_bar.inflateMenu(R.menu.change_source) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) - tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked = - getPrefBoolean(PreferKey.changeSourceLoadToc) + binding.toolBar.inflateMenu(R.menu.change_source) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.menu.findItem(R.id.menu_check_author) + ?.isChecked = AppConfig.changeSourceCheckAuthor + binding.toolBar.menu.findItem(R.id.menu_load_info) + ?.isChecked = AppConfig.changeSourceLoadInfo + binding.toolBar.menu.findItem(R.id.menu_load_toc) + ?.isChecked = AppConfig.changeSourceLoadToc } private fun initRecyclerView() { - adapter = ChangeSourceAdapter(requireContext(), this) - recycler_view.layoutManager = LinearLayoutManager(context) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter + adapter = ChangeSourceAdapter(requireContext(), viewModel, this) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { if (positionStart == 0) { - recycler_view.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) } } override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { if (toPosition == 0) { - recycler_view.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) } } }) } private fun initSearchView() { - val searchView = tool_bar.menu.findItem(R.id.menu_screen).actionView as SearchView + val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView searchView.setOnCloseListener { showTitle() false } searchView.setOnSearchClickListener { - tool_bar.title = "" - tool_bar.subtitle = "" + binding.toolBar.title = "" + binding.toolBar.subtitle = "" } searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -135,40 +139,67 @@ class ChangeSourceDialog : BaseDialogFragment(), private fun initLiveData() { viewModel.searchStateData.observe(viewLifecycleOwner, { - refresh_progress_bar.isAutoLoading = it + binding.refreshProgressBar.isAutoLoading = it if (it) { stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp) } else { stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp) } - tool_bar.menu.applyTint(requireContext()) + binding.toolBar.menu.applyTint(requireContext()) }) viewModel.searchBooksLiveData.observe(viewLifecycleOwner, { - val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) adapter.setItems(it) - diffResult.dispatchUpdatesTo(adapter) }) + launch { + appDb.bookSourceDao.flowGroupEnabled().collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupMenu() + } + } } private val stopMenuItem: MenuItem? - get() = tool_bar.menu.findItem(R.id.menu_stop) + get() = binding.toolBar.menu.findItem(R.id.menu_stop) override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { + R.id.menu_check_author -> { + AppConfig.changeSourceCheckAuthor = !item.isChecked + item.isChecked = !item.isChecked + viewModel.loadDbSearchBook() + } R.id.menu_load_toc -> { putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked) item.isChecked = !item.isChecked } + R.id.menu_load_info -> { + putPrefBoolean(PreferKey.changeSourceLoadInfo, !item.isChecked) + item.isChecked = !item.isChecked + } R.id.menu_stop -> viewModel.stopSearch() + R.id.menu_source_manage -> startActivity() + else -> if (item?.groupId == R.id.source_group) { + if (!item.isChecked) { + item.isChecked = true + if (item.title.toString() == getString(R.string.all_source)) { + putPrefString("searchGroup", "") + } else { + putPrefString("searchGroup", item.title.toString()) + } + viewModel.stopSearch() + viewModel.loadDbSearchBook() + } + } } return false } override fun changeTo(searchBook: SearchBook) { - val book = searchBook.toBook() - book.upInfoFromOld(callBack?.oldBook) - callBack?.changeTo(book) - dismiss() + changeSource(searchBook) + dismissAllowingStateLoss() } override val bookUrl: String? @@ -178,6 +209,56 @@ class ChangeSourceDialog : BaseDialogFragment(), viewModel.disableSource(searchBook) } + override fun topSource(searchBook: SearchBook) { + viewModel.topSource(searchBook) + } + + override fun bottomSource(searchBook: SearchBook) { + viewModel.bottomSource(searchBook) + } + + override fun deleteSource(searchBook: SearchBook) { + if (bookUrl == searchBook.bookUrl) { + viewModel.firstSourceOrNull(searchBook)?.let { + changeSource(it) + } + } + viewModel.del(searchBook) + } + + private fun changeSource(searchBook: SearchBook) { + val book = searchBook.toBook() + book.upInfoFromOld(callBack?.oldBook) + callBack?.changeTo(book) + searchBook.time = System.currentTimeMillis() + viewModel.updateSource(searchBook) + } + + /** + * 更新分组菜单 + */ + private fun upGroupMenu() { + val menu: Menu = binding.toolBar.menu + val selectedGroup = getPrefString("searchGroup") + menu.removeGroup(R.id.source_group) + val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) + var hasSelectedGroup = false + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.forEach { group -> + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let { + if (group == selectedGroup) { + it.isChecked = true + hasSelectedGroup = true + } + } + } + menu.setGroupCheckable(R.id.source_group, true, true) + if (!hasSelectedGroup) { + allItem.isChecked = true + } + } + interface CallBack { val oldBook: Book? fun changeTo(book: Book) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt index ec423c3dd..9c998e1a8 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt @@ -3,12 +3,13 @@ package io.legado.app.ui.book.changesource import android.app.Application import android.os.Bundle import android.os.Handler +import android.os.Looper import androidx.lifecycle.MutableLiveData -import io.legado.app.App -import io.legado.app.R +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook @@ -16,10 +17,11 @@ import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.model.webBook.WebBook import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher -import org.jetbrains.anko.debug +import splitties.init.appCtx import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.Executors import kotlin.math.min @@ -27,7 +29,7 @@ import kotlin.math.min class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { private val threadCount = AppConfig.threadCount private var searchPool: ExecutorCoroutineDispatcher? = null - val handler = Handler() + val handler = Handler(Looper.getMainLooper()) val searchStateData = MutableLiveData() val searchBooksLiveData = MutableLiveData>() var name: String = "" @@ -38,6 +40,8 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio private val searchBooks = CopyOnWriteArraySet() private var postTime = 0L private val sendRunnable = Runnable { upAdapter() } + private val searchGroup get() = appCtx.getPrefString("searchGroup") ?: "" + @Volatile private var searchIndex = -1 @@ -53,20 +57,23 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } private fun initSearchPool() { - searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + searchPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() searchIndex = -1 } fun loadDbSearchBook() { execute { - App.db.searchBookDao().getByNameAuthorEnable(name, author).let { - searchBooks.addAll(it) - if (it.size <= 1) { - upAdapter() - startSearch() - } else { - upAdapter() - } + searchBooks.clear() + upAdapter() + val sbs = if (AppConfig.changeSourceCheckAuthor) { + appDb.searchBookDao.getChangeSourceSearch(name, author, searchGroup) + } else { + appDb.searchBookDao.getChangeSourceSearch(name, "", searchGroup) + } + searchBooks.addAll(sbs) + searchBooksLiveData.postValue(searchBooks.toList()) + if (sbs.size <= 1) { + startSearch() } } } @@ -80,12 +87,13 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio searchBooksLiveData.postValue(books.sortedBy { it.originOrder }) } else { handler.removeCallbacks(sendRunnable) - handler.postDelayed(sendRunnable, 500 - System.currentTimeMillis() + postTime) + handler.postDelayed(sendRunnable, 500) } } private fun searchFinish(searchBook: SearchBook) { - App.db.searchBookDao().insert(searchBook) + if (searchBooks.contains(searchBook)) return + appDb.searchBookDao.insert(searchBook) if (screenKey.isEmpty()) { searchBooks.add(searchBook) } else if (searchBook.name.contains(screenKey)) { @@ -96,8 +104,15 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio private fun startSearch() { execute { + appDb.searchBookDao.clear(name, author) + searchBooks.clear() + upAdapter() bookSourceList.clear() - bookSourceList.addAll(App.db.bookSourceDao().allEnabled) + if (searchGroup.isBlank()) { + bookSourceList.addAll(appDb.bookSourceDao.allEnabled) + } else { + bookSourceList.addAll(appDb.bookSourceDao.getEnabledByGroup(searchGroup)) + } searchStateData.postValue(true) initSearchPool() for (i in 0 until threadCount) { @@ -112,73 +127,75 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio return } searchIndex++ - val source = bookSourceList[searchIndex] - val variableBook = SearchBook() - val task = WebBook(source) - .searchBook(name, variableBook = variableBook, scope = this, context = searchPool!!) - .timeout(60000L) - .onSuccess(IO) { - it.forEach { searchBook -> - if (searchBook.name == name && searchBook.author == author) { - if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { - if (searchBook.tocUrl.isEmpty()) { - loadBookInfo(searchBook.toBook()) + } + val source = bookSourceList[searchIndex] + val webBook = WebBook(source) + val task = webBook + .searchBook(viewModelScope, name, context = searchPool!!) + .timeout(60000L) + .onSuccess(searchPool) { + it.forEach { searchBook -> + if (searchBook.name == name) { + if ((AppConfig.changeSourceCheckAuthor && searchBook.author.contains(author)) + || !AppConfig.changeSourceCheckAuthor + ) { + if (searchBook.latestChapterTitle.isNullOrEmpty()) { + if (AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc) { + loadBookInfo(webBook, searchBook.toBook()) } else { - loadChapter(searchBook.toBook()) + searchFinish(searchBook) } } else { searchFinish(searchBook) } - return@forEach } } } - .onFinally { - synchronized(this) { - if (searchIndex < bookSourceList.lastIndex) { - search() - } else { - searchIndex++ - } - if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size, - threadCount) - ) { - searchStateData.postValue(false) - } + } + .onFinally(searchPool) { + synchronized(this) { + if (searchIndex < bookSourceList.lastIndex) { + search() + } else { + searchIndex++ + } + if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size + || searchIndex >= bookSourceList.lastIndex + threadCount + ) { + searchStateData.postValue(false) } } - tasks.add(task) - } + } + tasks.add(task) } - private fun loadBookInfo(book: Book) { - execute { - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getBookInfo(book, this) - .onSuccess { - loadChapter(it) - }.onError { - debug { context.getString(R.string.error_get_book_info) } - } - } ?: debug { context.getString(R.string.error_no_source) } - } + private fun loadBookInfo(webBook: WebBook, book: Book) { + webBook.getBookInfo(viewModelScope, book) + .onSuccess { + if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { + loadBookToc(webBook, book) + } else { + //从详情页里获取最新章节 + book.latestChapterTitle = it.latestChapterTitle + val searchBook = book.toSearchBook() + searchFinish(searchBook) + } + }.onError { + it.printStackTrace() + } } - private fun loadChapter(book: Book) { - execute { - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getChapterList(book, this) - .onSuccess(IO) { chapters -> - if (chapters.isNotEmpty()) { - book.latestChapterTitle = chapters.last().title - val searchBook: SearchBook = book.toSearchBook() - searchFinish(searchBook) - } - }.onError { - debug { context.getString(R.string.error_get_chapter_list) } - } - } ?: debug { R.string.error_no_source } - } + private fun loadBookToc(webBook: WebBook, book: Book) { + webBook.getChapterList(viewModelScope, book) + .onSuccess(IO) { chapters -> + if (chapters.isNotEmpty()) { + book.latestChapterTitle = chapters.last().title + val searchBook: SearchBook = book.toSearchBook() + searchFinish(searchBook) + } + }.onError { + it.printStackTrace() + } } /** @@ -190,7 +207,8 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio if (key.isNullOrEmpty()) { loadDbSearchBook() } else { - val items = App.db.searchBookDao().getChangeSourceSearch(name, author, screenKey) + val items = + appDb.searchBookDao.getChangeSourceSearch(name, author, screenKey, searchGroup) searchBooks.clear() searchBooks.addAll(items) upAdapter() @@ -215,13 +233,58 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio fun disableSource(searchBook: SearchBook) { execute { - App.db.bookSourceDao().getBookSource(searchBook.origin)?.let { source -> + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> source.enabled = false - App.db.bookSourceDao().update(source) + appDb.bookSourceDao.update(source) } searchBooks.remove(searchBook) upAdapter() } } + fun topSource(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + val minOrder = appDb.bookSourceDao.minOrder - 1 + source.customOrder = minOrder + searchBook.originOrder = source.customOrder + appDb.bookSourceDao.update(source) + updateSource(searchBook) + } + upAdapter() + } + } + + fun bottomSource(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + val maxOrder = appDb.bookSourceDao.maxOrder + 1 + source.customOrder = maxOrder + searchBook.originOrder = source.customOrder + appDb.bookSourceDao.update(source) + updateSource(searchBook) + } + upAdapter() + } + } + + fun updateSource(searchBook: SearchBook) { + appDb.searchBookDao.update(searchBook) + } + + fun del(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + appDb.bookSourceDao.delete(source) + appDb.searchBookDao.delete(searchBook) + } + } + searchBooks.remove(searchBook) + upAdapter() + } + + fun firstSourceOrNull(searchBook: SearchBook): SearchBook? { + return searchBooks.firstOrNull { it.bookUrl != searchBook.bookUrl } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkAdapter.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkAdapter.kt deleted file mode 100644 index e9c7384ea..000000000 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkAdapter.kt +++ /dev/null @@ -1,62 +0,0 @@ -package io.legado.app.ui.book.chapterlist - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.paging.PagedListAdapter -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Bookmark -import kotlinx.android.synthetic.main.item_bookmark.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - - -class BookmarkAdapter(val callback: Callback) : PagedListAdapter(DIFF_CALLBACK) { - - companion object { - - @JvmField - val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean = - oldItem.time == newItem.time - - override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean = - oldItem.time == newItem.time - && oldItem.bookUrl == newItem.bookUrl - && oldItem.chapterName == newItem.chapterName - && oldItem.content == newItem.content - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_bookmark, parent, false)) - } - - override fun onBindViewHolder(holder: MyViewHolder, position: Int) { - getItem(position)?.let { - holder.bind(it, callback) - } - } - - class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - fun bind(bookmark: Bookmark, callback: Callback?) = with(itemView) { - tv_chapter_name.text = bookmark.chapterName - tv_content.text = bookmark.content - itemView.onClick { - callback?.onClick(bookmark) - } - itemView.onLongClick { - callback?.onLongClick(bookmark) - true - } - } - } - - interface Callback { - fun onClick(bookmark: Bookmark) - fun onLongClick(bookmark: Bookmark) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt deleted file mode 100644 index b24d438e8..000000000 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.legado.app.ui.book.chapterlist - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.os.AsyncTask -import android.os.Bundle -import android.view.View -import android.widget.EditText -import androidx.lifecycle.LiveData -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseFragment -import io.legado.app.data.entities.Bookmark -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton -import io.legado.app.lib.theme.ATH -import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.requestInputMethod -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.fragment_bookmark.* - - -class BookmarkFragment : VMBaseFragment(R.layout.fragment_bookmark), - BookmarkAdapter.Callback, - ChapterListViewModel.BookmarkCallBack { - override val viewModel: ChapterListViewModel - get() = getViewModelOfActivity(ChapterListViewModel::class.java) - - private lateinit var adapter: BookmarkAdapter - private var bookmarkLiveData: LiveData>? = null - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - viewModel.bookMarkCallBack = this - initRecyclerView() - initData() - } - - private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) - adapter = BookmarkAdapter(this) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - } - - private fun initData() { - viewModel.book?.let { book -> - bookmarkLiveData?.removeObservers(viewLifecycleOwner) - bookmarkLiveData = - LivePagedListBuilder( - App.db.bookmarkDao().observeByBook(book.bookUrl, book.name, book.author), 20 - ).build() - bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) }) - } - } - - override fun startBookmarkSearch(newText: String?) { - if (newText.isNullOrBlank()) { - initData() - } else { - bookmarkLiveData?.removeObservers(viewLifecycleOwner) - bookmarkLiveData = LivePagedListBuilder( - App.db.bookmarkDao().liveDataSearch( - viewModel.bookUrl, - newText - ), 20 - ).build() - bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) }) - } - } - - - override fun onClick(bookmark: Bookmark) { - val bookmarkData = Intent() - bookmarkData.putExtra("index", bookmark.chapterIndex) - bookmarkData.putExtra("pageIndex", bookmark.pageIndex) - activity?.setResult(Activity.RESULT_OK, bookmarkData) - activity?.finish() - } - - @SuppressLint("InflateParams") - override fun onLongClick(bookmark: Bookmark) { - viewModel.book?.let { book -> - requireContext().alert(R.string.bookmark) { - var editText: EditText? = null - message = book.name + " • " + bookmark.chapterName - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - setHint(R.string.note_content) - setText(bookmark.content) - } - } - } - yesButton { - editText?.text?.toString()?.let { editContent -> - AsyncTask.execute { - bookmark.content = editContent - App.db.bookmarkDao().update(bookmark) - } - } - } - noButton() - neutralButton(R.string.delete) { - App.db.bookmarkDao().delete(bookmark) - } - }.show().applyTint().requestInputMethod() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt deleted file mode 100644 index 5d4b3621b..000000000 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.legado.app.ui.book.chapterlist - -import android.content.Context -import android.view.View -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.data.entities.BookChapter -import io.legado.app.help.BookHelp -import io.legado.app.lib.theme.accentColor -import io.legado.app.utils.getCompatColor -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_bookmark.view.tv_chapter_name -import kotlinx.android.synthetic.main.item_chapter_list.view.* -import org.jetbrains.anko.sdk27.listeners.onClick - -class ChapterListAdapter(context: Context, val callback: Callback) : - SimpleRecyclerAdapter(context, R.layout.item_chapter_list) { - - val cacheFileNames = hashSetOf() - - override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList) { - with(holder.itemView) { - val isDur = callback.durChapterIndex() == item.index - val cached = callback.isLocalBook - || cacheFileNames.contains(BookHelp.formatChapterName(item)) - if (payloads.isEmpty()) { - if (isDur) { - tv_chapter_name.setTextColor(context.accentColor) - } else { - tv_chapter_name.setTextColor(context.getCompatColor(R.color.primaryText)) - } - tv_chapter_name.text = item.title - if (!item.tag.isNullOrEmpty()) { - tv_tag.text = item.tag - tv_tag.visible() - } - upHasCache(this, isDur, cached) - } else { - upHasCache(this, isDur, cached) - } - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { - getItem(holder.layoutPosition)?.let { - callback.openChapter(it) - } - } - } - - private fun upHasCache(itemView: View, isDur: Boolean, cached: Boolean) = itemView.apply { - tv_chapter_name.paint.isFakeBoldText = cached - iv_checked.setImageResource(R.drawable.ic_outline_cloud_24) - iv_checked.visible(!cached) - if (isDur) { - iv_checked.setImageResource(R.drawable.ic_check) - iv_checked.visible() - } - } - - interface Callback { - val isLocalBook: Boolean - fun openChapter(bookChapter: BookChapter) - fun durChapterIndex(): Int - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt deleted file mode 100644 index d4339bb23..000000000 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -package io.legado.app.ui.book.chapterlist - -import android.annotation.SuppressLint -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.lifecycle.LiveData -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseFragment -import io.legado.app.constant.EventBus -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.help.BookHelp -import io.legado.app.lib.theme.bottomBackground -import io.legado.app.lib.theme.getPrimaryTextColor -import io.legado.app.ui.widget.recycler.UpLinearLayoutManager -import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.ColorUtils -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.observeEvent -import kotlinx.android.synthetic.main.fragment_chapter_list.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jetbrains.anko.sdk27.listeners.onClick - -class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapter_list), - ChapterListAdapter.Callback, - ChapterListViewModel.ChapterListCallBack{ - override val viewModel: ChapterListViewModel - get() = getViewModelOfActivity(ChapterListViewModel::class.java) - - lateinit var adapter: ChapterListAdapter - private var durChapterIndex = 0 - private lateinit var mLayoutManager: UpLinearLayoutManager - private var tocLiveData: LiveData>? = null - private var scrollToDurChapter = false - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - viewModel.chapterCallBack = this - val bbg = bottomBackground - val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg)) - ll_chapter_base_info.setBackgroundColor(bbg) - tv_current_chapter_info.setTextColor(btc) - iv_chapter_top.setColorFilter(btc) - iv_chapter_bottom.setColorFilter(btc) - initRecyclerView() - initView() - initBook() - } - - private fun initRecyclerView() { - adapter = ChapterListAdapter(requireContext(), this) - mLayoutManager = UpLinearLayoutManager(requireContext()) - recycler_view.layoutManager = mLayoutManager - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - } - - private fun initView() { - iv_chapter_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } - iv_chapter_bottom.onClick { - if (adapter.itemCount > 0) { - mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) - } - } - tv_current_chapter_info.onClick { - mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) - } - } - - @SuppressLint("SetTextI18n") - private fun initBook() { - launch { - initDoc() - viewModel.book?.let { - durChapterIndex = it.durChapterIndex - tv_current_chapter_info.text = - "${it.durChapterTitle}(${it.durChapterIndex + 1}/${it.totalChapterNum})" - initCacheFileNames(it) - } - } - } - - private fun initDoc() { - tocLiveData?.removeObservers(this@ChapterListFragment) - tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl) - tocLiveData?.observe(viewLifecycleOwner, { - adapter.setItems(it) - if (!scrollToDurChapter) { - mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) - scrollToDurChapter = true - } - }) - } - - private fun initCacheFileNames(book: Book) { - launch(IO) { - adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) - withContext(Main) { - adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) - } - } - } - - override fun observeLiveBus() { - observeEvent(EventBus.SAVE_CONTENT) { chapter -> - viewModel.book?.bookUrl?.let { bookUrl -> - if (chapter.bookUrl == bookUrl) { - adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) - adapter.notifyItemChanged(chapter.index, true) - } - } - } - } - - override fun startChapterListSearch(newText: String?) { - if (newText.isNullOrBlank()) { - initDoc() - } else { - tocLiveData?.removeObservers(this) - tocLiveData = App.db.bookChapterDao().liveDataSearch(viewModel.bookUrl, newText) - tocLiveData?.observe(viewLifecycleOwner, { - adapter.setItems(it) - }) - } - } - - override val isLocalBook: Boolean - get() = viewModel.book?.isLocalBook() == true - - override fun durChapterIndex(): Int { - return durChapterIndex - } - - override fun openChapter(bookChapter: BookChapter) { - activity?.setResult(RESULT_OK, Intent().putExtra("index", bookChapter.index)) - activity?.finish() - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListViewModel.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListViewModel.kt deleted file mode 100644 index 7671facc9..000000000 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.legado.app.ui.book.chapterlist - - -import android.app.Application -import io.legado.app.App -import io.legado.app.base.BaseViewModel -import io.legado.app.data.entities.Book - -class ChapterListViewModel(application: Application) : BaseViewModel(application) { - var bookUrl: String = "" - var book: Book? = null - var chapterCallBack: ChapterListCallBack? = null - var bookMarkCallBack: BookmarkCallBack? = null - - fun initBook(bookUrl: String, success: () -> Unit) { - this.bookUrl = bookUrl - execute { - book = App.db.bookDao().getBook(bookUrl) - }.onSuccess { - success.invoke() - } - } - - fun startChapterListSearch(newText: String?) { - chapterCallBack?.startChapterListSearch(newText) - } - - fun startBookmarkSearch(newText: String?) { - bookMarkCallBack?.startBookmarkSearch(newText) - } - - interface ChapterListCallBack { - fun startChapterListSearch(newText: String?) - } - - interface BookmarkCallBack { - fun startBookmarkSearch(newText: String?) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt b/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt deleted file mode 100644 index a95ee3b54..000000000 --- a/app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt +++ /dev/null @@ -1,238 +0,0 @@ -package io.legado.app.ui.book.download - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import androidx.lifecycle.LiveData -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.AppConst -import io.legado.app.constant.EventBus -import io.legado.app.constant.PreferKey -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.BookGroup -import io.legado.app.help.BookHelp -import io.legado.app.service.help.Download -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker -import io.legado.app.ui.widget.dialog.TextListDialog -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_download.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArraySet - - -class DownloadActivity : VMBaseActivity(R.layout.activity_download), - FileChooserDialog.CallBack, - DownloadAdapter.CallBack { - private val exportRequestCode = 32 - private val exportBookPathKey = "exportBookPath" - lateinit var adapter: DownloadAdapter - private var groupLiveData: LiveData>? = null - private var booksLiveData: LiveData>? = null - private var menu: Menu? = null - private var exportPosition = -1 - private val groupList: ArrayList = arrayListOf() - private var groupId: Int = -1 - - override val viewModel: DownloadViewModel - get() = getViewModel(DownloadViewModel::class.java) - - override fun onActivityCreated(savedInstanceState: Bundle?) { - groupId = intent.getIntExtra("groupId", -1) - title_bar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all) - initRecyclerView() - initGroupData() - initBookData() - } - - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.download, menu) - return super.onCompatCreateOptionsMenu(menu) - } - - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - this.menu = menu - upMenu() - return super.onPrepareOptionsMenu(menu) - } - - private fun upMenu() { - menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> - subMenu.removeGroup(R.id.menu_group) - groupList.forEach { bookGroup -> - subMenu.add(R.id.menu_group, bookGroup.groupId, Menu.NONE, bookGroup.groupName) - } - } - } - - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_download -> launch(IO) { - if (adapter.downloadMap.isNullOrEmpty()) { - adapter.getItems().forEach { book -> - Download.start( - this@DownloadActivity, - book.bookUrl, - book.durChapterIndex, - book.totalChapterNum - ) - } - } else { - Download.stop(this@DownloadActivity) - } - } - R.id.menu_log -> { - TextListDialog.show(supportFragmentManager, getString(R.string.log), Download.logs) - } - R.id.menu_no_group -> { - title_bar.subtitle = getString(R.string.no_group) - groupId = AppConst.bookGroupNone.groupId - initBookData() - } - R.id.menu_all -> { - title_bar.subtitle = item.title - groupId = AppConst.bookGroupAll.groupId - initBookData() - } - else -> if (item.groupId == R.id.menu_group) { - title_bar.subtitle = item.title - groupId = item.itemId - initBookData() - } - } - return super.onCompatOptionsItemSelected(item) - } - - private fun initRecyclerView() { - recycler_view.layoutManager = LinearLayoutManager(this) - adapter = DownloadAdapter(this, this) - recycler_view.adapter = adapter - } - - private fun initBookData() { - booksLiveData?.removeObservers(this) - booksLiveData = when (groupId) { - AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll() - AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() - else -> App.db.bookDao().observeByGroup(groupId) - } - booksLiveData?.observe(this, { list -> - val booksDownload = list.filter { - it.isOnLineTxt() - } - val books = when (getPrefInt(PreferKey.bookshelfSort)) { - 1 -> booksDownload.sortedByDescending { it.latestChapterTime } - 2 -> booksDownload.sortedBy { it.name } - 3 -> booksDownload.sortedBy { it.order } - else -> booksDownload.sortedByDescending { it.durChapterTime } - } - adapter.setItems(books) - initCacheSize(books) - }) - } - - private fun initGroupData() { - groupLiveData?.removeObservers(this) - groupLiveData = App.db.bookGroupDao().liveDataAll() - groupLiveData?.observe(this, { - groupList.clear() - groupList.addAll(it) - adapter.notifyDataSetChanged() - upMenu() - }) - } - - private fun initCacheSize(books: List) { - launch(IO) { - books.forEach { book -> - val chapterCaches = hashSetOf() - val cacheNames = BookHelp.getChapterFiles(book) - App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> - if (cacheNames.contains(BookHelp.formatChapterName(chapter))) { - chapterCaches.add(chapter.url) - } - } - adapter.cacheChapters[book.bookUrl] = chapterCaches - withContext(Dispatchers.Main) { - adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) - } - } - } - } - - override fun observeLiveBus() { - observeEvent>>(EventBus.UP_DOWNLOAD) { - if (it.isEmpty()) { - menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_play_24dp) - menu?.applyTint(this) - } else { - menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp) - menu?.applyTint(this) - } - adapter.downloadMap = it - adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) - } - observeEvent(EventBus.SAVE_CONTENT) { - adapter.cacheChapters[it.bookUrl]?.add(it.url) - } - } - - override fun export(position: Int) { - exportPosition = position - val default = arrayListOf() - val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey) - if (!path.isNullOrEmpty()) { - default.add(path) - } - FilePicker.selectFolder(this, exportRequestCode, otherActions = default) { - startExport(it) - } - } - - private fun startExport(path: String) { - adapter.getItem(exportPosition)?.let { book -> - Snackbar.make(title_bar, R.string.exporting, Snackbar.LENGTH_INDEFINITE) - .show() - viewModel.export(path, book) { - title_bar.snackbar(it) - } - } - } - - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - exportRequestCode -> { - ACache.get(this@DownloadActivity).put(exportBookPathKey, currentPath) - startExport(currentPath) - } - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - exportRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - ACache.get(this@DownloadActivity).put(exportBookPathKey, uri.toString()) - startExport(uri.toString()) - } - } - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/download/DownloadViewModel.kt b/app/src/main/java/io/legado/app/ui/book/download/DownloadViewModel.kt deleted file mode 100644 index c4a3ea97f..000000000 --- a/app/src/main/java/io/legado/app/ui/book/download/DownloadViewModel.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.legado.app.ui.book.download - -import android.app.Application -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.BaseViewModel -import io.legado.app.constant.AppPattern -import io.legado.app.data.entities.Book -import io.legado.app.help.BookHelp -import io.legado.app.utils.* -import java.io.File - - -class DownloadViewModel(application: Application) : BaseViewModel(application) { - - - fun export(path: String, book: Book, finally: (msg: String) -> Unit) { - execute { - if (path.isContentPath()) { - val uri = Uri.parse(path) - DocumentFile.fromTreeUri(context, uri)?.let { - export(it, book) - } - } else { - export(FileUtils.createFolderIfNotExist(path), book) - } - }.onError { - finally(it.localizedMessage ?: "ERROR") - }.onSuccess { - finally(context.getString(R.string.success)) - } - } - - private fun export(doc: DocumentFile, book: Book) { - DocumentUtils.createFileIfNotExist(doc, "${book.name} 作者:${book.author}.txt") - ?.writeText(context, getAllContents(book)) - App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> - BookHelp.getContent(book, chapter).let { content -> - content?.split("\n")?.forEachIndexed { index, text -> - val matcher = AppPattern.imgPattern.matcher(text) - if (matcher.find()) { - var src = matcher.group(1) - src = NetworkUtils.getAbsoluteURL(chapter.url, src) - src?.let { - val vFile = BookHelp.getImage(book, src) - if (vFile.exists()) { - DocumentUtils.createFileIfNotExist(doc, - "${index}-${MD5Utils.md5Encode16(src)}.jpg", - subDirs = arrayOf("${book.name}_${book.author}", - "images", - chapter.title)) - ?.writeBytes(context, vFile.readBytes()) - } - } - } - } - } - } - } - - private fun export(file: File, book: Book) { - FileUtils.createFileIfNotExist(file, "${book.name} 作者:${book.author}.txt") - .writeText(getAllContents(book)) - App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> - BookHelp.getContent(book, chapter).let { content -> - content?.split("\n")?.forEachIndexed { index, text -> - val matcher = AppPattern.imgPattern.matcher(text) - if (matcher.find()) { - var src = matcher.group(1) - src = NetworkUtils.getAbsoluteURL(chapter.url, src) - src?.let { - val vFile = BookHelp.getImage(book, src) - if (vFile.exists()) { - FileUtils.createFileIfNotExist(file, - "${book.name}_${book.author}", - "images", - chapter.title, - "${index}-${MD5Utils.md5Encode16(src)}.jpg") - .writeBytes(vFile.readBytes()) - } - } - } - } - } - } - } - - private fun getAllContents(book: Book): String { - val stringBuilder = StringBuilder() - stringBuilder.append(book.name) - .append("\n") - .append(context.getString(R.string.author_show, book.author)) - App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> - BookHelp.getContent(book, chapter).let { - stringBuilder.append("\n\n") - .append(chapter.title) - .append("\n") - .append(it) - } - } - return stringBuilder.toString() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt index a447bb9e1..76d2b22f8 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt @@ -1,44 +1,56 @@ package io.legado.app.ui.book.explore import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.activity.viewModels import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ActivityExploreShowBinding +import io.legado.app.databinding.ViewLoadMoreBinding import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_explore_show.* -import org.jetbrains.anko.startActivity +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding -class ExploreShowActivity : VMBaseActivity(R.layout.activity_explore_show), +class ExploreShowActivity : VMBaseActivity(), ExploreShowAdapter.CallBack { - override val viewModel: ExploreShowViewModel - get() = getViewModel(ExploreShowViewModel::class.java) + override val binding by viewBinding(ActivityExploreShowBinding::inflate) + override val viewModel by viewModels() private lateinit var adapter: ExploreShowAdapter private lateinit var loadMoreView: LoadMoreView private var isLoading = true override fun onActivityCreated(savedInstanceState: Bundle?) { - title_bar.title = intent.getStringExtra("exploreName") + binding.titleBar.title = intent.getStringExtra("exploreName") initRecyclerView() - viewModel.booksData.observe(this, { upData(it) }) + viewModel.booksData.observe(this) { upData(it) } viewModel.initData(intent) + viewModel.errorLiveData.observe(this) { + loadMoreView.error(it) + } } private fun initRecyclerView() { adapter = ExploreShowAdapter(this, this) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) - recycler_view.adapter = adapter + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter loadMoreView = LoadMoreView(this) - adapter.addFooterView(loadMoreView) + adapter.addFooterView { + ViewLoadMoreBinding.bind(loadMoreView) + } loadMoreView.startLoad() - recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { + loadMoreView.setOnClickListener { + if (!isLoading) { + loadMoreView.hasMore() + scrollToBottom() + isLoading = true + } + } + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) if (!recyclerView.canScrollVertically(1)) { @@ -62,7 +74,9 @@ class ExploreShowActivity : VMBaseActivity(R.layout.activi loadMoreView.noMore(getString(R.string.empty)) } else if (books.isEmpty()) { loadMoreView.noMore() - } else if (adapter.getItems().contains(books.first()) && adapter.getItems().contains(books.last())) { + } else if (adapter.getItems().contains(books.first()) && adapter.getItems() + .contains(books.last()) + ) { loadMoreView.noMore() } else { adapter.addItems(books) @@ -70,9 +84,9 @@ class ExploreShowActivity : VMBaseActivity(R.layout.activi } override fun showBookInfo(book: Book) { - startActivity( - Pair("name", book.name), - Pair("author", book.author) - ) + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt index 322e4a7a3..99d6b9285 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt @@ -1,45 +1,53 @@ package io.legado.app.ui.book.explore import android.content.Context +import android.view.ViewGroup import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemSearchBinding import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover -import kotlinx.android.synthetic.main.item_bookshelf_list.view.tv_name -import kotlinx.android.synthetic.main.item_search.view.* -import org.jetbrains.anko.sdk27.listeners.onClick + class ExploreShowAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_search) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSearchBinding { + return ItemSearchBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { - holder.itemView.apply { - tv_name.text = item.name - tv_author.text = context.getString(R.string.author_show, item.author) + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchBinding, + item: SearchBook, + payloads: MutableList + ) { + binding.apply { + tvName.text = item.name + tvAuthor.text = context.getString(R.string.author_show, item.author) if (item.latestChapterTitle.isNullOrEmpty()) { - tv_lasted.gone() + tvLasted.gone() } else { - tv_lasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle) - tv_lasted.visible() + tvLasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle) + tvLasted.visible() } - tv_introduce.text = context.getString(R.string.intro_show, item.intro) + tvIntroduce.text = item.trimIntro(context) val kinds = item.getKindList() if (kinds.isEmpty()) { - ll_kind.gone() + llKind.gone() } else { - ll_kind.visible() - ll_kind.setLabels(kinds) + llKind.visible() + llKind.setLabels(kinds) } - iv_cover.load(item.coverUrl, item.name, item.author) + ivCover.load(item.coverUrl, item.name, item.author) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.showBookInfo(it.toBook()) } diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index f2d3c2ef8..9bdd8cffa 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -3,18 +3,20 @@ package io.legado.app.ui.book.explore import android.app.Application import android.content.Intent import androidx.lifecycle.MutableLiveData -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.msg import kotlinx.coroutines.Dispatchers.IO class ExploreShowViewModel(application: Application) : BaseViewModel(application) { val booksData = MutableLiveData>() + val errorLiveData = MutableLiveData() private var bookSource: BookSource? = null - private val variableBook = SearchBook() private var exploreUrl: String? = null private var page = 1 @@ -23,7 +25,7 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application val sourceUrl = intent.getStringExtra("sourceUrl") exploreUrl = intent.getStringExtra("exploreUrl") if (bookSource == null && sourceUrl != null) { - bookSource = App.db.bookSourceDao().getBookSource(sourceUrl) + bookSource = appDb.bookSourceDao.getBookSource(sourceUrl) } explore() } @@ -33,12 +35,15 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application val source = bookSource val url = exploreUrl if (source != null && url != null) { - WebBook(source).exploreBook(url, page, variableBook, this) + WebBook(source).exploreBook(viewModelScope, url, page) .timeout(30000L) .onSuccess(IO) { searchBooks -> booksData.postValue(searchBooks) - App.db.searchBookDao().insert(*searchBooks.toTypedArray()) + appDb.searchBookDao.insert(*searchBooks.toTypedArray()) page++ + }.onError { + it.printStackTrace() + errorLiveData.postValue(it.msg) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEdit.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEdit.kt new file mode 100644 index 000000000..c2a3610cf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupEdit.kt @@ -0,0 +1,55 @@ +package io.legado.app.ui.book.group + +import android.content.Context +import android.view.LayoutInflater +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.alert +import io.legado.app.utils.requestInputMethod + +object GroupEdit { + + fun show(context: Context, layoutInflater: LayoutInflater, bookGroup: BookGroup) = context.run { + alert(title = getString(R.string.group_edit)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setText(bookGroup.groupName) + } + if (bookGroup.groupId >= 0) { + neutralButton(R.string.delete) { + deleteGroup(context, bookGroup) + } + } + customView { alertBinding.root } + yesButton { + alertBinding.editView.text?.toString()?.let { + bookGroup.groupName = it + Coroutine.async { + appDb.bookGroupDao.update(bookGroup) + } + } + } + noButton() + }.show().requestInputMethod() + } + + private fun deleteGroup(context: Context, bookGroup: BookGroup) = context.run { + alert(R.string.delete, R.string.sure_del) { + okButton { + Coroutine.async { + appDb.bookGroupDao.delete(bookGroup) + val books = appDb.bookDao.getBooksByGroup(bookGroup.groupId) + books.forEach { + it.group = it.group - bookGroup.groupId + } + appDb.bookDao.update(*books.toTypedArray()) + } + } + noButton() + }.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt index 0782c7c39..ca6271a15 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt @@ -3,50 +3,47 @@ package io.legado.app.ui.book.group import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.EditText import androidx.appcompat.widget.Toolbar -import androidx.recyclerview.widget.DiffUtil +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb import io.legado.app.data.entities.BookGroup -import io.legado.app.help.AppConfig -import io.legado.app.lib.dialogs.* +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemBookGroupManageBinding +import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel +import io.legado.app.utils.getSize import io.legado.app.utils.requestInputMethod +import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_group_manage.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import java.util.* -import kotlin.collections.ArrayList +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { - private lateinit var viewModel: GroupViewModel + private val viewModel: GroupViewModel by viewModels() private lateinit var adapter: GroupAdapter - private val callBack: CallBack? get() = parentFragment as? CallBack + private val binding by viewBinding(DialogRecyclerViewBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -55,74 +52,49 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(GroupViewModel::class.java) return inflater.inflate(R.layout.dialog_recycler_view, container) } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.title = getString(R.string.group_manage) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = getString(R.string.group_manage) + initView() initData() initMenu() } - private fun initData() { + private fun initView() { adapter = GroupAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - tv_ok.setTextColor(requireContext().accentColor) - tv_ok.visible() - tv_ok.onClick { dismiss() } - App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, { - val diffResult = - DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it)) - adapter.setItems(it, diffResult) - }) - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) itemTouchCallback.isCanDrag = true - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + binding.tvOk.setTextColor(requireContext().accentColor) + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + dismissAllowingStateLoss() + } } - private fun initMenu() { - tool_bar.setOnMenuItemClickListener(this) - tool_bar.inflateMenu(R.menu.book_group_manage) - tool_bar.menu.let { - it.applyTint(requireContext()) - it.findItem(R.id.menu_group_all) - .isChecked = AppConfig.bookGroupAllShow - it.findItem(R.id.menu_group_local) - .isChecked = AppConfig.bookGroupLocalShow - it.findItem(R.id.menu_group_audio) - .isChecked = AppConfig.bookGroupAudioShow - it.findItem(R.id.menu_group_none) - .isChecked = AppConfig.bookGroupNoneShow + private fun initData() { + launch { + appDb.bookGroupDao.flowAll().collect { + adapter.setItems(it) + } } } + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.book_group_manage) + binding.toolBar.menu.applyTint(requireContext()) + } + override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_add -> addGroup() - R.id.menu_group_all -> { - item.isChecked = !item.isChecked - AppConfig.bookGroupAllShow = item.isChecked - callBack?.upGroup() - } - R.id.menu_group_local -> { - item.isChecked = !item.isChecked - AppConfig.bookGroupLocalShow = item.isChecked - callBack?.upGroup() - } - R.id.menu_group_audio -> { - item.isChecked = !item.isChecked - AppConfig.bookGroupAudioShow = item.isChecked - callBack?.upGroup() - } - R.id.menu_group_none -> { - item.isChecked = !item.isChecked - AppConfig.bookGroupNoneShow = item.isChecked - } } return true } @@ -130,101 +102,63 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener @SuppressLint("InflateParams") private fun addGroup() { alert(title = getString(R.string.add_group)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) } + customView { alertBinding.root } yesButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotBlank()) { viewModel.addGroup(it) } } } noButton() - }.show().applyTint().requestInputMethod() - } - - @SuppressLint("InflateParams") - private fun editGroup(bookGroup: BookGroup) { - alert(title = getString(R.string.group_edit)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - setText(bookGroup.groupName) - } - } - } - yesButton { - viewModel.upGroup(bookGroup.copy(groupName = editText?.text?.toString() ?: "")) - } - noButton() - }.show().applyTint().requestInputMethod() - } - - private fun deleteGroup(bookGroup: BookGroup) { - alert(R.string.delete, R.string.sure_del) { - okButton { - viewModel.delGroup(bookGroup) - } - noButton() - }.show().applyTint() - } - - private class GroupDiffCallBack( - private val oldItems: List, - private val newItems: List - ) : DiffUtil.Callback() { - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return true - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.groupName == newItem.groupName - } - + }.show().requestInputMethod() } private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_manage), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + ItemTouchCallback.Callback { private var isMoved = false - override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList) { - holder.itemView.apply { - setBackgroundColor(context.backgroundColor) - tv_group.text = item.groupName + override fun getViewBinding(parent: ViewGroup): ItemBookGroupManageBinding { + return ItemBookGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookGroupManageBinding, + item: BookGroup, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item.getManageName(context) + swShow.isChecked = item.show } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - tv_edit.onClick { getItem(holder.layoutPosition)?.let { editGroup(it) } } - tv_del.onClick { getItem(holder.layoutPosition)?.let { deleteGroup(it) } } + override fun registerListener(holder: ItemViewHolder, binding: ItemBookGroupManageBinding) { + binding.run { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { bookGroup -> + GroupEdit.show(context, layoutInflater, bookGroup) + } + } + swShow.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + getItem(holder.layoutPosition)?.let { + viewModel.upGroup(it.copy(show = isChecked)) + } + } + } } } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) isMoved = true return true } @@ -240,7 +174,4 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } } - interface CallBack { - fun upGroup() - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt index bee66bcc6..d202701ab 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt @@ -3,52 +3,48 @@ package io.legado.app.ui.book.group import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogBookGroupPickerBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.ItemGroupSelectBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel +import io.legado.app.utils.getSize import io.legado.app.utils.requestInputMethod -import kotlinx.android.synthetic.main.dialog_book_group_picker.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.recycler_view -import kotlinx.android.synthetic.main.dialog_recycler_view.tool_bar -import kotlinx.android.synthetic.main.item_group_select.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import java.util.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { companion object { const val tag = "groupSelectDialog" - fun show(manager: FragmentManager, groupId: Int, requestCode: Int = -1) { + fun show(manager: FragmentManager, groupId: Long, requestCode: Int = -1) { val fragment = GroupSelectDialog().apply { val bundle = Bundle() - bundle.putInt("groupId", groupId) + bundle.putLong("groupId", groupId) bundle.putInt("requestCode", requestCode) arguments = bundle } @@ -56,16 +52,16 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } } + private val binding by viewBinding(DialogBookGroupPickerBinding::bind) private var requestCode: Int = -1 - private lateinit var viewModel: GroupViewModel + private val viewModel: GroupViewModel by viewModels() private lateinit var adapter: GroupAdapter private var callBack: CallBack? = null - private var groupId = 0 + private var groupId: Long = 0 override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -74,15 +70,14 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(GroupViewModel::class.java) return inflater.inflate(R.layout.dialog_book_group_picker, container) } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) + binding.toolBar.setBackgroundColor(primaryColor) callBack = activity as? CallBack arguments?.let { - groupId = it.getInt("groupId") + groupId = it.getLong("groupId") requestCode = it.getInt("requestCode", -1) } initView() @@ -90,31 +85,33 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } private fun initView() { - tool_bar.title = getString(R.string.group_select) - tool_bar.inflateMenu(R.menu.book_group_manage) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) - tool_bar.menu.setGroupVisible(R.id.menu_groups, false) + binding.toolBar.title = getString(R.string.group_select) + binding.toolBar.inflateMenu(R.menu.book_group_manage) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) itemTouchCallback.isCanDrag = true - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) - tv_cancel.onClick { dismiss() } - tv_ok.setTextColor(requireContext().accentColor) - tv_ok.onClick { + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.setTextColor(requireContext().accentColor) + binding.tvOk.setOnClickListener { callBack?.upGroup(requestCode, groupId) - dismiss() + dismissAllowingStateLoss() } } private fun initData() { - App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, { - adapter.setItems(it) - }) + launch { + appDb.bookGroupDao.flowSelect().collect { + adapter.setItems(it) + } + } } override fun onMenuItemClick(item: MenuItem?): Boolean { @@ -127,63 +124,66 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener @SuppressLint("InflateParams") private fun addGroup() { alert(title = getString(R.string.add_group)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) } + customView { alertBinding.root } yesButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotBlank()) { viewModel.addGroup(it) } } } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } @SuppressLint("InflateParams") private fun editGroup(bookGroup: BookGroup) { alert(title = getString(R.string.group_edit)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - setText(bookGroup.groupName) - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setText(bookGroup.groupName) } + customView { alertBinding.root } yesButton { - viewModel.upGroup(bookGroup.copy(groupName = editText?.text?.toString() ?: "")) + alertBinding.editView.text?.toString()?.let { + viewModel.upGroup(bookGroup.copy(groupName = it)) + } } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_select), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + ItemTouchCallback.Callback { private var isMoved: Boolean = false - override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList) { - holder.itemView.apply { - setBackgroundColor(context.backgroundColor) - cb_group.text = item.groupName - cb_group.isChecked = (groupId and item.groupId) > 0 + override fun getViewBinding(parent: ViewGroup): ItemGroupSelectBinding { + return ItemGroupSelectBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupSelectBinding, + item: BookGroup, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + cbGroup.text = item.groupName + cbGroup.isChecked = (groupId and item.groupId) > 0 } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - cb_group.setOnCheckedChangeListener { buttonView, isChecked -> - getItem(holder.layoutPosition)?.let { - if (buttonView.isPressed) { + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupSelectBinding) { + binding.run { + cbGroup.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + getItem(holder.layoutPosition)?.let { groupId = if (isChecked) { groupId + it.groupId } else { @@ -192,13 +192,12 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } } } - tv_edit.onClick { getItem(holder.layoutPosition)?.let { editGroup(it) } } + tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } } } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) isMoved = true return true } @@ -215,6 +214,6 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener } interface CallBack { - fun upGroup(requestCode: Int, groupId: Int) + fun upGroup(requestCode: Int, groupId: Long) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt index 23555015f..3b78e45f8 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt @@ -1,43 +1,43 @@ package io.legado.app.ui.book.group import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.BookGroup class GroupViewModel(application: Application) : BaseViewModel(application) { fun addGroup(groupName: String) { execute { - var id = 1 - val idsSum = App.db.bookGroupDao().idsSum - while (id and idsSum != 0) { + var id = 1L + val idsSum = appDb.bookGroupDao.idsSum + while (id and idsSum != 0L) { id = id.shl(1) } val bookGroup = BookGroup( groupId = id, groupName = groupName, - order = App.db.bookGroupDao().maxOrder.plus(1) + order = appDb.bookGroupDao.maxOrder.plus(1) ) - App.db.bookGroupDao().insert(bookGroup) + appDb.bookGroupDao.insert(bookGroup) } } fun upGroup(vararg bookGroup: BookGroup) { execute { - App.db.bookGroupDao().update(*bookGroup) + appDb.bookGroupDao.update(*bookGroup) } } fun delGroup(vararg bookGroup: BookGroup) { execute { - App.db.bookGroupDao().delete(*bookGroup) + appDb.bookGroupDao.delete(*bookGroup) bookGroup.forEach { group -> - val books = App.db.bookDao().getBooksByGroup(group.groupId) + val books = appDb.bookDao.getBooksByGroup(group.groupId) books.forEach { it.group = it.group - group.groupId } - App.db.bookDao().update(*books.toTypedArray()) + appDb.bookDao.update(*books.toTypedArray()) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index 011def56a..cfdf84d02 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.book.info import android.annotation.SuppressLint -import android.app.Activity import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle @@ -9,6 +8,8 @@ import android.view.Menu import android.view.MenuItem import android.widget.CheckBox import android.widget.LinearLayout +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions.bitmapTransform @@ -16,55 +17,91 @@ import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.BookType import io.legado.app.constant.Theme +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ActivityBookInfoBinding import io.legado.app.help.BlurTransformation import io.legado.app.help.ImageLoader -import io.legado.app.help.IntentDataHelp import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor -import io.legado.app.ui.audio.AudioPlayActivity +import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changesource.ChangeSourceDialog -import io.legado.app.ui.book.chapterlist.ChapterListActivity import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_info.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.toast +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class BookInfoActivity : - VMBaseActivity(R.layout.activity_book_info, toolBarTheme = Theme.Dark), + VMBaseActivity(toolBarTheme = Theme.Dark), GroupSelectDialog.CallBack, - ChapterListAdapter.CallBack, ChangeSourceDialog.CallBack, ChangeCoverDialog.CallBack { - private val requestCodeChapterList = 568 - private val requestCodeSourceEdit = 562 - private val requestCodeRead = 432 + private val tocActivityResult = registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.bookData.value?.let { book -> + launch { + withContext(IO) { + viewModel.durChapterIndex = it.first + book.durChapterIndex = it.first + book.durChapterPos = it.second + appDb.bookDao.update(book) + } + viewModel.chapterListData.value?.let { chapterList -> + binding.tvToc.text = + getString(R.string.toc_s, chapterList[book.durChapterIndex].title) + } + startReadActivity(book) + } + } + } ?: let { + if (!viewModel.inBookshelf) { + viewModel.delBook() + } + } + } + private val readBookResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + viewModel.refreshData(intent) + if (it.resultCode == RESULT_OK) { + viewModel.inBookshelf = true + upTvBookshelf() + } + } + private val infoEditResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == RESULT_OK) { + viewModel.upEditBook() + } + } - override val viewModel: BookInfoViewModel - get() = getViewModel(BookInfoViewModel::class.java) + override val binding by viewBinding(ActivityBookInfoBinding::inflate) + override val viewModel by viewModels() @SuppressLint("PrivateResource") override fun onActivityCreated(savedInstanceState: Bundle?) { - title_bar.transparent() - arc_view.setBgColor(backgroundColor) - ll_info.setBackgroundColor(backgroundColor) - scroll_view.setBackgroundColor(backgroundColor) - fl_action.setBackgroundColor(bottomBackground) - tv_shelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground))) + binding.titleBar.transparent() + binding.arcView.setBgColor(backgroundColor) + binding.llInfo.setBackgroundColor(backgroundColor) + binding.scrollView.setBackgroundColor(backgroundColor) + binding.flAction.setBackgroundColor(bottomBackground) + binding.tvShelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground))) + binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) viewModel.bookData.observe(this, { showBook(it) }) viewModel.chapterListData.observe(this, { upLoading(false, it) }) viewModel.initData(intent) @@ -76,18 +113,33 @@ class BookInfoActivity : return super.onCompatCreateOptionsMenu(menu) } + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_can_update)?.isChecked = + viewModel.bookData.value?.canUpdate ?: true + menu.findItem(R.id.menu_login)?.isVisible = + !viewModel.bookSource?.loginUrl.isNullOrBlank() + return super.onMenuOpened(featureId, menu) + } + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_edit -> { if (viewModel.inBookshelf) { viewModel.bookData.value?.let { - startActivityForResult( - requestCodeSourceEdit, - Pair("bookUrl", it.bookUrl) + infoEditResult.launch( + Intent(this, BookInfoEditActivity::class.java) + .putExtra("bookUrl", it.bookUrl) ) } } else { - toast(R.string.after_add_bookshelf) + toastOnUi(R.string.after_add_bookshelf) + } + } + R.id.menu_share_it -> { + viewModel.bookData.value?.let { + val bookJson = GSON.toJson(it) + val shareStr = "${it.bookUrl}#$bookJson" + shareWithQr(shareStr, it.name) } } R.id.menu_refresh -> { @@ -99,6 +151,12 @@ class BookInfoActivity : viewModel.loadBookInfo(it, false) } } + R.id.menu_copy_book_url -> viewModel.bookData.value?.bookUrl?.let { + sendToClip(it) + } ?: toastOnUi(R.string.no_book) + R.id.menu_copy_toc_url -> viewModel.bookData.value?.tocUrl?.let { + sendToClip(it) + } ?: toastOnUi(R.string.no_book) R.id.menu_can_update -> { if (viewModel.inBookshelf) { viewModel.bookData.value?.let { @@ -106,7 +164,7 @@ class BookInfoActivity : viewModel.saveBook() } } else { - toast(R.string.after_add_bookshelf) + toastOnUi(R.string.after_add_bookshelf) } } R.id.menu_clear_cache -> viewModel.clearCache() @@ -114,38 +172,31 @@ class BookInfoActivity : return super.onCompatOptionsItemSelected(item) } - override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean { - menu?.findItem(R.id.menu_can_update)?.isChecked = - viewModel.bookData.value?.canUpdate ?: true - return super.onMenuOpened(featureId, menu) - } - - private fun showBook(book: Book) { + private fun showBook(book: Book) = binding.run { showCover(book) - tv_name.text = book.name - tv_author.text = getString(R.string.author_show, book.getRealAuthor()) - tv_origin.text = getString(R.string.origin_show, book.originName) - tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle) - tv_toc.text = getString(R.string.toc_s, getString(R.string.loading)) - tv_intro.text = book.getDisplayIntro() + tvName.text = book.name + tvAuthor.text = getString(R.string.author_show, book.getRealAuthor()) + tvOrigin.text = getString(R.string.origin_show, book.originName) + tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle) + tvIntro.text = book.getDisplayIntro() upTvBookshelf() val kinds = book.getKindList() if (kinds.isEmpty()) { - lb_kind.gone() + lbKind.gone() } else { - lb_kind.visible() - lb_kind.setLabels(kinds) + lbKind.visible() + lbKind.setLabels(kinds) } upGroup(book.group) } private fun showCover(book: Book) { - iv_cover.load(book.getDisplayCover(), book.name, book.author) + binding.ivCover.load(book.getDisplayCover(), book.name, book.author) ImageLoader.load(this, book.getDisplayCover()) .transition(DrawableTransitionOptions.withCrossFade(1500)) .thumbnail(defaultCover()) .apply(bitmapTransform(BlurTransformation(this, 25))) - .into(bg_book) //模糊、渐变、缩小效果 + .into(binding.bgBook) //模糊、渐变、缩小效果 } private fun defaultCover(): RequestBuilder { @@ -156,18 +207,18 @@ class BookInfoActivity : private fun upLoading(isLoading: Boolean, chapterList: List? = null) { when { isLoading -> { - tv_toc.text = getString(R.string.toc_s, getString(R.string.loading)) + binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) } chapterList.isNullOrEmpty() -> { - tv_toc.text = getString(R.string.toc_s, getString(R.string.error_load_toc)) + binding.tvToc.text = getString(R.string.toc_s, getString(R.string.error_load_toc)) } else -> { viewModel.bookData.value?.let { if (it.durChapterIndex < chapterList.size) { - tv_toc.text = + binding.tvToc.text = getString(R.string.toc_s, chapterList[it.durChapterIndex].title) } else { - tv_toc.text = getString(R.string.toc_s, chapterList.last().title) + binding.tvToc.text = getString(R.string.toc_s, chapterList.last().title) } } } @@ -176,34 +227,34 @@ class BookInfoActivity : private fun upTvBookshelf() { if (viewModel.inBookshelf) { - tv_shelf.text = getString(R.string.remove_from_bookshelf) + binding.tvShelf.text = getString(R.string.remove_from_bookshelf) } else { - tv_shelf.text = getString(R.string.add_to_shelf) + binding.tvShelf.text = getString(R.string.add_to_shelf) } } - private fun upGroup(groupId: Int) { + private fun upGroup(groupId: Long) { viewModel.loadGroup(groupId) { if (it.isNullOrEmpty()) { - tv_group.text = getString(R.string.group_s, getString(R.string.no_group)) + binding.tvGroup.text = getString(R.string.group_s, getString(R.string.no_group)) } else { - tv_group.text = getString(R.string.group_s, it) + binding.tvGroup.text = getString(R.string.group_s, it) } } } - private fun initOnClick() { - iv_cover.onClick { + private fun initOnClick() = binding.run { + ivCover.setOnClickListener { viewModel.bookData.value?.let { ChangeCoverDialog.show(supportFragmentManager, it.name, it.author) } } - tv_read.onClick { + tvRead.setOnClickListener { viewModel.bookData.value?.let { readBook(it) } } - tv_shelf.onClick { + tvShelf.setOnClickListener { if (viewModel.inBookshelf) { deleteBook() } else { @@ -212,17 +263,19 @@ class BookInfoActivity : } } } - tv_origin.onClick { + tvOrigin.setOnClickListener { viewModel.bookData.value?.let { - startActivity(Pair("data", it.origin)) + startActivity { + putExtra("data", it.origin) + } } } - tv_change_source.onClick { + tvChangeSource.setOnClickListener { viewModel.bookData.value?.let { ChangeSourceDialog.show(supportFragmentManager, it.name, it.author) } } - tv_toc_view.onClick { + tvTocView.setOnClickListener { if (!viewModel.inBookshelf) { viewModel.saveBook { viewModel.saveChapterList { @@ -233,16 +286,20 @@ class BookInfoActivity : openChapterList() } } - tv_change_group.onClick { + tvChangeGroup.setOnClickListener { viewModel.bookData.value?.let { GroupSelectDialog.show(supportFragmentManager, it.group) } } - tv_author.onClick { - startActivity(Pair("key", viewModel.bookData.value?.author)) + tvAuthor.setOnClickListener { + startActivity { + putExtra("key", viewModel.bookData.value?.author) + } } - tv_name.onClick { - startActivity(Pair("key", viewModel.bookData.value?.name)) + tvName.setOnClickListener { + startActivity { + putExtra("key", viewModel.bookData.value?.name) + } } } @@ -261,7 +318,7 @@ class BookInfoActivity : setPadding(16.dp, 0, 16.dp, 0) addView(checkBox) } - customView = view + customView { view } positiveButton(R.string.yes) { viewModel.delBook(checkBox.isChecked) { finish() @@ -279,14 +336,11 @@ class BookInfoActivity : private fun openChapterList() { if (viewModel.chapterListData.value.isNullOrEmpty()) { - toast(R.string.chapter_list_empty) + toastOnUi(R.string.chapter_list_empty) return } viewModel.bookData.value?.let { - startActivityForResult( - requestCodeChapterList, - Pair("bookUrl", it.bookUrl) - ) + tocActivityResult.launch(it.bookUrl) } } @@ -306,16 +360,15 @@ class BookInfoActivity : private fun startReadActivity(book: Book) { when (book.type) { - BookType.audio -> startActivityForResult( - requestCodeRead, - Pair("bookUrl", book.bookUrl), - Pair("inBookshelf", viewModel.inBookshelf) + BookType.audio -> readBookResult.launch( + Intent(this, AudioPlayActivity::class.java) + .putExtra("bookUrl", book.bookUrl) + .putExtra("inBookshelf", viewModel.inBookshelf) ) - else -> startActivityForResult( - requestCodeRead, - Pair("bookUrl", book.bookUrl), - Pair("inBookshelf", viewModel.inBookshelf), - Pair("key", IntentDataHelp.putData(book)) + else -> readBookResult.launch( + Intent(this, ReadBookActivity::class.java) + .putExtra("bookUrl", book.bookUrl) + .putExtra("inBookshelf", viewModel.inBookshelf) ) } } @@ -336,21 +389,7 @@ class BookInfoActivity : } } - override fun openChapter(chapter: BookChapter) { - if (chapter.index != viewModel.durChapterIndex) { - viewModel.bookData.value?.let { - it.durChapterIndex = chapter.index - it.durChapterPos = 0 - readBook(it) - } - } - } - - override fun durChapterIndex(): Int { - return viewModel.durChapterIndex - } - - override fun upGroup(requestCode: Int, groupId: Int) { + override fun upGroup(requestCode: Int, groupId: Long) { upGroup(groupId) viewModel.bookData.value?.group = groupId if (viewModel.inBookshelf) { @@ -358,33 +397,4 @@ class BookInfoActivity : } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - requestCodeSourceEdit -> - if (resultCode == Activity.RESULT_OK) { - viewModel.upEditBook() - } - requestCodeChapterList -> - if (resultCode == Activity.RESULT_OK) { - viewModel.bookData.value?.let { - data?.getIntExtra("index", it.durChapterIndex)?.let { index -> - if (it.durChapterIndex != index) { - it.durChapterIndex = index - it.durChapterPos = 0 - } - startReadActivity(it) - } - } - } else { - if (!viewModel.inBookshelf) { - viewModel.delBook() - } - } - requestCodeRead -> if (resultCode == Activity.RESULT_OK) { - viewModel.inBookshelf = true - upTvBookshelf() - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index fb2a16bf8..ddb0f9b0d 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -3,15 +3,17 @@ package io.legado.app.ui.book.info import android.app.Application import android.content.Intent import androidx.lifecycle.MutableLiveData -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource import io.legado.app.help.BookHelp import io.legado.app.model.localBook.LocalBook import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.ReadBook +import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers.IO class BookInfoViewModel(application: Application) : BaseViewModel(application) { @@ -19,15 +21,26 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { val chapterListData = MutableLiveData>() var durChapterIndex = 0 var inBookshelf = false + var bookSource: BookSource? = null fun initData(intent: Intent) { execute { val name = intent.getStringExtra("name") ?: "" val author = intent.getStringExtra("author") ?: "" - App.db.bookDao().getBook(name, author)?.let { book -> + appDb.bookDao.getBook(name, author)?.let { book -> inBookshelf = true setBook(book) - } ?: App.db.searchBookDao().getFirstByNameAuthor(name, author)?.toBook()?.let { book -> + } ?: appDb.searchBookDao.getFirstByNameAuthor(name, author)?.toBook()?.let { book -> + setBook(book) + } + } + } + + fun refreshData(intent: Intent) { + execute { + val name = intent.getStringExtra("name") ?: "" + val author = intent.getStringExtra("author") ?: "" + appDb.bookDao.getBook(name, author)?.let { book -> setBook(book) } } @@ -36,10 +49,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { private fun setBook(book: Book) { durChapterIndex = book.durChapterIndex bookData.postValue(book) + initBookSource(book) if (book.tocUrl.isEmpty()) { loadBookInfo(book) } else { - val chapterList = App.db.bookChapterDao().getChapterList(book.bookUrl) + val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl) if (chapterList.isNotEmpty()) { chapterListData.postValue(chapterList) } else { @@ -48,6 +62,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } + private fun initBookSource(book: Book) { + bookSource = if (book.isLocalBook()) { + null + } else { + appDb.bookSourceDao.getBookSource(book.origin) + } + } + fun loadBookInfo( book: Book, canReName: Boolean = true, changeDruChapterIndex: ((chapters: List) -> Unit)? = null, @@ -56,20 +78,20 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { if (book.isLocalBook()) { loadChapter(book, changeDruChapterIndex) } else { - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getBookInfo(book, this, canReName = canReName) + bookSource?.let { bookSource -> + WebBook(bookSource).getBookInfo(this, book, canReName = canReName) .onSuccess(IO) { bookData.postValue(book) if (inBookshelf) { - App.db.bookDao().update(book) + appDb.bookDao.update(book) } loadChapter(it, changeDruChapterIndex) }.onError { - toast(R.string.error_get_book_info) + context.toastOnUi(R.string.error_get_book_info) } } ?: let { - chapterListData.postValue(null) - toast(R.string.error_no_source) + chapterListData.postValue(emptyList()) + context.toastOnUi(R.string.error_no_source) } } } @@ -82,18 +104,18 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { execute { if (book.isLocalBook()) { LocalBook.getChapterList(book).let { - App.db.bookDao().update(book) - App.db.bookChapterDao().insert(*it.toTypedArray()) + appDb.bookDao.update(book) + appDb.bookChapterDao.insert(*it.toTypedArray()) chapterListData.postValue(it) } } else { - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getChapterList(book, this) + bookSource?.let { bookSource -> + WebBook(bookSource).getChapterList(this, book) .onSuccess(IO) { if (it.isNotEmpty()) { if (inBookshelf) { - App.db.bookDao().update(book) - App.db.bookChapterDao().insert(*it.toTypedArray()) + appDb.bookDao.update(book) + appDb.bookChapterDao.insert(*it.toTypedArray()) } if (changeDruChapterIndex == null) { chapterListData.postValue(it) @@ -101,31 +123,25 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { changeDruChapterIndex(it) } } else { - toast(R.string.chapter_list_empty) + context.toastOnUi(R.string.chapter_list_empty) } }.onError { - chapterListData.postValue(null) - toast(R.string.error_get_chapter_list) + chapterListData.postValue(emptyList()) + context.toastOnUi(R.string.error_get_chapter_list) } } ?: let { - chapterListData.postValue(null) - toast(R.string.error_no_source) + chapterListData.postValue(emptyList()) + context.toastOnUi(R.string.error_no_source) } } }.onError { - toast("LoadTocError:${it.localizedMessage}") + context.toastOnUi("LoadTocError:${it.localizedMessage}") } } - fun loadGroup(groupId: Int, success: ((groupNames: String?) -> Unit)) { + fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) { execute { - val groupNames = arrayListOf() - App.db.bookGroupDao().all.forEach { - if (groupId and it.groupId > 0) { - groupNames.add(it.groupName) - } - } - groupNames.joinToString(",") + appDb.bookGroupDao.getGroupNames(groupId).joinToString(",") }.onSuccess { success.invoke(it) } @@ -133,29 +149,43 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { fun changeTo(newBook: Book) { execute { + var oldTocSize: Int = newBook.totalChapterNum if (inBookshelf) { - bookData.value?.changeTo(newBook) + bookData.value?.let { + oldTocSize = it.totalChapterNum + it.changeTo(newBook) + } } bookData.postValue(newBook) + initBookSource(newBook) if (newBook.tocUrl.isEmpty()) { - loadBookInfo(newBook, false) { upChangeDurChapterIndex(newBook, it) } + loadBookInfo(newBook, false) { + upChangeDurChapterIndex(newBook, oldTocSize, it) + } } else { - loadChapter(newBook) { upChangeDurChapterIndex(newBook, it) } + loadChapter(newBook) { + upChangeDurChapterIndex(newBook, oldTocSize, it) + } } } } - private fun upChangeDurChapterIndex(book: Book, chapters: List) { + private fun upChangeDurChapterIndex( + book: Book, + oldTocSize: Int, + chapters: List + ) { execute { - book.durChapterIndex = BookHelp.getDurChapterIndexByChapterTitle( - book.durChapterTitle, + book.durChapterIndex = BookHelp.getDurChapter( book.durChapterIndex, + oldTocSize, + book.durChapterTitle, chapters ) book.durChapterTitle = chapters[book.durChapterIndex].title if (inBookshelf) { - App.db.bookDao().insert(book) - App.db.bookChapterDao().insert(*chapters.toTypedArray()) + appDb.bookDao.update(book) + appDb.bookChapterDao.insert(*chapters.toTypedArray()) } bookData.postValue(book) chapterListData.postValue(chapters) @@ -166,13 +196,13 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { execute { bookData.value?.let { book -> if (book.order == 0) { - book.order = App.db.bookDao().maxOrder + 1 + book.order = appDb.bookDao.maxOrder + 1 } - App.db.bookDao().getBook(book.name, book.author)?.let { + appDb.bookDao.getBook(book.name, book.author)?.let { book.durChapterPos = it.durChapterPos book.durChapterTitle = it.durChapterTitle } - App.db.bookDao().insert(book) + book.save() if (ReadBook.book?.name == book.name && ReadBook.book?.author == book.author) { ReadBook.book = book } @@ -185,7 +215,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { fun saveChapterList(success: (() -> Unit)?) { execute { chapterListData.value?.let { - App.db.bookChapterDao().insert(*it.toTypedArray()) + appDb.bookChapterDao.insert(*it.toTypedArray()) } }.onSuccess { success?.invoke() @@ -196,16 +226,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { execute { bookData.value?.let { book -> if (book.order == 0) { - book.order = App.db.bookDao().maxOrder + 1 + book.order = appDb.bookDao.maxOrder + 1 } - App.db.bookDao().getBook(book.name, book.author)?.let { + appDb.bookDao.getBook(book.name, book.author)?.let { book.durChapterPos = it.durChapterPos book.durChapterTitle = it.durChapterTitle } - App.db.bookDao().insert(book) + book.save() } chapterListData.value?.let { - App.db.bookChapterDao().insert(*it.toTypedArray()) + appDb.bookChapterDao.insert(*it.toTypedArray()) } inBookshelf = true }.onSuccess { @@ -216,7 +246,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) { execute { bookData.value?.let { - it.delete() + Book.delete(it) inBookshelf = false if (it.isLocalBook()) { LocalBook.deleteBook(it, deleteOriginal) @@ -231,15 +261,15 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { execute { BookHelp.clearCache(bookData.value!!) }.onSuccess { - toast(R.string.clear_cache_success) + context.toastOnUi(R.string.clear_cache_success) }.onError { - toast(it.stackTraceToString()) + context.toastOnUi(it.stackTraceToString()) } } fun upEditBook() { bookData.value?.let { - App.db.bookDao().getBook(it.bookUrl)?.let { book -> + appDb.bookDao.getBook(it.bookUrl)?.let { book -> bookData.postValue(book) } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt deleted file mode 100644 index cacd737cd..000000000 --- a/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.legado.app.ui.book.info - -import android.content.Context -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.data.entities.BookChapter -import io.legado.app.lib.theme.accentColor -import kotlinx.android.synthetic.main.item_chapter_list.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.textColorResource - -class ChapterListAdapter(context: Context, var callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_chapter_list) { - - override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList) { - holder.itemView.apply { - tv_chapter_name.text = item.title - if (item.index == callBack.durChapterIndex()) { - tv_chapter_name.setTextColor(context.accentColor) - } else { - tv_chapter_name.textColorResource = R.color.secondaryText - } - - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - this.onClick { - getItem(holder.layoutPosition)?.let { - callBack.openChapter(it) - } - } - } - } - - interface CallBack { - fun openChapter(chapter: BookChapter) - fun durChapterIndex(): Int - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt index 211c56cb3..805edfe5f 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -1,32 +1,37 @@ package io.legado.app.ui.book.info.edit import android.app.Activity -import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.Book -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.databinding.ActivityBookInfoEditBinding +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_info_edit.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.toast +import io.legado.app.utils.viewbindingdelegate.viewBinding import java.io.File class BookInfoEditActivity : - VMBaseActivity(R.layout.activity_book_info_edit), + VMBaseActivity(), ChangeCoverDialog.CallBack { - private val resultSelectCover = 132 + private val selectCoverResult = + registerForActivityResult(ActivityResultContracts.GetContent()) { + it?.let { uri -> + coverChangeTo(uri) + } + } - override val viewModel: BookInfoEditViewModel - get() = getViewModel(BookInfoEditViewModel::class.java) + override val binding by viewBinding(ActivityBookInfoEditBinding::inflate) + override val viewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel.bookData.observe(this, { upView(it) }) @@ -50,42 +55,42 @@ class BookInfoEditActivity : return super.onCompatOptionsItemSelected(item) } - private fun initEvent() { - tv_change_cover.onClick { + private fun initEvent() = binding.run { + tvChangeCover.setOnClickListener { viewModel.bookData.value?.let { ChangeCoverDialog.show(supportFragmentManager, it.name, it.author) } } - tv_select_cover.onClick { - selectImage() + tvSelectCover.setOnClickListener { + selectCoverResult.launch("image/*") } - tv_refresh_cover.onClick { - viewModel.book?.customCoverUrl = tie_cover_url.text?.toString() + tvRefreshCover.setOnClickListener { + viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() upCover() } } - private fun upView(book: Book) { - tie_book_name.setText(book.name) - tie_book_author.setText(book.author) - tie_cover_url.setText(book.getDisplayCover()) - tie_book_intro.setText(book.getDisplayIntro()) + private fun upView(book: Book) = binding.run { + tieBookName.setText(book.name) + tieBookAuthor.setText(book.author) + tieCoverUrl.setText(book.getDisplayCover()) + tieBookIntro.setText(book.getDisplayIntro()) upCover() } private fun upCover() { viewModel.book.let { - iv_cover.load(it?.getDisplayCover(), it?.name, it?.author) + binding.ivCover.load(it?.getDisplayCover(), it?.name, it?.author) } } - private fun saveData() { + private fun saveData() = binding.run { viewModel.book?.let { book -> - book.name = tie_book_name.text?.toString() ?: "" - book.author = tie_book_author.text?.toString() ?: "" - val customCoverUrl = tie_cover_url.text?.toString() + book.name = tieBookName.text?.toString() ?: "" + book.author = tieBookAuthor.text?.toString() ?: "" + val customCoverUrl = tieCoverUrl.text?.toString() book.customCoverUrl = if (customCoverUrl == book.coverUrl) null else customCoverUrl - book.customIntro = tie_book_intro.text?.toString() + book.customIntro = tieBookIntro.text?.toString() viewModel.saveBook(book) { setResult(Activity.RESULT_OK) finish() @@ -93,31 +98,24 @@ class BookInfoEditActivity : } } - private fun selectImage() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "image/*" - startActivityForResult(intent, resultSelectCover) - } - override fun coverChangeTo(coverUrl: String) { viewModel.book?.customCoverUrl = coverUrl - tie_cover_url.setText(coverUrl) + binding.tieCoverUrl.setText(coverUrl) upCover() } private fun coverChangeTo(uri: Uri) { - if (uri.toString().isContentPath()) { + if (uri.isContentScheme()) { val doc = DocumentFile.fromSingleUri(this, uri) doc?.name?.let { - var file = this.externalFilesDir + var file = this.externalFiles file = FileUtils.createFileIfNotExist(file, "covers", it) kotlin.runCatching { DocumentUtils.readBytes(this, doc.uri) }.getOrNull()?.let { byteArray -> file.writeBytes(byteArray) coverChangeTo(file.absolutePath) - } ?: toast("获取文件出错") + } ?: toastOnUi("获取文件出错") } } else { PermissionsCompat.Builder(this) @@ -130,7 +128,7 @@ class BookInfoEditActivity : RealPathUtil.getPath(this, uri)?.let { path -> val imgFile = File(path) if (imgFile.exists()) { - var file = this.externalFilesDir + var file = this.externalFiles file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) file.writeBytes(imgFile.readBytes()) coverChangeTo(file.absolutePath) @@ -141,16 +139,4 @@ class BookInfoEditActivity : } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - resultSelectCover -> { - if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - coverChangeTo(uri) - } - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt index a0604035e..1265d9b4d 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt @@ -2,8 +2,8 @@ package io.legado.app.ui.book.info.edit import android.app.Application import androidx.lifecycle.MutableLiveData -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.service.help.ReadBook @@ -13,7 +13,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio fun loadBook(bookUrl: String) { execute { - book = App.db.bookDao().getBook(bookUrl) + book = appDb.bookDao.getBook(bookUrl) book?.let { bookData.postValue(it) } @@ -25,7 +25,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio if (ReadBook.book?.bookUrl == book.bookUrl) { ReadBook.book = book } - App.db.bookDao().insert(book) + appDb.bookDao.update(book) }.onSuccess { success?.invoke() } diff --git a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt index 86c5e5ecd..d9c5998ac 100644 --- a/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt @@ -1,59 +1,71 @@ package io.legado.app.ui.book.local -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.provider.DocumentsContract import android.view.Menu import android.view.MenuItem -import android.view.View +import androidx.activity.viewModels import androidx.appcompat.widget.PopupMenu import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LiveData import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.data.appDb +import io.legado.app.databinding.ActivityImportBookBinding +import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.theme.backgroundColor -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker +import io.legado.app.ui.document.FilePicker import io.legado.app.ui.widget.SelectActionBar import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_import_book.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jetbrains.anko.sdk27.listeners.onClick import java.io.File import java.util.* - -class ImportBookActivity : VMBaseActivity(R.layout.activity_import_book), - FileChooserDialog.CallBack, +/** + * 导入本地书籍界面 + */ +class ImportBookActivity : VMBaseActivity(), PopupMenu.OnMenuItemClickListener, - ImportBookAdapter.CallBack { - private val requestCodeSelectFolder = 342 + ImportBookAdapter.CallBack, + SelectActionBar.CallBack { + + override val binding by viewBinding(ActivityImportBookBinding::inflate) + override val viewModel by viewModels() + private var rootDoc: DocumentFile? = null private val subDocs = arrayListOf() private lateinit var adapter: ImportBookAdapter - private var localUriLiveData: LiveData>? = null private var sdPath = FileUtils.getSdCardPath() private var path = sdPath - - override val viewModel: ImportBookViewModel - get() = getViewModel(ImportBookViewModel::class.java) + private val selectFolder = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + AppConfig.importBookPath = uri.toString() + initRootDoc() + } else { + uri.path?.let { path -> + AppConfig.importBookPath = path + initRootDoc() + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { initView() initEvent() initData() - upRootDoc() + initRootDoc() } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -61,41 +73,11 @@ class ImportBookActivity : VMBaseActivity(R.layout.activity return super.onCompatCreateOptionsMenu(menu) } - private fun initView() { - lay_top.setBackgroundColor(backgroundColor) - recycler_view.layoutManager = LinearLayoutManager(this) - adapter = ImportBookAdapter(this, this) - recycler_view.adapter = adapter - select_action_bar.setMainActionText(R.string.add_to_shelf) - select_action_bar.inflateMenu(R.menu.import_book_sel) - select_action_bar.setOnMenuItemClickListener(this) - select_action_bar.setCallBack(object : SelectActionBar.CallBack { - override fun selectAll(selectAll: Boolean) { - adapter.selectAll(selectAll) - } - - override fun revertSelection() { - adapter.revertSelection() - } - - override fun onClickMainAction() { - viewModel.addToBookshelf(adapter.selectedUris) { - upPath() - } - } - }) - - } - - private fun initEvent() { - tv_go_back.onClick { - goBackDir() - } - } - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_select_folder -> FilePicker.selectFolder(this, requestCodeSelectFolder) + R.id.menu_select_folder -> selectFolder.launch(null) + R.id.menu_scan_folder -> scanFolder() + R.id.menu_import_file_name -> alertImportFileName() } return super.onCompatOptionsItemSelected(item) } @@ -104,152 +86,222 @@ class ImportBookActivity : VMBaseActivity(R.layout.activity when (item?.itemId) { R.id.menu_del_selection -> viewModel.deleteDoc(adapter.selectedUris) { - upPath() + adapter.removeSelection() } } return false } + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + viewModel.addToBookshelf(adapter.selectedUris) { + adapter.notifyDataSetChanged() + } + } + + private fun initView() { + binding.layTop.setBackgroundColor(backgroundColor) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + adapter = ImportBookAdapter(this, this) + binding.recyclerView.adapter = adapter + binding.selectActionBar.setMainActionText(R.string.add_to_shelf) + binding.selectActionBar.inflateMenu(R.menu.import_book_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun initEvent() { + binding.tvGoBack.setOnClickListener { + goBackDir() + } + } + private fun initData() { - localUriLiveData?.removeObservers(this) - localUriLiveData = App.db.bookDao().observeLocalUri() - localUriLiveData?.observe(this, { - adapter.upBookHas(it) - }) + launch { + appDb.bookDao.flowLocalUri().collect { + adapter.upBookHas(it) + } + } } - private fun upRootDoc() { - AppConfig.importBookPath?.let { - if (it.isContentPath()) { - val rootUri = Uri.parse(it) - rootDoc = DocumentFile.fromTreeUri(this, rootUri) - subDocs.clear() - } else { - rootDoc = null - subDocs.clear() - path = it + private fun initRootDoc() { + val lastPath = AppConfig.importBookPath + when { + lastPath.isNullOrEmpty() -> { + binding.tvEmptyMsg.visible() + selectFolder.launch(null) } - } ?: let { - // 没有权限就显示一个授权提示和按钮 - if (PermissionsCompat.check(this, *Permissions.Group.STORAGE)) { - hint_per.visibility = View.GONE - } else { - hint_per.visibility = View.VISIBLE - tv_request_per.onClick { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - hint_per.visibility = View.GONE - initData() - upRootDoc() - } - .request() + lastPath.isContentScheme() -> { + val rootUri = Uri.parse(lastPath) + rootDoc = DocumentFile.fromTreeUri(this, rootUri) + if (rootDoc == null) { + binding.tvEmptyMsg.visible() + selectFolder.launch(null) + } else { + subDocs.clear() + upPath() } } + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> { + binding.tvEmptyMsg.visible() + selectFolder.launch(null) + } + else -> { + binding.tvEmptyMsg.visible() + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + rootDoc = null + subDocs.clear() + path = lastPath + upPath() + } + .request() + } } - upPath() } - @SuppressLint("SetTextI18n") @Synchronized private fun upPath() { - rootDoc?.let { rootDoc -> - var path = rootDoc.name.toString() + File.separator - var lastDoc = rootDoc - for (doc in subDocs) { - lastDoc = doc - path = path + doc.name + File.separator - } - tv_path.text = path - adapter.selectedUris.clear() - adapter.clearItems() - launch(IO) { - val docList = DocumentUtils.listFiles( - this@ImportBookActivity, - lastDoc.uri - ) - for (i in docList.lastIndex downTo 0) { - val item = docList[i] - if (item.name.startsWith(".")) { - docList.removeAt(i) - } else if (!item.isDir - && !item.name.endsWith(".txt", true) - && !item.name.endsWith(".epub", true) - ) { - docList.removeAt(i) - } - } - docList.sortWith(compareBy({ !it.isDir }, { it.name })) - withContext(Main) { - adapter.setData(docList) + rootDoc?.let { + upDocs(it) + } ?: upFiles() + } + + private fun upDocs(rootDoc: DocumentFile) { + binding.tvEmptyMsg.gone() + var path = rootDoc.name.toString() + File.separator + var lastDoc = rootDoc + for (doc in subDocs) { + lastDoc = doc + path = path + doc.name + File.separator + } + binding.tvPath.text = path + adapter.selectedUris.clear() + adapter.clearItems() + launch(IO) { + val docList = DocumentUtils.listFiles(this@ImportBookActivity, lastDoc.uri) + for (i in docList.lastIndex downTo 0) { + val item = docList[i] + if (item.name.startsWith(".")) { + docList.removeAt(i) + } else if (!item.isDir + && !item.name.endsWith(".txt", true) + && !item.name.endsWith(".epub", true) + && !item.name.endsWith(".umd", true) + ) { + docList.removeAt(i) } } - } ?: let { - tv_path.text = path.replace(sdPath, "SD") - val docList = arrayListOf() - File(path).listFiles()?.forEach { - if (it.isDirectory) { - if (!it.name.startsWith(".")) - docList.add( - DocItem( - it.name, - DocumentsContract.Document.MIME_TYPE_DIR, - it.length(), - Date(it.lastModified()), - Uri.parse(it.absolutePath) - ) - ) - } else if (it.name.endsWith(".txt", true) - || it.name.endsWith(".epub", true) - ) { + docList.sortWith(compareBy({ !it.isDir }, { it.name })) + withContext(Main) { + adapter.setItems(docList) + } + } + } + + private fun upFiles() { + binding.tvEmptyMsg.gone() + binding.tvPath.text = path.replace(sdPath, "SD") + val docList = arrayListOf() + File(path).listFiles()?.forEach { + if (it.isDirectory) { + if (!it.name.startsWith(".")) docList.add( DocItem( it.name, - it.extension, + DocumentsContract.Document.MIME_TYPE_DIR, it.length(), Date(it.lastModified()), - Uri.parse(it.absolutePath) + Uri.fromFile(it) ) ) - } + } else if (it.name.endsWith(".txt", true) + || it.name.endsWith(".epub", true) + || it.name.endsWith(".umd", true) + ) { + docList.add( + DocItem( + it.name, + it.extension, + it.length(), + Date(it.lastModified()), + Uri.fromFile(it) + ) + ) } - docList.sortWith(compareBy({ !it.isDir }, { it.name })) - adapter.setData(docList) } + docList.sortWith(compareBy({ !it.isDir }, { it.name })) + adapter.setItems(docList) } - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - requestCodeSelectFolder -> { - AppConfig.importBookPath = currentPath - upRootDoc() + /** + * 扫描当前文件夹 + */ + private fun scanFolder() { + rootDoc?.let { doc -> + adapter.clearItems() + val lastDoc = subDocs.lastOrNull() ?: doc + binding.refreshProgressBar.isAutoLoading = true + launch(IO) { + viewModel.scanDoc(lastDoc, true, find) { + launch { + binding.refreshProgressBar.isAutoLoading = false + } + } + } + } ?: let { + val lastPath = AppConfig.importBookPath + if (lastPath.isNullOrEmpty()) { + toastOnUi(R.string.empty_msg_import_book) + } else { + adapter.clearItems() + val file = File(path) + binding.refreshProgressBar.isAutoLoading = true + launch(IO) { + viewModel.scanFile(file, true, find) { + launch { + binding.refreshProgressBar.isAutoLoading = false + } + } + } } } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - requestCodeSelectFolder -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { - contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - AppConfig.importBookPath = it.toString() - upRootDoc() - } + private fun alertImportFileName() { + alert(R.string.import_file_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setText(AppConfig.bookImportFileName) + } + customView { alertBinding.root } + okButton { + AppConfig.bookImportFileName = alertBinding.editView.text?.toString() } + cancelButton() + }.show() + } + + private val find: (docItem: DocItem) -> Unit = { + launch { + adapter.addItem(it) } } @Synchronized override fun nextDoc(uri: Uri) { - if (uri.toString().isContentPath()) { + if (uri.toString().isContentScheme()) { subDocs.add(DocumentFile.fromSingleUri(this, uri)!!) } else { - path = uri.toString() + path = uri.path.toString() } upPath() } @@ -282,7 +334,7 @@ class ImportBookActivity : VMBaseActivity(R.layout.activity } override fun upCountView() { - select_action_bar.upCountView(adapter.selectedUris.size, adapter.checkableCount) + binding.selectActionBar.upCountView(adapter.selectedUris.size, adapter.checkableCount) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt index 3e7ab7097..86896c35b 100644 --- a/app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt @@ -2,37 +2,97 @@ package io.legado.app.ui.book.local import android.content.Context import android.net.Uri +import android.view.ViewGroup import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.constant.AppConst +import io.legado.app.databinding.ItemImportBookBinding import io.legado.app.utils.* -import kotlinx.android.synthetic.main.item_import_book.view.* -import org.jetbrains.anko.sdk27.listeners.onClick class ImportBookAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_import_book) { + RecyclerAdapter(context) { var selectedUris = hashSetOf() var checkableCount = 0 - private var bookshelf = arrayListOf() + private var bookFileNames = arrayListOf() - fun upBookHas(uriList: List) { - bookshelf.clear() - bookshelf.addAll(uriList) - notifyDataSetChanged() + override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding { + return ItemImportBookBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { upCheckableCount() } - fun setData(data: List) { - setItems(data) + override fun convert( + holder: ItemViewHolder, + binding: ItemImportBookBinding, + item: DocItem, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + if (item.isDir) { + ivIcon.setImageResource(R.drawable.ic_folder) + ivIcon.visible() + cbSelect.invisible() + llBrief.gone() + cbSelect.isChecked = false + } else { + if (bookFileNames.contains(item.name)) { + ivIcon.setImageResource(R.drawable.ic_book_has) + ivIcon.visible() + cbSelect.invisible() + } else { + ivIcon.invisible() + cbSelect.visible() + } + llBrief.visible() + tvTag.text = item.name.substringAfterLast(".") + tvSize.text = StringUtils.toSize(item.size) + tvDate.text = AppConst.dateFormat.format(item.date) + cbSelect.isChecked = selectedUris.contains(item.uri.toString()) + } + tvName.text = item.name + } else { + cbSelect.isChecked = selectedUris.contains(item.uri.toString()) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemImportBookBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (it.isDir) { + callBack.nextDoc(it.uri) + } else if (!bookFileNames.contains(it.name)) { + if (!selectedUris.contains(it.uri.toString())) { + selectedUris.add(it.uri.toString()) + } else { + selectedUris.remove(it.uri.toString()) + } + notifyItemChanged(holder.layoutPosition, true) + callBack.upCountView() + } + } + } + } + + fun upBookHas(bookUrls: List) { + bookFileNames.clear() + bookUrls.forEach { + val path = Uri.decode(it) + bookFileNames.add(FileUtils.getName(path)) + } + notifyDataSetChanged() upCheckableCount() } private fun upCheckableCount() { checkableCount = 0 getItems().forEach { - if (!it.isDir && !bookshelf.contains(it.uri.toString())) { + if (!it.isDir && !bookFileNames.contains(it.name)) { checkableCount++ } } @@ -42,7 +102,7 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) : fun selectAll(selectAll: Boolean) { if (selectAll) { getItems().forEach { - if (!it.isDir && !bookshelf.contains(it.uri.toString())) { + if (!it.isDir && !bookFileNames.contains(it.name)) { selectedUris.add(it.uri.toString()) } } @@ -66,51 +126,10 @@ class ImportBookAdapter(context: Context, val callBack: CallBack) : callBack.upCountView() } - override fun convert(holder: ItemViewHolder, item: DocItem, payloads: MutableList) { - holder.itemView.apply { - if (payloads.isEmpty()) { - if (item.isDir) { - iv_icon.setImageResource(R.drawable.ic_folder) - iv_icon.visible() - cb_select.invisible() - ll_brief.gone() - cb_select.isChecked = false - } else { - if (bookshelf.contains(item.uri.toString())) { - iv_icon.setImageResource(R.drawable.ic_book_has) - iv_icon.visible() - cb_select.invisible() - } else { - iv_icon.invisible() - cb_select.visible() - } - ll_brief.visible() - tv_tag.text = item.name.substringAfterLast(".") - tv_size.text = StringUtils.toSize(item.size) - tv_date.text = AppConst.dateFormat.format(item.date) - cb_select.isChecked = selectedUris.contains(item.uri.toString()) - } - tv_name.text = item.name - } else { - cb_select.isChecked = selectedUris.contains(item.uri.toString()) - } - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { - getItem(holder.layoutPosition)?.let { - if (it.isDir) { - callBack.nextDoc(it.uri) - } else if (!bookshelf.contains(it.uri.toString())) { - if (!selectedUris.contains(it.uri.toString())) { - selectedUris.add(it.uri.toString()) - } else { - selectedUris.remove(it.uri.toString()) - } - notifyItemChanged(holder.layoutPosition, true) - callBack.upCountView() - } + fun removeSelection() { + for (i in getItems().lastIndex downTo 0) { + if (getItem(i)?.uri.toString() in selectedUris) { + removeItem(i) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt index d3be59c5c..b7f1243c7 100644 --- a/app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt @@ -5,6 +5,11 @@ import android.net.Uri import androidx.documentfile.provider.DocumentFile import io.legado.app.base.BaseViewModel import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.DocItem +import io.legado.app.utils.DocumentUtils +import io.legado.app.utils.isContentScheme +import java.io.File +import java.util.* class ImportBookViewModel(application: Application) : BaseViewModel(application) { @@ -12,7 +17,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) fun addToBookshelf(uriList: HashSet, finally: () -> Unit) { execute { uriList.forEach { - LocalBook.importFile(it) + LocalBook.importFile(Uri.parse(it)) } }.onFinally { finally.invoke() @@ -22,11 +27,69 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) fun deleteDoc(uriList: HashSet, finally: () -> Unit) { execute { uriList.forEach { - DocumentFile.fromSingleUri(context, Uri.parse(it))?.delete() + val uri = Uri.parse(it) + if (uri.isContentScheme()) { + DocumentFile.fromSingleUri(context, uri)?.delete() + } else { + uri.path?.let { path -> + File(path).delete() + } + } } }.onFinally { finally.invoke() } } + fun scanDoc( + documentFile: DocumentFile, + isRoot: Boolean, + find: (docItem: DocItem) -> Unit, + finally: (() -> Unit)? = null + ) { + val docList = DocumentUtils.listFiles(context, documentFile.uri) + docList.forEach { docItem -> + if (docItem.isDir) { + DocumentFile.fromSingleUri(context, docItem.uri)?.let { + scanDoc(it, false, find) + } + } else if (docItem.name.endsWith(".txt", true) + || docItem.name.endsWith(".epub", true) + ) { + find(docItem) + } + } + if (isRoot) { + finally?.invoke() + } + } + + fun scanFile( + file: File, + isRoot: Boolean, + find: (docItem: DocItem) -> Unit, + finally: (() -> Unit)? = null + ) { + file.listFiles()?.forEach { + if (it.isDirectory) { + scanFile(it, false, find) + } else if (it.name.endsWith(".txt", true) + || it.name.endsWith(".epub", true) + ) { + find( + DocItem( + it.name, + it.extension, + it.length(), + Date(it.lastModified()), + Uri.parse(it.absolutePath) + ) + ) + } + } + if (isRoot) { + finally?.invoke() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 7840123db..65bbfc910 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -4,68 +4,61 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.content.res.Configuration -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.* import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.get import androidx.core.view.isVisible import androidx.core.view.size import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import io.legado.app.BuildConfig import io.legado.app.R -import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.constant.Status import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.help.BookHelp +import io.legado.app.data.entities.BookProgress import io.legado.app.help.ReadBookConfig -import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.ReadTipConfig import io.legado.app.help.storage.Backup -import io.legado.app.help.storage.SyncBookProgress import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.lib.theme.ATH +import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.accentColor import io.legado.app.receiver.TimeBatteryReceiver import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.changesource.ChangeSourceDialog -import io.legado.app.ui.book.chapterlist.ChapterListActivity import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.read.config.* import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR import io.legado.app.ui.book.read.page.ContentTextView -import io.legado.app.ui.book.read.page.PageView -import io.legado.app.ui.book.read.page.TextPageFactory -import io.legado.app.ui.book.read.page.delegate.PageDelegate +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.provider.TextPageFactory +import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity -import io.legado.app.ui.login.SourceLogin -import io.legado.app.ui.replacerule.ReplaceRuleActivity -import io.legado.app.ui.replacerule.edit.ReplaceEditDialog +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.dict.DictDialog +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.replace.ReplaceRuleActivity +import io.legado.app.ui.replace.edit.ReplaceEditActivity import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_read.* -import kotlinx.android.synthetic.main.view_read_menu.* -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.toast -class ReadBookActivity : VMBaseActivity(R.layout.activity_book_read), +class ReadBookActivity : ReadBookBaseActivity(), View.OnTouchListener, - PageView.CallBack, + ReadView.CallBack, TextActionMenu.CallBack, ContentTextView.CallBack, ReadMenu.CallBack, @@ -74,47 +67,84 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo ReadBook.CallBack, AutoReadDialog.CallBack, TocRegexDialog.CallBack, - ReplaceEditDialog.CallBack, ColorPickerDialogListener { - private val requestCodeChapterList = 568 - private val requestCodeEditSource = 111 - private val requestCodeReplace = 312 - private var menu: Menu? = null - private var textActionMenu: TextActionMenu? = null - override val viewModel: ReadBookViewModel - get() = getViewModel(ReadBookViewModel::class.java) + private val tocActivity = + registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.openChapter(it.first, it.second) + } + } + private val sourceEditActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it ?: return@registerForActivityResult + if (it.resultCode == RESULT_OK) { + viewModel.upBookSource { + upView() + } + } + } + private val replaceActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it ?: return@registerForActivityResult + if (it.resultCode == RESULT_OK) { + viewModel.replaceRuleChanged() + } + } + private val searchContentActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it ?: return@registerForActivityResult + it.data?.let { data -> + data.getIntExtra("index", ReadBook.durChapterIndex).let { index -> + viewModel.searchContentQuery = data.getStringExtra("query") ?: "" + val indexWithinChapter = data.getIntExtra("indexWithinChapter", 0) + skipToSearch(index, indexWithinChapter) + } + } + } + private var menu: Menu? = null + val textActionMenu: TextActionMenu by lazy { + TextActionMenu(this, this) + } - override val scope: CoroutineScope get() = this override val isInitFinish: Boolean get() = viewModel.isInitFinish - - private val mHandler = Handler() - private val keepScreenRunnable: Runnable = - Runnable { ReadBookActivityHelp.keepScreenOn(window, false) } - private val autoPageRunnable: Runnable = Runnable { autoPagePlus() } + override val isScroll: Boolean get() = binding.readView.isScroll + private val mHandler = Handler(Looper.getMainLooper()) + private val keepScreenRunnable = Runnable { keepScreenOn(window, false) } + private val autoPageRunnable = Runnable { autoPagePlus() } + private val backupRunnable = Runnable { + if (!BuildConfig.DEBUG) { + ReadBook.uploadProgress() + Backup.autoBack(this) + } + } override var autoPageProgress = 0 override var isAutoPage = false private var screenTimeOut: Long = 0 private var timeBatteryReceiver: TimeBatteryReceiver? = null - override val pageFactory: TextPageFactory get() = page_view.pageFactory - override val headerHeight: Int get() = page_view.curPage.headerHeight - - override fun onCreate(savedInstanceState: Bundle?) { - ReadBook.msg = null - ReadBookActivityHelp.setOrientation(this) - super.onCreate(savedInstanceState) - } + private var loadStates: Boolean = false + override val pageFactory: TextPageFactory get() = binding.readView.pageFactory + override val headerHeight: Int get() = binding.readView.curPage.headerHeight + private val menuLayoutIsVisible get() = bottomDialog > 0 || binding.readMenu.isVisible + @SuppressLint("ClickableViewAccessibility") override fun onActivityCreated(savedInstanceState: Bundle?) { - ReadBookActivityHelp.upLayoutInDisplayCutoutMode(window) - initView() + super.onActivityCreated(savedInstanceState) + binding.cursorLeft.setColorFilter(accentColor) + binding.cursorRight.setColorFilter(accentColor) + binding.cursorLeft.setOnTouchListener(this) + binding.cursorRight.setOnTouchListener(this) upScreenTimeOut() ReadBook.callBack = this ReadBook.titleDate.observe(this) { - title_bar.title = it + binding.readMenu.setTitle(it) upMenu() upView() } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) viewModel.initData(intent) } @@ -125,8 +155,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - page_view.upStatusBar() - ReadBook.loadContent(resetPageOffset = false) + binding.readView.upStatusBar() } override fun onResume() { @@ -134,11 +163,13 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo ReadBook.readStartTime = System.currentTimeMillis() upSystemUiVisibility() timeBatteryReceiver = TimeBatteryReceiver.register(this) - page_view.upTime() + binding.readView.upTime() } override fun onPause() { super.onPause() + autoPageStop() + mHandler.removeCallbacks(backupRunnable) ReadBook.saveRead() timeBatteryReceiver?.let { unregisterReceiver(it) @@ -146,62 +177,13 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } upSystemUiVisibility() if (!BuildConfig.DEBUG) { - SyncBookProgress.uploadBookProgress() + ReadBook.uploadProgress() Backup.autoBack(this) } } - override fun upNavigationBarColor() { - when { - read_menu == null -> return - read_menu.isVisible -> { - ATH.setNavigationBarColorAuto(this) - } - ReadBookConfig.bg is ColorDrawable -> { - ATH.setNavigationBarColorAuto(this, ReadBookConfig.bgMeanColor) - } - else -> { - ATH.setNavigationBarColorAuto(this, Color.BLACK) - } - } - } - - /** - * 初始化View - */ - private fun initView() { - cursor_left.setColorFilter(accentColor) - cursor_right.setColorFilter(accentColor) - cursor_left.setOnTouchListener(this) - cursor_right.setOnTouchListener(this) - tv_chapter_name.onClick { - ReadBook.webBook?.let { - startActivityForResult( - requestCodeEditSource, - Pair("data", it.bookSource.bookSourceUrl) - ) - } - } - tv_chapter_url.onClick { - runCatching { - val url = tv_chapter_url.text.toString() - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(url) - startActivity(intent) - } - } - } - - fun showPaddingConfig() { - PaddingConfigDialog().show(supportFragmentManager, "paddingConfig") - } - - fun showBgTextConfig() { - BgTextConfigDialog().show(supportFragmentManager, "bgTextConfig") - } - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.read_book, menu) + menuInflater.inflate(R.menu.book_read, menu) return super.onCompatCreateOptionsMenu(menu) } @@ -211,21 +193,25 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.onPrepareOptionsMenu(menu) } + /** + * 更新菜单 + */ private fun upMenu() { - menu?.let { menu -> - ReadBook.book?.let { book -> - val onLine = !book.isLocalBook() - for (i in 0 until menu.size) { - val item = menu[i] - when (item.groupId) { - R.id.menu_group_on_line -> item.isVisible = onLine - R.id.menu_group_local -> item.isVisible = !onLine - R.id.menu_group_text -> item.isVisible = book.isLocalTxt() - R.id.menu_group_login -> - item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() - else -> if (item.itemId == R.id.menu_enable_replace) { - item.isChecked = book.useReplaceRule - } + val menu = menu + val book = ReadBook.book + if (menu != null && book != null) { + val onLine = !book.isLocalBook() + for (i in 0 until menu.size) { + val item = menu[i] + when (item.groupId) { + R.id.menu_group_on_line, + R.id.menu_group_on_line_ns -> item.isVisible = onLine + R.id.menu_group_local -> item.isVisible = !onLine + R.id.menu_group_text -> item.isVisible = book.isLocalTxt() + else -> when (item.itemId) { + R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() + R.id.menu_re_segment -> item.isChecked = book.getReSegment() + R.id.menu_reverse_content -> item.isVisible = onLine } } } @@ -238,47 +224,85 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_change_source -> { - read_menu.runMenuOut() + binding.readMenu.runMenuOut() ReadBook.book?.let { ChangeSourceDialog.show(supportFragmentManager, it.name, it.author) } } R.id.menu_refresh -> { - ReadBook.book?.let { - ReadBook.curTextChapter = null - page_view.upContent() - viewModel.refreshContent(it) + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + ReadBook.curTextChapter = null + binding.readView.upContent() + viewModel.refreshContent(it) + } + } + } + R.id.menu_download -> showDownloadDialog() + R.id.menu_add_bookmark -> { + val book = ReadBook.book + val page = ReadBook.curTextChapter?.page(ReadBook.durPageIndex()) + if (book != null && page != null) { + val bookmark = book.createBookMark().apply { + chapterIndex = ReadBook.durChapterIndex + chapterPos = ReadBook.durChapterPos + chapterName = page.title + bookText = page.text.trim() + } + showBookMark(bookmark) } } - R.id.menu_download -> ReadBookActivityHelp.showDownloadDialog(this) - R.id.menu_add_bookmark -> ReadBookActivityHelp.showBookMark(this) R.id.menu_copy_text -> TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) R.id.menu_update_toc -> ReadBook.book?.let { loadChapterList(it) } R.id.menu_enable_replace -> ReadBook.book?.let { - it.useReplaceRule = !it.useReplaceRule - menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.useReplaceRule - onReplaceRuleSave() + it.setUseReplaceRule(!it.getUseReplaceRule()) + menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule() + viewModel.replaceRuleChanged() + } + R.id.menu_re_segment -> ReadBook.book?.let { + it.setReSegment(!it.getReSegment()) + menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment() + ReadBook.loadContent(false) + } + R.id.menu_page_anim -> showPageAnimConfig { + binding.readView.upPageAnim() } R.id.menu_book_info -> ReadBook.book?.let { - startActivity( - Pair("name", it.name), - Pair("author", it.author) - ) + startActivity { + putExtra("name", it.name) + putExtra("author", it.author) + } } R.id.menu_toc_regex -> TocRegexDialog.show( supportFragmentManager, ReadBook.book?.tocUrl ) - R.id.menu_login -> ReadBook.webBook?.bookSource?.let { - startActivity( - Pair("sourceUrl", it.bookSourceUrl), - Pair("loginUrl", it.loginUrl) - ) + R.id.menu_reverse_content -> ReadBook.book?.let { + viewModel.reverseContent(it) + } + R.id.menu_set_charset -> showCharsetConfig() + R.id.menu_image_style -> { + val imgStyles = + arrayListOf(Book.imgStyleDefault, Book.imgStyleFull, Book.imgStyleText) + selector( + R.string.image_style, + imgStyles + ) { _, index -> + ReadBook.book?.setImageStyle(imgStyles[index]) + ReadBook.loadContent(false) + } } - R.id.menu_set_charset -> ReadBookActivityHelp.showCharsetConfig(this) + R.id.menu_get_progress -> ReadBook.book?.let { + viewModel.syncBookProgress(it) { progress -> + sureSyncProgress(progress) + } + } + R.id.menu_help -> showReadMenuHelp() } return super.onCompatOptionsItemSelected(item) } @@ -292,12 +316,12 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo val isDown = action == 0 if (keyCode == KeyEvent.KEYCODE_MENU) { - if (isDown && !read_menu.cnaShowMenu) { - read_menu.runMenuIn() + if (isDown && !binding.readMenu.cnaShowMenu) { + binding.readMenu.runMenuIn() return true } - if (!isDown && !read_menu.cnaShowMenu) { - read_menu.cnaShowMenu = true + if (!isDown && !binding.readMenu.cnaShowMenu) { + binding.readMenu.cnaShowMenu = true return true } } @@ -308,31 +332,42 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 按键事件 */ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - when (keyCode) { - getPrefInt(PreferKey.prevKey) -> { + if (menuLayoutIsVisible) { + return super.onKeyDown(keyCode, event) + } + when { + isPrevKey(keyCode) -> { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) + binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV) return true } } - getPrefInt(PreferKey.nextKey) -> { + isNextKey(keyCode) -> { if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) return true } } - KeyEvent.KEYCODE_VOLUME_UP -> { - if (volumeKeyPage(PageDelegate.Direction.PREV)) { + keyCode == KeyEvent.KEYCODE_VOLUME_UP -> { + if (volumeKeyPage(PageDirection.PREV)) { return true } } - KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDelegate.Direction.NEXT)) { + keyCode == KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (volumeKeyPage(PageDirection.NEXT)) { return true } } - KeyEvent.KEYCODE_SPACE -> { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + keyCode == KeyEvent.KEYCODE_PAGE_UP -> { + binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV) + return true + } + keyCode == KeyEvent.KEYCODE_PAGE_DOWN -> { + binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) + return true + } + keyCode == KeyEvent.KEYCODE_SPACE -> { + binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) return true } } @@ -358,7 +393,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { - if (volumeKeyPage(PageDelegate.Direction.NONE)) { + if (volumeKeyPage(PageDirection.NONE)) { return true } } @@ -370,7 +405,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo ) { if (BaseReadAloudService.isPlay()) { ReadAloud.pause(this) - toast(R.string.read_aloud_pause) + toastOnUi(R.string.read_aloud_pause) return true } if (isAutoPage) { @@ -388,18 +423,18 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * view触摸,文字选择 */ @SuppressLint("ClickableViewAccessibility") - override fun onTouch(v: View, event: MotionEvent): Boolean { + override fun onTouch(v: View, event: MotionEvent): Boolean = binding.run { when (event.action) { - MotionEvent.ACTION_DOWN -> textActionMenu?.dismiss() + MotionEvent.ACTION_DOWN -> textActionMenu.dismiss() MotionEvent.ACTION_MOVE -> { when (v.id) { - R.id.cursor_left -> page_view.curPage.selectStartMove( - event.rawX + cursor_left.width, - event.rawY - cursor_left.height + R.id.cursor_left -> readView.curPage.selectStartMove( + event.rawX + cursorLeft.width, + event.rawY - cursorLeft.height ) - R.id.cursor_right -> page_view.curPage.selectEndMove( - event.rawX - cursor_right.width, - event.rawY - cursor_right.height + R.id.cursor_right -> readView.curPage.selectEndMove( + event.rawX - cursorRight.width, + event.rawY - cursorRight.height ) } } @@ -411,68 +446,77 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo /** * 更新文字选择开始位置 */ - override fun upSelectedStart(x: Float, y: Float, top: Float) { - cursor_left.x = x - cursor_left.width - cursor_left.y = y - cursor_left.visible(true) - text_menu_position.x = x - text_menu_position.y = top + override fun upSelectedStart(x: Float, y: Float, top: Float) = binding.run { + cursorLeft.x = x - cursorLeft.width + cursorLeft.y = y + cursorLeft.visible(true) + textMenuPosition.x = x + textMenuPosition.y = top } /** * 更新文字选择结束位置 */ - override fun upSelectedEnd(x: Float, y: Float) { - cursor_right.x = x - cursor_right.y = y - cursor_right.visible(true) + override fun upSelectedEnd(x: Float, y: Float) = binding.run { + cursorRight.x = x + cursorRight.y = y + cursorRight.visible(true) } /** * 取消文字选择 */ - override fun onCancelSelect() { - cursor_left.invisible() - cursor_right.invisible() - textActionMenu?.dismiss() + override fun onCancelSelect() = binding.run { + cursorLeft.invisible() + cursorRight.invisible() + textActionMenu.dismiss() } /** * 显示文本操作菜单 */ - override fun showTextActionMenu() { - textActionMenu ?: let { - textActionMenu = TextActionMenu(this, this) - } - textActionMenu?.let { popup -> - popup.contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) - val popupHeight = popup.contentView.measuredHeight - val x = text_menu_position.x.toInt() - var y = text_menu_position.y.toInt() - popupHeight - if (y < statusBarHeight) { - y = (cursor_left.y + cursor_left.height).toInt() - } - if (cursor_right.y > y && cursor_right.y < y + popupHeight) { - y = (cursor_right.y + cursor_right.height).toInt() - } - if (!popup.isShowing) { - popup.showAtLocation(text_menu_position, Gravity.TOP or Gravity.START, x, y) - } else { - popup.update(x, y, WRAP_CONTENT, WRAP_CONTENT) - } + override fun showTextActionMenu() = binding.run { + textActionMenu.contentView.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED + ) + val popupHeight = textActionMenu.contentView.measuredHeight + val x = textMenuPosition.x.toInt() + var y = textMenuPosition.y.toInt() - popupHeight + if (y < statusBarHeight) { + y = (cursorLeft.y + cursorLeft.height).toInt() + } + if (cursorRight.y > y && cursorRight.y < y + popupHeight) { + y = (cursorRight.y + cursorRight.height).toInt() + } + if (!textActionMenu.isShowing) { + textActionMenu.showAtLocation( + textMenuPosition, Gravity.TOP or Gravity.START, x, y + ) + } else { + textActionMenu.update(x, y, WRAP_CONTENT, WRAP_CONTENT) } } /** * 当前选择的文本 */ - override val selectedText: String get() = page_view.curPage.selectedText + override val selectedText: String get() = binding.readView.curPage.selectedText /** * 文本选择菜单操作 */ override fun onMenuItemSelected(itemId: Int): Boolean { when (itemId) { + R.id.menu_bookmark -> binding.readView.curPage.let { + val bookmark = it.createBookmark() + if (bookmark == null) { + toastOnUi(R.string.create_bookmark_error) + } else { + showBookMark(bookmark) + } + return true + } R.id.menu_replace -> { val scopes = arrayListOf() ReadBook.book?.name?.let { @@ -481,13 +525,24 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo ReadBook.bookSource?.bookSourceUrl?.let { scopes.add(it) } - ReplaceEditDialog.show( - supportFragmentManager, - pattern = selectedText, - scope = scopes.joinToString(";") + replaceActivity.launch( + ReplaceEditActivity.startIntent( + this, + pattern = selectedText, + scope = scopes.joinToString(";") + ) ) return true } + R.id.menu_search_content -> { + viewModel.searchContentQuery = selectedText + openSearchActivity(selectedText) + return true + } + R.id.menu_dict -> { + DictDialog.dict(supportFragmentManager, selectedText) + return true + } } return false } @@ -495,23 +550,23 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo /** * 文本选择菜单操作完成 */ - override fun onMenuActionFinally() { - textActionMenu?.dismiss() - page_view.curPage.cancelSelect() - page_view.isTextSelected = false + override fun onMenuActionFinally() = binding.run { + textActionMenu.dismiss() + readView.curPage.cancelSelect() + readView.isTextSelected = false } /** * 音量键翻页 */ - private fun volumeKeyPage(direction: PageDelegate.Direction): Boolean { - if (!read_menu.isVisible) { + private fun volumeKeyPage(direction: PageDirection): Boolean { + if (!binding.readMenu.isVisible) { if (getPrefBoolean("volumeKeyPage", true)) { if (getPrefBoolean("volumeKeyPageOnPlay") || BaseReadAloudService.pause ) { - page_view.pageDelegate?.isCancel = false - page_view.pageDelegate?.keyTurnPage(direction) + binding.readView.pageDelegate?.isCancel = false + binding.readView.pageDelegate?.keyTurnPage(direction) return true } } @@ -532,16 +587,23 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo intent.removeExtra("readAloud") ReadBook.readAloud() } + loadStates = true } /** * 更新内容 */ - override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { - autoPageProgress = 0 + override fun upContent( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) { launch { - page_view.upContent(relativePosition, resetPageOffset) - seek_read_page.progress = ReadBook.durPageIndex + autoPageProgress = 0 + binding.readView.upContent(relativePosition, resetPageOffset) + binding.readMenu.setSeekPage(ReadBook.durPageIndex()) + loadStates = false + success?.invoke() } } @@ -550,23 +612,13 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo */ override fun upView() { launch { - ReadBook.curTextChapter?.let { - tv_chapter_name.text = it.title - tv_chapter_name.visible() - if (!ReadBook.isLocalBook) { - tv_chapter_url.text = it.url - tv_chapter_url.visible() - } else { - tv_chapter_url.gone() - } - seek_read_page.max = it.pageSize.minus(1) - seek_read_page.progress = ReadBook.durPageIndex - tv_pre.isEnabled = ReadBook.durChapterIndex != 0 - tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 - } ?: let { - tv_chapter_name.gone() - tv_chapter_url.gone() - } + binding.readMenu.upBookView() + } + } + + override fun upPageAnim() { + launch { + binding.readView.upPageAnim() } } @@ -574,9 +626,10 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 页面改变 */ override fun pageChanged() { - autoPageProgress = 0 launch { - seek_read_page.progress = ReadBook.durPageIndex + autoPageProgress = 0 + binding.readMenu.setSeekPage(ReadBook.durPageIndex()) + mHandler.postDelayed(backupRunnable, 600000) } } @@ -584,7 +637,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 显示菜单 */ override fun showMenuBar() { - read_menu.runMenuIn() + binding.readMenu.runMenuIn() } override val oldBook: Book? @@ -594,7 +647,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo viewModel.changeTo(book) } - override fun clickCenter() { + override fun showActionMenu() { when { BaseReadAloudService.isRun -> { showReadAloudDialog() @@ -603,11 +656,16 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo AutoReadDialog().show(supportFragmentManager, "autoRead") } else -> { - read_menu.runMenuIn() + binding.readMenu.runMenuIn() } } } + override fun showReadMenuHelp() { + val text = String(assets.open("help/readMenuHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) + } + /** * 显示朗读菜单 */ @@ -619,40 +677,66 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 自动翻页 */ override fun autoPage() { + ReadAloud.stop(this) if (isAutoPage) { autoPageStop() } else { isAutoPage = true - page_view.upContent() - page_view.upContent(1) + binding.readView.upContent() + binding.readView.upContent(1) autoPagePlus() + binding.readMenu.setAutoPage(true) } - read_menu.setAutoPage(isAutoPage) } override fun autoPageStop() { isAutoPage = false mHandler.removeCallbacks(autoPageRunnable) - page_view.upContent() + binding.readView.upContent() + binding.readMenu.setAutoPage(false) } private fun autoPagePlus() { + var delayMillis = ReadBookConfig.autoReadSpeed * 1000L / binding.readView.height + var scrollOffset = 1 + if (delayMillis < 20) { + var delayInt=delayMillis.toInt() + if(delayInt==0)delayInt =1 + scrollOffset = 20 / delayInt + delayMillis = 20 + } mHandler.removeCallbacks(autoPageRunnable) - autoPageProgress++ - if (autoPageProgress >= ReadBookConfig.autoReadSpeed * 10) { - autoPageProgress = 0 - page_view.fillPage(PageDelegate.Direction.NEXT) - } else { - page_view.invalidate() + if (!menuLayoutIsVisible) { + if (binding.readView.isScroll) { + binding.readView.curPage.scroll(-scrollOffset) + } else { + autoPageProgress += scrollOffset + if (autoPageProgress >= binding.readView.height) { + autoPageProgress = 0 + if (!binding.readView.fillPage(PageDirection.NEXT)) { + autoPageStop() + } + } else { + binding.readView.invalidate() + } + } + } + mHandler.postDelayed(autoPageRunnable, delayMillis) + } + + override fun openSourceEditActivity() { + ReadBook.webBook?.let { + sourceEditActivity.launch(Intent(this, BookSourceEditActivity::class.java).apply { + putExtra("data", it.bookSource.bookSourceUrl) + }) } - mHandler.postDelayed(autoPageRunnable, 100) } /** * 替换 */ override fun openReplaceRule() { - startActivityForResult(requestCodeReplace) + replaceActivity.launch(Intent(this, ReplaceRuleActivity::class.java)) } /** @@ -660,20 +744,19 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo */ override fun openChapterList() { ReadBook.book?.let { - startActivityForResult( - requestCodeChapterList, - Pair("bookUrl", it.bookUrl) - ) + tocActivity.launch(it.bookUrl) } } /** - * 替换规则变化 + * 打开搜索界面 */ - override fun onReplaceRuleSave() { - Coroutine.async { - BookHelp.upReplaceRules() - ReadBook.loadContent(resetPageOffset = false) + override fun openSearchActivity(searchWord: String?) { + ReadBook.book?.let { + searchContentActivity.launch(Intent(this, SearchContentActivity::class.java).apply { + putExtra("bookUrl", it.bookUrl) + putExtra("searchWord", searchWord ?: viewModel.searchContentQuery) + }) } } @@ -695,14 +778,25 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 更新状态栏,导航栏 */ override fun upSystemUiVisibility() { - ReadBookActivityHelp.upSystemUiVisibility(window, isInMultiWindow, !read_menu.isVisible) + upSystemUiVisibility(isInMultiWindow, !binding.readMenu.isVisible) upNavigationBarColor() } + override fun showLogin() { + ReadBook.webBook?.bookSource?.let { + startActivity { + putExtra("sourceUrl", it.bookSourceUrl) + putExtra("loginUrl", it.loginUrl) + putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME]) + } + } + } + /** * 朗读按钮 */ override fun onClickReadAloud() { + autoPageStop() when { !BaseReadAloudService.isRun -> ReadBook.readAloud() BaseReadAloudService.pause -> ReadAloud.resume(this) @@ -713,17 +807,22 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo /** * colorSelectDialog */ - override fun onColorSelected(dialogId: Int, color: Int) = with(ReadBookConfig.durConfig) { + override fun onColorSelected(dialogId: Int, color: Int) = ReadBookConfig.durConfig.run { when (dialogId) { TEXT_COLOR -> { - setTextColor(color) + setCurTextColor(color) postEvent(EventBus.UP_CONFIG, false) } BG_COLOR -> { - setBg(0, "#${color.hexString}") + setCurBg(0, "#${color.hexString}") ReadBookConfig.upBg() postEvent(EventBus.UP_CONFIG, false) } + TIP_COLOR -> { + ReadTipConfig.tipColor = color + postEvent(EventBus.TIP_COLOR, "") + postEvent(EventBus.UP_CONFIG, true) + } } } @@ -739,18 +838,46 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == Activity.RESULT_OK) { - when (requestCode) { - requestCodeEditSource -> viewModel.upBookSource() - requestCodeChapterList -> - data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> - if (index != ReadBook.durChapterIndex) { - viewModel.openChapter(index) - } + private fun sureSyncProgress(progress: BookProgress) { + alert(R.string.get_book_progress) { + setMessage(R.string.current_progress_exceeds_cloud) + okButton { + ReadBook.setProgress(progress) + } + noButton() + }.show() + } + + private fun skipToSearch(index: Int, indexWithinChapter: Int) { + viewModel.openChapter(index) { + val pages = ReadBook.curTextChapter?.pages ?: return@openChapter + val positions = ReadBook.searchResultPositions( + pages, + indexWithinChapter, + viewModel.searchContentQuery + ) + ReadBook.skipToPage(positions[0]) { + launch { + binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) + delay(20L) + when (positions[3]) { + 0 -> binding.readView.curPage.selectEndMoveIndex( + 0, + positions[1], + positions[2] + viewModel.searchContentQuery.length - 1 + ) + 1 -> binding.readView.curPage.selectEndMoveIndex( + 0, + positions[1] + 1, + positions[4] + ) + //consider change page, jump to scroll position + -1 -> binding.readView.curPage + .selectEndMoveIndex(1, 0, positions[4]) } - requestCodeReplace -> onReplaceRuleSave() + binding.readView.isTextSelected = true + delay(100L) + } } } } @@ -758,14 +885,14 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun finish() { ReadBook.book?.let { if (!ReadBook.inBookshelf) { - this.alert(title = getString(R.string.add_to_shelf)) { - message = getString(R.string.check_add_bookshelf, it.name) + alert(title = getString(R.string.add_to_shelf)) { + setMessage(getString(R.string.check_add_bookshelf, it.name)) okButton { ReadBook.inBookshelf = true setResult(Activity.RESULT_OK) } noButton { viewModel.removeFromBookshelf { super.finish() } } - }.show().applyTint() + }.show() } else { super.finish() } @@ -775,22 +902,21 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo override fun onDestroy() { super.onDestroy() mHandler.removeCallbacks(keepScreenRunnable) - textActionMenu?.dismiss() - page_view.onDestroy() + textActionMenu.dismiss() + binding.readView.onDestroy() ReadBook.msg = null if (!BuildConfig.DEBUG) { - SyncBookProgress.uploadBookProgress() Backup.autoBack(this) } } - override fun observeLiveBus() { + override fun observeLiveBus() = binding.run { super.observeLiveBus() - observeEvent(EventBus.TIME_CHANGED) { page_view.upTime() } - observeEvent(EventBus.BATTERY_CHANGED) { page_view.upBattery(it) } + observeEvent(EventBus.TIME_CHANGED) { readView.upTime() } + observeEvent(EventBus.BATTERY_CHANGED) { readView.upBattery(it) } observeEvent(EventBus.OPEN_CHAPTER) { - viewModel.openChapter(it.index, ReadBook.durPageIndex) - page_view.upContent() + viewModel.openChapter(it.index, ReadBook.durChapterPos) + readView.upContent() } observeEvent(EventBus.MEDIA_BUTTON) { if (it) { @@ -801,22 +927,21 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } observeEvent(EventBus.UP_CONFIG) { upSystemUiVisibility() - page_view.upBg() - page_view.upTipStyle() - page_view.upStyle() + readView.upBg() + readView.upStyle() if (it) { ReadBook.loadContent(resetPageOffset = false) } else { - page_view.upContent(resetPageOffset = false) + readView.upContent(resetPageOffset = false) } } observeEvent(EventBus.ALOUD_STATE) { if (it == Status.STOP || it == Status.PAUSE) { ReadBook.curTextChapter?.let { textChapter -> - val page = textChapter.page(ReadBook.durPageIndex) + val page = textChapter.getPageByReadPos(ReadBook.durChapterPos) if (page != null) { page.removePageAloudSpan() - page_view.upContent(resetPageOffset = false) + readView.upContent(resetPageOffset = false) } } } @@ -825,9 +950,9 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo launch(IO) { if (BaseReadAloudService.isPlay()) { ReadBook.curTextChapter?.let { textChapter -> - val pageStart = - chapterStart - textChapter.getReadLength(ReadBook.durPageIndex) - textChapter.page(ReadBook.durPageIndex)?.upPageAloudSpan(pageStart) + val pageStart = chapterStart - ReadBook.durChapterPos + textChapter.getPageByReadPos(ReadBook.durChapterPos) + ?.upPageAloudSpan(pageStart) upContent() } } @@ -837,10 +962,10 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo upScreenTimeOut() } observeEvent(PreferKey.textSelectAble) { - page_view.curPage.upSelectAble(it) + readView.curPage.upSelectAble(it) } observeEvent(PreferKey.showBrightnessView) { - read_menu.upBrightnessState() + readMenu.upBrightnessState() } } @@ -856,16 +981,16 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo */ override fun screenOffTimerStart() { if (screenTimeOut < 0) { - ReadBookActivityHelp.keepScreenOn(window, true) + keepScreenOn(window, true) return } val t = screenTimeOut - sysScreenOffTime if (t > 0) { mHandler.removeCallbacks(keepScreenRunnable) - ReadBookActivityHelp.keepScreenOn(window, true) + keepScreenOn(window, true) mHandler.postDelayed(keepScreenRunnable, screenTimeOut) } else { - ReadBookActivityHelp.keepScreenOn(window, false) + keepScreenOn(window, false) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivityHelp.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivityHelp.kt deleted file mode 100644 index 5360e08e6..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivityHelp.kt +++ /dev/null @@ -1,189 +0,0 @@ -package io.legado.app.ui.book.read - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.pm.ActivityInfo -import android.os.AsyncTask -import android.os.Build -import android.view.LayoutInflater -import android.view.View -import android.view.Window -import android.view.WindowManager -import android.widget.EditText -import io.legado.app.App -import io.legado.app.R -import io.legado.app.data.entities.Bookmark -import io.legado.app.help.AppConfig -import io.legado.app.help.ReadBookConfig -import io.legado.app.lib.dialogs.* -import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.ThemeStore -import io.legado.app.lib.theme.backgroundColor -import io.legado.app.service.help.Download -import io.legado.app.service.help.ReadBook -import io.legado.app.ui.widget.text.AutoCompleteTextView -import io.legado.app.utils.applyTint -import io.legado.app.utils.requestInputMethod -import kotlinx.android.synthetic.main.dialog_download_choice.view.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import org.jetbrains.anko.layoutInflater - - -object ReadBookActivityHelp { - - /** - * 更新状态栏,导航栏 - */ - fun upSystemUiVisibility( - window: Window, - isInMultiWindow: Boolean, - toolBarHide: Boolean = true - ) { - var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_IMMERSIVE - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) - if (!isInMultiWindow) { - flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - } - if (ReadBookConfig.hideNavigationBar) { - flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - } - if (toolBarHide) { - if (ReadBookConfig.hideStatusBar) { - flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN - } - if (ReadBookConfig.hideNavigationBar) { - flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - } - } - window.decorView.systemUiVisibility = flag - if (toolBarHide) { - ATH.setLightStatusBar(window, ReadBookConfig.durConfig.statusIconDark()) - } else { - ATH.setLightStatusBarAuto( - window, - ThemeStore.statusBarColor(App.INSTANCE, AppConfig.isTransparentStatusBar) - ) - } - } - - /** - * 屏幕方向 - */ - @SuppressLint("SourceLockedOrientationActivity") - fun setOrientation(activity: Activity) = activity.apply { - when (AppConfig.requestedDirection) { - "0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - "1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - "2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - "3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR - } - } - - - /** - * 保持亮屏 - */ - fun keepScreenOn(window: Window, on: Boolean) { - if (on) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - - /** - * 适配刘海 - */ - fun upLayoutInDisplayCutoutMode(window: Window) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && AppConfig.readBodyToLh) { - window.attributes = window.attributes.apply { - layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } - } - } - - @SuppressLint("InflateParams") - fun showDownloadDialog(context: Context) { - ReadBook.book?.let { book -> - context.alert(titleResource = R.string.download_offline) { - var view: View? = null - customView { - LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null) - .apply { - view = this - setBackgroundColor(context.backgroundColor) - edit_start.setText((book.durChapterIndex + 1).toString()) - edit_end.setText(book.totalChapterNum.toString()) - } - } - yesButton { - view?.apply { - val start = edit_start?.text?.toString()?.toInt() ?: 0 - val end = edit_end?.text?.toString()?.toInt() ?: book.totalChapterNum - Download.start(context, book.bookUrl, start - 1, end - 1) - } - } - noButton() - }.show().applyTint() - } - } - - @SuppressLint("InflateParams") - fun showBookMark(context: Context) = with(context) { - val book = ReadBook.book ?: return - val textChapter = ReadBook.curTextChapter ?: return - context.alert(title = getString(R.string.bookmark_add)) { - var editText: EditText? = null - message = book.name + " • " + textChapter.title - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - setHint(R.string.note_content) - } - } - } - yesButton { - editText?.text?.toString()?.let { editContent -> - AsyncTask.execute { - val bookmark = Bookmark( - bookUrl = book.bookUrl, - bookName = book.name, - chapterIndex = ReadBook.durChapterIndex, - pageIndex = ReadBook.durPageIndex, - chapterName = textChapter.title, - content = editContent - ) - App.db.bookmarkDao().insert(bookmark) - } - } - } - noButton() - }.show().applyTint().requestInputMethod() - } - - @SuppressLint("InflateParams") - fun showCharsetConfig(context: Context) = with(context) { - val charsets = - arrayListOf("UTF-8", "GB2312", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") - alert(R.string.set_charset) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setFilterValues(charsets) - edit_view.setText(ReadBook.book?.charset) - } - } - okButton { - val text = editText?.text?.toString() - text?.let { - ReadBook.setCharset(it) - } - } - cancelButton() - }.show().applyTint() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt new file mode 100644 index 000000000..0e325f294 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt @@ -0,0 +1,241 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.view.* +import androidx.activity.viewModels +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst.charsets +import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ActivityBookReadBinding +import io.legado.app.databinding.DialogDownloadChoiceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.AppConfig +import io.legado.app.help.LocalConfig +import io.legado.app.help.ReadBookConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.service.help.CacheBook +import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.config.BgTextConfigDialog +import io.legado.app.ui.book.read.config.ClickActionConfigDialog +import io.legado.app.ui.book.read.config.PaddingConfigDialog +import io.legado.app.ui.book.toc.BookmarkDialog +import io.legado.app.utils.getPrefString +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 阅读界面 + */ +abstract class ReadBookBaseActivity : + VMBaseActivity(imageBg = false) { + + override val binding by viewBinding(ActivityBookReadBinding::inflate) + override val viewModel by viewModels() + var bottomDialog = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + ReadBook.msg = null + setOrientation() + upLayoutInDisplayCutoutMode() + super.onCreate(savedInstanceState) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + if (!LocalConfig.readHelpVersionIsLast) { + showClickRegionalConfig() + } + } + + fun showPaddingConfig() { + PaddingConfigDialog().show(supportFragmentManager, "paddingConfig") + } + + fun showBgTextConfig() { + BgTextConfigDialog().show(supportFragmentManager, "bgTextConfig") + } + + fun showClickRegionalConfig() { + ClickActionConfigDialog().show(supportFragmentManager, "clickActionConfig") + } + + /** + * 屏幕方向 + */ + @SuppressLint("SourceLockedOrientationActivity") + fun setOrientation() { + when (AppConfig.screenOrientation) { + "0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + "1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + "2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + "3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + } + } + + /** + * 更新状态栏,导航栏 + */ + fun upSystemUiVisibility( + isInMultiWindow: Boolean, + toolBarHide: Boolean = true + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.let { + if (toolBarHide) { + if (ReadBookConfig.hideStatusBar) { + it.hide(WindowInsets.Type.statusBars()) + } + if (ReadBookConfig.hideNavigationBar) { + it.hide(WindowInsets.Type.navigationBars()) + } + } else { + it.show(WindowInsets.Type.statusBars()) + it.show(WindowInsets.Type.navigationBars()) + } + } + } + upSystemUiVisibilityO(isInMultiWindow, toolBarHide) + if (toolBarHide) { + ATH.setLightStatusBar(this, ReadBookConfig.durConfig.curStatusIconDark()) + } else { + ATH.setLightStatusBarAuto( + this, + ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar) + ) + } + } + + @Suppress("DEPRECATION") + private fun upSystemUiVisibilityO( + isInMultiWindow: Boolean, + toolBarHide: Boolean = true + ) { + var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_IMMERSIVE + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) + if (!isInMultiWindow) { + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + if (toolBarHide) { + if (ReadBookConfig.hideStatusBar) { + flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN + } + if (ReadBookConfig.hideNavigationBar) { + flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + } + } + window.decorView.systemUiVisibility = flag + } + + override fun upNavigationBarColor() { + when { + binding.readMenu.isVisible -> super.upNavigationBarColor() + bottomDialog > 0 -> super.upNavigationBarColor() + else -> if (AppConfig.immNavigationBar) { + ATH.setNavigationBarColorAuto(this, Color.TRANSPARENT) + } else { + ATH.setNavigationBarColorAuto(this, Color.parseColor("#20000000")) + } + } + } + + /** + * 保持亮屏 + */ + fun keepScreenOn(window: Window, on: Boolean) { + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + /** + * 适配刘海 + */ + private fun upLayoutInDisplayCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ReadBookConfig.readBodyToLh) { + window.attributes = window.attributes.apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + } + + @SuppressLint("InflateParams") + fun showDownloadDialog() { + ReadBook.book?.let { book -> + alert(titleResource = R.string.offline_cache) { + val alertBinding = DialogDownloadChoiceBinding.inflate(layoutInflater).apply { + root.setBackgroundColor(root.context.backgroundColor) + editStart.setText((book.durChapterIndex + 1).toString()) + editEnd.setText(book.totalChapterNum.toString()) + } + customView { alertBinding.root } + yesButton { + alertBinding.run { + val start = editStart.text?.toString()?.toInt() ?: 0 + val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum + CacheBook.start(this@ReadBookBaseActivity, book.bookUrl, start - 1, end - 1) + } + } + noButton() + }.show() + } + } + + @SuppressLint("InflateParams") + fun showBookMark(bookmark: Bookmark) { + BookmarkDialog.start(supportFragmentManager, bookmark) + } + + fun showCharsetConfig() { + alert(R.string.set_charset) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(charsets) + editView.setText(ReadBook.book?.charset) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + ReadBook.setCharset(it) + } + } + cancelButton() + }.show() + } + + fun showPageAnimConfig(success: () -> Unit) { + val items = arrayListOf() + items.add(getString(R.string.btn_default_s)) + items.add(getString(R.string.page_anim_cover)) + items.add(getString(R.string.page_anim_slide)) + items.add(getString(R.string.page_anim_simulation)) + items.add(getString(R.string.page_anim_scroll)) + items.add(getString(R.string.page_anim_none)) + selector(R.string.page_anim, items) { _, i -> + ReadBook.book?.setPageAnim(i - 1) + success() + } + } + + fun isPrevKey(keyCode: Int): Boolean { + val prevKeysStr = getPrefString(PreferKey.prevKeys) + return prevKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } + + fun isNextKey(keyCode: Int): Boolean { + val nextKeysStr = getPrefString(PreferKey.nextKeys) + return nextKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index b8efe1cee..968ffbd80 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -2,44 +2,47 @@ package io.legado.app.ui.book.read import android.app.Application import android.content.Intent -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.BookProgress import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp -import io.legado.app.help.IntentDataHelp +import io.legado.app.help.ContentProcessor +import io.legado.app.help.storage.BookWebDav import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.PreciseSearch import io.legado.app.model.webBook.WebBook import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook +import io.legado.app.utils.msg +import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.withContext class ReadBookViewModel(application: Application) : BaseViewModel(application) { - var isInitFinish = false + var searchContentQuery = "" fun initData(intent: Intent) { execute { ReadBook.inBookshelf = intent.getBooleanExtra("inBookshelf", true) - IntentDataHelp.getData(intent.getStringExtra("key"))?.let { - initBook(it) - } ?: intent.getStringExtra("bookUrl")?.let { - App.db.bookDao().getBook(it)?.let { book -> - initBook(book) - } - } ?: App.db.bookDao().lastReadBook?.let { - initBook(it) + val bookUrl = intent.getStringExtra("bookUrl") + val book = when { + bookUrl.isNullOrEmpty() -> appDb.bookDao.lastReadBook + else -> appDb.bookDao.getBook(bookUrl) + } ?: ReadBook.book + when { + book != null -> initBook(book) + else -> ReadBook.upMsg(context.getString(R.string.no_book)) } }.onFinally { - if (ReadBook.inBookshelf) { - ReadBook.saveRead() - } + ReadBook.saveRead() } } @@ -51,7 +54,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { autoChangeSource(book.name, book.author) return } - ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) + ReadBook.chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) if (ReadBook.chapterSize == 0) { if (book.tocUrl.isEmpty()) { loadBookInfo(book) @@ -64,14 +67,13 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } ReadBook.loadContent(resetPageOffset = true) } + syncBookProgress(book) } else { ReadBook.book = book if (ReadBook.durChapterIndex != book.durChapterIndex) { ReadBook.durChapterIndex = book.durChapterIndex - ReadBook.durPageIndex = book.durChapterPos - ReadBook.prevTextChapter = null - ReadBook.curTextChapter = null - ReadBook.nextTextChapter = null + ReadBook.durChapterPos = book.durChapterPos + ReadBook.clearTextChapter() } ReadBook.titleDate.postValue(book.name) ReadBook.upWebBook(book) @@ -80,7 +82,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { autoChangeSource(book.name, book.author) return } - ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) + ReadBook.chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) if (ReadBook.chapterSize == 0) { if (book.tocUrl.isEmpty()) { loadBookInfo(book) @@ -94,6 +96,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ReadBook.loadContent(resetPageOffset = true) } } + if (!BaseReadAloudService.isRun) { + syncBookProgress(book) + } } } @@ -101,15 +106,13 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { book: Book, changeDruChapterIndex: ((chapters: List) -> Unit)? = null, ) { - execute { - if (book.isLocalBook()) { - loadChapterList(book, changeDruChapterIndex) - } else { - ReadBook.webBook?.getBookInfo(book, this, canReName = false) - ?.onSuccess { - loadChapterList(book, changeDruChapterIndex) - } - } + if (book.isLocalBook()) { + loadChapterList(book, changeDruChapterIndex) + } else { + ReadBook.webBook?.getBookInfo(viewModelScope, book, canReName = false) + ?.onSuccess { + loadChapterList(book, changeDruChapterIndex) + } } } @@ -117,12 +120,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { book: Book, changeDruChapterIndex: ((chapters: List) -> Unit)? = null, ) { - execute { - if (book.isLocalBook()) { + if (book.isLocalBook()) { + execute { LocalBook.getChapterList(book).let { - App.db.bookChapterDao().delByBook(book.bookUrl) - App.db.bookChapterDao().insert(*it.toTypedArray()) - App.db.bookDao().update(book) + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + appDb.bookDao.update(book) ReadBook.chapterSize = it.size if (it.isEmpty()) { ReadBook.upMsg(context.getString(R.string.error_load_toc)) @@ -131,37 +134,62 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ReadBook.loadContent(resetPageOffset = true) } } - } else { - ReadBook.webBook?.getChapterList(book, this) - ?.onSuccess(IO) { cList -> - if (cList.isNotEmpty()) { - if (changeDruChapterIndex == null) { - App.db.bookChapterDao().insert(*cList.toTypedArray()) - App.db.bookDao().update(book) - ReadBook.chapterSize = cList.size - ReadBook.upMsg(null) - ReadBook.loadContent(resetPageOffset = true) - } else { - changeDruChapterIndex(cList) - } + }.onError { + ReadBook.upMsg("LoadTocError:${it.localizedMessage}") + } + } else { + ReadBook.webBook?.getChapterList(viewModelScope, book) + ?.onSuccess(IO) { cList -> + if (cList.isNotEmpty()) { + if (changeDruChapterIndex == null) { + appDb.bookChapterDao.insert(*cList.toTypedArray()) + appDb.bookDao.update(book) + ReadBook.chapterSize = cList.size + ReadBook.upMsg(null) + ReadBook.loadContent(resetPageOffset = true) } else { - ReadBook.upMsg(context.getString(R.string.error_load_toc)) + changeDruChapterIndex(cList) } - }?.onError { + } else { ReadBook.upMsg(context.getString(R.string.error_load_toc)) } - } - }.onError { - ReadBook.upMsg("LoadTocError:${it.localizedMessage}") + }?.onError { + ReadBook.upMsg(context.getString(R.string.error_load_toc)) + } } } + fun syncBookProgress( + book: Book, + syncBookProgress: Boolean = AppConfig.syncBookProgress, + alertSync: ((progress: BookProgress) -> Unit)? = null + ) { + if (syncBookProgress) + execute { + BookWebDav.getBookProgress(book) + }.onSuccess { + it?.let { progress -> + if (progress.durChapterIndex < book.durChapterIndex || + (progress.durChapterIndex == book.durChapterIndex && progress.durChapterPos < book.durChapterPos) + ) { + alertSync?.invoke(progress) + } else { + ReadBook.setProgress(progress) + } + } + } + } + fun changeTo(newBook: Book) { execute { + var oldTocSize: Int = newBook.totalChapterNum ReadBook.upMsg(null) - ReadBook.book?.changeTo(newBook) + ReadBook.book?.let { + oldTocSize = it.totalChapterNum + it.changeTo(newBook) + } ReadBook.book = newBook - App.db.bookSourceDao().getBookSource(newBook.origin)?.let { + appDb.bookSourceDao.getBookSource(newBook.origin)?.let { ReadBook.webBook = WebBook(it) } ReadBook.prevTextChapter = null @@ -172,11 +200,11 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } if (newBook.tocUrl.isEmpty()) { loadBookInfo(newBook) { - upChangeDurChapterIndex(newBook, it) + upChangeDurChapterIndex(newBook, oldTocSize, it) } } else { loadChapterList(newBook) { - upChangeDurChapterIndex(newBook, it) + upChangeDurChapterIndex(newBook, oldTocSize, it) } } } @@ -185,81 +213,77 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { private fun autoChangeSource(name: String, author: String) { if (!AppConfig.autoChangeSource) return execute { - App.db.bookSourceDao().allTextEnabled.forEach { source -> - try { - val variableBook = SearchBook() - WebBook(source) - .searchBookSuspend(this, name, variableBook = variableBook) - .getOrNull(0)?.let { - if (it.name == name && (it.author == author || author == "")) { - val book = it.toBook() - book.upInfoFromOld(ReadBook.book) - changeTo(book) - return@execute - } - } - } catch (e: Exception) { - //nothing - } + val sources = appDb.bookSourceDao.allTextEnabled + val book = PreciseSearch.searchFirstBook(this, sources, name, author) + if (book != null) { + book.upInfoFromOld(ReadBook.book) + changeTo(book) + } else { + throw Exception("自动换源失败") } }.onStart { ReadBook.upMsg(context.getString(R.string.source_auto_changing)) + }.onError { + context.toastOnUi(it.msg) }.onFinally { ReadBook.upMsg(null) } } - private fun upChangeDurChapterIndex(book: Book, chapters: List) { + private fun upChangeDurChapterIndex(book: Book, oldTocSize: Int, chapters: List) { execute { - ReadBook.durChapterIndex = BookHelp.getDurChapterIndexByChapterTitle( - book.durChapterTitle, + ReadBook.durChapterIndex = BookHelp.getDurChapter( book.durChapterIndex, + oldTocSize, + book.durChapterTitle, chapters ) book.durChapterIndex = ReadBook.durChapterIndex book.durChapterTitle = chapters[ReadBook.durChapterIndex].title - App.db.bookDao().update(book) - App.db.bookChapterDao().insert(*chapters.toTypedArray()) + appDb.bookDao.update(book) + appDb.bookChapterDao.insert(*chapters.toTypedArray()) ReadBook.chapterSize = chapters.size ReadBook.upMsg(null) ReadBook.loadContent(resetPageOffset = true) } } - fun openChapter(index: Int, pageIndex: Int = 0) { - ReadBook.prevTextChapter = null - ReadBook.curTextChapter = null - ReadBook.nextTextChapter = null + fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) { + ReadBook.clearTextChapter() ReadBook.callBack?.upContent() if (index != ReadBook.durChapterIndex) { ReadBook.durChapterIndex = index - ReadBook.durPageIndex = pageIndex + ReadBook.durChapterPos = durChapterPos } ReadBook.saveRead() - ReadBook.loadContent(resetPageOffset = true) + ReadBook.loadContent(resetPageOffset = true) { + success?.invoke() + } } fun removeFromBookshelf(success: (() -> Unit)?) { execute { - ReadBook.book?.delete() + Book.delete(ReadBook.book) }.onSuccess { success?.invoke() } } - fun upBookSource() { + fun upBookSource(success: (() -> Unit)?) { execute { ReadBook.book?.let { book -> - App.db.bookSourceDao().getBookSource(book.origin)?.let { + appDb.bookSourceDao.getBookSource(book.origin)?.let { ReadBook.webBook = WebBook(it) } } + }.onSuccess { + success?.invoke() } } fun refreshContent(book: Book) { execute { - App.db.bookChapterDao().getChapter(book.bookUrl, ReadBook.durChapterIndex) + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) ?.let { chapter -> BookHelp.delContent(book, chapter) ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) @@ -267,6 +291,28 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } + fun reverseContent(book: Book) { + execute { + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?.let { chapter -> + BookHelp.reverseContent(book, chapter) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + } + + /** + * 替换规则变化 + */ + fun replaceRuleChanged() { + execute { + ReadBook.book?.let { + ContentProcessor.get(it.name, it.origin).upReplaceRules() + ReadBook.loadContent(resetPageOffset = false) + } + } + } + override fun onCleared() { super.onCleared() if (BaseReadAloudService.pause) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt index 6d069ff68..21c5804e2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -4,56 +4,55 @@ import android.content.Context import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable import android.util.AttributeSet +import android.view.LayoutInflater import android.view.WindowManager import android.view.animation.Animation import android.widget.FrameLayout import android.widget.SeekBar +import androidx.core.view.isGone import androidx.core.view.isVisible -import io.legado.app.App import io.legado.app.R -import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ViewReadMenuBinding import io.legado.app.help.AppConfig -import io.legado.app.help.ReadBookConfig +import io.legado.app.help.LocalConfig +import io.legado.app.help.ThemeConfig import io.legado.app.lib.theme.* import io.legado.app.service.help.ReadBook +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.utils.* -import kotlinx.android.synthetic.main.view_read_menu.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - -class ReadMenu : FrameLayout { +import splitties.views.onLongClick + +/** + * 阅读界面菜单 + */ +class ReadMenu @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { var cnaShowMenu: Boolean = false - private var callBack: CallBack? = null + private val callBack: CallBack get() = activity as CallBack + private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true) private lateinit var menuTopIn: Animation private lateinit var menuTopOut: Animation private lateinit var menuBottomIn: Animation private lateinit var menuBottomOut: Animation - private val bgColor: Int - private val textColor: Int - private var bottomBackgroundList: ColorStateList + private val bgColor: Int = context.bottomBackground + private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + private val bottomBackgroundList: ColorStateList = Selector.colorBuild() + .setDefaultColor(bgColor) + .setPressedColor(ColorUtils.darkenColor(bgColor)) + .create() private var onMenuOutEnd: (() -> Unit)? = null val showBrightnessView get() = context.getPrefBoolean(PreferKey.showBrightnessView, true) - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - init { - callBack = activity as? CallBack - bgColor = context.bottomBackground - textColor = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) - bottomBackgroundList = Selector.colorBuild() - .setDefaultColor(bgColor) - .setPressedColor(ColorUtils.darkenColor(bgColor)) - .create() - inflate(context, R.layout.view_read_menu, this) + initView() + upBrightnessState() + bindEvent() + } + + private fun initView() = binding.run { if (AppConfig.isNightTheme) { fabNightTheme.setImageResource(R.drawable.ic_daytime) } else { @@ -63,40 +62,43 @@ class ReadMenu : FrameLayout { val brightnessBackground = GradientDrawable() brightnessBackground.cornerRadius = 5F.dp brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f)) - ll_brightness.background = brightnessBackground - ll_bottom_bg.setBackgroundColor(bgColor) + llBrightness.background = brightnessBackground + llBottomBg.setBackgroundColor(bgColor) + fabSearch.backgroundTintList = bottomBackgroundList + fabSearch.setColorFilter(textColor) fabAutoPage.backgroundTintList = bottomBackgroundList fabAutoPage.setColorFilter(textColor) fabReplaceRule.backgroundTintList = bottomBackgroundList fabReplaceRule.setColorFilter(textColor) fabNightTheme.backgroundTintList = bottomBackgroundList fabNightTheme.setColorFilter(textColor) - tv_pre.setTextColor(textColor) - tv_next.setTextColor(textColor) - iv_catalog.setColorFilter(textColor) - tv_catalog.setTextColor(textColor) - iv_read_aloud.setColorFilter(textColor) - tv_read_aloud.setTextColor(textColor) - iv_font.setColorFilter(textColor) - tv_font.setTextColor(textColor) - iv_setting.setColorFilter(textColor) - tv_setting.setTextColor(textColor) - vw_bg.onClick { } - vwNavigationBar.onClick { } - seek_brightness.progress = context.getPrefInt("brightness", 100) - upBrightnessState() - bindEvent() + tvPre.setTextColor(textColor) + tvNext.setTextColor(textColor) + ivCatalog.setColorFilter(textColor) + tvCatalog.setTextColor(textColor) + ivReadAloud.setColorFilter(textColor) + tvReadAloud.setTextColor(textColor) + ivFont.setColorFilter(textColor) + tvFont.setTextColor(textColor) + ivSetting.setColorFilter(textColor) + tvSetting.setTextColor(textColor) + vwBg.setOnClickListener(null) + vwNavigationBar.setOnClickListener(null) + llBrightness.setOnClickListener(null) + seekBrightness.post { + seekBrightness.progress = AppConfig.readBrightness + } } fun upBrightnessState() { if (brightnessAuto()) { - iv_brightness_auto.setColorFilter(context.accentColor) - seek_brightness.isEnabled = false + binding.ivBrightnessAuto.setColorFilter(context.accentColor) + binding.seekBrightness.isEnabled = false } else { - iv_brightness_auto.setColorFilter(context.buttonDisabledColor) - seek_brightness.isEnabled = true + binding.ivBrightnessAuto.setColorFilter(context.buttonDisabledColor) + binding.seekBrightness.isEnabled = true } - setScreenBrightness(context.getPrefInt("brightness", 100)) + setScreenBrightness(AppConfig.readBrightness) } /** @@ -116,17 +118,17 @@ class ReadMenu : FrameLayout { fun runMenuIn() { this.visible() - title_bar.visible() - bottom_menu.visible() - title_bar.startAnimation(menuTopIn) - bottom_menu.startAnimation(menuBottomIn) + binding.titleBar.visible() + binding.bottomMenu.visible() + binding.titleBar.startAnimation(menuTopIn) + binding.bottomMenu.startAnimation(menuBottomIn) } fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) { this.onMenuOutEnd = onMenuOutEnd if (this.isVisible) { - title_bar.startAnimation(menuTopOut) - bottom_menu.startAnimation(menuBottomOut) + binding.titleBar.startAnimation(menuTopOut) + binding.bottomMenu.startAnimation(menuBottomOut) } } @@ -134,93 +136,100 @@ class ReadMenu : FrameLayout { return context.getPrefBoolean("brightnessAuto", true) || !showBrightnessView } - private fun bindEvent() { - iv_brightness_auto.onClick { + private fun bindEvent() = binding.run { + tvChapterName.setOnClickListener { + callBack.openSourceEditActivity() + } + tvChapterUrl.setOnClickListener { + context.openUrl(binding.tvChapterUrl.text.toString()) + } + tvLogin.setOnClickListener { + callBack.showLogin() + } + ivBrightnessAuto.setOnClickListener { context.putPrefBoolean("brightnessAuto", !brightnessAuto()) upBrightnessState() } //亮度调节 - seek_brightness.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - setScreenBrightness(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) { + seekBrightness.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + setScreenBrightness(progress) + } } - override fun onStopTrackingTouch(seekBar: SeekBar?) { - context.putPrefInt("brightness", seek_brightness.progress) + override fun onStopTrackingTouch(seekBar: SeekBar) { + AppConfig.readBrightness = seekBar.progress } }) //阅读进度 - seek_read_page.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) { - - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - - } + seekReadPage.setOnSeekBarChangeListener(object : SeekBarChangeListener { override fun onStopTrackingTouch(seekBar: SeekBar) { ReadBook.skipToPage(seekBar.progress) } + }) + //搜索 + fabSearch.setOnClickListener { + runMenuOut { + callBack.openSearchActivity(null) + } + } + //自动翻页 - fabAutoPage.onClick { + fabAutoPage.setOnClickListener { runMenuOut { - callBack?.autoPage() + callBack.autoPage() } } //替换 - fabReplaceRule.onClick { callBack?.openReplaceRule() } + fabReplaceRule.setOnClickListener { callBack.openReplaceRule() } //夜间模式 - fabNightTheme.onClick { + fabNightTheme.setOnClickListener { AppConfig.isNightTheme = !AppConfig.isNightTheme - App.INSTANCE.applyDayNight() - postEvent(EventBus.RECREATE, "") + ThemeConfig.applyDayNight(context) } //上一章 - tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } //下一章 - tv_next.onClick { ReadBook.moveToNextChapter(true) } + tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) } //目录 - ll_catalog.onClick { + llCatalog.setOnClickListener { runMenuOut { - callBack?.openChapterList() + callBack.openChapterList() } } //朗读 - ll_read_aloud.onClick { + llReadAloud.setOnClickListener { runMenuOut { - callBack?.onClickReadAloud() + callBack.onClickReadAloud() } } - ll_read_aloud.onLongClick { - runMenuOut { callBack?.showReadAloudDialog() } - true + llReadAloud.onLongClick { + runMenuOut { callBack.showReadAloudDialog() } } //界面 - ll_font.onClick { + llFont.setOnClickListener { runMenuOut { - callBack?.showReadStyle() + callBack.showReadStyle() } } //设置 - ll_setting.onClick { + llSetting.setOnClickListener { runMenuOut { - callBack?.showMoreSetting() + callBack.showMoreSetting() } } } @@ -231,25 +240,21 @@ class ReadMenu : FrameLayout { menuBottomIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in) menuTopIn.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) { - callBack?.upSystemUiVisibility() - ll_brightness.visible(showBrightnessView) + callBack.upSystemUiVisibility() + binding.llBrightness.visible(showBrightnessView) } override fun onAnimationEnd(animation: Animation) { - vw_menu_bg.onClick { runMenuOut() } - vwNavigationBar.layoutParams = vwNavigationBar.layoutParams.apply { - height = - if (ReadBookConfig.hideNavigationBar - && SystemUtils.isNavigationBarExist(activity) - ) - context.navigationBarHeight - else 0 + binding.vwMenuBg.setOnClickListener { runMenuOut() } + binding.vwNavigationBar.layoutParams = binding.vwNavigationBar.layoutParams.apply { + height = activity!!.navigationBarHeight + } + if (!LocalConfig.readMenuHelpVersionIsLast) { + callBack.showReadMenuHelp() } } - override fun onAnimationRepeat(animation: Animation) { - - } + override fun onAnimationRepeat(animation: Animation) = Unit }) //隐藏菜单 @@ -258,25 +263,52 @@ class ReadMenu : FrameLayout { AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out) menuTopOut.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) { - vw_menu_bg.setOnClickListener(null) + binding.vwMenuBg.setOnClickListener(null) } override fun onAnimationEnd(animation: Animation) { this@ReadMenu.invisible() - title_bar.invisible() - bottom_menu.invisible() + binding.titleBar.invisible() + binding.bottomMenu.invisible() cnaShowMenu = false onMenuOutEnd?.invoke() - callBack?.upSystemUiVisibility() + callBack.upSystemUiVisibility() } - override fun onAnimationRepeat(animation: Animation) { + override fun onAnimationRepeat(animation: Animation) = Unit + }) + } + + fun setTitle(title: String) { + binding.titleBar.title = title + } + fun upBookView() { + binding.tvLogin.isGone = ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() + ReadBook.curTextChapter?.let { + binding.tvChapterName.text = it.title + binding.tvChapterName.visible() + if (!ReadBook.isLocalBook) { + binding.tvChapterUrl.text = it.url + binding.tvChapterUrl.visible() + } else { + binding.tvChapterUrl.gone() } - }) + binding.seekReadPage.max = it.pageSize.minus(1) + binding.seekReadPage.progress = ReadBook.durPageIndex() + binding.tvPre.isEnabled = ReadBook.durChapterIndex != 0 + binding.tvNext.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 + } ?: let { + binding.tvChapterName.gone() + binding.tvChapterUrl.gone() + } + } + + fun setSeekPage(seek: Int) { + binding.seekReadPage.progress = seek } - fun setAutoPage(autoPage: Boolean) { + fun setAutoPage(autoPage: Boolean) = binding.run { if (autoPage) { fabAutoPage.setImageResource(R.drawable.ic_auto_page_stop) fabAutoPage.contentDescription = context.getString(R.string.auto_next_page_stop) @@ -291,11 +323,15 @@ class ReadMenu : FrameLayout { fun autoPage() fun openReplaceRule() fun openChapterList() + fun openSearchActivity(searchWord: String?) + fun openSourceEditActivity() fun showReadStyle() fun showMoreSetting() fun showReadAloudDialog() fun upSystemUiVisibility() fun onClickReadAloud() + fun showReadMenuHelp() + fun showLogin() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt index 5c8d1d214..811f813f3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt @@ -8,6 +8,7 @@ import android.content.pm.ResolveInfo import android.net.Uri import android.os.Build import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener import android.view.LayoutInflater import android.view.Menu import android.view.ViewGroup @@ -19,88 +20,101 @@ import androidx.appcompat.view.menu.MenuItemImpl import androidx.core.view.isVisible import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ItemTextBinding +import io.legado.app.databinding.PopupActionMenuBinding import io.legado.app.service.BaseReadAloudService -import io.legado.app.utils.gone -import io.legado.app.utils.isAbsUrl -import io.legado.app.utils.sendToClip -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_fillet_text.view.* -import kotlinx.android.synthetic.main.popup_action_menu.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.share -import org.jetbrains.anko.toast +import io.legado.app.utils.* import java.util.* @SuppressLint("RestrictedApi") class TextActionMenu(private val context: Context, private val callBack: CallBack) : PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT), TextToSpeech.OnInitListener { - + private val binding = PopupActionMenuBinding.inflate(LayoutInflater.from(context)) private val adapter = Adapter(context) - private val menu = MenuBuilder(context) - private val moreMenu = MenuBuilder(context) + private val menuItems: List + private val visibleMenuItems = arrayListOf() + private val moreMenuItems = arrayListOf() + private val ttsListener by lazy { + TTSUtteranceListener() + } init { @SuppressLint("InflateParams") - contentView = LayoutInflater.from(context).inflate(R.layout.popup_action_menu, null) + contentView = binding.root isTouchable = true isOutsideTouchable = false isFocusable = false - initRecyclerView() - setOnDismissListener { - contentView.apply { - iv_menu_more.setImageResource(R.drawable.ic_more_vert) - recycler_view_more.gone() - adapter.setItems(menu.visibleItems) - recycler_view.visible() - } - } - } - - private fun initRecyclerView() = with(contentView) { - recycler_view.adapter = adapter - recycler_view_more.adapter = adapter - SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) - adapter.setItems(menu.visibleItems) + val myMenu = MenuBuilder(context) + val otherMenu = MenuBuilder(context) + SupportMenuInflater(context).inflate(R.menu.content_select_action, myMenu) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - onInitializeMenu(moreMenu) + onInitializeMenu(otherMenu) } - if (moreMenu.size() > 0) { - iv_menu_more.visible() + menuItems = myMenu.visibleItems + otherMenu.visibleItems + visibleMenuItems.addAll(menuItems.subList(0, 5)) + moreMenuItems.addAll(menuItems.subList(5, menuItems.lastIndex)) + binding.recyclerView.adapter = adapter + binding.recyclerViewMore.adapter = adapter + setOnDismissListener { + if (!context.getPrefBoolean(PreferKey.expandTextMenu)) { + binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert) + binding.recyclerViewMore.gone() + adapter.setItems(visibleMenuItems) + binding.recyclerView.visible() + } } - iv_menu_more.onClick { - if (recycler_view.isVisible) { - iv_menu_more.setImageResource(R.drawable.ic_arrow_back) - adapter.setItems(moreMenu.visibleItems) - recycler_view.gone() - recycler_view_more.visible() + binding.ivMenuMore.setOnClickListener { + if (binding.recyclerView.isVisible) { + binding.ivMenuMore.setImageResource(R.drawable.ic_arrow_back) + adapter.setItems(moreMenuItems) + binding.recyclerView.gone() + binding.recyclerViewMore.visible() } else { - iv_menu_more.setImageResource(R.drawable.ic_more_vert) - recycler_view_more.gone() - adapter.setItems(menu.visibleItems) - recycler_view.visible() + binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert) + binding.recyclerViewMore.gone() + adapter.setItems(visibleMenuItems) + binding.recyclerView.visible() } } + upMenu() + } + + fun upMenu() { + val expandTextMenu = context.getPrefBoolean(PreferKey.expandTextMenu) + if (expandTextMenu) { + adapter.setItems(menuItems) + binding.ivMenuMore.gone() + } else { + adapter.setItems(visibleMenuItems) + binding.ivMenuMore.visible() + } } inner class Adapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_text) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemTextBinding { + return ItemTextBinding.inflate(inflater, parent, false) + } override fun convert( holder: ItemViewHolder, + binding: ItemTextBinding, item: MenuItemImpl, payloads: MutableList ) { - with(holder.itemView) { - text_view.text = item.title + with(binding) { + textView.text = item.title } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { if (!callBack.onMenuItemSelected(it.itemId)) { onMenuItemSelected(it) @@ -117,13 +131,13 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac R.id.menu_share_str -> context.share(callBack.selectedText) R.id.menu_aloud -> { if (BaseReadAloudService.isRun) { - context.toast(R.string.alouding_disable) + context.toastOnUi(R.string.alouding_disable) return } readAloud(callBack.selectedText) } R.id.menu_browser -> { - try { + kotlin.runCatching { val intent = if (callBack.selectedText.isAbsUrl()) { Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(callBack.selectedText) @@ -134,9 +148,9 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac } } context.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - context.toast(e.localizedMessage ?: "ERROR") + }.onFailure { + it.printStackTrace() + context.toastOnUi(it.localizedMessage ?: "ERROR") } } else -> item.intent?.let { @@ -156,22 +170,29 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac private fun readAloud(text: String) { lastText = text if (textToSpeech == null) { - textToSpeech = TextToSpeech(context, this) + textToSpeech = TextToSpeech(context, this).apply { + setOnUtteranceProgressListener(ttsListener) + } return } if (!ttsInitFinish) return if (text == "") return - if (textToSpeech?.isSpeaking == true) + if (textToSpeech?.isSpeaking == true) { textToSpeech?.stop() + } textToSpeech?.speak(text, TextToSpeech.QUEUE_ADD, null, "select_text") lastText = "" } @Synchronized override fun onInit(status: Int) { - textToSpeech?.language = Locale.CHINA - ttsInitFinish = true - readAloud(lastText) + if (status == TextToSpeech.SUCCESS) { + textToSpeech?.language = Locale.CHINA + ttsInitFinish = true + readAloud(lastText) + } else { + context.toastOnUi(R.string.tts_init_failed) + } } @RequiresApi(Build.VERSION_CODES.M) @@ -201,7 +222,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac */ @RequiresApi(Build.VERSION_CODES.M) private fun onInitializeMenu(menu: Menu) { - try { + kotlin.runCatching { var menuItemOrder = 100 for (resolveInfo in getSupportedActivities()) { menu.add( @@ -209,8 +230,24 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac menuItemOrder++, resolveInfo.loadLabel(context.packageManager) ).intent = createProcessTextIntentForResolveInfo(resolveInfo) } - } catch (e: Exception) { - context.toast("获取文字操作菜单出错:${e.localizedMessage}") + }.onFailure { + context.toastOnUi("获取文字操作菜单出错:${it.localizedMessage}") + } + } + + private inner class TTSUtteranceListener : UtteranceProgressListener() { + + override fun onStart(utteranceId: String?) { + + } + + override fun onDone(utteranceId: String?) { + textToSpeech?.shutdown() + textToSpeech = null + } + + override fun onError(utteranceId: String?) { + } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt index 11c2fed44..bd7b835b8 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt @@ -1,29 +1,30 @@ package io.legado.app.ui.book.read.config +import android.content.DialogInterface import android.os.Bundle -import android.util.DisplayMetrics import android.view.* import android.widget.SeekBar import io.legado.app.R import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogAutoReadBinding import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.utils.ColorUtils -import kotlinx.android.synthetic.main.dialog_auto_read.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding + class AutoReadDialog : BaseDialogFragment() { var callBack: CallBack? = null + private val binding by viewBinding(DialogAutoReadBinding::bind) + override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) @@ -36,67 +37,74 @@ class AutoReadDialog : BaseDialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + (activity as ReadBookActivity).bottomDialog++ callBack = activity as? CallBack return inflater.inflate(R.layout.dialog_auto_read, container) } - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { val bg = requireContext().bottomBackground val isLight = ColorUtils.isColorLight(bg) val textColor = requireContext().getPrimaryTextColor(isLight) - root_view.setBackgroundColor(bg) - tv_read_speed_title.setTextColor(textColor) - tv_read_speed.setTextColor(textColor) - iv_catalog.setColorFilter(textColor) - tv_catalog.setTextColor(textColor) - iv_main_menu.setColorFilter(textColor) - tv_main_menu.setTextColor(textColor) - iv_auto_page_stop.setColorFilter(textColor) - tv_auto_page_stop.setTextColor(textColor) - iv_setting.setColorFilter(textColor) - tv_setting.setTextColor(textColor) + root.setBackgroundColor(bg) + tvReadSpeedTitle.setTextColor(textColor) + tvReadSpeed.setTextColor(textColor) + ivCatalog.setColorFilter(textColor) + tvCatalog.setTextColor(textColor) + ivMainMenu.setColorFilter(textColor) + tvMainMenu.setTextColor(textColor) + ivAutoPageStop.setColorFilter(textColor) + tvAutoPageStop.setTextColor(textColor) + ivSetting.setColorFilter(textColor) + tvSetting.setTextColor(textColor) initOnChange() initData() initEvent() } private fun initData() { - val speed = if (ReadBookConfig.autoReadSpeed < 10) 10 else ReadBookConfig.autoReadSpeed - tv_read_speed.text = String.format("%ds", speed) - seek_auto_read.progress = speed + val speed = if (ReadBookConfig.autoReadSpeed < 2) 2 else ReadBookConfig.autoReadSpeed + binding.tvReadSpeed.text = String.format("%ds", speed) + binding.seekAutoRead.progress = speed } private fun initOnChange() { - seek_auto_read.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + binding.seekAutoRead.setOnSeekBarChangeListener(object : SeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - val speed = if (progress < 10) 10 else progress - tv_read_speed.text = String.format("%ds", speed) + val speed = if (progress < 2) 2 else progress + binding.tvReadSpeed.text = String.format("%ds", speed) } - override fun onStartTrackingTouch(seekBar: SeekBar) = Unit - override fun onStopTrackingTouch(seekBar: SeekBar) { ReadBookConfig.autoReadSpeed = - if (seek_auto_read.progress < 10) 10 else seek_auto_read.progress + if (binding.seekAutoRead.progress < 2) 2 else binding.seekAutoRead.progress upTtsSpeechRate() } }) } private fun initEvent() { - ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() } - ll_setting.onClick { + binding.llMainMenu.setOnClickListener { + callBack?.showMenuBar() + dismissAllowingStateLoss() + } + binding.llSetting.setOnClickListener { ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") } - ll_catalog.onClick { callBack?.openChapterList() } - ll_auto_page_stop.onClick { + binding.llCatalog.setOnClickListener { callBack?.openChapterList() } + binding.llAutoPageStop.setOnClickListener { callBack?.autoPageStop() - dismiss() + dismissAllowingStateLoss() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt new file mode 100644 index 000000000..f0d8ac752 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.EventBus +import io.legado.app.databinding.ItemBgImageBinding +import io.legado.app.help.ImageLoader +import io.legado.app.help.ReadBookConfig +import io.legado.app.utils.postEvent + +import java.io.File + +class BgAdapter(context: Context, val textColor: Int) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBgImageBinding { + return ItemBgImageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBgImageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + ImageLoader.load( + context, + context.assets.open("bg${File.separator}$item").readBytes() + ) + .centerCrop() + .into(ivBg) + tvName.setTextColor(textColor) + tvName.text = item.substringBeforeLast(".") + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBgImageBinding) { + holder.itemView.apply { + this.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + ReadBookConfig.durConfig.setCurBg(1, it) + ReadBookConfig.upBg() + postEvent(EventBus.UP_CONFIG, false) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index 12ead916b..b70f72854 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -1,69 +1,70 @@ package io.legado.app.ui.book.read.config import android.annotation.SuppressLint -import android.app.Activity.RESULT_OK -import android.content.Context import android.content.DialogInterface -import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Bundle -import android.util.DisplayMetrics import android.view.* +import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.jaredrummler.android.colorpicker.ColorPickerDialog import io.legado.app.R import io.legado.app.base.BaseDialogFragment -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.constant.EventBus -import io.legado.app.help.ImageLoader +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogReadBgTextBinding +import io.legado.app.databinding.ItemBgImageBinding +import io.legado.app.help.DefaultData import io.legado.app.help.ReadBookConfig -import io.legado.app.help.http.HttpHelper -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getSecondaryTextColor import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker -import io.legado.app.ui.widget.text.AutoCompleteTextView +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_read_bg_text.* -import kotlinx.android.synthetic.main.item_bg_image.view.* -import org.jetbrains.anko.sdk27.listeners.onCheckedChange -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding import java.io.File -class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { +class BgTextConfigDialog : BaseDialogFragment() { companion object { const val TEXT_COLOR = 121 const val BG_COLOR = 122 } - private val requestCodeBg = 123 - private val requestCodeExport = 131 - private val requestCodeImport = 132 + private val binding by viewBinding(DialogReadBgTextBinding::bind) private val configFileName = "readConfig.zip" private lateinit var adapter: BgAdapter - var primaryTextColor = 0 - var secondaryTextColor = 0 + private var primaryTextColor = 0 + private var secondaryTextColor = 0 + private val importFormNet = "网络导入" + private val selectBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { + it ?: return@registerForActivityResult + setBgFromUri(it) + } + private val selectExportDir = registerForActivityResult(FilePicker()) { + it ?: return@registerForActivityResult + exportConfig(it) + } + private val selectImportDoc = registerForActivityResult(FilePicker()) { + it ?: return@registerForActivityResult + if (it.toString() == importFormNet) { + importNetConfigAlert() + } else { + importConfig(it) + } + } override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) @@ -81,6 +82,7 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { container: ViewGroup?, savedInstanceState: Bundle? ): View? { + (activity as ReadBookActivity).bottomDialog++ return inflater.inflate(R.layout.dialog_read_bg_text, container) } @@ -93,56 +95,91 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) ReadBookConfig.save() + (activity as ReadBookActivity).bottomDialog-- } - private fun initView() { + private fun initView() = binding.run { val bg = requireContext().bottomBackground val isLight = ColorUtils.isColorLight(bg) primaryTextColor = requireContext().getPrimaryTextColor(isLight) secondaryTextColor = requireContext().getSecondaryTextColor(isLight) - root_view.setBackgroundColor(bg) - sw_dark_status_icon.setTextColor(primaryTextColor) - tv_bg_image.setTextColor(primaryTextColor) + rootView.setBackgroundColor(bg) + tvNameTitle.setTextColor(primaryTextColor) + tvName.setTextColor(secondaryTextColor) + ivEdit.setColorFilter(secondaryTextColor) + tvRestore.setTextColor(primaryTextColor) + swDarkStatusIcon.setTextColor(primaryTextColor) + ivImport.setColorFilter(primaryTextColor) + ivExport.setColorFilter(primaryTextColor) + ivDelete.setColorFilter(primaryTextColor) + tvBgImage.setTextColor(primaryTextColor) + adapter = BgAdapter(requireContext(), secondaryTextColor) + recyclerView.adapter = adapter + adapter.addHeaderView { + ItemBgImageBinding.inflate(layoutInflater, it, false).apply { + tvName.setTextColor(secondaryTextColor) + tvName.text = getString(R.string.select_image) + ivBg.setImageResource(R.drawable.ic_image) + ivBg.setColorFilter(primaryTextColor) + root.setOnClickListener { + selectBgImage.launch("image/*") + } + } + } + requireContext().assets.list("bg")?.let { + adapter.setItems(it.toList()) + } } @SuppressLint("InflateParams") private fun initData() = with(ReadBookConfig.durConfig) { - sw_dark_status_icon.isChecked = statusIconDark() - adapter = BgAdapter(requireContext()) - recycler_view.layoutManager = - LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false) - recycler_view.adapter = adapter - val headerView = LayoutInflater.from(requireContext()) - .inflate(R.layout.item_bg_image, recycler_view, false) - adapter.addHeaderView(headerView) - headerView.tv_name.setTextColor(secondaryTextColor) - headerView.tv_name.text = getString(R.string.select_image) - headerView.iv_bg.setImageResource(R.drawable.ic_image) - headerView.iv_bg.setColorFilter(primaryTextColor) - headerView.onClick { selectImage() } - requireContext().assets.list("bg/")?.let { - adapter.setItems(it.toList()) - } + binding.tvName.text = name.ifBlank { "文字" } + binding.swDarkStatusIcon.isChecked = curStatusIconDark() } + @SuppressLint("InflateParams") private fun initEvent() = with(ReadBookConfig.durConfig) { - sw_dark_status_icon.onCheckedChange { buttonView, isChecked -> - if (buttonView?.isPressed == true) { - setStatusIconDark(isChecked) - (activity as? ReadBookActivity)?.upSystemUiVisibility() + binding.ivEdit.setOnClickListener { + alert(R.string.style_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setText(ReadBookConfig.durConfig.name) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + binding.tvName.text = it + ReadBookConfig.durConfig.name = it + } + } + cancelButton() + }.show() + } + binding.tvRestore.setOnClickListener { + val defaultConfigs = DefaultData.readConfigs + val layoutNames = defaultConfigs.map { it.name } + selector("选择预设布局", layoutNames) { _, i -> + if (i >= 0) { + ReadBookConfig.durConfig = defaultConfigs[i] + initData() + postEvent(EventBus.UP_CONFIG, true) + } } } - tv_text_color.onClick { + binding.swDarkStatusIcon.setOnCheckedChangeListener { _, isChecked -> + setCurStatusIconDark(isChecked) + (activity as? ReadBookActivity)?.upSystemUiVisibility() + } + binding.tvTextColor.setOnClickListener { ColorPickerDialog.newBuilder() - .setColor(textColor()) + .setColor(curTextColor()) .setShowAlphaSlider(false) .setDialogType(ColorPickerDialog.TYPE_CUSTOM) .setDialogId(TEXT_COLOR) .show(requireActivity()) } - tv_bg_color.onClick { + binding.tvBgColor.setOnClickListener { val bgColor = - if (bgType() == 0) Color.parseColor(bgStr()) + if (curBgType() == 0) Color.parseColor(curBgStr()) else Color.parseColor("#015A86") ColorPickerDialog.newBuilder() .setColor(bgColor) @@ -151,67 +188,43 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { .setDialogId(BG_COLOR) .show(requireActivity()) } - tv_default.onClick { - ReadBookConfig.resetDur() - postEvent(EventBus.UP_CONFIG, false) - } - tv_import.onClick { - val importFormNet = "网络导入" - val otherActions = arrayListOf(importFormNet) - FilePicker.selectFile( - this@BgTextConfigDialog, - requestCodeImport, - allowExtensions = arrayOf("zip"), - otherActions = otherActions - ) { action -> - when (action) { - importFormNet -> importNetConfigAlert() - } - } - } - tv_export.onClick { - FilePicker.selectFolder(this@BgTextConfigDialog, requestCodeExport) + binding.ivImport.setOnClickListener { + selectImportDoc.launch( + FilePickerParam( + mode = FilePicker.FILE, + title = getString(R.string.import_str), + allowExtensions = arrayOf("zip"), + otherActions = arrayOf(importFormNet) + ) + ) } - } - - private fun selectImage() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "image/*" - startActivityForResult(intent, requestCodeBg) - } - - inner class BgAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_bg_image) { - - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - with(holder.itemView) { - ImageLoader.load(context, context.assets.open("bg/$item").readBytes()) - .centerCrop() - .into(iv_bg) - tv_name.setTextColor(secondaryTextColor) - tv_name.text = item.substringBeforeLast(".") - } + binding.ivExport.setOnClickListener { + selectExportDir.launch( + FilePickerParam( + title = getString(R.string.export_str) + ) + ) } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - this.onClick { - getItemByLayoutPosition(holder.layoutPosition)?.let { - ReadBookConfig.durConfig.setBg(1, it) - ReadBookConfig.upBg() - postEvent(EventBus.UP_CONFIG, false) - } - } + binding.ivDelete.setOnClickListener { + if (ReadBookConfig.deleteDur()) { + postEvent(EventBus.UP_CONFIG, true) + dismissAllowingStateLoss() + } else { + toastOnUi("数量已是最少,不能删除.") } } } @Suppress("BlockingMethodInNonBlockingContext") private fun exportConfig(uri: Uri) { + val exportFileName = if (ReadBookConfig.config.name.isBlank()) { + configFileName + } else { + "${ReadBookConfig.config.name}.zip" + } execute { val exportFiles = arrayListOf() - val configDirPath = FileUtils.getPath(requireContext().eCacheDir, "readConfig") + val configDirPath = FileUtils.getPath(requireContext().externalCache, "readConfig") FileUtils.deleteFile(configDirPath) val configDir = FileUtils.createFolderIfNotExist(configDirPath) val configExportPath = FileUtils.getPath(configDir, "readConfig.json") @@ -229,32 +242,50 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { exportFiles.add(fontExportFile) } } - if (ReadBookConfig.durConfig.bgType() == 2) { - val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStr()) - val bgFile = File(ReadBookConfig.durConfig.bgStr()) + if (ReadBookConfig.durConfig.bgType == 2) { + val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStr) + val bgFile = File(ReadBookConfig.durConfig.bgStr) + if (bgFile.exists()) { + val bgExportFile = File(FileUtils.getPath(configDir, bgName)) + bgFile.copyTo(bgExportFile) + exportFiles.add(bgExportFile) + } + } + if (ReadBookConfig.durConfig.bgTypeNight == 2) { + val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrNight) + val bgFile = File(ReadBookConfig.durConfig.bgStrNight) if (bgFile.exists()) { val bgExportFile = File(FileUtils.getPath(configDir, bgName)) bgFile.copyTo(bgExportFile) exportFiles.add(bgExportFile) } } - val configZipPath = FileUtils.getPath(requireContext().eCacheDir, configFileName) + if (ReadBookConfig.durConfig.bgTypeEInk == 2) { + val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrEInk) + val bgFile = File(ReadBookConfig.durConfig.bgStrEInk) + if (bgFile.exists()) { + val bgExportFile = File(FileUtils.getPath(configDir, bgName)) + bgFile.copyTo(bgExportFile) + exportFiles.add(bgExportFile) + } + } + val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName) if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) { - if (uri.isContentPath()) { + if (uri.isContentScheme()) { DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc -> - treeDoc.findFile(configFileName)?.delete() - treeDoc.createFile("", configFileName) + treeDoc.findFile(exportFileName)?.delete() + treeDoc.createFile("", exportFileName) ?.writeBytes(requireContext(), File(configZipPath).readBytes()) } } else { - val exportPath = FileUtils.getPath(File(uri.path!!), configFileName) + val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName) FileUtils.deleteFile(exportPath) FileUtils.createFileIfNotExist(exportPath) .writeBytes(File(configZipPath).readBytes()) } } }.onSuccess { - toast("导出成功, 文件名为 $configFileName") + toastOnUi("导出成功, 文件名为 $exportFileName") }.onError { it.printStackTrace() longToast("导出失败:${it.localizedMessage}") @@ -264,34 +295,33 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { @SuppressLint("InflateParams") private fun importNetConfigAlert() { alert("输入地址") { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + customView { alertBinding.root } okButton { - editText?.text?.toString()?.let { url -> + alertBinding.editView.text?.toString()?.let { url -> importNetConfig(url) } } - noButton { } - }.show().applyTint() + noButton() + }.show() } private fun importNetConfig(url: String) { execute { - HttpHelper.simpleGetBytesAsync(url)?.let { + @Suppress("BlockingMethodInNonBlockingContext") + okHttpClient.newCall { + url(url) + }.bytes().let { importConfig(it) - } ?: throw Exception("获取失败") + } }.onError { longToast(it.msg) } } - @Suppress("BlockingMethodInNonBlockingContext") private fun importConfig(uri: Uri) { execute { + @Suppress("BlockingMethodInNonBlockingContext") importConfig(uri.readBytes(requireContext())!!) }.onError { it.printStackTrace() @@ -299,85 +329,34 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { } } - @Suppress("BlockingMethodInNonBlockingContext") + @Suppress("BlockingMethodInNonBlockingContext", "BlockingMethodInNonBlockingContext") private fun importConfig(byteArray: ByteArray) { execute { - val configZipPath = FileUtils.getPath(requireContext().eCacheDir, configFileName) - FileUtils.deleteFile(configZipPath) - val zipFile = FileUtils.createFileIfNotExist(configZipPath) - zipFile.writeBytes(byteArray) - val configDirPath = FileUtils.getPath(requireContext().eCacheDir, "readConfig") - FileUtils.deleteFile(configDirPath) - ZipUtils.unzipFile(zipFile, FileUtils.createFolderIfNotExist(configDirPath)) - val configDir = FileUtils.createFolderIfNotExist(configDirPath) - val configFile = FileUtils.getFile(configDir, "readConfig.json") - val config: ReadBookConfig.Config = GSON.fromJsonObject(configFile.readText())!! - if (config.textFont.isNotEmpty()) { - val fontName = FileUtils.getName(config.textFont) - val fontPath = - FileUtils.getPath(requireContext().externalFilesDir, "font", fontName) - FileUtils.getFile(configDir, fontName).copyTo(File(fontPath)) - config.textFont = fontPath - } - if (config.bgType() == 2) { - val bgName = FileUtils.getName(config.bgStr()) - val bgPath = FileUtils.getPath(requireContext().externalFilesDir, "bg", bgName) - if (!FileUtils.exist(bgPath)) { - FileUtils.getFile(configDir, bgName).copyTo(File(bgPath)) - } - } - ReadBookConfig.durConfig = config - postEvent(EventBus.UP_CONFIG, true) + ReadBookConfig.import(byteArray) }.onSuccess { - toast("导入成功") + ReadBookConfig.durConfig = it + postEvent(EventBus.UP_CONFIG, true) + toastOnUi("导入成功") }.onError { it.printStackTrace() longToast("导入失败:${it.localizedMessage}") } } - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - requestCodeImport -> importConfig(Uri.fromFile(File(currentPath))) - requestCodeExport -> exportConfig(Uri.fromFile(File(currentPath))) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - requestCodeBg -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - setBgFromUri(uri) - } - } - requestCodeImport -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - importConfig(uri) - } - } - requestCodeExport -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - exportConfig(uri) - } - } - } - } - private fun setBgFromUri(uri: Uri) { - if (uri.toString().isContentPath()) { + if (uri.toString().isContentScheme()) { val doc = DocumentFile.fromSingleUri(requireContext(), uri) doc?.name?.let { val file = - FileUtils.createFileIfNotExist(requireContext().externalFilesDir, "bg", it) + FileUtils.createFileIfNotExist(requireContext().externalFiles, "bg", it) kotlin.runCatching { DocumentUtils.readBytes(requireContext(), doc.uri) }.getOrNull()?.let { byteArray -> file.writeBytes(byteArray) - ReadBookConfig.durConfig.setBg(2, file.absolutePath) + ReadBookConfig.durConfig.setCurBg(2, file.absolutePath) ReadBookConfig.upBg() postEvent(EventBus.UP_CONFIG, false) - } ?: toast("获取文件出错") + } ?: toastOnUi("获取文件出错") } } else { PermissionsCompat.Builder(this) @@ -388,7 +367,7 @@ class BgTextConfigDialog : BaseDialogFragment(), FileChooserDialog.CallBack { .rationale(R.string.bg_image_per) .onGranted { RealPathUtil.getPath(requireContext(), uri)?.let { path -> - ReadBookConfig.durConfig.setBg(2, path) + ReadBookConfig.durConfig.setCurBg(2, path) ReadBookConfig.upBg() postEvent(EventBus.UP_CONFIG, false) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt index af8408284..514e0a350 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt @@ -10,8 +10,7 @@ import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.accentColor import io.legado.app.ui.widget.text.StrokeTextView -import io.legado.app.utils.applyTint -import org.jetbrains.anko.sdk27.listeners.onClick + class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { @@ -24,7 +23,7 @@ class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView( if (!isInEditMode) { upUi(AppConfig.chineseConverterType) } - onClick { + setOnClickListener { selectType() } } @@ -45,7 +44,7 @@ class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView( upUi(i) onChanged?.invoke() } - }.show().applyTint() + }.show() } fun onChanged(unit: () -> Unit) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt new file mode 100644 index 000000000..e351be654 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt @@ -0,0 +1,133 @@ +package io.legado.app.ui.book.read.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogClickActionConfigBinding +import io.legado.app.help.AppConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class ClickActionConfigDialog : BaseDialogFragment() { + private val binding by viewBinding(DialogClickActionConfigBinding::bind) + private val actions = linkedMapOf() + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.setBackgroundDrawableResource(R.color.transparent) + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + (activity as ReadBookActivity).bottomDialog++ + return inflater.inflate(R.layout.dialog_click_action_config, container) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + view.setBackgroundColor(getCompatColor(R.color.translucent)) + actions[-1] = getString(R.string.non_action) + actions[0] = getString(R.string.menu) + actions[1] = getString(R.string.next_page) + actions[2] = getString(R.string.prev_page) + actions[3] = getString(R.string.next_chapter) + actions[4] = getString(R.string.previous_chapter) + initData() + initViewEvent() + } + + private fun initData() = binding.run { + tvTopLeft.text = actions[AppConfig.clickActionTL] + tvTopCenter.text = actions[AppConfig.clickActionTC] + tvTopRight.text = actions[AppConfig.clickActionTR] + tvMiddleLeft.text = actions[AppConfig.clickActionML] + tvMiddleRight.text = actions[AppConfig.clickActionMR] + tvBottomLeft.text = actions[AppConfig.clickActionBL] + tvBottomCenter.text = actions[AppConfig.clickActionBC] + tvBottomRight.text = actions[AppConfig.clickActionBR] + } + + private fun initViewEvent() { + binding.ivClose.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvTopLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTL, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvTopCenter.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTC, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvTopRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTR, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvMiddleLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionML, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvMiddleRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionMR, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBL, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomCenter.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBC, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBR, action) + (it as? TextView)?.text = actions[action] + } + } + } + + private fun selectAction(success: (action: Int) -> Unit) { + selector( + getString(R.string.select_action), + actions.values.toList() + ) { _, index -> + success.invoke(actions.keys.toList()[index]) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt index 4a3b4bd23..15f601c27 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt @@ -1,9 +1,9 @@ package io.legado.app.ui.book.read.config import android.annotation.SuppressLint +import android.content.DialogInterface import android.content.SharedPreferences import android.os.Bundle -import android.util.DisplayMetrics import android.view.* import android.widget.LinearLayout import androidx.fragment.app.DialogFragment @@ -15,7 +15,8 @@ import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.bottomBackground -import io.legado.app.ui.book.read.ReadBookActivityHelp +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.read.page.ReadView import io.legado.app.utils.dp import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent @@ -25,10 +26,6 @@ class MoreConfigDialog : DialogFragment() { override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) @@ -45,7 +42,8 @@ class MoreConfigDialog : DialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { + (activity as ReadBookActivity).bottomDialog++ val view = LinearLayout(context) view.setBackgroundColor(requireContext().bottomBackground) view.id = R.id.tag1 @@ -62,6 +60,11 @@ class MoreConfigDialog : DialogFragment() { .commit() } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + class ReadPreferenceFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { @@ -105,24 +108,32 @@ class MoreConfigDialog : DialogFragment() { } PreferKey.keepLight -> postEvent(key, true) PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) - getString(R.string.pk_requested_direction) -> { - activity?.let { - ReadBookActivityHelp.setOrientation(it) - } + PreferKey.screenOrientation -> { + (activity as? ReadBookActivity)?.setOrientation() } PreferKey.textFullJustify, - PreferKey.textBottomJustify -> { + PreferKey.textBottomJustify, + PreferKey.useZhLayout -> { postEvent(EventBus.UP_CONFIG, true) } PreferKey.showBrightnessView -> { postEvent(PreferKey.showBrightnessView, "") } + PreferKey.expandTextMenu -> { + (activity as? ReadBookActivity)?.textActionMenu?.upMenu() + } } } override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { "customPageKey" -> PageKeyDialog(requireContext()).show() + "clickRegionalConfig" -> { + (activity as? ReadBookActivity)?.showClickRegionalConfig() + } + "fullScreenGesturesSupport" -> { + ((activity as? ReadBookActivity)?.findViewById(R.id.read_view) as ReadView).setRect9x() + } } return super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt index dda13716a..e44eeb8b2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.book.read.config import android.content.DialogInterface import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,18 +9,19 @@ import android.view.WindowManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadPaddingBinding import io.legado.app.help.ReadBookConfig +import io.legado.app.utils.getSize import io.legado.app.utils.postEvent -import kotlinx.android.synthetic.main.dialog_read_padding.* +import io.legado.app.utils.viewbindingdelegate.viewBinding class PaddingConfigDialog : BaseDialogFragment() { + private val binding by viewBinding(DialogReadPaddingBinding::bind) + override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } + val dm = requireActivity().getSize() dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) val attr = it.attributes @@ -49,89 +49,85 @@ class PaddingConfigDialog : BaseDialogFragment() { ReadBookConfig.save() } - private fun initData() = ReadBookConfig.apply { + private fun initData() = binding.run { //正文 - dsb_padding_top.progress = paddingTop - dsb_padding_bottom.progress = paddingBottom - dsb_padding_left.progress = paddingLeft - dsb_padding_right.progress = paddingRight + dsbPaddingTop.progress = ReadBookConfig.paddingTop + dsbPaddingBottom.progress = ReadBookConfig.paddingBottom + dsbPaddingLeft.progress = ReadBookConfig.paddingLeft + dsbPaddingRight.progress = ReadBookConfig.paddingRight //页眉 - dsb_header_padding_top.progress = headerPaddingTop - dsb_header_padding_bottom.progress = headerPaddingBottom - dsb_header_padding_left.progress = headerPaddingLeft - dsb_header_padding_right.progress = headerPaddingRight + dsbHeaderPaddingTop.progress = ReadBookConfig.headerPaddingTop + dsbHeaderPaddingBottom.progress = ReadBookConfig.headerPaddingBottom + dsbHeaderPaddingLeft.progress = ReadBookConfig.headerPaddingLeft + dsbHeaderPaddingRight.progress = ReadBookConfig.headerPaddingRight //页脚 - dsb_footer_padding_top.progress = footerPaddingTop - dsb_footer_padding_bottom.progress = footerPaddingBottom - dsb_footer_padding_left.progress = footerPaddingLeft - dsb_footer_padding_right.progress = footerPaddingRight - cb_show_top_line.isChecked = showHeaderLine - cb_show_bottom_line.isChecked = showFooterLine + dsbFooterPaddingTop.progress = ReadBookConfig.footerPaddingTop + dsbFooterPaddingBottom.progress = ReadBookConfig.footerPaddingBottom + dsbFooterPaddingLeft.progress = ReadBookConfig.footerPaddingLeft + dsbFooterPaddingRight.progress = ReadBookConfig.footerPaddingRight + cbShowTopLine.isChecked = ReadBookConfig.showHeaderLine + cbShowBottomLine.isChecked = ReadBookConfig.showFooterLine } - private fun initView() = with(ReadBookConfig) { + private fun initView() = binding.run { //正文 - dsb_padding_top.onChanged = { - paddingTop = it + dsbPaddingTop.onChanged = { + ReadBookConfig.paddingTop = it postEvent(EventBus.UP_CONFIG, true) } - dsb_padding_bottom.onChanged = { - paddingBottom = it + dsbPaddingBottom.onChanged = { + ReadBookConfig.paddingBottom = it postEvent(EventBus.UP_CONFIG, true) } - dsb_padding_left.onChanged = { - paddingLeft = it + dsbPaddingLeft.onChanged = { + ReadBookConfig.paddingLeft = it postEvent(EventBus.UP_CONFIG, true) } - dsb_padding_right.onChanged = { - paddingRight = it + dsbPaddingRight.onChanged = { + ReadBookConfig.paddingRight = it postEvent(EventBus.UP_CONFIG, true) } //页眉 - dsb_header_padding_top.onChanged = { - headerPaddingTop = it + dsbHeaderPaddingTop.onChanged = { + ReadBookConfig.headerPaddingTop = it postEvent(EventBus.UP_CONFIG, true) } - dsb_header_padding_bottom.onChanged = { - headerPaddingBottom = it + dsbHeaderPaddingBottom.onChanged = { + ReadBookConfig.headerPaddingBottom = it postEvent(EventBus.UP_CONFIG, true) } - dsb_header_padding_left.onChanged = { - headerPaddingLeft = it + dsbHeaderPaddingLeft.onChanged = { + ReadBookConfig.headerPaddingLeft = it postEvent(EventBus.UP_CONFIG, true) } - dsb_header_padding_right.onChanged = { - headerPaddingRight = it + dsbHeaderPaddingRight.onChanged = { + ReadBookConfig.headerPaddingRight = it postEvent(EventBus.UP_CONFIG, true) } //页脚 - dsb_footer_padding_top.onChanged = { - footerPaddingTop = it + dsbFooterPaddingTop.onChanged = { + ReadBookConfig.footerPaddingTop = it postEvent(EventBus.UP_CONFIG, true) } - dsb_footer_padding_bottom.onChanged = { - footerPaddingBottom = it + dsbFooterPaddingBottom.onChanged = { + ReadBookConfig.footerPaddingBottom = it postEvent(EventBus.UP_CONFIG, true) } - dsb_footer_padding_left.onChanged = { - footerPaddingLeft = it + dsbFooterPaddingLeft.onChanged = { + ReadBookConfig.footerPaddingLeft = it postEvent(EventBus.UP_CONFIG, true) } - dsb_footer_padding_right.onChanged = { - footerPaddingRight = it + dsbFooterPaddingRight.onChanged = { + ReadBookConfig.footerPaddingRight = it postEvent(EventBus.UP_CONFIG, true) } - cb_show_top_line.onCheckedChangeListener = { cb, isChecked -> - if (cb.isPressed) { - showHeaderLine = isChecked - postEvent(EventBus.UP_CONFIG, true) - } + cbShowTopLine.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.showHeaderLine = isChecked + postEvent(EventBus.UP_CONFIG, true) } - cb_show_bottom_line.onCheckedChangeListener = { cb, isChecked -> - if (cb.isPressed) { - showFooterLine = isChecked - postEvent(EventBus.UP_CONFIG, true) - } + cbShowBottomLine.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.showFooterLine = isChecked + postEvent(EventBus.UP_CONFIG, true) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt index cfd632453..1d85a5963 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt @@ -5,47 +5,55 @@ import android.content.Context import android.view.KeyEvent import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogPageKeyBinding import io.legado.app.lib.theme.backgroundColor -import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefString import io.legado.app.utils.hideSoftInput -import io.legado.app.utils.putPrefInt -import io.legado.app.utils.removePref -import kotlinx.android.synthetic.main.dialog_page_key.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.putPrefString +import splitties.views.onClick class PageKeyDialog(context: Context) : Dialog(context, R.style.AppTheme_AlertDialog) { + private val binding = DialogPageKeyBinding.inflate(layoutInflater) + init { - setContentView(R.layout.dialog_page_key) - content_view.setBackgroundColor(context.backgroundColor) - et_prev.setText(context.getPrefInt(PreferKey.prevKey).toString()) - et_next.setText(context.getPrefInt(PreferKey.nextKey).toString()) - tv_ok.onClick { - val prevKey = et_prev.text?.toString() - if (prevKey.isNullOrEmpty()) { - context.removePref(PreferKey.prevKey) - } else { - context.putPrefInt(PreferKey.prevKey, prevKey.toInt()) + setContentView(binding.root) + binding.run { + contentView.setBackgroundColor(context.backgroundColor) + etPrev.setText(context.getPrefString(PreferKey.prevKeys)) + etNext.setText(context.getPrefString(PreferKey.nextKeys)) + tvReset.onClick { + etPrev.setText("") + etNext.setText("") } - val nextKey = et_next.text?.toString() - if (nextKey.isNullOrEmpty()) { - context.removePref(PreferKey.nextKey) - } else { - context.putPrefInt(PreferKey.nextKey, nextKey.toInt()) + tvOk.setOnClickListener { + context.putPrefString(PreferKey.prevKeys, etPrev.text?.toString()) + context.putPrefString(PreferKey.nextKeys, etNext.text?.toString()) + dismiss() } - dismiss() } } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode != KeyEvent.KEYCODE_BACK) { - if (et_prev.hasFocus()) { - et_prev.setText(keyCode.toString()) - } else if (et_next.hasFocus()) { - et_next.setText(keyCode.toString()) + if (keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_DEL) { + if (binding.etPrev.hasFocus()) { + val editableText = binding.etPrev.editableText + if (editableText.isEmpty() or editableText.endsWith(",")) { + editableText.append(keyCode.toString()) + } else { + editableText.append(",").append(keyCode.toString()) + } + return true + } else if (binding.etNext.hasFocus()) { + val editableText = binding.etNext.editableText + if (editableText.isEmpty() or editableText.endsWith(",")) { + editableText.append(keyCode.toString()) + } else { + editableText.append(",").append(keyCode.toString()) + } + return true } - return true } return super.onKeyDown(keyCode, event) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt index 3ae466454..38bf88920 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.book.read.config import android.content.SharedPreferences import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,30 +9,28 @@ import android.widget.LinearLayout import androidx.fragment.app.DialogFragment import androidx.preference.ListPreference import androidx.preference.Preference -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.backgroundColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.utils.getPrefLong +import io.legado.app.utils.getSize import io.legado.app.utils.postEvent +import splitties.init.appCtx class ReadAloudConfigDialog : DialogFragment() { private val readAloudPreferTag = "readAloudPreferTag" override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } + val dm = requireActivity().getSize() dialog?.window?.let { - - it.setBackgroundDrawableResource(R.color.transparent) + it.setBackgroundDrawableResource(R.color.transparent) it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) } } @@ -42,7 +39,7 @@ class ReadAloudConfigDialog : DialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { val view = LinearLayout(requireContext()) view.setBackgroundColor(requireContext().backgroundColor) view.id = R.id.tag1 @@ -64,9 +61,9 @@ class ReadAloudConfigDialog : DialogFragment() { private val speakEngineSummary: String get() { - val eid = App.INSTANCE.getPrefLong(PreferKey.speakEngine) - val ht = App.db.httpTTSDao().get(eid) - return ht?.name ?: getString(R.string.local_tts) + val eid = appCtx.getPrefLong(PreferKey.speakEngine) + val ht = appDb.httpTTSDao.get(eid) + return ht?.name ?: getString(R.string.system_tts) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -121,7 +118,6 @@ class ReadAloudConfigDialog : DialogFragment() { when (preference) { is ListPreference -> { val index = preference.findIndexOfValue(value) - // Set the summary to reflect the new value. preference.summary = if (index >= 0) preference.entries[index] else null } else -> { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt index 7fcd0f26d..d1fd5b0bb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt @@ -1,34 +1,34 @@ package io.legado.app.ui.book.read.config +import android.content.DialogInterface import android.os.Bundle -import android.util.DisplayMetrics import android.view.* import android.widget.SeekBar import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadAloudBinding import io.legado.app.help.AppConfig import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.utils.ColorUtils import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.observeEvent import io.legado.app.utils.putPrefBoolean -import kotlinx.android.synthetic.main.dialog_read_aloud.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding + class ReadAloudDialog : BaseDialogFragment() { - var callBack: CallBack? = null + private var callBack: CallBack? = null + private val binding by viewBinding(DialogReadAloudBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) @@ -41,11 +41,17 @@ class ReadAloudDialog : BaseDialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + (activity as ReadBookActivity).bottomDialog++ callBack = activity as? CallBack return inflater.inflate(R.layout.dialog_read_aloud, container) } @@ -54,100 +60,96 @@ class ReadAloudDialog : BaseDialogFragment() { val bg = requireContext().bottomBackground val isLight = ColorUtils.isColorLight(bg) val textColor = requireContext().getPrimaryTextColor(isLight) - root_view.setBackgroundColor(bg) - tv_pre.setTextColor(textColor) - tv_next.setTextColor(textColor) - iv_play_prev.setColorFilter(textColor) - iv_play_pause.setColorFilter(textColor) - iv_play_next.setColorFilter(textColor) - iv_stop.setColorFilter(textColor) - iv_timer.setColorFilter(textColor) - tv_timer.setTextColor(textColor) - tv_tts_speed.setTextColor(textColor) - iv_catalog.setColorFilter(textColor) - tv_catalog.setTextColor(textColor) - iv_main_menu.setColorFilter(textColor) - tv_main_menu.setTextColor(textColor) - iv_to_backstage.setColorFilter(textColor) - tv_to_backstage.setTextColor(textColor) - iv_setting.setColorFilter(textColor) - tv_setting.setTextColor(textColor) - cb_tts_follow_sys.setTextColor(textColor) - initOnChange() + binding.run { + rootView.setBackgroundColor(bg) + tvPre.setTextColor(textColor) + tvNext.setTextColor(textColor) + ivPlayPrev.setColorFilter(textColor) + ivPlayPause.setColorFilter(textColor) + ivPlayNext.setColorFilter(textColor) + ivStop.setColorFilter(textColor) + ivTimer.setColorFilter(textColor) + tvTimer.setTextColor(textColor) + tvTtsSpeed.setTextColor(textColor) + ivCatalog.setColorFilter(textColor) + tvCatalog.setTextColor(textColor) + ivMainMenu.setColorFilter(textColor) + tvMainMenu.setTextColor(textColor) + ivToBackstage.setColorFilter(textColor) + tvToBackstage.setTextColor(textColor) + ivSetting.setColorFilter(textColor) + tvSetting.setTextColor(textColor) + cbTtsFollowSys.setTextColor(textColor) + } initData() initEvent() } - private fun initData() { + private fun initData() = binding.run { upPlayState() upTimerText(BaseReadAloudService.timeMinute) - seek_timer.progress = BaseReadAloudService.timeMinute - cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) - seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked - seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate + seekTimer.progress = BaseReadAloudService.timeMinute + cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) + seekTtsSpeechRate.isEnabled = !cbTtsFollowSys.isChecked + seekTtsSpeechRate.progress = AppConfig.ttsSpeechRate } - private fun initOnChange() { - cb_tts_follow_sys.setOnCheckedChangeListener { buttonView, isChecked -> - if (buttonView.isPressed) { - requireContext().putPrefBoolean("ttsFollowSys", isChecked) - seek_tts_SpeechRate.isEnabled = !isChecked - upTtsSpeechRate() - } + private fun initEvent() = binding.run { + llMainMenu.setOnClickListener { + callBack?.showMenuBar() + dismissAllowingStateLoss() } - seek_tts_SpeechRate.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit + llSetting.setOnClickListener { + ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") + } + tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) } + ivStop.setOnClickListener { + ReadAloud.stop(requireContext()) + dismissAllowingStateLoss() + } + ivPlayPause.setOnClickListener { callBack?.onClickReadAloud() } + ivPlayPrev.setOnClickListener { ReadAloud.prevParagraph(requireContext()) } + ivPlayNext.setOnClickListener { ReadAloud.nextParagraph(requireContext()) } + llCatalog.setOnClickListener { callBack?.openChapterList() } + llToBackstage.setOnClickListener { callBack?.finish() } + cbTtsFollowSys.setOnCheckedChangeListener { _, isChecked -> + requireContext().putPrefBoolean("ttsFollowSys", isChecked) + seekTtsSpeechRate.isEnabled = !isChecked + upTtsSpeechRate() + } + seekTtsSpeechRate.setOnSeekBarChangeListener(object : SeekBarChangeListener { - override fun onStopTrackingTouch(seekBar: SeekBar?) { - AppConfig.ttsSpeechRate = seek_tts_SpeechRate.progress + override fun onStopTrackingTouch(seekBar: SeekBar) { + AppConfig.ttsSpeechRate = seekBar.progress upTtsSpeechRate() } }) - seek_timer.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + seekTimer.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { upTimerText(progress) } - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - ReadAloud.setTimer(requireContext(), seek_timer.progress) + override fun onStopTrackingTouch(seekBar: SeekBar) { + ReadAloud.setTimer(requireContext(), seekTimer.progress) } }) } - private fun initEvent() { - ll_main_menu.onClick { callBack?.showMenuBar(); dismiss() } - ll_setting.onClick { - ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") - } - tv_pre.onClick { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } - tv_next.onClick { ReadBook.moveToNextChapter(true) } - iv_stop.onClick { ReadAloud.stop(requireContext()); dismiss() } - iv_play_pause.onClick { callBack?.onClickReadAloud() } - iv_play_prev.onClick { ReadAloud.prevParagraph(requireContext()) } - iv_play_next.onClick { ReadAloud.nextParagraph(requireContext()) } - ll_catalog.onClick { callBack?.openChapterList() } - ll_to_backstage.onClick { callBack?.finish() } - } - private fun upPlayState() { if (!BaseReadAloudService.pause) { - iv_play_pause.setImageResource(R.drawable.ic_pause_24dp) + binding.ivPlayPause.setImageResource(R.drawable.ic_pause_24dp) } else { - iv_play_pause.setImageResource(R.drawable.ic_play_24dp) + binding.ivPlayPause.setImageResource(R.drawable.ic_play_24dp) } val bg = requireContext().bottomBackground val isLight = ColorUtils.isColorLight(bg) val textColor = requireContext().getPrimaryTextColor(isLight) - iv_play_pause.setColorFilter(textColor) + binding.ivPlayPause.setColorFilter(textColor) } private fun upTimerText(timeMinute: Int) { - tv_timer.text = requireContext().getString(R.string.timer_m, timeMinute) + binding.tvTimer.text = requireContext().getString(R.string.timer_m, timeMinute) } private fun upTtsSpeechRate() { @@ -160,7 +162,7 @@ class ReadAloudDialog : BaseDialogFragment() { override fun observeLiveBus() { observeEvent(EventBus.ALOUD_STATE) { upPlayState() } - observeEvent(EventBus.TTS_DS) { seek_timer.progress = it } + observeEvent(EventBus.TTS_DS) { binding.seekTimer.progress = it } } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt index 689752171..7f984491c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -1,40 +1,38 @@ package io.legado.app.ui.book.read.config -import android.annotation.SuppressLint import android.content.DialogInterface import android.os.Bundle -import android.util.DisplayMetrics import android.view.* import androidx.core.view.get import io.legado.app.R import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadBookStyleBinding +import io.legado.app.databinding.ItemReadStyleBinding import io.legado.app.help.ReadBookConfig -import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.widget.font.FontSelectDialog -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_read.* -import kotlinx.android.synthetic.main.dialog_read_book_style.* -import kotlinx.android.synthetic.main.dialog_title_config.view.* -import org.jetbrains.anko.sdk27.listeners.onCheckedChange -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dp +import io.legado.app.utils.getIndexById +import io.legado.app.utils.postEvent +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onLongClick class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { - + private val binding by viewBinding(DialogReadBookStyleBinding::bind) val callBack get() = activity as? ReadBookActivity + private lateinit var styleAdapter: StyleAdapter override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window?.let { it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) it.setBackgroundDrawableResource(R.color.background) @@ -52,6 +50,7 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { container: ViewGroup?, savedInstanceState: Bundle? ): View? { + (activity as ReadBookActivity).bottomDialog++ return inflater.inflate(R.layout.dialog_read_book_style, container) } @@ -64,212 +63,132 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) ReadBookConfig.save() + (activity as ReadBookActivity).bottomDialog-- } - private fun initView() { + private fun initView() = binding.run { val bg = requireContext().bottomBackground val isLight = ColorUtils.isColorLight(bg) val textColor = requireContext().getPrimaryTextColor(isLight) - root_view.setBackgroundColor(bg) - tv_page_anim.setTextColor(textColor) - tv_bg_ts.setTextColor(textColor) - tv_share_layout.setTextColor(textColor) - dsb_text_size.valueFormat = { + rootView.setBackgroundColor(bg) + tvPageAnim.setTextColor(textColor) + tvBgTs.setTextColor(textColor) + tvShareLayout.setTextColor(textColor) + dsbTextSize.valueFormat = { (it + 5).toString() } - dsb_text_letter_spacing.valueFormat = { + dsbTextLetterSpacing.valueFormat = { ((it - 50) / 100f).toString() } - dsb_line_size.valueFormat = { ((it - 10) / 10f).toString() } - dsb_paragraph_spacing.valueFormat = { (it / 10f).toString() } + dsbLineSize.valueFormat = { ((it - 10) / 10f).toString() } + dsbParagraphSpacing.valueFormat = { (it / 10f).toString() } + styleAdapter = StyleAdapter() + rvStyle.adapter = styleAdapter + styleAdapter.addFooterView { + ItemReadStyleBinding.inflate(layoutInflater, it, false).apply { + ivStyle.setPadding(6.dp, 6.dp, 6.dp, 6.dp) + ivStyle.setText(null) + ivStyle.setColorFilter(textColor) + ivStyle.borderColor = textColor + ivStyle.setImageResource(R.drawable.ic_add) + root.setOnClickListener { + ReadBookConfig.configList.add(ReadBookConfig.Config()) + showBgTextConfig(ReadBookConfig.configList.lastIndex) + } + } + } } private fun initData() { - cb_share_layout.isChecked = ReadBookConfig.shareLayout - ReadBookConfig.pageAnim.let { - if (it >= 0 && it < rg_page_anim.childCount) { - rg_page_anim.check(rg_page_anim[it].id) - } - } - upStyle() - setBg() - upBg() + binding.cbShareLayout.isChecked = ReadBookConfig.shareLayout + upView() + styleAdapter.setItems(ReadBookConfig.configList) } - private fun initViewEvent() { - chinese_converter.onChanged { + private fun initViewEvent() = binding.run { + chineseConverter.onChanged { postEvent(EventBus.UP_CONFIG, true) } - tv_title_mode.onClick { - showTitleConfig() - } - text_font_weight_converter.onChanged { + textFontWeightConverter.onChanged { postEvent(EventBus.UP_CONFIG, true) } - tv_text_font.onClick { + tvTextFont.setOnClickListener { FontSelectDialog().show(childFragmentManager, "fontSelectDialog") } - tv_text_indent.onClick { + tvTextIndent.setOnClickListener { selector( title = getString(R.string.text_indent), items = resources.getStringArray(R.array.indent).toList() ) { _, index -> - ReadBookConfig.bodyIndentCount = index + ReadBookConfig.paragraphIndent = " ".repeat(index) postEvent(EventBus.UP_CONFIG, true) } } - tv_padding.onClick { - dismiss() + tvPadding.setOnClickListener { + dismissAllowingStateLoss() callBack?.showPaddingConfig() } - tv_tip.onClick { + tvTip.setOnClickListener { TipConfigDialog().show(childFragmentManager, "tipConfigDialog") } - rg_page_anim.onCheckedChange { _, checkedId -> - ReadBookConfig.pageAnim = rg_page_anim.getIndexById(checkedId) - callBack?.page_view?.upPageAnim() + rgPageAnim.setOnCheckedChangeListener { _, checkedId -> + ReadBook.book?.setPageAnim(-1) + ReadBookConfig.pageAnim = binding.rgPageAnim.getIndexById(checkedId) + callBack?.upPageAnim() } - cb_share_layout.onCheckedChangeListener = { checkBox, isChecked -> - if (checkBox.isPressed) { - ReadBookConfig.shareLayout = isChecked - upStyle() - postEvent(EventBus.UP_CONFIG, true) - } + cbShareLayout.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.shareLayout = isChecked + upView() + postEvent(EventBus.UP_CONFIG, true) } - dsb_text_size.onChanged = { + dsbTextSize.onChanged = { ReadBookConfig.textSize = it + 5 postEvent(EventBus.UP_CONFIG, true) } - dsb_text_letter_spacing.onChanged = { + dsbTextLetterSpacing.onChanged = { ReadBookConfig.letterSpacing = (it - 50) / 100f postEvent(EventBus.UP_CONFIG, true) } - dsb_line_size.onChanged = { + dsbLineSize.onChanged = { ReadBookConfig.lineSpacingExtra = it postEvent(EventBus.UP_CONFIG, true) } - dsb_paragraph_spacing.onChanged = { + dsbParagraphSpacing.onChanged = { ReadBookConfig.paragraphSpacing = it postEvent(EventBus.UP_CONFIG, true) } - bg0.onClick { changeBg(0) } - bg0.onLongClick { showBgTextConfig(0) } - bg1.onClick { changeBg(1) } - bg1.onLongClick { showBgTextConfig(1) } - bg2.onClick { changeBg(2) } - bg2.onLongClick { showBgTextConfig(2) } - bg3.onClick { changeBg(3) } - bg3.onLongClick { showBgTextConfig(3) } - bg4.onClick { changeBg(4) } - bg4.onLongClick { showBgTextConfig(4) } - } - - @SuppressLint("InflateParams") - private fun showTitleConfig() = ReadBookConfig.apply { - requireContext().alert(R.string.title) { - val rootView = LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_title_config, null).apply { - rg_title_mode.checkByIndex(titleMode) - dsb_title_size.progress = titleSize - dsb_title_top.progress = titleTopSpacing - dsb_title_bottom.progress = titleBottomSpacing - rg_title_mode.onCheckedChange { _, checkedId -> - titleMode = rg_title_mode.getIndexById(checkedId) - postEvent(EventBus.UP_CONFIG, true) - } - dsb_title_size.onChanged = { - titleSize = it - postEvent(EventBus.UP_CONFIG, true) - } - dsb_title_top.onChanged = { - titleTopSpacing = it - postEvent(EventBus.UP_CONFIG, true) - } - dsb_title_bottom.onChanged = { - titleBottomSpacing = it - postEvent(EventBus.UP_CONFIG, true) - } - } - customView = rootView - }.show().applyTint() } private fun changeBg(index: Int) { - if (ReadBookConfig.styleSelect != index) { + val oldIndex = ReadBookConfig.styleSelect + if (index != oldIndex) { ReadBookConfig.styleSelect = index ReadBookConfig.upBg() - upStyle() - upBg() + upView() + styleAdapter.notifyItemChanged(oldIndex) + styleAdapter.notifyItemChanged(index) postEvent(EventBus.UP_CONFIG, true) } } private fun showBgTextConfig(index: Int): Boolean { - dismiss() + dismissAllowingStateLoss() changeBg(index) callBack?.showBgTextConfig() return true } - private fun upStyle() { - ReadBookConfig.let { - dsb_text_size.progress = it.textSize - 5 - dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50 - dsb_line_size.progress = it.lineSpacingExtra - dsb_paragraph_spacing.progress = it.paragraphSpacing - } - } - - private fun setBg() = ReadBookConfig.apply { - bg0.setTextColor(getConfig(0).textColor()) - bg1.setTextColor(getConfig(1).textColor()) - bg2.setTextColor(getConfig(2).textColor()) - bg3.setTextColor(getConfig(3).textColor()) - bg4.setTextColor(getConfig(4).textColor()) - for (i in 0..4) { - val iv = when (i) { - 1 -> bg1 - 2 -> bg2 - 3 -> bg3 - 4 -> bg4 - else -> bg0 + private fun upView() = binding.run { + ReadBook.pageAnim().let { + if (it >= 0 && it < rgPageAnim.childCount) { + rgPageAnim.check(rgPageAnim[it].id) } - iv.setImageDrawable(getConfig(i).bgDrawable(100, 150)) } - } - - private fun upBg() = ReadBookConfig.apply { - bg0.borderColor = getConfig(0).textColor() - bg0.setTextBold(false) - bg1.borderColor = getConfig(1).textColor() - bg1.setTextBold(false) - bg2.borderColor = getConfig(2).textColor() - bg2.setTextBold(false) - bg3.borderColor = getConfig(3).textColor() - bg3.setTextBold(false) - bg4.borderColor = getConfig(4).textColor() - bg4.setTextBold(false) - when (styleSelect) { - 1 -> { - bg1.borderColor = accentColor - bg1.setTextBold(true) - } - 2 -> { - bg2.borderColor = accentColor - bg2.setTextBold(true) - } - 3 -> { - bg3.borderColor = accentColor - bg3.setTextBold(true) - } - 4 -> { - bg4.borderColor = accentColor - bg4.setTextBold(true) - } - else -> { - bg0.borderColor = accentColor - bg0.setTextBold(true) - } + ReadBookConfig.let { + dsbTextSize.progress = it.textSize - 5 + dsbTextLetterSpacing.progress = (it.letterSpacing * 100).toInt() + 50 + dsbLineSize.progress = it.lineSpacingExtra + dsbParagraphSpacing.progress = it.paragraphSpacing } } @@ -282,4 +201,48 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack { postEvent(EventBus.UP_CONFIG, true) } } + + inner class StyleAdapter : + RecyclerAdapter(requireContext()) { + + override fun getViewBinding(parent: ViewGroup): ItemReadStyleBinding { + return ItemReadStyleBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemReadStyleBinding, + item: ReadBookConfig.Config, + payloads: MutableList + ) { + binding.apply { + ivStyle.setText(item.name.ifBlank { "文字" }) + ivStyle.setTextColor(item.curTextColor()) + ivStyle.setImageDrawable(item.curBgDrawable(100, 150)) + if (ReadBookConfig.styleSelect == holder.layoutPosition) { + ivStyle.borderColor = accentColor + ivStyle.setTextBold(true) + } else { + ivStyle.borderColor = item.curTextColor() + ivStyle.setTextBold(false) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemReadStyleBinding) { + binding.apply { + ivStyle.setOnClickListener { + if (ivStyle.isInView) { + changeBg(holder.layoutPosition) + } + } + ivStyle.onLongClick(ivStyle.isInView) { + if (ivStyle.isInView) { + showBgTextConfig(holder.layoutPosition) + } + } + } + } + + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt index 6ee1d5bda..b2d576ebd 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt @@ -3,53 +3,66 @@ package io.legado.app.ui.book.read.config import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.LiveData +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.HttpTTS +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogHttpTtsEditBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemHttpTtsBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.cancelButton -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryColor import io.legado.app.service.help.ReadAloud +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_http_tts_edit.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_http_tts.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import splitties.init.appCtx + class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val ttsUrlKey = "ttsUrlKey" + lateinit var adapter: Adapter + private val viewModel: SpeakEngineViewModel by viewModels() + private var engineId = appCtx.getPrefLong(PreferKey.speakEngine) + private val importDocResult = registerForActivityResult(FilePicker()) { + it?.let { + viewModel.importLocal(it) + } + } + private val exportDirResult = registerForActivityResult(FilePicker()) { + it?.let { + viewModel.export(it) + } + } override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } - lateinit var adapter: Adapter - lateinit var viewModel: SpeakEngineViewModel - private var httpTTSData: LiveData>? = null - var engineId = App.INSTANCE.getPrefLong(PreferKey.speakEngine) - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(SpeakEngineViewModel::class.java) return inflater.inflate(R.layout.dialog_recycler_view, container) } @@ -59,106 +72,151 @@ class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener initData() } - private fun initView() { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.setTitle(R.string.speak_engine) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) + private fun initView() = binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.setTitle(R.string.speak_engine) + ATH.applyEdgeEffectColor(recyclerView) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) adapter = Adapter(requireContext()) - recycler_view.adapter = adapter - tv_footer_left.setText(R.string.local_tts) - tv_footer_left.visible() - tv_footer_left.onClick { + recyclerView.adapter = adapter + tvFooterLeft.setText(R.string.system_tts) + tvFooterLeft.visible() + tvFooterLeft.setOnClickListener { removePref(PreferKey.speakEngine) - dismiss() + dismissAllowingStateLoss() } - tv_ok.visible() - tv_ok.onClick { + tvOk.visible() + tvOk.setOnClickListener { putPrefLong(PreferKey.speakEngine, engineId) - dismiss() + dismissAllowingStateLoss() } - tv_cancel.visible() - tv_cancel.onClick { - dismiss() + tvCancel.visible() + tvCancel.setOnClickListener { + dismissAllowingStateLoss() } } - private fun initMenu() { - tool_bar.inflateMenu(R.menu.speak_engine) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + private fun initMenu() = binding.run { + toolBar.inflateMenu(R.menu.speak_engine) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@SpeakEngineDialog) } private fun initData() { - httpTTSData?.removeObservers(this) - httpTTSData = App.db.httpTTSDao().observeAll() - httpTTSData?.observe(this, { - adapter.setItems(it) - }) + launch { + appDb.httpTTSDao.flowAll().collect { + adapter.setItems(it) + } + } } override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_add -> editHttpTTS() R.id.menu_default -> viewModel.importDefault() + R.id.menu_import_local -> importDocResult.launch( + FilePickerParam( + mode = FilePicker.FILE, + allowExtensions = arrayOf("txt", "json") + ) + ) + R.id.menu_import_onLine -> importAlert() + R.id.menu_export -> exportDirResult.launch(null) } return true } + private fun importAlert() { + val aCache = ACache.get(requireContext(), cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(ttsUrlKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(ttsUrlKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { url -> + if (!cacheUrls.contains(url)) { + cacheUrls.add(0, url) + aCache.put(ttsUrlKey, cacheUrls.joinToString(",")) + } + viewModel.importOnLine(url) + } + } + }.show() + } + @SuppressLint("InflateParams") private fun editHttpTTS(v: HttpTTS? = null) { val httpTTS = v?.copy() ?: HttpTTS() requireContext().alert(titleResource = R.string.speak_engine) { - var rootView: View? = null - customView { - LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_http_tts_edit, null).apply { - rootView = this - tv_name.setText(httpTTS.name) - tv_url.setText(httpTTS.url) - } - } + val alertBinding = DialogHttpTtsEditBinding.inflate(layoutInflater) + alertBinding.tvName.setText(httpTTS.name) + alertBinding.tvUrl.setText(httpTTS.url) + customView { alertBinding.root } cancelButton() okButton { - rootView?.apply { - httpTTS.name = tv_name.text.toString() - httpTTS.url = tv_url.text.toString() - App.db.httpTTSDao().insert(httpTTS) + alertBinding.apply { + httpTTS.name = tvName.text.toString() + httpTTS.url = tvUrl.text.toString() + appDb.httpTTSDao.insert(httpTTS) ReadAloud.upReadAloudClass() } } - }.show().applyTint() + neutralButton(R.string.help) { + val helpStr = String( + requireContext().assets.open("help/httpTTSHelp.md").readBytes() + ) + TextDialog.show(childFragmentManager, helpStr, TextDialog.MD) + } + }.show() } inner class Adapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_http_tts) { + RecyclerAdapter(context) { - override fun convert(holder: ItemViewHolder, item: HttpTTS, payloads: MutableList) { - holder.itemView.apply { - cb_name.text = item.name - cb_name.isChecked = item.id == engineId + override fun getViewBinding(parent: ViewGroup): ItemHttpTtsBinding { + return ItemHttpTtsBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemHttpTtsBinding, + item: HttpTTS, + payloads: MutableList + ) { + binding.apply { + cbName.text = item.name + cbName.isChecked = item.id == engineId } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - cb_name.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemHttpTtsBinding) { + binding.apply { + cbName.setOnClickListener { getItem(holder.layoutPosition)?.let { httpTTS -> engineId = httpTTS.id - notifyItemRangeChanged(0, getActualItemCount()) + notifyItemRangeChanged(0, itemCount) } } - iv_edit.onClick { + ivEdit.setOnClickListener { editHttpTTS(getItem(holder.layoutPosition)) } - iv_menu_delete.onClick { + ivMenuDelete.setOnClickListener { getItem(holder.layoutPosition)?.let { httpTTS -> - App.db.httpTTSDao().delete(httpTTS) + appDb.httpTTSDao.delete(httpTTS) } } } } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt index 490eb6efd..db11b0383 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt @@ -1,34 +1,72 @@ package io.legado.app.ui.book.read.config import android.app.Application -import io.legado.app.App +import android.net.Uri import io.legado.app.base.BaseViewModel -import io.legado.app.data.entities.TxtTocRule -import io.legado.app.help.DefaultValueHelp -import io.legado.app.help.http.HttpHelper -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonArray +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.help.DefaultData +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.* class SpeakEngineViewModel(application: Application) : BaseViewModel(application) { fun importDefault() { execute { - DefaultValueHelp.initHttpTTS() + DefaultData.importDefaultHttpTTS() } } - fun importOnLine(url: String, finally: (msg: String) -> Unit) { + fun importOnLine(url: String) { execute { - HttpHelper.simpleGetAsync(url)?.let { json -> - GSON.fromJsonArray(json)?.let { - App.db.txtTocRule().insert(*it.toTypedArray()) - } + okHttpClient.newCall { + url(url) + }.text("utf-8").let { json -> + import(json) + } + }.onSuccess { + context.toastOnUi("导入成功") + }.onError { + context.toastOnUi("导入失败") + } + } + + fun importLocal(uri: Uri) { + execute { + uri.readText(context)?.let { + import(it) } }.onSuccess { - finally("导入成功") + context.toastOnUi("导入成功") }.onError { - finally("导入失败") + context.toastOnUi("导入失败") } } + fun import(text: String) { + when { + text.isJsonArray() -> { + GSON.fromJsonArray(text)?.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) + } + } + text.isJsonObject() -> { + GSON.fromJsonObject(text)?.let { + appDb.httpTTSDao.insert(it) + } + } + else -> { + throw Exception("格式不对") + } + } + } + + fun export(uri: Uri) { + execute { + val httpTTS = appDb.httpTTSDao.all + uri.writeBytes(context, "httpTts.json", GSON.toJson(httpTTS).toByteArray()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt index fbff59f3e..d720d1e51 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt @@ -10,10 +10,10 @@ import io.legado.app.help.ReadBookConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.accentColor import io.legado.app.ui.widget.text.StrokeTextView -import io.legado.app.utils.applyTint -import org.jetbrains.anko.sdk27.listeners.onClick -class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { + +class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : + StrokeTextView(context, attrs) { private val spannableString = SpannableString("中/粗/细") private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) @@ -24,7 +24,7 @@ class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : StrokeTe if (!isInEditMode) { upUi(ReadBookConfig.textBold) } - onClick { + setOnClickListener { selectType() } } @@ -46,7 +46,7 @@ class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : StrokeTe upUi(i) onChanged?.invoke() } - }.show().applyTint() + }.show() } fun onChanged(unit: () -> Unit) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt index 583afb4d4..a6540f9c9 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt @@ -1,28 +1,31 @@ package io.legado.app.ui.book.read.config import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import com.jaredrummler.android.colorpicker.ColorPickerDialog import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogTipConfigBinding +import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadTipConfig import io.legado.app.lib.dialogs.selector -import io.legado.app.utils.postEvent -import kotlinx.android.synthetic.main.dialog_tip_config.* -import org.jetbrains.anko.sdk27.listeners.onCheckedChange -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding + class TipConfigDialog : BaseDialogFragment() { + companion object { + const val TIP_COLOR = 7897 + } + + private val binding by viewBinding(DialogTipConfigBinding::bind) + override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.let { - it.windowManager?.defaultDisplay?.getMetrics(dm) - } dialog?.window ?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) } @@ -38,218 +41,165 @@ class TipConfigDialog : BaseDialogFragment() { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { initView() initEvent() + observeEvent(EventBus.TIP_COLOR) { + upTvTipColor() + } } - private fun initView() { - tv_header_left.text = ReadTipConfig.tipHeaderLeftStr - tv_header_middle.text = ReadTipConfig.tipHeaderMiddleStr - tv_header_right.text = ReadTipConfig.tipHeaderRightStr - tv_footer_left.text = ReadTipConfig.tipFooterLeftStr - tv_footer_middle.text = ReadTipConfig.tipFooterMiddleStr - tv_footer_right.text = ReadTipConfig.tipFooterRightStr - sw_hide_header.isChecked = ReadTipConfig.hideHeader - sw_hide_footer.isChecked = ReadTipConfig.hideFooter + private fun initView() = binding.run { + rgTitleMode.checkByIndex(ReadBookConfig.titleMode) + dsbTitleSize.progress = ReadBookConfig.titleSize + dsbTitleTop.progress = ReadBookConfig.titleTopSpacing + dsbTitleBottom.progress = ReadBookConfig.titleBottomSpacing + + tvHeaderShow.text = ReadTipConfig.getHeaderModes(requireContext())[ReadTipConfig.headerMode] + tvFooterShow.text = ReadTipConfig.getFooterModes(requireContext())[ReadTipConfig.footerMode] + + tvHeaderLeft.text = ReadTipConfig.tipHeaderLeftStr + tvHeaderMiddle.text = ReadTipConfig.tipHeaderMiddleStr + tvHeaderRight.text = ReadTipConfig.tipHeaderRightStr + tvFooterLeft.text = ReadTipConfig.tipFooterLeftStr + tvFooterMiddle.text = ReadTipConfig.tipFooterMiddleStr + tvFooterRight.text = ReadTipConfig.tipFooterRightStr + + upTvTipColor() } - private fun initEvent() { - tv_header_left.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderMiddle == i) { - tipHeaderMiddle = none - tv_header_middle.text = tipArray[none] - } - if (tipHeaderRight == i) { - tipHeaderRight = none - tv_header_right.text = tipArray[none] - } - if (tipFooterLeft == i) { - tipFooterLeft = none - tv_footer_left.text = tipArray[none] - } - if (tipFooterMiddle == i) { - tipFooterMiddle = none - tv_footer_middle.text = tipArray[none] - } - if (tipFooterRight == i) { - tipFooterRight = none - tv_footer_right.text = tipArray[none] - } - } - tipHeaderLeft = i - tv_header_left.text = tipArray[i] - } + private fun upTvTipColor() { + binding.tvTipColor.text = + if (ReadTipConfig.tipColor == 0) { + "跟随正文" + } else { + "#${ReadTipConfig.tipColor.hexString}" + } + } + + private fun initEvent() = binding.run { + rgTitleMode.setOnCheckedChangeListener { _, checkedId -> + ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId) + postEvent(EventBus.UP_CONFIG, true) + } + dsbTitleSize.onChanged = { + ReadBookConfig.titleSize = it + postEvent(EventBus.UP_CONFIG, true) + } + dsbTitleTop.onChanged = { + ReadBookConfig.titleTopSpacing = it + postEvent(EventBus.UP_CONFIG, true) + } + dsbTitleBottom.onChanged = { + ReadBookConfig.titleBottomSpacing = it + postEvent(EventBus.UP_CONFIG, true) + } + llHeaderShow.setOnClickListener { + val headerModes = ReadTipConfig.getHeaderModes(requireContext()) + selector(items = headerModes.values.toList()) { _, i -> + ReadTipConfig.headerMode = headerModes.keys.toList()[i] + tvHeaderShow.text = headerModes[ReadTipConfig.headerMode] postEvent(EventBus.UP_CONFIG, true) } } - tv_header_middle.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderLeft == i) { - tipHeaderLeft = none - tv_header_left.text = tipArray[none] - } - if (tipHeaderRight == i) { - tipHeaderRight = none - tv_header_right.text = tipArray[none] - } - if (tipFooterLeft == i) { - tipFooterLeft = none - tv_footer_left.text = tipArray[none] - } - if (tipFooterMiddle == i) { - tipFooterMiddle = none - tv_footer_middle.text = tipArray[none] - } - if (tipFooterRight == i) { - tipFooterRight = none - tv_footer_right.text = tipArray[none] - } - } - tipHeaderMiddle = i - tv_header_middle.text = tipArray[i] - } + llFooterShow.setOnClickListener { + val footerModes = ReadTipConfig.getFooterModes(requireContext()) + selector(items = footerModes.values.toList()) { _, i -> + ReadTipConfig.footerMode = footerModes.keys.toList()[i] + tvFooterShow.text = footerModes[ReadTipConfig.footerMode] postEvent(EventBus.UP_CONFIG, true) } } - tv_header_right.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderLeft == i) { - tipHeaderLeft = none - tv_header_left.text = tipArray[none] - } - if (tipHeaderMiddle == i) { - tipHeaderMiddle = none - tv_header_middle.text = tipArray[none] - } - if (tipFooterLeft == i) { - tipFooterLeft = none - tv_footer_left.text = tipArray[none] - } - if (tipFooterMiddle == i) { - tipFooterMiddle = none - tv_footer_middle.text = tipArray[none] - } - if (tipFooterRight == i) { - tipFooterRight = none - tv_footer_right.text = tipArray[none] - } - } - tipHeaderRight = i - tv_header_right.text = tipArray[i] - } + llHeaderLeft.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipHeaderLeft = i + tvHeaderLeft.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } - tv_footer_left.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderLeft == i) { - tipHeaderLeft = none - tv_header_left.text = tipArray[none] - } - if (tipHeaderMiddle == i) { - tipHeaderMiddle = none - tv_header_middle.text = tipArray[none] - } - if (tipHeaderRight == i) { - tipHeaderRight = none - tv_header_right.text = tipArray[none] - } - if (tipFooterMiddle == i) { - tipFooterMiddle = none - tv_footer_middle.text = tipArray[none] - } - if (tipFooterRight == i) { - tipFooterRight = none - tv_footer_right.text = tipArray[none] - } - } - tipFooterLeft = i - tv_footer_left.text = tipArray[i] - } + llHeaderMiddle.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipHeaderMiddle = i + tvHeaderMiddle.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } - tv_footer_middle.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderLeft == i) { - tipHeaderLeft = none - tv_header_left.text = tipArray[none] - } - if (tipHeaderMiddle == i) { - tipHeaderMiddle = none - tv_header_middle.text = tipArray[none] - } - if (tipHeaderRight == i) { - tipHeaderRight = none - tv_header_right.text = tipArray[none] - } - if (tipFooterLeft == i) { - tipFooterLeft = none - tv_footer_left.text = tipArray[none] - } - if (tipFooterRight == i) { - tipFooterRight = none - tv_footer_right.text = tipArray[none] - } - } - tipFooterMiddle = i - tv_footer_middle.text = tipArray[i] - } + llHeaderRight.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipHeaderRight = i + tvHeaderRight.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } - tv_footer_right.onClick { - selector(items = ReadTipConfig.tipArray.toList()) { _, i -> - ReadTipConfig.apply { - if (i != none) { - if (tipHeaderLeft == i) { - tipHeaderLeft = none - tv_header_left.text = tipArray[none] - } - if (tipHeaderMiddle == i) { - tipHeaderMiddle = none - tv_header_middle.text = tipArray[none] - } - if (tipHeaderRight == i) { - tipHeaderRight = none - tv_header_right.text = tipArray[none] - } - if (tipFooterLeft == i) { - tipFooterLeft = none - tv_footer_left.text = tipArray[none] - } - if (tipFooterMiddle == i) { - tipFooterMiddle = none - tv_footer_middle.text = tipArray[none] - } - } - tipFooterRight = i - tv_footer_right.text = tipArray[i] - } + llFooterLeft.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipFooterLeft = i + tvFooterLeft.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } - sw_hide_header.onCheckedChange { buttonView, isChecked -> - if (buttonView?.isPressed == true) { - ReadTipConfig.hideHeader = isChecked + llFooterMiddle.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipFooterMiddle = i + tvFooterMiddle.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } - sw_hide_footer.onCheckedChange { buttonView, isChecked -> - if (buttonView?.isPressed == true) { - ReadTipConfig.hideFooter = isChecked + llFooterRight.setOnClickListener { + selector(items = ReadTipConfig.tips) { _, i -> + clearRepeat(i) + ReadTipConfig.tipFooterRight = i + tvFooterRight.text = ReadTipConfig.tips[i] postEvent(EventBus.UP_CONFIG, true) } } + llTipColor.setOnClickListener { + selector(items = arrayListOf("跟随正文", "自定义")) { _, i -> + when (i) { + 0 -> { + ReadTipConfig.tipColor = 0 + upTvTipColor() + postEvent(EventBus.UP_CONFIG, true) + } + 1 -> ColorPickerDialog.newBuilder() + .setShowAlphaSlider(false) + .setDialogType(ColorPickerDialog.TYPE_CUSTOM) + .setDialogId(TIP_COLOR) + .show(requireActivity()) + } + } + } + } + + private fun clearRepeat(repeat: Int) = ReadTipConfig.apply { + if (repeat != none) { + if (tipHeaderLeft == repeat) { + tipHeaderLeft = none + binding.tvHeaderLeft.text = tips[none] + } + if (tipHeaderMiddle == repeat) { + tipHeaderMiddle = none + binding.tvHeaderMiddle.text = tips[none] + } + if (tipHeaderRight == repeat) { + tipHeaderRight = none + binding.tvHeaderRight.text = tips[none] + } + if (tipFooterLeft == repeat) { + tipFooterLeft = none + binding.tvFooterLeft.text = tips[none] + } + if (tipFooterMiddle == repeat) { + tipFooterMiddle = none + binding.tvFooterMiddle.text = tips[none] + } + if (tipFooterRight == repeat) { + tipFooterRight = none + binding.tvFooterRight.text = tips[none] + } + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt index c0bed1512..6e597fbe9 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt @@ -3,56 +3,49 @@ package io.legado.app.ui.book.read.config import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.FragmentManager -import androidx.lifecycle.LiveData +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogTocRegexBinding +import io.legado.app.databinding.DialogTocRegexEditBinding +import io.legado.app.databinding.ItemTocRegexBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.cancelButton -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_toc_regex.* -import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.* -import kotlinx.android.synthetic.main.item_toc_regex.view.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import org.jetbrains.anko.sdk27.listeners.onClick import java.util.* - class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { private val importTocRuleKey = "tocRuleUrl" private lateinit var adapter: TocRegexAdapter - private var tocRegexLiveData: LiveData>? = null var selectedName: String? = null private var durRegex: String? = null - lateinit var viewModel: TocRegexViewModel + private val viewModel: TocRegexViewModel by viewModels() + private val binding by viewBinding(DialogTocRegexBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt()) } @@ -61,52 +54,49 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(TocRegexViewModel::class.java) return inflater.inflate(R.layout.dialog_toc_regex, container) } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) + binding.toolBar.setBackgroundColor(primaryColor) durRegex = arguments?.getString("tocRegex") - tool_bar.setTitle(R.string.txt_toc_regex) - tool_bar.inflateMenu(R.menu.txt_toc_regex) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + binding.toolBar.setTitle(R.string.txt_toc_regex) + binding.toolBar.inflateMenu(R.menu.txt_toc_regex) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) initView() initData() } - private fun initView() { + private fun initView() = binding.run { adapter = TocRegexAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) itemTouchCallback.isCanDrag = true - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) - tv_cancel.onClick { - dismiss() + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recyclerView) + tvCancel.setOnClickListener { + dismissAllowingStateLoss() } - tv_ok.onClick { + tvOk.setOnClickListener { adapter.getItems().forEach { tocRule -> if (selectedName == tocRule.name) { val callBack = activity as? CallBack callBack?.onTocRegexDialogResult(tocRule.rule) - dismiss() - return@onClick + dismissAllowingStateLoss() + return@setOnClickListener } } } } private fun initData() { - tocRegexLiveData?.removeObservers(viewLifecycleOwner) - tocRegexLiveData = App.db.txtTocRule().observeAll() - tocRegexLiveData?.observe(viewLifecycleOwner, { tocRules -> - initSelectedName(tocRules) - adapter.setItems(tocRules) - }) + launch { + appDb.txtTocRuleDao.observeAll().collect { tocRules -> + initSelectedName(tocRules) + adapter.setItems(tocRules) + } + } } private fun initSelectedName(tocRules: List) { @@ -144,101 +134,106 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { if (!cacheUrls.contains(defaultUrl)) { cacheUrls.add(0, defaultUrl) } - requireContext().alert(titleResource = R.string.import_book_source_on_line) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = this.edit_view - edit_view.setFilterValues(cacheUrls) - edit_view.delCallBack = { - cacheUrls.remove(it) - aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) - } + requireContext().alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + alertBinding.apply { + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) } } + customView { alertBinding.root } okButton { - val text = editText?.text?.toString() + val text = alertBinding.editView.text?.toString() text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) } - Snackbar.make(tool_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() + Snackbar.make(binding.toolBar, R.string.importing, Snackbar.LENGTH_INDEFINITE) + .show() viewModel.importOnLine(it) { msg -> - tool_bar.snackbar(msg) + binding.toolBar.snackbar(msg) } } } cancelButton() - }.show().applyTint() + }.show() } @SuppressLint("InflateParams") private fun editRule(rule: TxtTocRule? = null) { val tocRule = rule?.copy() ?: TxtTocRule() requireContext().alert(titleResource = R.string.txt_toc_regex) { - var rootView: View? = null - customView { - LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_toc_regex_edit, null).apply { - rootView = this - tv_rule_name.setText(tocRule.name) - tv_rule_regex.setText(tocRule.rule) - } + val alertBinding = DialogTocRegexEditBinding.inflate(layoutInflater) + alertBinding.apply { + tvRuleName.setText(tocRule.name) + tvRuleRegex.setText(tocRule.rule) } + customView { alertBinding.root } okButton { - rootView?.apply { - tocRule.name = tv_rule_name.text.toString() - tocRule.rule = tv_rule_regex.text.toString() + alertBinding.apply { + tocRule.name = tvRuleName.text.toString() + tocRule.rule = tvRuleRegex.text.toString() viewModel.saveRule(tocRule) } } cancelButton() - }.show().applyTint() + }.show() } inner class TocRegexAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_toc_regex), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + override fun getViewBinding(parent: ViewGroup): ItemTocRegexBinding { + return ItemTocRegexBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: TxtTocRule, payloads: MutableList) { - holder.itemView.apply { + override fun convert( + holder: ItemViewHolder, + binding: ItemTocRegexBinding, + item: TxtTocRule, + payloads: MutableList + ) { + binding.apply { if (payloads.isEmpty()) { - setBackgroundColor(context.backgroundColor) - rb_regex_name.text = item.name - rb_regex_name.isChecked = item.name == selectedName - swt_enabled.isChecked = item.enable + root.setBackgroundColor(context.backgroundColor) + rbRegexName.text = item.name + rbRegexName.isChecked = item.name == selectedName + swtEnabled.isChecked = item.enable } else { - rb_regex_name.isChecked = item.name == selectedName + rbRegexName.isChecked = item.name == selectedName } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - rb_regex_name.setOnCheckedChangeListener { buttonView, isChecked -> + override fun registerListener(holder: ItemViewHolder, binding: ItemTocRegexBinding) { + binding.apply { + rbRegexName.setOnCheckedChangeListener { buttonView, isChecked -> if (buttonView.isPressed && isChecked) { selectedName = getItem(holder.layoutPosition)?.name updateItems(0, itemCount - 1, true) } } - swt_enabled.setOnCheckedChangeListener { buttonView, isChecked -> + swtEnabled.setOnCheckedChangeListener { buttonView, isChecked -> if (buttonView.isPressed) { getItem(holder.layoutPosition)?.let { it.enable = isChecked launch(IO) { - App.db.txtTocRule().update(it) + appDb.txtTocRuleDao.update(it) } } } } - iv_edit.onClick { + ivEdit.setOnClickListener { editRule(getItem(holder.layoutPosition)) } - iv_delete.onClick { + ivDelete.setOnClickListener { getItem(holder.layoutPosition)?.let { item -> launch(IO) { - App.db.txtTocRule().delete(item) + appDb.txtTocRuleDao.delete(item) } } } @@ -247,11 +242,10 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { private var isMoved = false - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) isMoved = true - return super.onMove(srcPosition, targetPosition) + return super.swap(srcPosition, targetPosition) } override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { @@ -261,7 +255,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { item.serialNumber = index + 1 } launch(IO) { - App.db.txtTocRule().update(*getItems().toTypedArray()) + appDb.txtTocRuleDao.update(*getItems().toTypedArray()) } } isMoved = false diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt index 140a781e4..d1ce1ae26 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt @@ -1,11 +1,13 @@ package io.legado.app.ui.book.read.config import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.TxtTocRule -import io.legado.app.help.http.HttpHelper -import io.legado.app.model.localBook.AnalyzeTxtFile +import io.legado.app.help.DefaultData +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonArray @@ -14,24 +16,25 @@ class TocRegexViewModel(application: Application) : BaseViewModel(application) { fun saveRule(rule: TxtTocRule) { execute { if (rule.serialNumber < 0) { - rule.serialNumber = App.db.txtTocRule().lastOrderNum + 1 + rule.serialNumber = appDb.txtTocRuleDao.lastOrderNum + 1 } - App.db.txtTocRule().insert(rule) + appDb.txtTocRuleDao.insert(rule) } } fun importDefault() { execute { - App.db.txtTocRule().deleteDefault() - AnalyzeTxtFile.getDefaultEnabledRules() + DefaultData.importDefaultTocRules() } } fun importOnLine(url: String, finally: (msg: String) -> Unit) { execute { - HttpHelper.simpleGetAsync(url)?.let { json -> + okHttpClient.newCall { + url(url) + }.text("utf-8").let { json -> GSON.fromJsonArray(json)?.let { - App.db.txtTocRule().insert(*it.toTypedArray()) + appDb.txtTocRuleDao.insert(*it.toTypedArray()) } } }.onSuccess { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 295ccb0ef..a70a546bd 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -5,10 +5,10 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF import android.util.AttributeSet -import android.util.Log import android.view.View import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Bookmark import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.accentColor import io.legado.app.service.help.ReadBook @@ -17,15 +17,19 @@ import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ImageProvider +import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.utils.activity import io.legado.app.utils.getCompatColor import io.legado.app.utils.getPrefBoolean -import kotlinx.coroutines.CoroutineScope - +import io.legado.app.utils.toastOnUi +import kotlin.math.min +/** + * 阅读内容界面 + */ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { - var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) + var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true) var upView: ((TextPage) -> Unit)? = null private val selectedPaint by lazy { Paint().apply { @@ -37,21 +41,19 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at private val visibleRect = RectF() private val selectStart = arrayOf(0, 0, 0) private val selectEnd = arrayOf(0, 0, 0) - private var textPage: TextPage = TextPage() + var textPage: TextPage = TextPage() + private set //滚动参数 private val pageFactory: TextPageFactory get() = callBack.pageFactory - private val maxScrollOffset = 100f - private var pageOffset = 0f + private var pageOffset = 0 init { callBack = activity as CallBack - contentDescription = textPage.text } fun setContent(textPage: TextPage) { this.textPage = textPage - contentDescription = textPage.text invalidate() } @@ -85,7 +87,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at textPage.textLines.forEach { textLine -> draw(canvas, textLine, relativeOffset) } - if (!ReadBookConfig.isScroll) return + if (!callBack.isScroll) return //滚动翻页 if (!pageFactory.hasNext()) return val nextPage = relativePage(1) @@ -110,19 +112,16 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at val lineTop = textLine.lineTop + relativeOffset val lineBase = textLine.lineBase + relativeOffset val lineBottom = textLine.lineBottom + relativeOffset - if (textLine.isImage) { - drawImage(canvas, textLine, lineTop, lineBottom) - } else { - drawChars( - canvas, - textLine.textChars, - lineTop, - lineBase, - lineBottom, - isTitle = textLine.isTitle, - isReadAloud = textLine.isReadAloud - ) - } + drawChars( + canvas, + textLine.textChars, + lineTop, + lineBase, + lineBottom, + textLine.isTitle, + textLine.isReadAloud, + textLine.isImage + ) } /** @@ -136,12 +135,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at lineBottom: Float, isTitle: Boolean, isReadAloud: Boolean, + isImageLine: Boolean ) { - val textPaint = if (isTitle) ChapterProvider.titlePaint else ChapterProvider.contentPaint + val textPaint = if (isTitle) { + ChapterProvider.titlePaint + } else { + ChapterProvider.contentPaint + } textPaint.color = if (isReadAloud) context.accentColor else ReadBookConfig.textColor textChars.forEach { - canvas.drawText(it.charData, it.start, lineBase, textPaint) + if (it.isImage) { + drawImage(canvas, it, lineTop, lineBottom, isImageLine) + } else { + canvas.drawText(it.charData, it.start, lineBase, textPaint) + } if (it.selected) { canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint) } @@ -153,17 +161,25 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at */ private fun drawImage( canvas: Canvas, - textLine: TextLine, + textChar: TextChar, lineTop: Float, lineBottom: Float, + isImageLine: Boolean ) { - textLine.textChars.forEach { textChar -> - ReadBook.book?.let { book -> - val rectF = RectF(textChar.start, lineTop, textChar.end, lineBottom) - ImageProvider.getImage(book, textPage.chapterIndex, textChar.charData, true) - ?.let { - canvas.drawBitmap(it, null, rectF, null) - } + val book = ReadBook.book ?: return + ImageProvider.getImage(book, textPage.chapterIndex, textChar.charData, true)?.let { + val rectF = if (isImageLine) { + RectF(textChar.start, lineTop, textChar.end, lineBottom) + } else { + /*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/ + val h = (textChar.end - textChar.start) / it.width * it.height + val div = (lineBottom - lineTop - h) / 2 + RectF(textChar.start, lineTop + div, textChar.end, lineBottom - div) + } + kotlin.runCatching { + canvas.drawBitmap(it, null, rectF, null) + }.onFailure { e -> + context.toastOnUi(e.localizedMessage) } } } @@ -171,36 +187,35 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at /** * 滚动事件 */ - fun onScroll(mOffset: Float) { - if (mOffset == 0f) return - var offset = mOffset - if (offset > maxScrollOffset) { - offset = maxScrollOffset - } else if (offset < -maxScrollOffset) { - offset = -maxScrollOffset - } - - pageOffset += offset + fun scroll(mOffset: Int) { + if (mOffset == 0) return + pageOffset += mOffset if (!pageFactory.hasPrev() && pageOffset > 0) { - pageOffset = 0f - } else if (!pageFactory.hasNext() && pageOffset < 0) { - pageOffset = 0f + pageOffset = 0 + } else if (!pageFactory.hasNext() + && pageOffset < 0 + && pageOffset + textPage.height < ChapterProvider.visibleHeight + ) { + val offset = (ChapterProvider.visibleHeight - textPage.height).toInt() + pageOffset = min(0, offset) } else if (pageOffset > 0) { pageFactory.moveToPrev(false) - textPage = pageFactory.currentPage - pageOffset -= textPage.height + textPage = pageFactory.curPage + pageOffset -= textPage.height.toInt() upView?.invoke(textPage) + contentDescription = textPage.text } else if (pageOffset < -textPage.height) { - pageOffset += textPage.height + pageOffset += textPage.height.toInt() pageFactory.moveToNext(false) - textPage = pageFactory.currentPage + textPage = pageFactory.curPage upView?.invoke(textPage) + contentDescription = textPage.text } invalidate() } fun resetPageOffset() { - pageOffset = 0f + pageOffset = 0 } /** @@ -212,36 +227,16 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit, ) { if (!selectAble) return - if (!visibleRect.contains(x, y)) return - var relativeOffset: Float - for (relativePos in 0..2) { - relativeOffset = relativeOffset(relativePos) - if (relativePos > 0) { - //滚动翻页 - if (!ReadBookConfig.isScroll) return - if (relativeOffset >= ChapterProvider.visibleHeight) return - } - val page = relativePage(relativePos) - for ((lineIndex, textLine) in page.textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - if (textChar.isImage) { - activity?.supportFragmentManager?.let { - PhotoDialog.show(it, page.chapterIndex, textChar.charData) - } - } else { - textChar.selected = true - invalidate() - select(relativePos, lineIndex, charIndex) - } - return - } - } - return + touch(x, y) { relativePos, textPage, _, lineIndex, _, charIndex, textChar -> + if (textChar.isImage) { + activity?.supportFragmentManager?.let { + PhotoDialog.show(it, textPage.chapterIndex, textChar.charData) } + } else { + textChar.selected = true + invalidate() + select(relativePos, lineIndex, charIndex) } - } } @@ -249,37 +244,23 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at * 开始选择符移动 */ fun selectStartMove(x: Float, y: Float) { - if (!visibleRect.contains(x, y)) return - var relativeOffset: Float - for (relativePos in 0..2) { - relativeOffset = relativeOffset(relativePos) - if (relativePos > 0) { - //滚动翻页 - if (!ReadBookConfig.isScroll) return - if (relativeOffset >= ChapterProvider.visibleHeight) return - } - for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - if (selectStart[0] != relativePos || selectStart[1] != lineIndex || selectStart[2] != charIndex) { - if (selectToInt(relativePos, lineIndex, charIndex) > selectToInt(selectEnd)) { - return - } - selectStart[0] = relativePos - selectStart[1] = lineIndex - selectStart[2] = charIndex - upSelectedStart( - textChar.start, - textLine.lineBottom + relativeOffset, - textLine.lineTop + relativeOffset - ) - upSelectChars() - } - return - } - } - return + touch(x, y) { relativePos, _, relativeOffset, lineIndex, textLine, charIndex, textChar -> + if (selectStart[0] != relativePos || + selectStart[1] != lineIndex || + selectStart[2] != charIndex + ) { + if (selectToInt(relativePos, lineIndex, charIndex) + < selectToInt(selectEnd) + ) { + selectStart[0] = relativePos + selectStart[1] = lineIndex + selectStart[2] = charIndex + upSelectedStart( + textChar.start, + textLine.lineBottom + relativeOffset, + textLine.lineTop + relativeOffset + ) + upSelectChars() } } } @@ -289,32 +270,57 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at * 结束选择符移动 */ fun selectEndMove(x: Float, y: Float) { + touch(x, y) { relativePos, _, relativeOffset, lineIndex, textLine, charIndex, textChar -> + if (selectEnd[0] != relativePos + || selectEnd[1] != lineIndex + || selectEnd[2] != charIndex + ) { + if (selectToInt(relativePos, lineIndex, charIndex) + > selectToInt(selectStart) + ) { + selectEnd[0] = relativePos + selectEnd[1] = lineIndex + selectEnd[2] = charIndex + upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) + upSelectChars() + } + } + } + } + + private fun touch( + x: Float, + y: Float, + touched: ( + relativePos: Int, + textPage: TextPage, + relativeOffset: Float, + lineIndex: Int, + textLine: TextLine, + charIndex: Int, + textChar: TextChar + ) -> Unit + ) { if (!visibleRect.contains(x, y)) return var relativeOffset: Float for (relativePos in 0..2) { relativeOffset = relativeOffset(relativePos) if (relativePos > 0) { //滚动翻页 - if (!ReadBookConfig.isScroll) return + if (!callBack.isScroll) return if (relativeOffset >= ChapterProvider.visibleHeight) return } - Log.e("y", "$y") - for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { - if (y > textLine.lineTop + relativeOffset && y < textLine.lineBottom + relativeOffset) { - Log.e("line", "$relativePos $lineIndex") + val textPage = relativePage(relativePos) + for ((lineIndex, textLine) in textPage.textLines.withIndex()) { + if (textLine.isTouch(y, relativeOffset)) { for ((charIndex, textChar) in textLine.textChars.withIndex()) { - if (x > textChar.start && x < textChar.end) { - Log.e("char", "$relativePos $lineIndex $charIndex") - if (selectEnd[0] != relativePos || selectEnd[1] != lineIndex || selectEnd[2] != charIndex) { - if (selectToInt(relativePos, lineIndex, charIndex) < selectToInt(selectStart)) { - return - } - selectEnd[0] = relativePos - selectEnd[1] = lineIndex - selectEnd[2] = charIndex - upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) - upSelectChars() - } + if (textChar.isTouch(x)) { + touched.invoke( + relativePos, textPage, + relativeOffset, + lineIndex, textLine, + charIndex, textChar + ) return } } @@ -331,8 +337,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at selectStart[0] = relativePage selectStart[1] = lineIndex selectStart[2] = charIndex - val textLine = relativePage(relativePage).textLines[lineIndex] - val textChar = textLine.textChars[charIndex] + val textLine = relativePage(relativePage).getLine(lineIndex) + val textChar = textLine.getTextChar(charIndex) upSelectedStart( textChar.start, textLine.lineBottom + relativeOffset(relativePage), @@ -348,37 +354,43 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at selectEnd[0] = relativePage selectEnd[1] = lineIndex selectEnd[2] = charIndex - val textLine = relativePage(relativePage).textLines[lineIndex] - val textChar = textLine.textChars[charIndex] + val textLine = relativePage(relativePage).getLine(lineIndex) + val textChar = textLine.getTextChar(charIndex) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage)) upSelectChars() } private fun upSelectChars() { - val last = if (ReadBookConfig.isScroll) 2 else 0 + val last = if (callBack.isScroll) 2 else 0 for (relativePos in 0..last) { for ((lineIndex, textLine) in relativePage(relativePos).textLines.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) { - textChar.selected = - if (relativePos == selectStart[0] - && relativePos == selectEnd[0] - && lineIndex == selectStart[1] - && lineIndex == selectEnd[1] - ) { + textChar.selected = when { + relativePos == selectStart[0] + && relativePos == selectEnd[0] + && lineIndex == selectStart[1] + && lineIndex == selectEnd[1] -> { charIndex in selectStart[2]..selectEnd[2] - } else if (relativePos == selectStart[0] && lineIndex == selectStart[1]) { + } + relativePos == selectStart[0] && lineIndex == selectStart[1] -> { charIndex >= selectStart[2] - } else if (relativePos == selectEnd[0] && lineIndex == selectEnd[1]) { + } + relativePos == selectEnd[0] && lineIndex == selectEnd[1] -> { charIndex <= selectEnd[2] - } else if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { + } + relativePos == selectStart[0] && relativePos == selectEnd[0] -> { lineIndex in (selectStart[1] + 1) until selectEnd[1] - } else if (relativePos == selectStart[0]) { + } + relativePos == selectStart[0] -> { lineIndex > selectStart[1] - } else if (relativePos == selectEnd[0]) { + } + relativePos == selectEnd[0] -> { lineIndex < selectEnd[1] - } else { + } + else -> { relativePos in selectStart[0] + 1 until selectEnd[0] } + } } } } @@ -394,7 +406,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at } fun cancelSelect() { - val last = if (ReadBookConfig.isScroll) 2 else 0 + val last = if (callBack.isScroll) 2 else 0 for (relativePos in 0..last) { relativePage(relativePos).textLines.forEach { textLine -> textLine.textChars.forEach { @@ -411,60 +423,90 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at val stringBuilder = StringBuilder() for (relativePos in selectStart[0]..selectEnd[0]) { val textPage = relativePage(relativePos) - if (relativePos == selectStart[0] && relativePos == selectEnd[0]) { - for (lineIndex in selectStart[1]..selectEnd[1]) { - if (lineIndex == selectStart[1] && lineIndex == selectEnd[1]) { - stringBuilder.append( - textPage.textLines[lineIndex].text.substring( - selectStart[2], - selectEnd[2] + 1 - ) - ) - } else if (lineIndex == selectStart[1]) { - stringBuilder.append( - textPage.textLines[lineIndex].text.substring( - selectStart[2] - ) - ) - } else if (lineIndex == selectEnd[1]) { - stringBuilder.append( - textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) - ) - } else { - stringBuilder.append(textPage.textLines[lineIndex].text) + when { + relativePos == selectStart[0] && relativePos == selectEnd[0] -> { + for (lineIndex in selectStart[1]..selectEnd[1]) { + when { + lineIndex == selectStart[1] && lineIndex == selectEnd[1] -> { + stringBuilder.append( + textPage.textLines[lineIndex].text + .substring(selectStart[2], selectEnd[2] + 1) + ) + } + lineIndex == selectStart[1] -> { + stringBuilder.append( + textPage.textLines[lineIndex].text + .substring(selectStart[2]) + ) + } + lineIndex == selectEnd[1] -> { + stringBuilder.append( + textPage.textLines[lineIndex].text + .substring(0, selectEnd[2] + 1) + ) + } + else -> { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } } } - } else if (relativePos == selectStart[0]) { - for (lineIndex in selectStart[1] until relativePage(relativePos).textLines.size) { - if (lineIndex == selectStart[1]) { - stringBuilder.append( - textPage.textLines[lineIndex].text.substring( - selectStart[2] - ) - ) - } else { - stringBuilder.append(textPage.textLines[lineIndex].text) + relativePos == selectStart[0] -> { + for (lineIndex in selectStart[1] until textPage.textLines.size) { + when (lineIndex) { + selectStart[1] -> { + stringBuilder.append( + textPage.textLines[lineIndex].text + .substring(selectStart[2]) + ) + } + else -> { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } } } - } else if (relativePos == selectEnd[0]) { - for (lineIndex in 0..selectEnd[1]) { - if (lineIndex == selectEnd[1]) { - stringBuilder.append( - textPage.textLines[lineIndex].text.substring(0, selectEnd[2] + 1) - ) - } else { - stringBuilder.append(textPage.textLines[lineIndex].text) + relativePos == selectEnd[0] -> { + for (lineIndex in 0..selectEnd[1]) { + when (lineIndex) { + selectEnd[1] -> { + stringBuilder.append( + textPage.textLines[lineIndex].text + .substring(0, selectEnd[2] + 1) + ) + } + else -> { + stringBuilder.append(textPage.textLines[lineIndex].text) + } + } } } - } else if (relativePos in selectStart[0] + 1 until selectEnd[0]) { - for (lineIndex in selectStart[1]..selectEnd[1]) { - stringBuilder.append(textPage.textLines[lineIndex].text) + relativePos in selectStart[0] + 1 until selectEnd[0] -> { + for (lineIndex in selectStart[1]..selectEnd[1]) { + stringBuilder.append(textPage.textLines[lineIndex].text) + } } } } return stringBuilder.toString() } + fun createBookmark(): Bookmark? { + val page = relativePage(selectStart[0]) + page.getTextChapter()?.let { chapter -> + ReadBook.book?.let { book -> + return book.createBookMark().apply { + chapterIndex = page.chapterIndex + chapterPos = chapter.getReadLength(page.index) + + page.getSelectStartLength(selectStart[1], selectStart[2]) + chapterName = chapter.title + bookText = selectedText + } + } + } + return null + } + private fun selectToInt(page: Int, line: Int, char: Int): Int { return page * 10000000 + line * 100000 + char } @@ -475,7 +517,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at private fun relativeOffset(relativePos: Int): Float { return when (relativePos) { - 0 -> pageOffset + 0 -> pageOffset.toFloat() 1 -> pageOffset + textPage.height else -> pageOffset + textPage.height + pageFactory.nextPage.height } @@ -485,7 +527,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at return when (relativePos) { 0 -> textPage 1 -> pageFactory.nextPage - else -> pageFactory.nextPagePlus + else -> pageFactory.nextPlusPage } } @@ -495,6 +537,6 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at fun onCancelSelect() val headerHeight: Int val pageFactory: TextPageFactory - val scope: CoroutineScope + val isScroll: Boolean } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt deleted file mode 100644 index e89ee5ae8..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt +++ /dev/null @@ -1,260 +0,0 @@ -package io.legado.app.ui.book.read.page - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.drawable.Drawable -import android.widget.FrameLayout -import androidx.core.view.isGone -import androidx.core.view.isInvisible -import io.legado.app.R -import io.legado.app.base.BaseActivity -import io.legado.app.constant.AppConst.timeFormat -import io.legado.app.help.ReadBookConfig -import io.legado.app.help.ReadTipConfig -import io.legado.app.ui.book.read.page.entities.TextPage -import io.legado.app.ui.book.read.page.provider.ChapterProvider -import io.legado.app.ui.widget.BatteryView -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.view_book_page.view.* -import org.jetbrains.anko.topPadding -import java.util.* - - -class ContentView(context: Context) : FrameLayout(context) { - - private var battery = 100 - private var tvTitle: BatteryView? = null - private var tvTime: BatteryView? = null - private var tvBattery: BatteryView? = null - private var tvPage: BatteryView? = null - private var tvTotalProgress: BatteryView? = null - private var tvPageAndTotal: BatteryView? = null - - val headerHeight: Int - get() = if (ReadBookConfig.hideStatusBar) { - if (ll_header.isGone) 0 else ll_header.height - } else context.statusBarHeight - - init { - //设置背景防止切换背景时文字重叠 - setBackgroundColor(context.getCompatColor(R.color.background)) - inflate(context, R.layout.view_book_page, this) - upTipStyle() - upStyle() - content_text_view.upView = { - setProgress(it) - } - } - - fun upStyle() { - ReadBookConfig.apply { - bv_header_left.typeface = ChapterProvider.typeface - tv_header_left.typeface = ChapterProvider.typeface - tv_header_middle.typeface = ChapterProvider.typeface - tv_header_right.typeface = ChapterProvider.typeface - bv_footer_left.typeface = ChapterProvider.typeface - tv_footer_left.typeface = ChapterProvider.typeface - tv_footer_middle.typeface = ChapterProvider.typeface - tv_footer_right.typeface = ChapterProvider.typeface - bv_header_left.setColor(textColor) - tv_header_left.setColor(textColor) - tv_header_middle.setColor(textColor) - tv_header_right.setColor(textColor) - bv_footer_left.setColor(textColor) - tv_footer_left.setColor(textColor) - tv_footer_middle.setColor(textColor) - tv_footer_right.setColor(textColor) - upStatusBar() - ll_header.setPadding( - headerPaddingLeft.dp, - headerPaddingTop.dp, - headerPaddingRight.dp, - headerPaddingBottom.dp - ) - ll_footer.setPadding( - footerPaddingLeft.dp, - footerPaddingTop.dp, - footerPaddingRight.dp, - footerPaddingBottom.dp - ) - vw_top_divider.visible(showHeaderLine) - vw_bottom_divider.visible(showFooterLine) - content_text_view.upVisibleRect() - } - upTime() - upBattery(battery) - } - - /** - * 显示状态栏时隐藏header - */ - fun upStatusBar() { - vw_status_bar.topPadding = context.statusBarHeight - vw_status_bar.isGone = - ReadBookConfig.hideStatusBar || (activity as? BaseActivity)?.isInMultiWindow == true - } - - fun upTipStyle() { - ReadTipConfig.apply { - tv_header_left.isInvisible = tipHeaderLeft != chapterTitle - bv_header_left.isInvisible = tipHeaderLeft == none || !tv_header_left.isInvisible - tv_header_right.isGone = tipHeaderRight == none - tv_header_middle.isGone = tipHeaderMiddle == none - tv_footer_left.isInvisible = tipFooterLeft != chapterTitle - bv_footer_left.isInvisible = tipFooterLeft == none || !tv_footer_left.isInvisible - tv_footer_right.isGone = tipFooterRight == none - tv_footer_middle.isGone = tipFooterMiddle == none - ll_header.isGone = hideHeader - ll_footer.isGone = hideFooter - } - tvTitle = when (ReadTipConfig.chapterTitle) { - ReadTipConfig.tipHeaderLeft -> tv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> tv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvTitle?.apply { - isBattery = false - textSize = 12f - } - tvTime = when (ReadTipConfig.time) { - ReadTipConfig.tipHeaderLeft -> bv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> bv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvTime?.apply { - isBattery = false - textSize = 12f - } - tvBattery = when (ReadTipConfig.battery) { - ReadTipConfig.tipHeaderLeft -> bv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> bv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvBattery?.apply { - isBattery = true - textSize = 10f - } - tvPage = when (ReadTipConfig.page) { - ReadTipConfig.tipHeaderLeft -> bv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> bv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvPage?.apply { - isBattery = false - textSize = 12f - } - tvTotalProgress = when (ReadTipConfig.totalProgress) { - ReadTipConfig.tipHeaderLeft -> bv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> bv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvTotalProgress?.apply { - isBattery = false - textSize = 12f - } - tvPageAndTotal = when (ReadTipConfig.pageAndTotal) { - ReadTipConfig.tipHeaderLeft -> bv_header_left - ReadTipConfig.tipHeaderMiddle -> tv_header_middle - ReadTipConfig.tipHeaderRight -> tv_header_right - ReadTipConfig.tipFooterLeft -> bv_footer_left - ReadTipConfig.tipFooterMiddle -> tv_footer_middle - ReadTipConfig.tipFooterRight -> tv_footer_right - else -> null - } - tvPageAndTotal?.apply { - isBattery = false - textSize = 12f - } - } - - fun setBg(bg: Drawable?) { - page_panel.background = bg - } - - fun upTime() { - tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) - } - - fun upBattery(battery: Int) { - this.battery = battery - tvBattery?.setBattery(battery) - } - - fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { - setProgress(textPage) - if (resetPageOffset) - resetPageOffset() - content_text_view.setContent(textPage) - } - - fun resetPageOffset() { - content_text_view.resetPageOffset() - } - - @SuppressLint("SetTextI18n") - fun setProgress(textPage: TextPage) = textPage.apply { - val title = textPage.title - tvTitle?.text = title - tvPage?.text = "${index.plus(1)}/$pageSize" - tvTotalProgress?.text = readProgress - tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" - } - - fun onScroll(offset: Float) { - content_text_view.onScroll(offset) - } - - fun upSelectAble(selectAble: Boolean) { - content_text_view.selectAble = selectAble - } - - fun selectText( - x: Float, y: Float, - select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit, - ) { - return content_text_view.selectText(x, y - headerHeight, select) - } - - fun selectStartMove(x: Float, y: Float) { - content_text_view.selectStartMove(x, y - headerHeight) - } - - fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { - content_text_view.selectStartMoveIndex(relativePage, lineIndex, charIndex) - } - - fun selectEndMove(x: Float, y: Float) { - content_text_view.selectEndMove(x, y - headerHeight) - } - - fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { - content_text_view.selectEndMoveIndex(relativePage, lineIndex, charIndex) - } - - fun cancelSelect() { - content_text_view.cancelSelect() - } - - val selectedText: String get() = content_text_view.selectedText - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index 3d4d6e266..cc4399d85 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -2,401 +2,279 @@ package io.legado.app.ui.book.read.page import android.annotation.SuppressLint import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RectF -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.ViewConfiguration +import android.graphics.drawable.Drawable +import android.view.LayoutInflater import android.widget.FrameLayout -import io.legado.app.help.AppConfig +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import io.legado.app.App +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.constant.AppConst.timeFormat +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ViewBookPageBinding import io.legado.app.help.ReadBookConfig -import io.legado.app.lib.theme.accentColor +import io.legado.app.help.ReadTipConfig import io.legado.app.service.help.ReadBook -import io.legado.app.ui.book.read.page.delegate.* -import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.provider.ChapterProvider -import io.legado.app.utils.activity -import io.legado.app.utils.screenshot -import kotlinx.android.synthetic.main.activity_book_read.view.* -import kotlin.math.abs - -class PageView(context: Context, attrs: AttributeSet) : - FrameLayout(context, attrs), - DataSource { - - val callBack: CallBack get() = activity as CallBack - var pageFactory: TextPageFactory = TextPageFactory(this) - var pageDelegate: PageDelegate? = null - - var prevPage: ContentView = ContentView(context) - var curPage: ContentView = ContentView(context) - var nextPage: ContentView = ContentView(context) - val defaultAnimationSpeed = 300 - private var pressDown = false - private var isMove = false - - //起始点 - var startX: Float = 0f - var startY: Float = 0f - - //上一个触碰点 - var lastX: Float = 0f - var lastY: Float = 0f - - //触碰点 - var touchX: Float = 0f - var touchY: Float = 0f - - //是否停止动画动作 - var isAbortAnim = false - - //长按 - private var longPressed = false - private val longPressTimeout = 600L - private val longPressRunnable = Runnable { - longPressed = true - onLongPress() - } - var isTextSelected = false - private var pressOnTextSelected = false - private var firstRelativePage = 0 - private var firstLineIndex: Int = 0 - private var firstCharIndex: Int = 0 - - val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop } - private val centerRectF = RectF(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) - private val autoPageRect by lazy { Rect() } - private val autoPagePint by lazy { - Paint().apply { - color = context.accentColor +import io.legado.app.ui.widget.BatteryView +import io.legado.app.utils.* +import java.util.* + +/** + * 阅读界面 + */ +class PageView(context: Context) : FrameLayout(context) { + private val binding = ViewBookPageBinding.inflate(LayoutInflater.from(context), this, true) + private var battery = 100 + private var tvTitle: BatteryView? = null + private var tvTime: BatteryView? = null + private var tvBattery: BatteryView? = null + private var tvPage: BatteryView? = null + private var tvTotalProgress: BatteryView? = null + private var tvPageAndTotal: BatteryView? = null + private var tvBookName: BatteryView? = null + private var tvTimeBattery: BatteryView? = null + + val headerHeight: Int + get() { + val h1 = if (ReadBookConfig.hideStatusBar) 0 else context.statusBarHeight + val h2 = if (binding.llHeader.isGone) 0 else binding.llHeader.height + return h1 + h2 } - } init { - addView(nextPage) - addView(curPage) - addView(prevPage) - upBg() - setWillNotDraw(false) - upPageAnim() - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) - centerRectF.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) - prevPage.x = -w.toFloat() - pageDelegate?.setViewSize(w, h) - if (oldw != 0 && oldh != 0) { - ReadBook.loadContent(resetPageOffset = false) + if (!isInEditMode) { + //设置背景防止切换背景时文字重叠 + setBackgroundColor(context.getCompatColor(R.color.background)) + upStyle() } - } - - override fun dispatchDraw(canvas: Canvas) { - super.dispatchDraw(canvas) - pageDelegate?.onDraw(canvas) - if (callBack.isAutoPage) { - nextPage.screenshot()?.let { - val bottom = - page_view.height * callBack.autoPageProgress / (ReadBookConfig.autoReadSpeed * 10) - autoPageRect.set(0, 0, page_view.width, bottom) - canvas.drawBitmap(it, autoPageRect, autoPageRect, null) - canvas.drawRect( - 0f, - bottom.toFloat() - 1, - page_view.width.toFloat(), - bottom.toFloat(), - autoPagePint - ) - } + binding.contentTextView.upView = { + setProgress(it) } } - override fun computeScroll() { - pageDelegate?.scroll() - } - - override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { - return true - } - - /** - * 触摸事件 - */ - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent): Boolean { - callBack.screenOffTimerStart() - when (event.action) { - MotionEvent.ACTION_DOWN -> { - if (isTextSelected) { - curPage.cancelSelect() - isTextSelected = false - pressOnTextSelected = true - } else { - pressOnTextSelected = false - } - longPressed = false - postDelayed(longPressRunnable, longPressTimeout) - pressDown = true - isMove = false - pageDelegate?.onTouch(event) - pageDelegate?.onDown() - setStartPoint(event.x, event.y) + fun upStyle() = binding.run { + upTipStyle() + ReadBookConfig.let { + val tipColor = with(ReadTipConfig) { + if (tipColor == 0) it.textColor else tipColor } - MotionEvent.ACTION_MOVE -> { - pressDown = true - if (!isMove) { - isMove = - abs(startX - event.x) > slopSquare || abs(startY - event.y) > slopSquare - } - if (isMove) { - longPressed = false - removeCallbacks(longPressRunnable) - if (isTextSelected) { - selectText(event.x, event.y) - } else { - pageDelegate?.onTouch(event) - } - } - } - MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { - removeCallbacks(longPressRunnable) - if (!pressDown) return true - if (!isMove) { - if (!longPressed && !pressOnTextSelected) { - onSingleTapUp() - return true - } - } - if (isTextSelected) { - callBack.showTextActionMenu() - } else if (isMove) { - pageDelegate?.onTouch(event) - } - pressOnTextSelected = false + bvHeaderLeft.setColor(tipColor) + tvHeaderLeft.setColor(tipColor) + tvHeaderMiddle.setColor(tipColor) + tvHeaderRight.setColor(tipColor) + bvFooterLeft.setColor(tipColor) + tvFooterLeft.setColor(tipColor) + tvFooterMiddle.setColor(tipColor) + tvFooterRight.setColor(tipColor) + upStatusBar() + llHeader.setPadding( + it.headerPaddingLeft.dp, + it.headerPaddingTop.dp, + it.headerPaddingRight.dp, + it.headerPaddingBottom.dp + ) + llFooter.setPadding( + it.footerPaddingLeft.dp, + it.footerPaddingTop.dp, + it.footerPaddingRight.dp, + it.footerPaddingBottom.dp + ) + vwTopDivider.visible(it.showHeaderLine) + vwBottomDivider.visible(it.showFooterLine) + pageNvBar.layoutParams = pageNvBar.layoutParams.apply { + height = if (it.hideNavigationBar) 0 else App.navigationBarHeight } } - return true - } - - fun upStatusBar() { - curPage.upStatusBar() - prevPage.upStatusBar() - nextPage.upStatusBar() + contentTextView.upVisibleRect() + upTime() + upBattery(battery) } /** - * 保存开始位置 + * 显示状态栏时隐藏header */ - fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { - startX = x - startY = y - lastX = x - lastY = y - touchX = x - touchY = y - - if (invalidate) { - invalidate() - } + fun upStatusBar() = with(binding.vwStatusBar) { + setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom) + isGone = + ReadBookConfig.hideStatusBar || (activity as? BaseActivity<*>)?.isInMultiWindow == true } - /** - * 保存当前位置 - */ - fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { - lastX = touchX - lastY = touchY - touchX = x - touchY = y - if (invalidate) { - invalidate() + private fun upTipStyle() = binding.run { + ReadTipConfig.apply { + tvHeaderLeft.isInvisible = tipHeaderLeft != chapterTitle + bvHeaderLeft.isInvisible = + tipHeaderLeft == none || !tvHeaderLeft.isInvisible + tvHeaderRight.isGone = tipHeaderRight == none + tvHeaderMiddle.isGone = tipHeaderMiddle == none + tvFooterLeft.isInvisible = tipFooterLeft != chapterTitle + bvFooterLeft.isInvisible = + tipFooterLeft == none || !tvFooterLeft.isInvisible + tvFooterRight.isGone = tipFooterRight == none + tvFooterMiddle.isGone = tipFooterMiddle == none + llHeader.isGone = when (headerMode) { + 1 -> false + 2 -> true + else -> !ReadBookConfig.hideStatusBar + } + llFooter.isGone = when (footerMode) { + 1 -> true + else -> false + } + } + tvTitle = getTipView(ReadTipConfig.chapterTitle) + tvTitle?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTime = getTipView(ReadTipConfig.time) + tvTime?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvBattery = getTipView(ReadTipConfig.battery) + tvBattery?.apply { + isBattery = true + textSize = 10f + } + tvPage = getTipView(ReadTipConfig.page) + tvPage?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTotalProgress = getTipView(ReadTipConfig.totalProgress) + tvTotalProgress?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvPageAndTotal = getTipView(ReadTipConfig.pageAndTotal) + tvPageAndTotal?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvBookName = getTipView(ReadTipConfig.bookName) + tvBookName?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTimeBattery = getTipView(ReadTipConfig.timeBattery) + tvTimeBattery?.apply { + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f } - pageDelegate?.onScroll() } - /** - * 长按选择 - */ - private fun onLongPress() { - curPage.selectText(startX, startY) { relativePage, lineIndex, charIndex -> - isTextSelected = true - firstRelativePage = relativePage - firstLineIndex = lineIndex - firstCharIndex = charIndex - curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + private fun getTipView(tip: Int): BatteryView? = binding.run { + return when (tip) { + ReadTipConfig.tipHeaderLeft -> + if (tip == ReadTipConfig.chapterTitle) tvHeaderLeft else bvHeaderLeft + ReadTipConfig.tipHeaderMiddle -> tvHeaderMiddle + ReadTipConfig.tipHeaderRight -> tvHeaderRight + ReadTipConfig.tipFooterLeft -> + if (tip == ReadTipConfig.chapterTitle) tvFooterLeft else bvFooterLeft + ReadTipConfig.tipFooterMiddle -> tvFooterMiddle + ReadTipConfig.tipFooterRight -> tvFooterRight + else -> null } } - /** - * 单击 - */ - private fun onSingleTapUp(): Boolean { - if (isTextSelected) { - isTextSelected = false - return true - } - if (centerRectF.contains(startX, startY)) { - if (!isAbortAnim) { - callBack.clickCenter() - } - } else if (ReadBookConfig.clickTurnPage) { - if (startX > width / 2 || AppConfig.clickAllNext) { - pageDelegate?.nextPageByAnim(defaultAnimationSpeed) - } else { - pageDelegate?.prevPageByAnim(defaultAnimationSpeed) - } - } - return true + fun setBg(bg: Drawable?) { + binding.pagePanel.background = bg } - /** - * 选择文本 - */ - private fun selectText(x: Float, y: Float) { - curPage.selectText(x, y) { relativePage, lineIndex, charIndex -> - when { - relativePage > firstRelativePage -> { - curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) - } - relativePage < firstRelativePage -> { - curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) - } - lineIndex > firstLineIndex -> { - curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) - } - lineIndex < firstLineIndex -> { - curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) - } - charIndex > firstCharIndex -> { - curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) - } - else -> { - curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) - curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) - } - } - } + fun upTime() { + tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) + upTimeBattery() } - fun onDestroy() { - pageDelegate?.onDestroy() - curPage.cancelSelect() + fun upBattery(battery: Int) { + this.battery = battery + tvBattery?.setBattery(battery) + upTimeBattery() } - fun fillPage(direction: PageDelegate.Direction) { - when (direction) { - PageDelegate.Direction.PREV -> { - pageFactory.moveToPrev(true) - } - PageDelegate.Direction.NEXT -> { - pageFactory.moveToNext(true) - } - else -> Unit + @SuppressLint("SetTextI18n") + private fun upTimeBattery() { + tvTimeBattery?.let { + val time = timeFormat.format(Date(System.currentTimeMillis())) + it.text = "$time $battery%" } } - fun upPageAnim() { - pageDelegate?.onDestroy() - pageDelegate = null - pageDelegate = when (ReadBookConfig.pageAnim) { - 0 -> CoverPageDelegate(this) - 1 -> SlidePageDelegate(this) - 2 -> SimulationPageDelegate(this) - 3 -> ScrollPageDelegate(this) - else -> NoAnimPageDelegate(this) + fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { + setProgress(textPage) + if (resetPageOffset) { + resetPageOffset() } - upContent() + binding.contentTextView.setContent(textPage) } - override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { - if (ReadBookConfig.isScroll && !callBack.isAutoPage) { - curPage.setContent(pageFactory.currentPage, resetPageOffset) - } else { - curPage.resetPageOffset() - when (relativePosition) { - -1 -> prevPage.setContent(pageFactory.prevPage) - 1 -> nextPage.setContent(pageFactory.nextPage) - else -> { - curPage.setContent(pageFactory.currentPage) - nextPage.setContent(pageFactory.nextPage) - prevPage.setContent(pageFactory.prevPage) - } - } - } - callBack.screenOffTimerStart() + fun setContentDescription(content: String) { + binding.contentTextView.contentDescription = content } - fun upTipStyle() { - curPage.upTipStyle() - prevPage.upTipStyle() - nextPage.upTipStyle() + fun resetPageOffset() { + binding.contentTextView.resetPageOffset() } - fun upStyle() { - ChapterProvider.upStyle() - curPage.upStyle() - prevPage.upStyle() - nextPage.upStyle() + @SuppressLint("SetTextI18n") + fun setProgress(textPage: TextPage) = textPage.apply { + tvBookName?.text = ReadBook.book?.name + tvTitle?.text = textPage.title + tvPage?.text = "${index.plus(1)}/$pageSize" + tvTotalProgress?.text = readProgress + tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" } - fun upBg() { - ReadBookConfig.bg ?: let { - ReadBookConfig.upBg() - } - curPage.setBg(ReadBookConfig.bg) - prevPage.setBg(ReadBookConfig.bg) - nextPage.setBg(ReadBookConfig.bg) + fun scroll(offset: Int) { + binding.contentTextView.scroll(offset) } - fun upTime() { - curPage.upTime() - prevPage.upTime() - nextPage.upTime() + fun upSelectAble(selectAble: Boolean) { + binding.contentTextView.selectAble = selectAble } - fun upBattery(battery: Int) { - curPage.upBattery(battery) - prevPage.upBattery(battery) - nextPage.upBattery(battery) + fun selectText( + x: Float, y: Float, + select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit, + ) { + return binding.contentTextView.selectText(x, y - headerHeight, select) } - override val currentChapter: TextChapter? - get() { - return if (callBack.isInitFinish) ReadBook.textChapter(0) else null - } + fun selectStartMove(x: Float, y: Float) { + binding.contentTextView.selectStartMove(x, y - headerHeight) + } - override val nextChapter: TextChapter? - get() { - return if (callBack.isInitFinish) ReadBook.textChapter(1) else null - } + fun selectStartMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + binding.contentTextView.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } - override val prevChapter: TextChapter? - get() { - return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null - } + fun selectEndMove(x: Float, y: Float) { + binding.contentTextView.selectEndMove(x, y - headerHeight) + } - override fun hasNextChapter(): Boolean { - return ReadBook.durChapterIndex < ReadBook.chapterSize - 1 + fun selectEndMoveIndex(relativePage: Int, lineIndex: Int, charIndex: Int) { + binding.contentTextView.selectEndMoveIndex(relativePage, lineIndex, charIndex) } - override fun hasPrevChapter(): Boolean { - return ReadBook.durChapterIndex > 0 + fun cancelSelect() { + binding.contentTextView.cancelSelect() } - interface CallBack { - val isInitFinish: Boolean - val isAutoPage: Boolean - val autoPageProgress: Int - fun clickCenter() - fun screenOffTimerStart() - fun showTextActionMenu() + fun createBookmark(): Bookmark? { + return binding.contentTextView.createBookmark() } -} + + val selectedText: String get() = binding.contentTextView.selectedText + + val textPage get() = binding.contentTextView.textPage +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt new file mode 100644 index 000000000..b6de252c8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt @@ -0,0 +1,566 @@ +package io.legado.app.ui.book.read.page + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.WindowInsets +import android.widget.FrameLayout +import io.legado.app.help.AppConfig +import io.legado.app.help.ReadBookConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.page.api.DataSource +import io.legado.app.ui.book.read.page.delegate.* +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.TextPageFactory +import io.legado.app.utils.activity +import io.legado.app.utils.screenshot +import java.text.BreakIterator +import java.util.* +import kotlin.math.abs + + +class ReadView(context: Context, attrs: AttributeSet) : + FrameLayout(context, attrs), + DataSource { + + val callBack: CallBack get() = activity as CallBack + var pageFactory: TextPageFactory = TextPageFactory(this) + var pageDelegate: PageDelegate? = null + private set(value) { + field?.onDestroy() + field = null + field = value + upContent() + } + var isScroll = false + var prevPage: PageView = PageView(context) + var curPage: PageView = PageView(context) + var nextPage: PageView = PageView(context) + val defaultAnimationSpeed = 300 + private var pressDown = false + private var isMove = false + + //起始点 + var startX: Float = 0f + var startY: Float = 0f + + //上一个触碰点 + var lastX: Float = 0f + var lastY: Float = 0f + + //触碰点 + var touchX: Float = 0f + var touchY: Float = 0f + + //是否停止动画动作 + var isAbortAnim = false + + //长按 + private var longPressed = false + private val longPressTimeout = 600L + private val longPressRunnable = Runnable { + longPressed = true + onLongPress() + } + var isTextSelected = false + private var pressOnTextSelected = false + private var firstRelativePage = 0 + private var firstLineIndex: Int = 0 + private var firstCharIndex: Int = 0 + + val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop } + private val tlRect = RectF() + private val tcRect = RectF() + private val trRect = RectF() + private val mlRect = RectF() + private val mcRect = RectF() + private val mrRect = RectF() + private val blRect = RectF() + private val bcRect = RectF() + private val brRect = RectF() + private val autoPageRect by lazy { Rect() } + private val autoPagePint by lazy { Paint().apply { color = context.accentColor } } + private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) } + + init { + addView(nextPage) + addView(curPage) + addView(prevPage) + if (!isInEditMode) { + upBg() + setWillNotDraw(false) + upPageAnim() + } + setRect9x() + } + + fun setRect9x() { + val edge = if (AppConfig.fullScreenGesturesSupport) 200f else 0f + tlRect.set(0f + edge, 0f, width * 0.33f, height * 0.33f) + tcRect.set(width * 0.33f, 0f, width * 0.66f, height * 0.33f) + trRect.set(width * 0.36f, 0f, width - 0f - edge, height * 0.33f) + mlRect.set(0f + edge, height * 0.33f, width * 0.33f, height * 0.66f) + mcRect.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) + mrRect.set(width * 0.66f, height * 0.33f, width - 0f - edge, height * 0.66f) + blRect.set(0f + edge, height * 0.66f, width * 0.33f, height - 10f - edge) + bcRect.set(width * 0.33f, height * 0.66f, width * 0.66f, height - 0f - edge) + brRect.set(width * 0.66f, height * 0.66f, width - 0f - edge, height - 0f - edge) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + setRect9x() + prevPage.x = -w.toFloat() + pageDelegate?.setViewSize(w, h) + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + pageDelegate?.onDraw(canvas) + if (!isInEditMode && callBack.isAutoPage && !isScroll) { + // TODO 自动翻页 + nextPage.screenshot()?.let { + val bottom = callBack.autoPageProgress + autoPageRect.set(0, 0, width, bottom) + canvas.drawBitmap(it, autoPageRect, autoPageRect, null) + canvas.drawRect( + 0f, + bottom.toFloat() - 1, + width.toFloat(), + bottom.toFloat(), + autoPagePint + ) + } + } + } + + override fun computeScroll() { + pageDelegate?.scroll() + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + /** + * 触摸事件 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + callBack.screenOffTimerStart() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val insets = + this.rootWindowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.mandatorySystemGestures()) + val height = activity?.windowManager?.currentWindowMetrics?.bounds?.height() + if (height != null) { + if (event.y > height.minus(insets.bottom)) { + return true + } + } + } + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (isTextSelected) { + curPage.cancelSelect() + isTextSelected = false + pressOnTextSelected = true + } else { + pressOnTextSelected = false + } + longPressed = false + postDelayed(longPressRunnable, longPressTimeout) + pressDown = true + isMove = false + pageDelegate?.onTouch(event) + pageDelegate?.onDown() + setStartPoint(event.x, event.y) + } + MotionEvent.ACTION_MOVE -> { + if (!isMove) { + isMove = + abs(startX - event.x) > slopSquare || abs(startY - event.y) > slopSquare + } + if (isMove) { + longPressed = false + removeCallbacks(longPressRunnable) + if (isTextSelected) { + selectText(event.x, event.y) + } else { + pageDelegate?.onTouch(event) + } + } + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + removeCallbacks(longPressRunnable) + if (!pressDown) return true + pressDown = false + if (!isMove) { + if (!longPressed && !pressOnTextSelected) { + onSingleTapUp() + return true + } + } + if (isTextSelected) { + callBack.showTextActionMenu() + } else if (isMove) { + pageDelegate?.onTouch(event) + } + pressOnTextSelected = false + } + } + return true + } + + fun upStatusBar() { + curPage.upStatusBar() + prevPage.upStatusBar() + nextPage.upStatusBar() + } + + /** + * 保存开始位置 + */ + fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { + startX = x + startY = y + lastX = x + lastY = y + touchX = x + touchY = y + + if (invalidate) { + invalidate() + } + } + + /** + * 保存当前位置 + */ + fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { + lastX = touchX + lastY = touchY + touchX = x + touchY = y + if (invalidate) { + invalidate() + } + pageDelegate?.onScroll() + } + + /** + * 长按选择 + */ + private fun onLongPress() { + kotlin.runCatching { + with(curPage.textPage) { + curPage.selectText(startX, startY) { relativePage, lineIndex, charIndex -> + isTextSelected = true + firstRelativePage = relativePage + firstLineIndex = lineIndex + firstCharIndex = charIndex + var lineStart = lineIndex + var lineEnd = lineIndex + var start: Int + var end: Int + if (lineIndex - 1 > 0 && lineIndex + 1 < lineSize) { + // 中间行 + val lineText = with(textLines) { + get(lineIndex - 1).text + get(lineIndex).text + get(lineIndex + 1).text + } + boundary.setText(lineText) + start = boundary.first() + end = boundary.next() + val cIndex = textLines[lineIndex - 1].text.length + charIndex + while (end != BreakIterator.DONE) { + if (cIndex in start until end) { + break + } + start = end + end = boundary.next() + } + if (start < textLines[lineIndex - 1].text.length) { + lineStart = lineIndex - 1 + } else { + start -= textLines[lineIndex - 1].text.length + } + if (end > textLines[lineIndex - 1].text.length + textLines[lineIndex].text.length) { + lineEnd = lineIndex + 1 + end = (end - textLines[lineIndex - 1].text.length + - textLines[lineIndex].text.length) + } else { + end = end - textLines[lineIndex - 1].text.length - 1 + } + } else if (lineIndex - 1 > 0) { + // 尾行 + val lineText = with(textLines) { + get(lineIndex - 1).text + get(lineIndex).text + } + boundary.setText(lineText) + start = boundary.first() + end = boundary.next() + val cIndex = textLines[lineIndex - 1].text.length + charIndex + while (end != BreakIterator.DONE) { + if (cIndex in start until end) { + break + } + start = end + end = boundary.next() + } + if (start < textLines[lineIndex - 1].text.length) { + lineStart = lineIndex - 1 + } else { + start -= textLines[lineIndex - 1].text.length + } + end = end - textLines[lineIndex - 1].text.length - 1 + } else if (lineIndex + 1 < lineSize) { + // 首行 + val lineText = with(textLines) { + get(lineIndex).text + get(lineIndex + 1).text + } + boundary.setText(lineText) + start = boundary.first() + end = boundary.next() + while (end != BreakIterator.DONE) { + if (charIndex in start until end) { + break + } + start = end + end = boundary.next() + } + if (end > textLines[lineIndex].text.length) { + lineEnd = lineIndex + 1 + end -= textLines[lineIndex].text.length + } else { + end -= 1 + } + } else { + // 单行 + val lineText = textLines[lineIndex].text + boundary.setText(lineText) + start = boundary.first() + end = boundary.next() + while (end != BreakIterator.DONE) { + if (charIndex in start until end) { + break + } + start = end + end = boundary.next() + } + end -= 1 + } + curPage.selectStartMoveIndex(firstRelativePage, lineStart, start) + curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end) + } + } + } + } + + /** + * 单击 + */ + private fun onSingleTapUp() { + when { + isTextSelected -> isTextSelected = false + mcRect.contains(startX, startY) -> if (!isAbortAnim) { + click(AppConfig.clickActionMC) + } + bcRect.contains(startX, startY) -> { + click(AppConfig.clickActionBC) + } + blRect.contains(startX, startY) -> { + click(AppConfig.clickActionBL) + } + brRect.contains(startX, startY) -> { + click(AppConfig.clickActionBR) + } + mlRect.contains(startX, startY) -> { + click(AppConfig.clickActionML) + } + mrRect.contains(startX, startY) -> { + click(AppConfig.clickActionMR) + } + tlRect.contains(startX, startY) -> { + click(AppConfig.clickActionTL) + } + tcRect.contains(startX, startY) -> { + click(AppConfig.clickActionTC) + } + trRect.contains(startX, startY) -> { + click(AppConfig.clickActionTR) + } + } + } + + private fun click(action: Int) { + when (action) { + 0 -> callBack.showActionMenu() + 1 -> pageDelegate?.nextPageByAnim(defaultAnimationSpeed) + 2 -> pageDelegate?.prevPageByAnim(defaultAnimationSpeed) + 3 -> ReadBook.moveToNextChapter(true) + 4 -> ReadBook.moveToPrevChapter(upContent = true, toLast = false) + } + } + + /** + * 选择文本 + */ + private fun selectText(x: Float, y: Float) { + curPage.selectText(x, y) { relativePage, lineIndex, charIndex -> + when { + relativePage > firstRelativePage -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + relativePage < firstRelativePage -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + lineIndex > firstLineIndex -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + lineIndex < firstLineIndex -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + charIndex > firstCharIndex -> { + curPage.selectStartMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectEndMoveIndex(relativePage, lineIndex, charIndex) + } + else -> { + curPage.selectEndMoveIndex(firstRelativePage, firstLineIndex, firstCharIndex) + curPage.selectStartMoveIndex(relativePage, lineIndex, charIndex) + } + } + } + } + + fun onDestroy() { + pageDelegate?.onDestroy() + curPage.cancelSelect() + } + + fun fillPage(direction: PageDirection): Boolean { + return when (direction) { + PageDirection.PREV -> { + pageFactory.moveToPrev(true) + } + PageDirection.NEXT -> { + pageFactory.moveToNext(true) + } + else -> false + } + } + + fun upPageAnim() { + isScroll = ReadBook.pageAnim() == 3 + when (ReadBook.pageAnim()) { + 0 -> if (pageDelegate !is CoverPageDelegate) { + pageDelegate = CoverPageDelegate(this) + } + 1 -> if (pageDelegate !is SlidePageDelegate) { + pageDelegate = SlidePageDelegate(this) + } + 2 -> if (pageDelegate !is SimulationPageDelegate) { + pageDelegate = SimulationPageDelegate(this) + } + 3 -> if (pageDelegate !is ScrollPageDelegate) { + pageDelegate = ScrollPageDelegate(this) + } + else -> if (pageDelegate !is NoAnimPageDelegate) { + pageDelegate = NoAnimPageDelegate(this) + } + } + } + + override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { + curPage.setContentDescription(pageFactory.curPage.text) + if (isScroll && !callBack.isAutoPage) { + curPage.setContent(pageFactory.curPage, resetPageOffset) + } else { + curPage.resetPageOffset() + when (relativePosition) { + -1 -> prevPage.setContent(pageFactory.prevPage) + 1 -> nextPage.setContent(pageFactory.nextPage) + else -> { + curPage.setContent(pageFactory.curPage) + nextPage.setContent(pageFactory.nextPage) + prevPage.setContent(pageFactory.prevPage) + } + } + } + callBack.screenOffTimerStart() + } + + fun upStyle() { + ChapterProvider.upStyle() + curPage.upStyle() + prevPage.upStyle() + nextPage.upStyle() + } + + fun upBg() { + ReadBookConfig.bg ?: let { + ReadBookConfig.upBg() + } + curPage.setBg(ReadBookConfig.bg) + prevPage.setBg(ReadBookConfig.bg) + nextPage.setBg(ReadBookConfig.bg) + } + + fun upTime() { + curPage.upTime() + prevPage.upTime() + nextPage.upTime() + } + + fun upBattery(battery: Int) { + curPage.upBattery(battery) + prevPage.upBattery(battery) + nextPage.upBattery(battery) + } + + override val currentChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(0) else null + } + + override val nextChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(1) else null + } + + override val prevChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null + } + + override fun hasNextChapter(): Boolean { + return ReadBook.durChapterIndex < ReadBook.chapterSize - 1 + } + + override fun hasPrevChapter(): Boolean { + return ReadBook.durChapterIndex > 0 + } + + interface CallBack { + val isInitFinish: Boolean + val isAutoPage: Boolean + val autoPageProgress: Int + fun showActionMenu() + fun screenOffTimerStart() + fun showTextActionMenu() + fun autoPageStop() + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt similarity index 79% rename from app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt rename to app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt index 3a128f969..bb7df2d66 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt @@ -1,11 +1,11 @@ -package io.legado.app.ui.book.read.page +package io.legado.app.ui.book.read.page.api import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.page.entities.TextChapter interface DataSource { - val pageIndex: Int get() = ReadBook.durChapterPos() + val pageIndex: Int get() = ReadBook.durPageIndex() val currentChapter: TextChapter? diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt similarity index 79% rename from app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt rename to app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt index 75fd79094..ecf0e56a7 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageFactory.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.book.read.page +package io.legado.app.ui.book.read.page.api abstract class PageFactory(protected val dataSource: DataSource) { @@ -14,9 +14,9 @@ abstract class PageFactory(protected val dataSource: DataSource) { abstract val prevPage: DATA - abstract val currentPage: DATA + abstract val curPage: DATA - abstract val nextPagePlus: DATA + abstract val nextPlusPage: DATA abstract fun hasNext(): Boolean diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt index 278b8fffc..c0024c81c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt @@ -3,9 +3,10 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas import android.graphics.Matrix import android.graphics.drawable.GradientDrawable -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection -class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { +class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { private val bitmapMatrix = Matrix() private val shadowDrawableR: GradientDrawable @@ -21,19 +22,19 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { if (!isRunning) return val offsetX = touchX - startX - if ((mDirection == Direction.NEXT && offsetX > 0) - || (mDirection == Direction.PREV && offsetX < 0) + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) ) { return } val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth - if (mDirection == Direction.PREV) { + if (mDirection == PageDirection.PREV) { bitmapMatrix.setTranslate(distanceX, 0.toFloat()) curBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } addShadow(distanceX.toInt(), canvas) - } else if (mDirection == Direction.NEXT) { + } else if (mDirection == PageDirection.NEXT) { bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) nextBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } @@ -53,14 +54,14 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onAnimStop() { if (!isCancel) { - pageView.fillPage(mDirection) + readView.fillPage(mDirection) } } override fun onAnimStart(animationSpeed: Int) { val distanceX: Float when (mDirection) { - Direction.NEXT -> distanceX = + PageDirection.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX if (dis > viewWidth) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt index a224edd5b..3f8c29ca3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt @@ -2,29 +2,30 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Bitmap import android.view.MotionEvent -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.utils.screenshot -abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { +abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) { protected var curBitmap: Bitmap? = null protected var prevBitmap: Bitmap? = null protected var nextBitmap: Bitmap? = null - override fun setDirection(direction: Direction) { + override fun setDirection(direction: PageDirection) { super.setDirection(direction) setBitmap() } private fun setBitmap() { when (mDirection) { - Direction.PREV -> { + PageDirection.PREV -> { prevBitmap?.recycle() prevBitmap = prevPage.screenshot() curBitmap?.recycle() curBitmap = curPage.screenshot() } - Direction.NEXT -> { + PageDirection.NEXT -> { nextBitmap?.recycle() nextBitmap = nextPage.screenshot() curBitmap?.recycle() @@ -43,7 +44,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie onScroll(event) } MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { - onAnimStart(pageView.defaultAnimationSpeed) + onAnimStart(readView.defaultAnimationSpeed) } } } @@ -71,7 +72,7 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie val deltaX = (focusX - startX).toInt() val deltaY = (focusY - startY).toInt() val distance = deltaX * deltaX + deltaY * deltaY - isMoved = distance > pageView.slopSquare + isMoved = distance > readView.slopSquare if (isMoved) { if (sumX - startX > 0) { //如果上一页不存在 @@ -79,22 +80,22 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie noNext = true return } - setDirection(Direction.PREV) + setDirection(PageDirection.PREV) } else { //如果不存在表示没有下一页了 if (!hasNext()) { noNext = true return } - setDirection(Direction.NEXT) + setDirection(PageDirection.NEXT) } } } if (isMoved) { - isCancel = if (mDirection == Direction.NEXT) sumX > lastX else sumX < lastX + isCancel = if (mDirection == PageDirection.NEXT) sumX > lastX else sumX < lastX isRunning = true //设置触摸点 - pageView.setTouchPoint(sumX, sumY) + readView.setTouchPoint(sumX, sumY) } } @@ -103,30 +104,30 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie isMoved = false isRunning = false if (!scroller.isFinished) { - pageView.isAbortAnim = true + readView.isAbortAnim = true scroller.abortAnimation() if (!isCancel) { - pageView.fillPage(mDirection) - pageView.invalidate() + readView.fillPage(mDirection) + readView.invalidate() } } else { - pageView.isAbortAnim = false + readView.isAbortAnim = false } } override fun nextPageByAnim(animationSpeed: Int) { abortAnim() if (!hasNext()) return - setDirection(Direction.NEXT) - pageView.setTouchPoint(viewWidth.toFloat(), 0f) + setDirection(PageDirection.NEXT) + readView.setStartPoint(viewWidth.toFloat(), 0f, false) onAnimStart(animationSpeed) } override fun prevPageByAnim(animationSpeed: Int) { abortAnim() if (!hasPrev()) return - setDirection(Direction.PREV) - pageView.setTouchPoint(0f, 0f) + setDirection(PageDirection.PREV) + readView.setStartPoint(0f, 0f, false) onAnimStart(animationSpeed) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt index 7414df401..9809a4f71 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt @@ -1,13 +1,13 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView -class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { +class NoAnimPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { override fun onAnimStart(animationSpeed: Int) { if (!isCancel) { - pageView.fillPage(mDirection) + readView.fillPage(mDirection) } stopScroll() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index 3a2dabc1d..2b417240c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -3,51 +3,52 @@ package io.legado.app.ui.book.read.page.delegate import android.content.Context import android.graphics.Canvas import android.view.MotionEvent -import android.view.animation.DecelerateInterpolator +import android.view.animation.LinearInterpolator import android.widget.Scroller import androidx.annotation.CallSuper import com.google.android.material.snackbar.Snackbar import io.legado.app.R -import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection import kotlin.math.abs -abstract class PageDelegate(protected val pageView: PageView) { +abstract class PageDelegate(protected val readView: ReadView) { - protected val context: Context = pageView.context + protected val context: Context = readView.context //起始点 - protected val startX: Float get() = pageView.startX - protected val startY: Float get() = pageView.startY + protected val startX: Float get() = readView.startX + protected val startY: Float get() = readView.startY //上一个触碰点 - protected val lastX: Float get() = pageView.lastX - protected val lastY: Float get() = pageView.lastY + protected val lastX: Float get() = readView.lastX + protected val lastY: Float get() = readView.lastY //触碰点 - protected val touchX: Float get() = pageView.touchX - protected val touchY: Float get() = pageView.touchY + protected val touchX: Float get() = readView.touchX + protected val touchY: Float get() = readView.touchY - protected val nextPage: ContentView get() = pageView.nextPage - protected val curPage: ContentView get() = pageView.curPage - protected val prevPage: ContentView get() = pageView.prevPage + protected val nextPage: PageView get() = readView.nextPage + protected val curPage: PageView get() = readView.curPage + protected val prevPage: PageView get() = readView.prevPage - protected var viewWidth: Int = pageView.width - protected var viewHeight: Int = pageView.height + protected var viewWidth: Int = readView.width + protected var viewHeight: Int = readView.height protected val scroller: Scroller by lazy { - Scroller(pageView.context, DecelerateInterpolator()) + Scroller(readView.context, LinearInterpolator()) } private val snackBar: Snackbar by lazy { - Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT) + Snackbar.make(readView, "", Snackbar.LENGTH_SHORT) } var isMoved = false var noNext = true //移动方向 - var mDirection = Direction.NONE + var mDirection = PageDirection.NONE var isCancel = false var isRunning = false var isStarted = false @@ -65,7 +66,7 @@ abstract class PageDelegate(protected val pageView: PageView) { scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) isRunning = true isStarted = true - pageView.invalidate() + readView.invalidate() } protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, animationSpeed: Int) { @@ -77,15 +78,15 @@ abstract class PageDelegate(protected val pageView: PageView) { scroller.startScroll(startX, startY, dx, dy, duration) isRunning = true isStarted = true - pageView.invalidate() + readView.invalidate() } protected fun stopScroll() { isStarted = false - pageView.post { + readView.post { isMoved = false isRunning = false - pageView.invalidate() + readView.invalidate() } } @@ -96,7 +97,7 @@ abstract class PageDelegate(protected val pageView: PageView) { fun scroll() { if (scroller.computeScrollOffset()) { - pageView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) + readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) } else if (isStarted) { onAnimStop() stopScroll() @@ -117,17 +118,17 @@ abstract class PageDelegate(protected val pageView: PageView) { abstract fun prevPageByAnim(animationSpeed: Int) - open fun keyTurnPage(direction: Direction) { + open fun keyTurnPage(direction: PageDirection) { if (isRunning) return when (direction) { - Direction.NEXT -> nextPageByAnim(100) - Direction.PREV -> prevPageByAnim(100) + PageDirection.NEXT -> nextPageByAnim(100) + PageDirection.PREV -> prevPageByAnim(100) else -> return } } @CallSuper - open fun setDirection(direction: Direction) { + open fun setDirection(direction: PageDirection) { mDirection = direction } @@ -149,14 +150,14 @@ abstract class PageDelegate(protected val pageView: PageView) { //取消 isCancel = false //是下一章还是前一章 - setDirection(Direction.NONE) + setDirection(PageDirection.NONE) } /** * 判断是否有上一页 */ fun hasPrev(): Boolean { - val hasPrev = pageView.pageFactory.hasPrev() + val hasPrev = readView.pageFactory.hasPrev() if (!hasPrev) { if (!snackBar.isShown) { snackBar.setText(R.string.no_prev_page) @@ -170,8 +171,9 @@ abstract class PageDelegate(protected val pageView: PageView) { * 判断是否有下一页 */ fun hasNext(): Boolean { - val hasNext = pageView.pageFactory.hasNext() + val hasNext = readView.pageFactory.hasNext() if (!hasNext) { + readView.callBack.autoPageStop() if (!snackBar.isShown) { snackBar.setText(R.string.no_next_page) snackBar.show() @@ -184,8 +186,4 @@ abstract class PageDelegate(protected val pageView: PageView) { } - enum class Direction { - NONE, PREV, NEXT - } - } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt index 30078de89..26e8f98b0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -3,13 +3,14 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas import android.view.MotionEvent import android.view.VelocityTracker -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.provider.ChapterProvider -class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { +class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { // 滑动追踪的时间 private val velocityDuration = 1000 + //速度追踪器 private val mVelocity: VelocityTracker = VelocityTracker.obtain() @@ -35,13 +36,13 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { onScroll(event) } MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { - onAnimStart(pageView.defaultAnimationSpeed) + onAnimStart(readView.defaultAnimationSpeed) } } } override fun onScroll() { - curPage.onScroll(touchY - lastY) + curPage.scroll((touchY - lastY).toInt()) } override fun onDraw(canvas: Canvas) { @@ -67,12 +68,12 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { val div = if (pointerUp) count - 1 else count val focusX = sumX / div val focusY = sumY / div - pageView.setTouchPoint(sumX, sumY) + readView.setTouchPoint(sumX, sumY) if (!isMoved) { val deltaX = (focusX - startX).toInt() val deltaY = (focusY - startY).toInt() val distance = deltaX * deltaX + deltaY * deltaY - isMoved = distance > pageView.slopSquare + isMoved = distance > readView.slopSquare } if (isMoved) { isRunning = true @@ -89,26 +90,26 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { isMoved = false isRunning = false if (!scroller.isFinished) { - pageView.isAbortAnim = true + readView.isAbortAnim = true scroller.abortAnimation() } else { - pageView.isAbortAnim = false + readView.isAbortAnim = false } } override fun nextPageByAnim(animationSpeed: Int) { - if (pageView.isAbortAnim) { + if (readView.isAbortAnim) { return } - pageView.setStartPoint(0f, 0f, false) + readView.setStartPoint(0f, 0f, false) startScroll(0, 0, 0, -ChapterProvider.visibleHeight, animationSpeed) } override fun prevPageByAnim(animationSpeed: Int) { - if (pageView.isAbortAnim) { + if (readView.isAbortAnim) { return } - pageView.setStartPoint(0f, 0f, false) + readView.setStartPoint(0f, 0f, false) startScroll(0, 0, 0, ChapterProvider.visibleHeight, animationSpeed) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt index 5d0e97ec6..bb3622fbc 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt @@ -5,35 +5,44 @@ import android.graphics.drawable.GradientDrawable import android.os.Build import android.view.MotionEvent import io.legado.app.help.ReadBookConfig -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection import kotlin.math.* @Suppress("DEPRECATION") -class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { +class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { //不让x,y为0,否则在点计算时会有问题 private var mTouchX = 0.1f private var mTouchY = 0.1f + // 拖拽点对应的页脚 private var mCornerX = 1 private var mCornerY = 1 private val mPath0: Path = Path() private val mPath1: Path = Path() + // 贝塞尔曲线起始点 private val mBezierStart1 = PointF() + // 贝塞尔曲线控制点 private val mBezierControl1 = PointF() + // 贝塞尔曲线顶点 private val mBezierVertex1 = PointF() + // 贝塞尔曲线结束点 private var mBezierEnd1 = PointF() // 另一条贝塞尔曲线 // 贝塞尔曲线起始点 private val mBezierStart2 = PointF() + // 贝塞尔曲线控制点 private val mBezierControl2 = PointF() + // 贝塞尔曲线顶点 private val mBezierVertex2 = PointF() + // 贝塞尔曲线结束点 private var mBezierEnd2 = PointF() @@ -57,10 +66,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi // 是否属于右上左下 private var mIsRtOrLb = false private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() + // 背面颜色组 private var mBackShadowColors: IntArray + // 前面颜色组 private var mFrontShadowColors: IntArray + // 有阴影的GradientDrawable private var mBackShadowDrawableLR: GradientDrawable private var mBackShadowDrawableRL: GradientDrawable @@ -123,31 +135,31 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi } MotionEvent.ACTION_MOVE -> { if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) - || mDirection == Direction.PREV + || mDirection == PageDirection.PREV ) { - pageView.touchY = viewHeight.toFloat() + readView.touchY = viewHeight.toFloat() } if (startY > viewHeight / 3 && startY < viewHeight / 2 - && mDirection == Direction.NEXT + && mDirection == PageDirection.NEXT ) { - pageView.touchY = 1f + readView.touchY = 1f } } } } - override fun setDirection(direction: Direction) { + override fun setDirection(direction: PageDirection) { super.setDirection(direction) when (direction) { - Direction.PREV -> + PageDirection.PREV -> //上一页滑动不出现对角 if (startX > viewWidth / 2) { calcCornerXY(startX, viewHeight.toFloat()) } else { calcCornerXY(viewWidth - startX, viewHeight.toFloat()) } - Direction.NEXT -> + PageDirection.NEXT -> if (viewWidth / 2 > startX) { calcCornerXY(viewWidth - startX, startY) } @@ -160,12 +172,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi val dy: Float // dy 垂直方向滑动的距离,负值会使滚动向上滚动 if (isCancel) { - dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { (viewWidth - touchX) } else { -touchX } - if (mDirection != Direction.NEXT) { + if (mDirection != PageDirection.NEXT) { dx = -(viewWidth + touchX) } dy = if (mCornerY > 0) { @@ -174,7 +186,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi -touchY // 防止mTouchY最终变为0 } } else { - dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { -(viewWidth + touchX) } else { (viewWidth - touchX + viewWidth) @@ -190,21 +202,21 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi override fun onAnimStop() { if (!isCancel) { - pageView.fillPage(mDirection) + readView.fillPage(mDirection) } } override fun onDraw(canvas: Canvas) { if (!isRunning) return when (mDirection) { - Direction.NEXT -> { + PageDirection.NEXT -> { calcPoints() drawCurrentPageArea(canvas, curBitmap) drawNextPageAreaAndShadow(canvas, nextBitmap) drawCurrentPageShadow(canvas) drawCurrentBackArea(canvas, curBitmap) } - Direction.PREV -> { + PageDirection.PREV -> { calcPoints() drawCurrentPageArea(canvas, prevBitmap) drawNextPageAreaAndShadow(canvas, curBitmap) diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt index 9c3413f6a..417ea746a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt @@ -2,16 +2,17 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas import android.graphics.Matrix -import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection -class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { +class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { private val bitmapMatrix = Matrix() override fun onAnimStart(animationSpeed: Int) { val distanceX: Float when (mDirection) { - Direction.NEXT -> distanceX = + PageDirection.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX if (dis > viewWidth) { @@ -34,17 +35,17 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onDraw(canvas: Canvas) { val offsetX = touchX - startX - if ((mDirection == Direction.NEXT && offsetX > 0) - || (mDirection == Direction.PREV && offsetX < 0) + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) ) return val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth if (!isRunning) return - if (mDirection == Direction.PREV) { + if (mDirection == PageDirection.PREV) { bitmapMatrix.setTranslate(distanceX + viewWidth, 0.toFloat()) curBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } bitmapMatrix.setTranslate(distanceX, 0.toFloat()) prevBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } - } else if (mDirection == Direction.NEXT) { + } else if (mDirection == PageDirection.NEXT) { bitmapMatrix.setTranslate(distanceX, 0.toFloat()) nextBitmap?.let { canvas.drawBitmap(it, bitmapMatrix, null) } bitmapMatrix.setTranslate(distanceX - viewWidth, 0.toFloat()) @@ -54,7 +55,7 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onAnimStop() { if (!isCancel) { - pageView.fillPage(mDirection) + readView.fillPage(mDirection) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java new file mode 100644 index 000000000..dda7bd174 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlMesh.java @@ -0,0 +1,957 @@ +package io.legado.app.ui.book.read.page.delegate.curl; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.opengles.GL10; + +/** + * Class implementing actual curl/page rendering. + * + * @author harism + */ +public class CurlMesh { + + // Flag for rendering some lines used for developing. Shows + // curl position and one for the direction from the + // position given. Comes handy once playing around with different + // ways for following pointer. + private static final boolean DRAW_CURL_POSITION = false; + // Flag for drawing polygon outlines. Using this flag crashes on emulator + // due to reason unknown to me. Leaving it here anyway as seeing polygon + // outlines gives good insight how original rectangle is divided. + private static final boolean DRAW_POLYGON_OUTLINES = false; + // Flag for enabling shadow rendering. + private static final boolean DRAW_SHADOW = true; + // Flag for texture rendering. While this is likely something you + // don't want to do it's been used for development purposes as texture + // rendering is rather slow on emulator. + private static final boolean DRAW_TEXTURE = true; + + // Colors for shadow. Inner one is the color drawn next to surface where + // shadowed area starts and outer one is color shadow ends to. + private static final float[] SHADOW_INNER_COLOR = {0f, 0f, 0f, .5f}; + private static final float[] SHADOW_OUTER_COLOR = {0f, 0f, 0f, .0f}; + + // Let's avoid using 'new' as much as possible. Meaning we introduce arrays + // once here and reuse them on runtime. Doesn't really have very much effect + // but avoids some garbage collections from happening. + private final Array mArrDropShadowVertices; + private final Array mArrIntersections; + private final Array mArrOutputVertices; + private final Array mArrRotatedVertices; + private final Array mArrScanLines; + private final Array mArrSelfShadowVertices; + private final Array mArrTempShadowVertices; + private final Array mArrTempVertices; + + // Buffers for feeding rasterizer. + private final FloatBuffer mBufColors; + private FloatBuffer mBufCurlPositionLines; + private final FloatBuffer mBufShadowColors; + private final FloatBuffer mBufShadowVertices; + private final FloatBuffer mBufTexCoords; + private final FloatBuffer mBufVertices; + + private int mCurlPositionLinesCount; + private int mDropShadowCount; + + // Boolean for 'flipping' texture sideways. + private boolean mFlipTexture = false; + // Maximum number of split lines used for creating a curl. + private final int mMaxCurlSplits; + + // Bounding rectangle for this mesh. mRectagle[0] = top-left corner, + // mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3] + // bottom-right. + private final Vertex[] mRectangle = new Vertex[4]; + private int mSelfShadowCount; + + private boolean mTextureBack = false; + // Texture ids and other variables. + private int[] mTextureIds = null; + private final CurlPage mTexturePage = new CurlPage(); + private final RectF mTextureRectBack = new RectF(); + private final RectF mTextureRectFront = new RectF(); + + private int mVerticesCountBack; + private int mVerticesCountFront; + + /** + * Constructor for mesh object. + * + * @param maxCurlSplits Maximum number curl can be divided into. The bigger the value + * the smoother curl will be. With the cost of having more + * polygons for drawing. + */ + public CurlMesh(int maxCurlSplits) { + // There really is no use for 0 splits. + mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits; + + mArrScanLines = new Array(maxCurlSplits + 2); + mArrOutputVertices = new Array(7); + mArrRotatedVertices = new Array(4); + mArrIntersections = new Array(2); + mArrTempVertices = new Array(7 + 4); + for (int i = 0; i < 7 + 4; ++i) { + mArrTempVertices.add(new Vertex()); + } + + if (DRAW_SHADOW) { + mArrSelfShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrDropShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrTempShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) { + mArrTempShadowVertices.add(new ShadowVertex()); + } + } + + // Rectangle consists of 4 vertices. Index 0 = top-left, index 1 = + // bottom-left, index 2 = top-right and index 3 = bottom-right. + for (int i = 0; i < 4; ++i) { + mRectangle[i] = new Vertex(); + } + // Set up shadow penumbra direction to each vertex. We do fake 'self + // shadow' calculations based on this information. + mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1; + mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1; + + if (DRAW_CURL_POSITION) { + mCurlPositionLinesCount = 3; + ByteBuffer hvbb = ByteBuffer + .allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4); + hvbb.order(ByteOrder.nativeOrder()); + mBufCurlPositionLines = hvbb.asFloatBuffer(); + mBufCurlPositionLines.position(0); + } + + // There are 4 vertices from bounding rect, max 2 from adding split line + // to two corners and curl consists of max mMaxCurlSplits lines each + // outputting 2 vertices. + int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits); + ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4); + vbb.order(ByteOrder.nativeOrder()); + mBufVertices = vbb.asFloatBuffer(); + mBufVertices.position(0); + + if (DRAW_TEXTURE) { + ByteBuffer tbb = ByteBuffer + .allocateDirect(maxVerticesCount * 2 * 4); + tbb.order(ByteOrder.nativeOrder()); + mBufTexCoords = tbb.asFloatBuffer(); + mBufTexCoords.position(0); + } + + ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4); + cbb.order(ByteOrder.nativeOrder()); + mBufColors = cbb.asFloatBuffer(); + mBufColors.position(0); + + if (DRAW_SHADOW) { + int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2; + ByteBuffer scbb = ByteBuffer + .allocateDirect(maxShadowVerticesCount * 4 * 4); + scbb.order(ByteOrder.nativeOrder()); + mBufShadowColors = scbb.asFloatBuffer(); + mBufShadowColors.position(0); + + ByteBuffer sibb = ByteBuffer + .allocateDirect(maxShadowVerticesCount * 3 * 4); + sibb.order(ByteOrder.nativeOrder()); + mBufShadowVertices = sibb.asFloatBuffer(); + mBufShadowVertices.position(0); + + mDropShadowCount = mSelfShadowCount = 0; + } + } + + /** + * Adds vertex to buffers. + */ + private void addVertex(Vertex vertex) { + mBufVertices.put((float) vertex.mPosX); + mBufVertices.put((float) vertex.mPosY); + mBufVertices.put((float) vertex.mPosZ); + mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f); + mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f); + mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f); + mBufColors.put(Color.alpha(vertex.mColor) / 255f); + if (DRAW_TEXTURE) { + mBufTexCoords.put((float) vertex.mTexX); + mBufTexCoords.put((float) vertex.mTexY); + } + } + + /** + * Sets curl for this mesh. + * + * @param curlPos Position for curl 'center'. Can be any point on line collinear + * to curl. + * @param curlDir Curl direction, should be normalized. + * @param radius Radius of curl. + */ + public synchronized void curl(PointF curlPos, PointF curlDir, double radius) { + + // First add some 'helper' lines used for development. + if (DRAW_CURL_POSITION) { + mBufCurlPositionLines.position(0); + + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y - 1.0f); + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y + 1.0f); + mBufCurlPositionLines.put(curlPos.x - 1.0f); + mBufCurlPositionLines.put(curlPos.y); + mBufCurlPositionLines.put(curlPos.x + 1.0f); + mBufCurlPositionLines.put(curlPos.y); + + mBufCurlPositionLines.put(curlPos.x); + mBufCurlPositionLines.put(curlPos.y); + mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2); + mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2); + + mBufCurlPositionLines.position(0); + } + + // Actual 'curl' implementation starts here. + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + // Calculate curl angle from direction. + double curlAngle = Math.acos(curlDir.x); + curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle; + + // Initiate rotated rectangle which's is translated to curlPos and + // rotated so that curl direction heads to right (1,0). Vertices are + // ordered in ascending order based on x -coordinate at the same time. + // And using y -coordinate in very rare case in which two vertices have + // same x -coordinate. + mArrTempVertices.addAll(mArrRotatedVertices); + mArrRotatedVertices.clear(); + for (int i = 0; i < 4; ++i) { + Vertex v = mArrTempVertices.remove(0); + v.set(mRectangle[i]); + v.translate(-curlPos.x, -curlPos.y); + v.rotateZ(-curlAngle); + int j = 0; + for (; j < mArrRotatedVertices.size(); ++j) { + Vertex v2 = mArrRotatedVertices.get(j); + if (v.mPosX > v2.mPosX) { + break; + } + if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) { + break; + } + } + mArrRotatedVertices.add(j, v); + } + + // Rotated rectangle lines/vertex indices. We need to find bounding + // lines for rotated rectangle. After sorting vertices according to + // their x -coordinate we don't have to worry about vertices at indices + // 0 and 1. But due to inaccuracy it's possible vertex 3 is not the + // opposing corner from vertex 0. So we are calculating distance from + // vertex 0 to vertices 2 and 3 - and altering line indices if needed. + // Also vertices/lines are given in an order first one has x -coordinate + // at least the latter one. This property is used in getIntersections to + // see if there is an intersection. + int[][] lines = {{0, 1}, {0, 2}, {1, 3}, {2, 3}}; + { + // TODO: There really has to be more 'easier' way of doing this - + // not including extensive use of sqrt. + Vertex v0 = mArrRotatedVertices.get(0); + Vertex v2 = mArrRotatedVertices.get(2); + Vertex v3 = mArrRotatedVertices.get(3); + double dist2 = Math.sqrt((v0.mPosX - v2.mPosX) + * (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY) + * (v0.mPosY - v2.mPosY)); + double dist3 = Math.sqrt((v0.mPosX - v3.mPosX) + * (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY) + * (v0.mPosY - v3.mPosY)); + if (dist2 > dist3) { + lines[1][1] = 3; + lines[2][1] = 2; + } + } + + mVerticesCountFront = mVerticesCountBack = 0; + + if (DRAW_SHADOW) { + mArrTempShadowVertices.addAll(mArrDropShadowVertices); + mArrTempShadowVertices.addAll(mArrSelfShadowVertices); + mArrDropShadowVertices.clear(); + mArrSelfShadowVertices.clear(); + } + + // Length of 'curl' curve. + double curlLength = Math.PI * radius; + // Calculate scan lines. + // TODO: Revisit this code one day. There is room for optimization here. + mArrScanLines.clear(); + if (mMaxCurlSplits > 0) { + mArrScanLines.add((double) 0); + } + for (int i = 1; i < mMaxCurlSplits; ++i) { + mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1)); + } + // As mRotatedVertices is ordered regarding x -coordinate, adding + // this scan line produces scan area picking up vertices which are + // rotated completely. One could say 'until infinity'. + mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1); + + // Start from right most vertex. Pretty much the same as first scan area + // is starting from 'infinity'. + double scanXmax = mArrRotatedVertices.get(0).mPosX + 1; + + for (int i = 0; i < mArrScanLines.size(); ++i) { + // Once we have scanXmin and scanXmax we have a scan area to start + // working with. + double scanXmin = mArrScanLines.get(i); + // First iterate 'original' rectangle vertices within scan area. + for (int j = 0; j < mArrRotatedVertices.size(); ++j) { + Vertex v = mArrRotatedVertices.get(j); + // Test if vertex lies within this scan area. + // TODO: Frankly speaking, can't remember why equality check was + // added to both ends. Guessing it was somehow related to case + // where radius=0f, which, given current implementation, could + // be handled much more effectively anyway. + if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) { + // Pop out a vertex from temp vertices. + Vertex n = mArrTempVertices.remove(0); + n.set(v); + // This is done solely for triangulation reasons. Given a + // rotated rectangle it has max 2 vertices having + // intersection. + Array intersections = getIntersections( + mArrRotatedVertices, lines, n.mPosX); + // In a sense one could say we're adding vertices always in + // two, positioned at the ends of intersecting line. And for + // triangulation to work properly they are added based on y + // -coordinate. And this if-else is doing it for us. + if (intersections.size() == 1 + && intersections.get(0).mPosY > v.mPosY) { + // In case intersecting vertex is higher add it first. + mArrOutputVertices.addAll(intersections); + mArrOutputVertices.add(n); + } else if (intersections.size() <= 1) { + // Otherwise add original vertex first. + mArrOutputVertices.add(n); + mArrOutputVertices.addAll(intersections); + } else { + // There should never be more than 1 intersecting + // vertex. But if it happens as a fallback simply skip + // everything. + mArrTempVertices.add(n); + mArrTempVertices.addAll(intersections); + } + } + } + + // Search for scan line intersections. + Array intersections = getIntersections(mArrRotatedVertices, + lines, scanXmin); + + // We expect to get 0 or 2 vertices. In rare cases there's only one + // but in general given a scan line intersecting rectangle there + // should be 2 intersecting vertices. + if (intersections.size() == 2) { + // There were two intersections, add them based on y + // -coordinate, higher first, lower last. + Vertex v1 = intersections.get(0); + Vertex v2 = intersections.get(1); + if (v1.mPosY < v2.mPosY) { + mArrOutputVertices.add(v2); + mArrOutputVertices.add(v1); + } else { + mArrOutputVertices.addAll(intersections); + } + } else if (intersections.size() != 0) { + // This happens in a case in which there is a original vertex + // exactly at scan line or something went very much wrong if + // there are 3+ vertices. What ever the reason just return the + // vertices to temp vertices for later use. In former case it + // was handled already earlier once iterating through + // mRotatedVertices, in latter case it's better to avoid doing + // anything with them. + mArrTempVertices.addAll(intersections); + } + + // Add vertices found during this iteration to vertex etc buffers. + while (mArrOutputVertices.size() > 0) { + Vertex v = mArrOutputVertices.remove(0); + mArrTempVertices.add(v); + + // Local texture front-facing flag. + boolean textureFront; + + // Untouched vertices. + if (i == 0) { + textureFront = true; + mVerticesCountFront++; + } + // 'Completely' rotated vertices. + else if (i == mArrScanLines.size() - 1 || curlLength == 0) { + v.mPosX = -(curlLength + v.mPosX); + v.mPosZ = 2 * radius; + v.mPenumbraX = -v.mPenumbraX; + + textureFront = false; + mVerticesCountBack++; + } + // Vertex lies within 'curl'. + else { + // Even though it's not obvious from the if-else clause, + // here v.mPosX is between [-curlLength, 0]. And we can do + // calculations around a half cylinder. + double rotY = Math.PI * (v.mPosX / curlLength); + v.mPosX = radius * Math.sin(rotY); + v.mPosZ = radius - (radius * Math.cos(rotY)); + v.mPenumbraX *= Math.cos(rotY); + // Map color multiplier to [.1f, 1f] range. + v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math + .sin(rotY) + 1)); + + if (v.mPosZ >= radius) { + textureFront = false; + mVerticesCountBack++; + } else { + textureFront = true; + mVerticesCountFront++; + } + } + + // We use local textureFront for flipping backside texture + // locally. Plus additionally if mesh is in flip texture mode, + // we'll make the procedure "backwards". Also, until this point, + // texture coordinates are within [0, 1] range so we'll adjust + // them to final texture coordinates too. + if (textureFront != mFlipTexture) { + v.mTexX *= mTextureRectFront.right; + v.mTexY *= mTextureRectFront.bottom; + v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT); + } else { + v.mTexX *= mTextureRectBack.right; + v.mTexY *= mTextureRectBack.bottom; + v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK); + } + + // Move vertex back to 'world' coordinates. + v.rotateZ(curlAngle); + v.translate(curlPos.x, curlPos.y); + addVertex(v); + + // Drop shadow is cast 'behind' the curl. + if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) { + ShadowVertex sv = mArrTempShadowVertices.remove(0); + sv.mPosX = v.mPosX; + sv.mPosY = v.mPosY; + sv.mPosZ = v.mPosZ; + sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x; + sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y; + sv.mPenumbraColor = v.mPosZ / radius; + int idx = (mArrDropShadowVertices.size() + 1) / 2; + mArrDropShadowVertices.add(idx, sv); + } + // Self shadow is cast partly over mesh. + if (DRAW_SHADOW && v.mPosZ > radius) { + ShadowVertex sv = mArrTempShadowVertices.remove(0); + sv.mPosX = v.mPosX; + sv.mPosY = v.mPosY; + sv.mPosZ = v.mPosZ; + sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX; + sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY; + sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius); + int idx = (mArrSelfShadowVertices.size() + 1) / 2; + mArrSelfShadowVertices.add(idx, sv); + } + } + + // Switch scanXmin as scanXmax for next iteration. + scanXmax = scanXmin; + } + + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + // Add shadow Vertices. + if (DRAW_SHADOW) { + mBufShadowColors.position(0); + mBufShadowVertices.position(0); + mDropShadowCount = 0; + + for (int i = 0; i < mArrDropShadowVertices.size(); ++i) { + ShadowVertex sv = mArrDropShadowVertices.get(i); + mBufShadowVertices.put((float) sv.mPosX); + mBufShadowVertices.put((float) sv.mPosY); + mBufShadowVertices.put((float) sv.mPosZ); + mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX)); + mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY)); + mBufShadowVertices.put((float) sv.mPosZ); + for (int j = 0; j < 4; ++j) { + double color = SHADOW_OUTER_COLOR[j] + + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j]) + * sv.mPenumbraColor; + mBufShadowColors.put((float) color); + } + mBufShadowColors.put(SHADOW_OUTER_COLOR); + mDropShadowCount += 2; + } + mSelfShadowCount = 0; + for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) { + ShadowVertex sv = mArrSelfShadowVertices.get(i); + mBufShadowVertices.put((float) sv.mPosX); + mBufShadowVertices.put((float) sv.mPosY); + mBufShadowVertices.put((float) sv.mPosZ); + mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX)); + mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY)); + mBufShadowVertices.put((float) sv.mPosZ); + for (int j = 0; j < 4; ++j) { + double color = SHADOW_OUTER_COLOR[j] + + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j]) + * sv.mPenumbraColor; + mBufShadowColors.put((float) color); + } + mBufShadowColors.put(SHADOW_OUTER_COLOR); + mSelfShadowCount += 2; + } + mBufShadowColors.position(0); + mBufShadowVertices.position(0); + } + } + + /** + * Calculates intersections for given scan line. + */ + private Array getIntersections(Array vertices, + int[][] lineIndices, double scanX) { + mArrIntersections.clear(); + // Iterate through rectangle lines each re-presented as a pair of + // vertices. + for (int j = 0; j < lineIndices.length; j++) { + Vertex v1 = vertices.get(lineIndices[j][0]); + Vertex v2 = vertices.get(lineIndices[j][1]); + // Here we expect that v1.mPosX >= v2.mPosX and wont do intersection + // test the opposite way. + if (v1.mPosX > scanX && v2.mPosX < scanX) { + // There is an intersection, calculate coefficient telling 'how + // far' scanX is from v2. + double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX); + Vertex n = mArrTempVertices.remove(0); + n.set(v2); + n.mPosX = scanX; + n.mPosY += (v1.mPosY - v2.mPosY) * c; + if (DRAW_TEXTURE) { + n.mTexX += (v1.mTexX - v2.mTexX) * c; + n.mTexY += (v1.mTexY - v2.mTexY) * c; + } + if (DRAW_SHADOW) { + n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c; + n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c; + } + mArrIntersections.add(n); + } + } + return mArrIntersections; + } + + /** + * Getter for textures page for this mesh. + */ + public synchronized CurlPage getTexturePage() { + return mTexturePage; + } + + /** + * Renders our page curl mesh. + */ + public synchronized void onDrawFrame(GL10 gl) { + // First allocate texture if there is not one yet. + if (DRAW_TEXTURE && mTextureIds == null) { + // Generate texture. + mTextureIds = new int[2]; + gl.glGenTextures(2, mTextureIds, 0); + for (int textureId : mTextureIds) { + // Set texture attributes. + gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, + GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, + GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, + GL10.GL_CLAMP_TO_EDGE); + gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, + GL10.GL_CLAMP_TO_EDGE); + } + } + + if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + Bitmap texture = mTexturePage.getTexture(mTextureRectFront, + CurlPage.SIDE_FRONT); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0); + texture.recycle(); + + mTextureBack = mTexturePage.hasBackTexture(); + if (mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + texture = mTexturePage.getTexture(mTextureRectBack, + CurlPage.SIDE_BACK); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0); + texture.recycle(); + } else { + mTextureRectBack.set(mTextureRectFront); + } + + mTexturePage.recycle(); + reset(); + } + + // Some 'global' settings. + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + + // TODO: Drop shadow drawing is done temporarily here to hide some + // problems with its calculation. + if (DRAW_SHADOW) { + gl.glDisable(GL10.GL_TEXTURE_2D); + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_TEXTURE) { + gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords); + } + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices); + // Enable color array. + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors); + + // Draw front facing blank vertices. + gl.glDisable(GL10.GL_TEXTURE_2D); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront); + + // Draw front facing texture. + if (DRAW_TEXTURE) { + gl.glEnable(GL10.GL_BLEND); + gl.glEnable(GL10.GL_TEXTURE_2D); + + if (!mFlipTexture || !mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + } else { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + } + + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront); + + gl.glDisable(GL10.GL_BLEND); + gl.glDisable(GL10.GL_TEXTURE_2D); + } + + int backStartIdx = Math.max(0, mVerticesCountFront - 2); + int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx; + + // Draw back facing blank vertices. + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount); + + // Draw back facing texture. + if (DRAW_TEXTURE) { + gl.glEnable(GL10.GL_BLEND); + gl.glEnable(GL10.GL_TEXTURE_2D); + + if (mFlipTexture || !mTextureBack) { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]); + } else { + gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]); + } + + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount); + + gl.glDisable(GL10.GL_BLEND); + gl.glDisable(GL10.GL_TEXTURE_2D); + } + + // Disable textures and color array. + gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + + if (DRAW_POLYGON_OUTLINES) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glLineWidth(1.0f); + gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices); + gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_CURL_POSITION) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glLineWidth(1.0f); + gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f); + gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines); + gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2); + gl.glDisable(GL10.GL_BLEND); + } + + if (DRAW_SHADOW) { + gl.glEnable(GL10.GL_BLEND); + gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices); + gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount, + mSelfShadowCount); + gl.glDisableClientState(GL10.GL_COLOR_ARRAY); + gl.glDisable(GL10.GL_BLEND); + } + + gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); + } + + /** + * Resets mesh to 'initial' state. Meaning this mesh will draw a plain + * textured rectangle after call to this method. + */ + public synchronized void reset() { + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + for (int i = 0; i < 4; ++i) { + Vertex tmp = mArrTempVertices.get(0); + tmp.set(mRectangle[i]); + + if (mFlipTexture) { + tmp.mTexX *= mTextureRectBack.right; + tmp.mTexY *= mTextureRectBack.bottom; + tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK); + } else { + tmp.mTexX *= mTextureRectFront.right; + tmp.mTexY *= mTextureRectFront.bottom; + tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT); + } + + addVertex(tmp); + } + mVerticesCountFront = 4; + mVerticesCountBack = 0; + mBufVertices.position(0); + mBufColors.position(0); + if (DRAW_TEXTURE) { + mBufTexCoords.position(0); + } + + mDropShadowCount = mSelfShadowCount = 0; + } + + /** + * Resets allocated texture id forcing creation of new one. After calling + * this method you most likely want to set bitmap too as it's lost. This + * method should be called only once e.g GL context is re-created as this + * method does not release previous texture id, only makes sure new one is + * requested on next render. + */ + public synchronized void resetTexture() { + mTextureIds = null; + } + + /** + * If true, flips texture sideways. + */ + public synchronized void setFlipTexture(boolean flipTexture) { + mFlipTexture = flipTexture; + if (flipTexture) { + setTexCoords(1f, 0f, 0f, 1f); + } else { + setTexCoords(0f, 0f, 1f, 1f); + } + } + + /** + * Update mesh bounds. + */ + public void setRect(RectF r) { + mRectangle[0].mPosX = r.left; + mRectangle[0].mPosY = r.top; + mRectangle[1].mPosX = r.left; + mRectangle[1].mPosY = r.bottom; + mRectangle[2].mPosX = r.right; + mRectangle[2].mPosY = r.top; + mRectangle[3].mPosX = r.right; + mRectangle[3].mPosY = r.bottom; + } + + /** + * Sets texture coordinates to mRectangle vertices. + */ + private synchronized void setTexCoords(float left, float top, float right, + float bottom) { + mRectangle[0].mTexX = left; + mRectangle[0].mTexY = top; + mRectangle[1].mTexX = left; + mRectangle[1].mTexY = bottom; + mRectangle[2].mTexX = right; + mRectangle[2].mTexY = top; + mRectangle[3].mTexX = right; + mRectangle[3].mTexY = bottom; + } + + /** + * Simple fixed size array implementation. + */ + private class Array { + private final Object[] mArray; + private final int mCapacity; + private int mSize; + + public Array(int capacity) { + mCapacity = capacity; + mArray = new Object[capacity]; + } + + public void add(int index, T item) { + if (index < 0 || index > mSize || mSize >= mCapacity) { + throw new IndexOutOfBoundsException(); + } + for (int i = mSize; i > index; --i) { + mArray[i] = mArray[i - 1]; + } + mArray[index] = item; + ++mSize; + } + + public void add(T item) { + if (mSize >= mCapacity) { + throw new IndexOutOfBoundsException(); + } + mArray[mSize++] = item; + } + + public void addAll(Array array) { + if (mSize + array.size() > mCapacity) { + throw new IndexOutOfBoundsException(); + } + for (int i = 0; i < array.size(); ++i) { + mArray[mSize++] = array.get(i); + } + } + + public void clear() { + mSize = 0; + } + + @SuppressWarnings("unchecked") + public T get(int index) { + if (index < 0 || index >= mSize) { + throw new IndexOutOfBoundsException(); + } + return (T) mArray[index]; + } + + @SuppressWarnings("unchecked") + public T remove(int index) { + if (index < 0 || index >= mSize) { + throw new IndexOutOfBoundsException(); + } + T item = (T) mArray[index]; + for (int i = index; i < mSize - 1; ++i) { + mArray[i] = mArray[i + 1]; + } + --mSize; + return item; + } + + public int size() { + return mSize; + } + + } + + /** + * Holder for shadow vertex information. + */ + private class ShadowVertex { + public double mPenumbraColor; + public double mPenumbraX; + public double mPenumbraY; + public double mPosX; + public double mPosY; + public double mPosZ; + } + + /** + * Holder for vertex information. + */ + private class Vertex { + public int mColor; + public float mColorFactor; + public double mPenumbraX; + public double mPenumbraY; + public double mPosX; + public double mPosY; + public double mPosZ; + public double mTexX; + public double mTexY; + + public Vertex() { + mPosX = mPosY = mPosZ = mTexX = mTexY = 0; + mColorFactor = 1.0f; + } + + public void rotateZ(double theta) { + double cos = Math.cos(theta); + double sin = Math.sin(theta); + double x = mPosX * cos + mPosY * sin; + double y = mPosX * -sin + mPosY * cos; + mPosX = x; + mPosY = y; + double px = mPenumbraX * cos + mPenumbraY * sin; + double py = mPenumbraX * -sin + mPenumbraY * cos; + mPenumbraX = px; + mPenumbraY = py; + } + + public void set(Vertex vertex) { + mPosX = vertex.mPosX; + mPosY = vertex.mPosY; + mPosZ = vertex.mPosZ; + mTexX = vertex.mTexX; + mTexY = vertex.mTexY; + mPenumbraX = vertex.mPenumbraX; + mPenumbraY = vertex.mPenumbraY; + mColor = vertex.mColor; + mColorFactor = vertex.mColorFactor; + } + + public void translate(double dx, double dy) { + mPosX += dx; + mPosY += dy; + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java new file mode 100644 index 000000000..0caa17010 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/curl/CurlPage.java @@ -0,0 +1,195 @@ +package io.legado.app.ui.book.read.page.delegate.curl; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; + +/** + * Storage class for page textures, blend colors and possibly some other values + * in the future. + * + * @author harism + */ +public class CurlPage { + + public static final int SIDE_BACK = 2; + public static final int SIDE_BOTH = 3; + public static final int SIDE_FRONT = 1; + + private int mColorBack; + private int mColorFront; + private Bitmap mTextureBack; + private Bitmap mTextureFront; + private boolean mTexturesChanged; + + /** + * Default constructor. + */ + public CurlPage() { + reset(); + } + + /** + * Getter for color. + */ + public int getColor(int side) { + switch (side) { + case SIDE_FRONT: + return mColorFront; + default: + return mColorBack; + } + } + + /** + * Calculates the next highest power of two for a given integer. + */ + private int getNextHighestPO2(int n) { + n -= 1; + n = n | (n >> 1); + n = n | (n >> 2); + n = n | (n >> 4); + n = n | (n >> 8); + n = n | (n >> 16); + n = n | (n >> 32); + return n + 1; + } + + /** + * Generates nearest power of two sized Bitmap for give Bitmap. Returns this + * new Bitmap using default return statement + original texture coordinates + * are stored into RectF. + */ + private Bitmap getTexture(Bitmap bitmap, RectF textureRect) { + // Bitmap original size. + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + // Bitmap size expanded to next power of two. This is done due to + // the requirement on many devices, texture width and height should + // be power of two. + int newW = getNextHighestPO2(w); + int newH = getNextHighestPO2(h); + + // TODO: Is there another way to create a bigger Bitmap and copy + // original Bitmap to it more efficiently? Immutable bitmap anyone? + Bitmap bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.getConfig()); + Canvas c = new Canvas(bitmapTex); + c.drawBitmap(bitmap, 0, 0, null); + + // Calculate final texture coordinates. + float texX = (float) w / newW; + float texY = (float) h / newH; + textureRect.set(0f, 0f, texX, texY); + + return bitmapTex; + } + + /** + * Getter for textures. Creates Bitmap sized to nearest power of two, copies + * original Bitmap into it and returns it. RectF given as parameter is + * filled with actual texture coordinates in this new upscaled texture + * Bitmap. + */ + public Bitmap getTexture(RectF textureRect, int side) { + switch (side) { + case SIDE_FRONT: + return getTexture(mTextureFront, textureRect); + default: + return getTexture(mTextureBack, textureRect); + } + } + + /** + * Returns true if textures have changed. + */ + public boolean getTexturesChanged() { + return mTexturesChanged; + } + + /** + * Returns true if back siding texture exists and it differs from front + * facing one. + */ + public boolean hasBackTexture() { + return !mTextureFront.equals(mTextureBack); + } + + /** + * Recycles and frees underlying Bitmaps. + */ + public void recycle() { + if (mTextureFront != null) { + mTextureFront.recycle(); + } + mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + mTextureFront.eraseColor(mColorFront); + if (mTextureBack != null) { + mTextureBack.recycle(); + } + mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + mTextureBack.eraseColor(mColorBack); + mTexturesChanged = false; + } + + /** + * Resets this CurlPage into its initial state. + */ + public void reset() { + mColorBack = Color.WHITE; + mColorFront = Color.WHITE; + recycle(); + } + + /** + * Setter blend color. + */ + public void setColor(int color, int side) { + switch (side) { + case SIDE_FRONT: + mColorFront = color; + break; + case SIDE_BACK: + mColorBack = color; + break; + default: + mColorFront = mColorBack = color; + break; + } + } + + /** + * Setter for textures. + */ + public void setTexture(Bitmap texture, int side) { + if (texture == null) { + texture = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + if (side == SIDE_BACK) { + texture.eraseColor(mColorBack); + } else { + texture.eraseColor(mColorFront); + } + } + switch (side) { + case SIDE_FRONT: + if (mTextureFront != null) + mTextureFront.recycle(); + mTextureFront = texture; + break; + case SIDE_BACK: + if (mTextureBack != null) + mTextureBack.recycle(); + mTextureBack = texture; + break; + case SIDE_BOTH: + if (mTextureFront != null) + mTextureFront.recycle(); + if (mTextureBack != null) + mTextureBack.recycle(); + mTextureFront = mTextureBack = texture; + break; + } + mTexturesChanged = true; + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt new file mode 100644 index 000000000..713c6bd67 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt @@ -0,0 +1,5 @@ +package io.legado.app.ui.book.read.page.entities + +enum class PageDirection { + NONE, PREV, NEXT +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt index 3f183830d..abb704c94 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt @@ -7,18 +7,23 @@ data class TextChapter( val title: String, val url: String, val pages: List, - val pageLines: List, - val pageLengths: List, val chaptersSize: Int ) { + fun page(index: Int): TextPage? { return pages.getOrNull(index) } + fun getPageByReadPos(readPos: Int): TextPage? { + return page(getPageIndexByCharIndex(readPos)) + } + val lastPage: TextPage? get() = pages.lastOrNull() val lastIndex: Int get() = pages.lastIndex + val lastReadLength: Int get() = getReadLength(lastIndex) + val pageSize: Int get() = pages.size fun isLastIndex(index: Int): Boolean { @@ -29,11 +34,15 @@ data class TextChapter( var length = 0 val maxIndex = min(pageIndex, pages.size) for (index in 0 until maxIndex) { - length += pageLengths[index] + length += pages[index].charSize } return length } + fun getNextPageLength(length: Int): Int { + return getReadLength(getPageIndexByCharIndex(length) + 1) + } + fun getUnRead(pageIndex: Int): String { val stringBuilder = StringBuilder() if (pages.isNotEmpty()) { @@ -51,4 +60,15 @@ data class TextChapter( } return stringBuilder.toString() } + + fun getPageIndexByCharIndex(charIndex: Int): Int { + var length = 0 + pages.forEach { + length += it.charSize + if (length > charIndex) { + return it.index + } + } + return pages.lastIndex + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt index 6a0e9b0f4..abe2e0813 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt @@ -6,4 +6,10 @@ data class TextChar( var end: Float, var selected: Boolean = false, var isImage: Boolean = false -) \ No newline at end of file +) { + + fun isTouch(x: Float): Boolean { + return x > start && x < end + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index 9457bff37..8ef5dec58 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -4,6 +4,7 @@ import android.text.TextPaint import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider.textHeight +@Suppress("unused") data class TextLine( var text: String = "", val textChars: ArrayList = arrayListOf(), @@ -11,22 +12,22 @@ data class TextLine( var lineBase: Float = 0f, var lineBottom: Float = 0f, val isTitle: Boolean = false, - val isImage: Boolean = false, - var isReadAloud: Boolean = false + var isReadAloud: Boolean = false, + var isImage: Boolean = false ) { + val charSize: Int get() = textChars.size + fun upTopBottom(durY: Float, textPaint: TextPaint) { lineTop = ChapterProvider.paddingTop + durY lineBottom = lineTop + textPaint.textHeight lineBase = lineBottom - textPaint.fontMetrics.descent } - fun addTextChar(charData: String, start: Float, end: Float) { - textChars.add(TextChar(charData, start = start, end = end)) - } - - fun getTextCharAt(index: Int): TextChar { - return textChars[index] + fun getTextChar(index: Int): TextChar { + return textChars.getOrElse(index) { + textChars.last() + } } fun getTextCharReverseAt(index: Int): TextChar { @@ -36,4 +37,8 @@ data class TextLine( fun getTextCharsCount(): Int { return textChars.size } + + fun isTouch(y: Float, relativeOffset: Float): Boolean { + return y > lineTop + relativeOffset && y < lineBottom + relativeOffset + } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt index beb9e3673..6108710f7 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -2,23 +2,35 @@ package io.legado.app.ui.book.read.page.entities import android.text.Layout import android.text.StaticLayout -import io.legado.app.App import io.legado.app.R import io.legado.app.help.ReadBookConfig +import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.page.provider.ChapterProvider +import splitties.init.appCtx import java.text.DecimalFormat +import kotlin.math.min +@Suppress("unused") data class TextPage( var index: Int = 0, - var text: String = App.INSTANCE.getString(R.string.data_loading), + var text: String = appCtx.getString(R.string.data_loading), var title: String = "", val textLines: ArrayList = arrayListOf(), var pageSize: Int = 0, var chapterSize: Int = 0, var chapterIndex: Int = 0, - var height: Float = 0f + var height: Float = 0f, ) { + val lineSize get() = textLines.size + val charSize get() = text.length + + fun getLine(index: Int): TextLine { + return textLines.getOrElse(index) { + textLines.last() + } + } + fun upLinesPosition() = ChapterProvider.apply { if (!ReadBookConfig.textBottomJustify) return@apply if (textLines.size <= 1) return@apply @@ -60,7 +72,11 @@ data class TextPage( val char = textLine.text[i].toString() val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint) val x1 = x + cw - textLine.addTextChar(charData = char, start = x, end = x1) + textLine.textChars.add( + TextChar( + char, start = x, end = x1 + ) + ) x = x1 } textLines.add(textLine) @@ -119,4 +135,31 @@ data class TextPage( return percent } + fun getSelectStartLength(lineIndex: Int, charIndex: Int): Int { + var length = 0 + val maxIndex = min(lineIndex, lineSize) + for (index in 0 until maxIndex) { + length += textLines[index].charSize + } + return length + charIndex + } + + fun getTextChapter(): TextChapter? { + ReadBook.curTextChapter?.let { + if (it.position == chapterIndex) { + return it + } + } + ReadBook.nextTextChapter?.let { + if (it.position == chapterIndex) { + return it + } + } + ReadBook.prevTextChapter?.let { + if (it.position == chapterIndex) { + return it + } + } + return null + } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index a8cf1482d..9f3b17088 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -6,8 +6,8 @@ import android.os.Build import android.text.Layout import android.text.StaticLayout import android.text.TextPaint -import io.legado.app.App import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.AppConfig @@ -17,27 +17,59 @@ import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.utils.* +import splitties.init.appCtx import java.util.* @Suppress("DEPRECATION") object ChapterProvider { + @JvmStatic private var viewWidth = 0 + + @JvmStatic private var viewHeight = 0 + + @JvmStatic var paddingLeft = 0 + + @JvmStatic var paddingTop = 0 + + @JvmStatic var visibleWidth = 0 + + @JvmStatic var visibleHeight = 0 + + @JvmStatic var visibleRight = 0 + + @JvmStatic var visibleBottom = 0 + + @JvmStatic private var lineSpacingExtra = 0 + + @JvmStatic private var paragraphSpacing = 0 + + @JvmStatic private var titleTopSpacing = 0 + + @JvmStatic private var titleBottomSpacing = 0 - var typeface: Typeface = Typeface.SANS_SERIF + + @JvmStatic + var typeface: Typeface = Typeface.DEFAULT + + @JvmStatic lateinit var titlePaint: TextPaint + + @JvmStatic lateinit var contentPaint: TextPaint + private const val srcReplaceChar = "▩" + init { upStyle() } @@ -50,46 +82,72 @@ object ChapterProvider { bookChapter: BookChapter, contents: List, chapterSize: Int, - imageStyle: String?, ): TextChapter { val textPages = arrayListOf() - val pageLines = arrayListOf() - val pageLengths = arrayListOf() val stringBuilder = StringBuilder() var durY = 0f textPages.add(TextPage()) - contents.forEachIndexed { index, text -> - val matcher = AppPattern.imgPattern.matcher(text) - if (matcher.find()) { - var src = matcher.group(1) - if (!book.isEpub()) { - src = NetworkUtils.getAbsoluteURL(bookChapter.url, src) - } - src?.let { - durY = - setTypeImage( - book, bookChapter, src, durY, textPages, imageStyle - ) + contents.forEachIndexed { index, content -> + if (book.getImageStyle() == Book.imgStyleText) { + var text = content.replace(srcReplaceChar, "▣") + val srcList = LinkedList() + val sb = StringBuffer() + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { src -> + srcList.add(src) + ImageProvider.getImage(book, bookChapter.index, src) + matcher.appendReplacement(sb, srcReplaceChar) + } } - } else { + matcher.appendTail(sb) + text = sb.toString() val isTitle = index == 0 + val textPaint = if (isTitle) titlePaint else contentPaint if (!(isTitle && ReadBookConfig.titleMode == 2)) { - durY = - setTypeText( - text, durY, textPages, pageLines, - pageLengths, stringBuilder, isTitle - ) + durY = setTypeText( + text, durY, textPages, stringBuilder, + isTitle, textPaint, srcList + ) + } + } else if (book.getImageStyle() != Book.imgStyleText) { + val matcher = AppPattern.imgPattern.matcher(content) + var start = 0 + while (matcher.find()) { + val text = content.substring(start, matcher.start()) + if (text.isNotBlank()) { + val isTitle = index == 0 + val textPaint = if (isTitle) titlePaint else contentPaint + if (!(isTitle && ReadBookConfig.titleMode == 2)) { + durY = setTypeText( + text, durY, textPages, + stringBuilder, isTitle, textPaint + ) + } + } + durY = setTypeImage( + book, bookChapter, matcher.group(1)!!, + durY, textPages, book.getImageStyle() + ) + start = matcher.end() + } + if (start < content.length) { + val text = content.substring(start, content.length) + if (text.isNotBlank()) { + val isTitle = index == 0 + val textPaint = if (isTitle) titlePaint else contentPaint + if (!(isTitle && ReadBookConfig.titleMode == 2)) { + durY = setTypeText( + text, durY, textPages, + stringBuilder, isTitle, textPaint + ) + } + } } } } textPages.last().height = durY + 20.dp textPages.last().text = stringBuilder.toString() - if (pageLines.size < textPages.size) { - pageLines.add(textPages.last().textLines.size) - } - if (pageLengths.size < textPages.size) { - pageLengths.add(textPages.last().text.length) - } textPages.forEachIndexed { index, item -> item.index = index item.pageSize = textPages.size @@ -100,13 +158,9 @@ object ChapterProvider { } return TextChapter( - bookChapter.index, - bookChapter.title, - bookChapter.url, - textPages, - pageLines, - pageLengths, - chapterSize + bookChapter.index, bookChapter.title, + bookChapter.getAbsoluteURL().substringBefore(",{"), //getAbsoluteURL已经格式过 + textPages, chapterSize ) } @@ -128,9 +182,12 @@ object ChapterProvider { var height = it.height var width = it.width when (imageStyle?.toUpperCase(Locale.ROOT)) { - "FULL" -> { + Book.imgStyleFull -> { width = visibleWidth height = it.height * visibleWidth / it.width + } + Book.imgStyleText -> { + } else -> { if (it.width > visibleWidth) { @@ -181,14 +238,15 @@ object ChapterProvider { text: String, y: Float, textPages: ArrayList, - pageLines: ArrayList, - pageLengths: ArrayList, stringBuilder: StringBuilder, isTitle: Boolean, + textPaint: TextPaint, + srcList: LinkedList? = null ): Float { var durY = if (isTitle) y + titleTopSpacing else y - val textPaint = if (isTitle) titlePaint else contentPaint - val layout = StaticLayout( + val layout = if (ReadBookConfig.useZhLayout) { + ZhLayout(text, textPaint, visibleWidth) + } else StaticLayout( text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true ) for (lineIndex in 0 until layout.lineCount) { @@ -204,7 +262,8 @@ object ChapterProvider { textLine, words.toStringArray(), textPaint, - desiredWidth + desiredWidth, + srcList ) } else if (lineIndex == layout.lineCount - 1) { //最后一行 @@ -213,12 +272,7 @@ object ChapterProvider { val x = if (isTitle && ReadBookConfig.titleMode == 1) (visibleWidth - layout.getLineWidth(lineIndex)) / 2 else 0f - addCharsToLineLast( - textLine, - words.toStringArray(), - textPaint, - x - ) + addCharsToLineLast(textLine, words.toStringArray(), textPaint, x, srcList) } else { //中间行 textLine.text = words @@ -227,14 +281,13 @@ object ChapterProvider { words.toStringArray(), textPaint, desiredWidth, - 0f + 0f, + srcList ) } if (durY + textPaint.textHeight > visibleHeight) { //当前页面结束,设置各种值 textPages.last().text = stringBuilder.toString() - pageLines.add(textPages.last().textLines.size) - pageLengths.add(textPages.last().text.length) textPages.last().height = durY //新建页面 textPages.add(TextPage()) @@ -261,36 +314,37 @@ object ChapterProvider { words: Array, textPaint: TextPaint, desiredWidth: Float, + srcList: LinkedList? ) { var x = 0f if (!ReadBookConfig.textFullJustify) { - addCharsToLineLast( - textLine, - words, - textPaint, - x - ) + addCharsToLineLast(textLine, words, textPaint, x, srcList) return } - val bodyIndent = ReadBookConfig.bodyIndent + val bodyIndent = ReadBookConfig.paragraphIndent val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length bodyIndent.toStringArray().forEach { val x1 = x + icw - textLine.addTextChar( - charData = it, - start = paddingLeft + x, - end = paddingLeft + x1 - ) + if (srcList != null && it == srcReplaceChar) { + textLine.textChars.add( + TextChar( + srcList.removeFirst(), + start = paddingLeft + x, + end = paddingLeft + x1, + isImage = true + ) + ) + } else { + textLine.textChars.add( + TextChar( + it, start = paddingLeft + x, end = paddingLeft + x1 + ) + ) + } x = x1 } val words1 = words.copyOfRange(bodyIndent.length, words.size) - addCharsToLineMiddle( - textLine, - words1, - textPaint, - desiredWidth, - x - ) + addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList) } /** @@ -302,14 +356,10 @@ object ChapterProvider { textPaint: TextPaint, desiredWidth: Float, startX: Float, + srcList: LinkedList? ) { if (!ReadBookConfig.textFullJustify) { - addCharsToLineLast( - textLine, - words, - textPaint, - startX - ) + addCharsToLineLast(textLine, words, textPaint, startX, srcList) return } val gapCount: Int = words.lastIndex @@ -318,17 +368,25 @@ object ChapterProvider { words.forEachIndexed { index, s -> val cw = StaticLayout.getDesiredWidth(s, textPaint) val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) - textLine.addTextChar( - charData = s, - start = paddingLeft + x, - end = paddingLeft + x1 - ) + if (srcList != null && s == srcReplaceChar) { + textLine.textChars.add( + TextChar( + srcList.removeFirst(), + start = paddingLeft + x, + end = paddingLeft + x1, + isImage = true + ) + ) + } else { + textLine.textChars.add( + TextChar( + s, start = paddingLeft + x, end = paddingLeft + x1 + ) + ) + } x = x1 } - exceed( - textLine, - words - ) + exceed(textLine, words) } /** @@ -339,29 +397,38 @@ object ChapterProvider { words: Array, textPaint: TextPaint, startX: Float, + srcList: LinkedList? ) { var x = startX words.forEach { val cw = StaticLayout.getDesiredWidth(it, textPaint) val x1 = x + cw - textLine.addTextChar( - charData = it, - start = paddingLeft + x, - end = paddingLeft + x1 - ) + if (srcList != null && it == srcReplaceChar) { + textLine.textChars.add( + TextChar( + srcList.removeFirst(), + start = paddingLeft + x, + end = paddingLeft + x1, + isImage = true + ) + ) + } else { + textLine.textChars.add( + TextChar( + it, start = paddingLeft + x, end = paddingLeft + x1 + ) + ) + } x = x1 } - exceed( - textLine, - words - ) + exceed(textLine, words) } /** * 超出边界处理 */ private fun exceed(textLine: TextLine, words: Array) { - val endX = textLine.textChars.last().end + val endX = textLine.textChars.lastOrNull()?.end ?: return if (endX > visibleRight) { val cc = (endX - visibleRight) / words.size for (i in 0..words.lastIndex) { @@ -378,17 +445,30 @@ object ChapterProvider { * 更新样式 */ fun upStyle() { - typeface = try { - val fontPath = ReadBookConfig.textFont + typeface = getTypeface(ReadBookConfig.textFont) + getPaint(typeface).let { + titlePaint = it.first + contentPaint = it.second + } + //间距 + lineSpacingExtra = ReadBookConfig.lineSpacingExtra + paragraphSpacing = ReadBookConfig.paragraphSpacing + titleTopSpacing = ReadBookConfig.titleTopSpacing.dp + titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp + upVisibleSize() + } + + private fun getTypeface(fontPath: String): Typeface { + return kotlin.runCatching { when { - fontPath.isContentPath() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { - val fd = App.INSTANCE.contentResolver + fontPath.isContentScheme() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + val fd = appCtx.contentResolver .openFileDescriptor(Uri.parse(fontPath), "r")!! .fileDescriptor Typeface.Builder(fd).build() } - fontPath.isContentPath() -> { - Typeface.createFromFile(RealPathUtil.getPath(App.INSTANCE, Uri.parse(fontPath))) + fontPath.isContentScheme() -> { + Typeface.createFromFile(RealPathUtil.getPath(appCtx, Uri.parse(fontPath))) } fontPath.isNotEmpty() -> Typeface.createFromFile(fontPath) else -> when (AppConfig.systemTypefaces) { @@ -397,11 +477,14 @@ object ChapterProvider { else -> Typeface.SANS_SERIF } } - } catch (e: Exception) { + }.getOrElse { ReadBookConfig.textFont = "" ReadBookConfig.save() Typeface.SANS_SERIF - } + } ?: Typeface.DEFAULT + } + + private fun getPaint(typeface: Typeface): Pair { // 字体统一处理 val bold = Typeface.create(typeface, Typeface.BOLD) val normal = Typeface.create(typeface, Typeface.NORMAL) @@ -422,35 +505,31 @@ object ChapterProvider { } //标题 - titlePaint = TextPaint() - titlePaint.color = ReadBookConfig.textColor - titlePaint.letterSpacing = ReadBookConfig.letterSpacing - titlePaint.typeface = titleFont - titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat() - titlePaint.isAntiAlias = true + val tPaint = TextPaint() + tPaint.color = ReadBookConfig.textColor + tPaint.letterSpacing = ReadBookConfig.letterSpacing + tPaint.typeface = titleFont + tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat() + tPaint.isAntiAlias = true //正文 - contentPaint = TextPaint() - contentPaint.color = ReadBookConfig.textColor - contentPaint.letterSpacing = ReadBookConfig.letterSpacing - contentPaint.typeface = textFont - contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() - contentPaint.isAntiAlias = true - //间距 - lineSpacingExtra = ReadBookConfig.lineSpacingExtra - paragraphSpacing = ReadBookConfig.paragraphSpacing - titleTopSpacing = ReadBookConfig.titleTopSpacing.dp - titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp - upVisibleSize() + val cPaint = TextPaint() + cPaint.color = ReadBookConfig.textColor + cPaint.letterSpacing = ReadBookConfig.letterSpacing + cPaint.typeface = textFont + cPaint.textSize = ReadBookConfig.textSize.sp.toFloat() + cPaint.isAntiAlias = true + return Pair(tPaint, cPaint) } /** * 更新View尺寸 */ fun upViewSize(width: Int, height: Int) { - if (width > 0 && height > 0) { + if (width > 0 && height > 0 && (width != viewWidth || height != viewHeight)) { viewWidth = width viewHeight = height upVisibleSize() + postEvent(EventBus.UP_CONFIG, true) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt index c269a7e55..0bed03ea1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt @@ -3,7 +3,7 @@ package io.legado.app.ui.book.read.page.provider import android.graphics.Bitmap import io.legado.app.data.entities.Book import io.legado.app.help.BookHelp -import io.legado.app.model.localBook.EPUBFile +import io.legado.app.model.localBook.EpubFile import io.legado.app.utils.BitmapUtils import io.legado.app.utils.FileUtils import kotlinx.coroutines.runBlocking @@ -36,7 +36,7 @@ object ImageProvider { val vFile = BookHelp.getImage(book, src) if (!vFile.exists()) { if (book.isEpub()) { - EPUBFile.getImage(book, src)?.use { input -> + EpubFile.getImage(book, src)?.use { input -> val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath) FileOutputStream(newFile).use { output -> input.copyTo(output) @@ -54,13 +54,16 @@ object ImageProvider { ChapterProvider.visibleWidth, ChapterProvider.visibleHeight ) - setCache(chapterIndex, src, bitmap) + if (bitmap != null) { + setCache(chapterIndex, src, bitmap) + } bitmap } catch (e: Exception) { null } } + @Synchronized fun clearAllCache() { cache.forEach { indexCache -> indexCache.value.forEach { @@ -70,6 +73,7 @@ object ImageProvider { cache.clear() } + @Synchronized fun clearOut(chapterIndex: Int) { cache.forEach { indexCache -> if (indexCache.key !in chapterIndex - 1..chapterIndex + 1) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt similarity index 92% rename from app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt rename to app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt index 95917ae11..f824f05bb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt @@ -1,6 +1,8 @@ -package io.legado.app.ui.book.read.page +package io.legado.app.ui.book.read.page.provider import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.page.api.DataSource +import io.legado.app.ui.book.read.page.api.PageFactory import io.legado.app.ui.book.read.page.entities.TextPage class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource) { @@ -57,14 +59,13 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource false } - override val currentPage: TextPage + override val curPage: TextPage get() = with(dataSource) { ReadBook.msg?.let { return@with TextPage(text = it).format() } currentChapter?.let { - return@with it.page(pageIndex) - ?: TextPage(title = it.title).format() + return@with it.page(pageIndex) ?: TextPage(title = it.title).format() } return TextPage().format() } @@ -108,7 +109,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return TextPage().format() } - override val nextPagePlus: TextPage + override val nextPlusPage: TextPage get() = with(dataSource) { currentChapter?.let { if (pageIndex < it.pageSize - 2) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt new file mode 100644 index 000000000..648392e83 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt @@ -0,0 +1,261 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Rect +import android.text.Layout +import android.text.TextPaint +import io.legado.app.utils.toStringArray +import kotlin.math.max + +/** + * 针对中文的断行排版处理-by hoodie13 + * 因为StaticLayout对标点处理不符合国人习惯,继承Layout + * */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ZhLayout( + text: String, + textPaint: TextPaint, + width: Int +) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) { + private val defaultCapacity = 10 + var lineStart = IntArray(defaultCapacity) + var lineWidth = FloatArray(defaultCapacity) + private var lineCount = 0 + private val curPaint = textPaint + private val cnCharWitch = getDesiredWidth("我", textPaint) + + enum class BreakMod { NORMAL, BREAK_ONE_CHAR, BREAK_MORE_CHAR, CPS_1, CPS_2, CPS_3, } + class Locate { + var start: Float = 0f + var end: Float = 0f + } + + class Interval { + var total: Float = 0f + var single: Float = 0f + } + + init { + var line = 0 + val words = text.toStringArray() + var lineW = 0f + var cwPre = 0f + var length = 0 + words.forEachIndexed { index, s -> + val cw = getDesiredWidth(s, curPaint) + var breakMod: BreakMod + var breakLine = false + lineW += cw + var offset = 0f + var breakCharCnt = 0 + + if (lineW > width) { + /*禁止在行尾的标点处理*/ + breakMod = if (index >= 1 && isPrePanc(words[index - 1])) { + if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_2//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } + /*禁止在行首的标点处理*/ + else if (isPostPanc(words[index])) { + if (index >= 1 && isPostPanc(words[index - 1])) BreakMod.CPS_1//如果后面还有一个禁首标点则异常,不过三个连续行尾标点的用法不通用 + else if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_3//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } else { + BreakMod.NORMAL //无异常场景 + } + + /*判断上述逻辑解决不了的特殊情况*/ + var reCheck = false + var breakIndex = 0 + if (breakMod == BreakMod.CPS_1 && + (inCompressible(words[index]) || inCompressible(words[index - 1])) + ) reCheck = true + if (breakMod == BreakMod.CPS_2 && + (inCompressible(words[index - 1]) || inCompressible(words[index - 2])) + ) reCheck = true + if (breakMod == BreakMod.CPS_3 && + (inCompressible(words[index]) || inCompressible(words[index - 2])) + ) reCheck = true + if (breakMod > BreakMod.BREAK_MORE_CHAR + && index < words.lastIndex && isPostPanc(words[index + 1]) + ) reCheck = true + + /*特殊标点使用难保证显示效果,所以不考虑间隔,直接查找到能满足条件的分割字*/ + var breakLength = 0 + if (reCheck && index > 2) { + breakMod = BreakMod.NORMAL + for (i in (index) downTo 1) { + if (i == index) { + breakIndex = 0 + cwPre = 0f + } else { + breakIndex++ + breakLength += words[i].length + cwPre += getDesiredWidth(words[i], textPaint) + } + if (!isPostPanc(words[i]) && !isPrePanc(words[i - 1])) { + breakMod = BreakMod.BREAK_MORE_CHAR + break + } + } + } + + when (breakMod) { + BreakMod.NORMAL -> {//模式0 正常断行 + offset = cw + lineStart[line + 1] = length + breakCharCnt = 1 + } + BreakMod.BREAK_ONE_CHAR -> {//模式1 当前行下移一个字 + offset = cw + cwPre + lineStart[line + 1] = length - words[index - 1].length + breakCharCnt = 2 + } + BreakMod.BREAK_MORE_CHAR -> {//模式2 当前行下移多个字 + offset = cw + cwPre + lineStart[line + 1] = length - breakLength + breakCharCnt = breakIndex + 1 + } + BreakMod.CPS_1 -> {//模式3 两个后置标点压缩 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + BreakMod.CPS_2 -> { //模式4 前置标点压缩+前置标点压缩+字 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + BreakMod.CPS_3 -> {//模式5 前置标点压缩+字+后置标点压缩 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + } + breakLine = true + } + + /*当前行写满情况下的断行*/ + if (breakLine) { + lineWidth[line] = lineW - offset + lineW = offset + addLineArray(++line) + } + /*已到最后一个字符*/ + if ((words.lastIndex) == index) { + if (!breakLine) { + offset = 0f + lineStart[line + 1] = length + s.length + lineWidth[line] = lineW - offset + lineW = offset + addLineArray(++line) + } + /*写满断行、段落末尾、且需要下移字符,这种特殊情况下要额外多一行*/ + else if (breakCharCnt > 0) { + lineStart[line + 1] = lineStart[line] + breakCharCnt + lineWidth[line] = lineW + addLineArray(++line) + } + } + length += s.length + cwPre = cw + } + + lineCount = line + + } + + private fun addLineArray(line: Int) { + if (lineStart.size <= line + 1) { + lineStart = lineStart.copyOf(line + defaultCapacity) + lineWidth = lineWidth.copyOf(line + defaultCapacity) + } + } + + private fun isPostPanc(string: String): Boolean { + val panc = arrayOf( + ",", "。", ":", "?", "!", "、", "”", "’", ")", "》", "}", + "】", ")", ">", "]", "}", ",", ".", "?", "!", ":", "」", ";", ";" + ) + panc.forEach { + if (it == string) return true + } + return false + } + + private fun isPrePanc(string: String): Boolean { + val panc = arrayOf("“", "(", "《", "【", "‘", "‘", "(", "<", "[", "{", "「") + panc.forEach { + if (it == string) return true + } + return false + } + + private fun inCompressible(string: String): Boolean { + return getDesiredWidth(string, curPaint) < cnCharWitch + } + + private val gap = (cnCharWitch / 12.75).toFloat() + private fun getPostPancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + return max(textRect.left.toFloat() - gap, 0f) + } + + private fun getPrePancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + val d = max(cnCharWitch - textRect.right.toFloat() - gap, 0f) + return cnCharWitch / 2 - d + } + + fun getDesiredWidth(sting: String, paint: TextPaint) = paint.measureText(sting) + + override fun getLineCount(): Int { + return lineCount + } + + override fun getLineTop(line: Int): Int { + return 0 + } + + override fun getLineDescent(line: Int): Int { + return 0 + } + + override fun getLineStart(line: Int): Int { + return lineStart[line] + } + + override fun getParagraphDirection(line: Int): Int { + return 0 + } + + override fun getLineContainsTab(line: Int): Boolean { + return true + } + + override fun getLineDirections(line: Int): Directions? { + return null + } + + override fun getTopPadding(): Int { + return 0 + } + + override fun getBottomPadding(): Int { + return 0 + } + + override fun getLineWidth(line: Int): Float { + return lineWidth[line] + } + + override fun getEllipsisStart(line: Int): Int { + return 0 + } + + override fun getEllipsisCount(line: Int): Int { + return 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt index 05ea3c782..1aa9f0fff 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt @@ -1,25 +1,34 @@ package io.legado.app.ui.book.search import android.content.Context -import io.legado.app.R +import android.view.ViewGroup import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.Book -import kotlinx.android.synthetic.main.item_fillet_text.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.databinding.ItemFilletTextBinding + class BookAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_fillet_text) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { - with(holder.itemView) { - text_view.text = item.name + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: Book, + payloads: MutableList + ) { + binding.run { + textView.text = item.name } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { holder.itemView.apply { - onClick { + setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.showBookInfo(it) } diff --git a/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt deleted file mode 100644 index 4125489b8..000000000 --- a/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.legado.app.ui.book.search - -import android.os.Bundle -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.SearchBook - -class DiffCallBack(private val oldItems: List, private val newItems: List) : - DiffUtil.Callback() { - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return when { - oldItem.name != newItem.name -> false - oldItem.author != newItem.author -> false - else -> true - } - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return when { - oldItem.origins.size != newItem.origins.size -> false - oldItem.coverUrl != newItem.coverUrl -> false - oldItem.kind != newItem.kind -> false - oldItem.latestChapterTitle != newItem.latestChapterTitle -> false - oldItem.intro != newItem.intro -> false - else -> true - } - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val payload = Bundle() - val newItem = newItems[newItemPosition] - val oldItem = oldItems[oldItemPosition] - if (oldItem.name != newItem.name) payload.putString("name", newItem.name) - if (oldItem.author != newItem.author) payload.putString("author", newItem.author) - if (oldItem.origins.size != newItem.origins.size) - payload.putInt("origins", newItem.origins.size) - if (oldItem.coverUrl != newItem.coverUrl) payload.putString("cover", newItem.coverUrl) - if (oldItem.kind != newItem.kind) payload.putString("kind", newItem.kind) - if (oldItem.latestChapterTitle != newItem.latestChapterTitle) - payload.putString("last", newItem.latestChapterTitle) - if (oldItem.intro != newItem.intro) payload.putString("intro", newItem.intro) - if (payload.isEmpty) return null - return payload - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt index e59605495..f7bbd8297 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt @@ -1,52 +1,51 @@ package io.legado.app.ui.book.search -import io.legado.app.App -import io.legado.app.R +import android.view.ViewGroup import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.SearchKeyword +import io.legado.app.databinding.ItemFilletTextBinding import io.legado.app.ui.widget.anima.explosion_field.ExplosionField -import kotlinx.android.synthetic.main.item_fillet_text.view.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - +import splitties.views.onLongClick class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) : - SimpleRecyclerAdapter(activity, R.layout.item_fillet_text) { + RecyclerAdapter(activity) { private val explosionField = ExplosionField.attach2Window(activity) - override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList) { - with(holder.itemView) { - text_view.text = item.word + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: SearchKeyword, + payloads: MutableList + ) { + binding.run { + textView.text = item.word } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { holder.itemView.apply { - onClick { + setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.searchHistory(it.word) } } onLongClick { - it?.let { - explosionField.explode(it, true) - } + explosionField.explode(this, true) getItem(holder.layoutPosition)?.let { - GlobalScope.launch(IO) { - App.db.searchKeywordDao().delete(it) - } + callBack.deleteHistory(it) } - true } } } interface CallBack { fun searchHistory(key: String) + fun deleteHistory(searchKeyword: SearchKeyword) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index 2870818c2..30cb13b18 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -1,63 +1,71 @@ package io.legado.app.ui.book.search +import android.content.Context +import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View.GONE import android.view.View.VISIBLE +import android.widget.TextView +import androidx.activity.viewModels import androidx.appcompat.widget.SearchView -import androidx.lifecycle.LiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.flexbox.FlexboxLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword +import io.legado.app.databinding.ActivityBookSearchBinding import io.legado.app.lib.theme.* import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_search.* -import kotlinx.android.synthetic.main.view_search.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.startActivity -import java.text.Collator - -class SearchActivity : VMBaseActivity(R.layout.activity_book_search), +class SearchActivity : VMBaseActivity(), BookAdapter.CallBack, HistoryKeyAdapter.CallBack, SearchAdapter.CallBack { - override val viewModel: SearchViewModel - get() = getViewModel(SearchViewModel::class.java) + override val binding by viewBinding(ActivityBookSearchBinding::inflate) + override val viewModel by viewModels() lateinit var adapter: SearchAdapter private lateinit var bookAdapter: BookAdapter private lateinit var historyKeyAdapter: HistoryKeyAdapter private lateinit var loadMoreView: LoadMoreView - private var historyData: LiveData>? = null - private var bookData: LiveData>? = null + private lateinit var searchView: SearchView + private var historyFlowJob: Job? = null + private var booksFlowJob: Job? = null private var menu: Menu? = null private var precisionSearchMenuItem: MenuItem? = null private var groups = linkedSetOf() override fun onActivityCreated(savedInstanceState: Bundle?) { - ll_history.setBackgroundColor(backgroundColor) + binding.llHistory.setBackgroundColor(backgroundColor) + searchView = binding.titleBar.findViewById(R.id.search_view) initRecyclerView() initSearchView() initOtherView() - initLiveData() - initIntent() + initData() + receiptIntent(intent) + } + + override fun onNewIntent(data: Intent?) { + super.onNewIntent(data) + receiptIntent(data) } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -77,8 +85,8 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se !getPrefBoolean(PreferKey.precisionSearch) ) precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) - search_view.query?.toString()?.trim()?.let { - search_view.setQuery(it, true) + searchView.query?.toString()?.trim()?.let { + searchView.setQuery(it, true) } } R.id.menu_source_manage -> startActivity() @@ -89,8 +97,8 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } else { putPrefString("searchGroup", item.title.toString()) } - search_view.query?.toString()?.trim()?.let { - search_view.setQuery(it, true) + searchView.query?.toString()?.trim()?.let { + searchView.setQuery(it, true) } } } @@ -98,14 +106,14 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } private fun initSearchView() { - ATH.setTint(search_view, primaryTextColor) - search_view.onActionViewExpanded() - search_view.isSubmitButtonEnabled = true - search_view.queryHint = getString(R.string.search_book_key) - search_view.clearFocus() - search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search_book_key) + searchView.clearFocus() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { - search_view.clearFocus() + searchView.clearFocus() query?.let { viewModel.saveSearchKey(query) viewModel.search(it) @@ -120,8 +128,8 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se return false } }) - search_view.setOnQueryTextFocusChangeListener { _, hasFocus -> - if (!hasFocus && search_view.query.toString().trim().isEmpty()) { + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus && searchView.query.toString().trim().isEmpty()) { finish() } else { openOrCloseHistory(hasFocus) @@ -131,28 +139,35 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) - ATH.applyEdgeEffectColor(rv_bookshelf_search) - ATH.applyEdgeEffectColor(rv_history_key) + ATH.applyEdgeEffectColor(binding.recyclerView) + ATH.applyEdgeEffectColor(binding.rvBookshelfSearch) + ATH.applyEdgeEffectColor(binding.rvHistoryKey) bookAdapter = BookAdapter(this, this) - rv_bookshelf_search.layoutManager = FlexboxLayoutManager(this) - rv_bookshelf_search.adapter = bookAdapter + binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this) + binding.rvBookshelfSearch.adapter = bookAdapter historyKeyAdapter = HistoryKeyAdapter(this, this) - rv_history_key.layoutManager = FlexboxLayoutManager(this) - rv_history_key.adapter = historyKeyAdapter + binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this) + binding.rvHistoryKey.adapter = historyKeyAdapter adapter = SearchAdapter(this, this) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0) { - recycler_view.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount) + if (toPosition == 0) { + binding.recyclerView.scrollToPosition(0) } } }) loadMoreView = LoadMoreView(this) - recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) if (!recyclerView.canScrollVertically(1)) { @@ -163,26 +178,28 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } private fun initOtherView() { - fb_stop.backgroundTintList = + binding.fbStop.backgroundTintList = Selector.colorBuild() .setDefaultColor(accentColor) .setPressedColor(ColorUtils.darkenColor(accentColor)) .create() - fb_stop.onClick { + binding.fbStop.setOnClickListener { viewModel.stop() - refresh_progress_bar.isAutoLoading = false + binding.refreshProgressBar.isAutoLoading = false } - tv_clear_history.onClick { viewModel.clearHistory() } + binding.tvClearHistory.setOnClickListener { viewModel.clearHistory() } } - private fun initLiveData() { - App.db.bookSourceDao().liveGroupEnabled().observe(this, { - groups.clear() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + private fun initData() { + launch { + appDb.bookSourceDao.flowGroupEnabled().collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupMenu() } - upGroupMenu() - }) + } viewModel.searchBookLiveData.observe(this, { upSearchItems(it) }) @@ -195,11 +212,13 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se }) } - private fun initIntent() { - intent.getStringExtra("key")?.let { - search_view.setQuery(it, true) - } ?: let { - search_view.requestFocus() + private fun receiptIntent(intent: Intent? = null) { + val key = intent?.getStringExtra("key") + if (key.isNullOrBlank()) { + searchView.findViewById(androidx.appcompat.R.id.search_src_text) + .requestFocus() + } else { + searchView.setQuery(key, true) } } @@ -217,75 +236,79 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se */ private fun openOrCloseHistory(open: Boolean) { if (open) { - upHistory(search_view.query.toString()) - ll_history.visibility = VISIBLE + upHistory(searchView.query.toString()) + binding.llHistory.visibility = VISIBLE } else { - ll_history.visibility = GONE + binding.llHistory.visibility = GONE } } /** * 更新分组菜单 */ - private fun upGroupMenu() { - val selectedGroup = getPrefString("searchGroup") ?: "" - menu?.removeGroup(R.id.source_group) - var item = menu?.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) - if (selectedGroup == "") { - item?.isChecked = true - } - groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE)) - .map { - item = menu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it) - if (it == selectedGroup) { - item?.isChecked = true + private fun upGroupMenu() = menu?.let { menu -> + val selectedGroup = getPrefString("searchGroup") + menu.removeGroup(R.id.source_group) + val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) + var hasSelectedGroup = false + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.forEach { group -> + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let { + if (group == selectedGroup) { + it.isChecked = true + hasSelectedGroup = true } } - menu?.setGroupCheckable(R.id.source_group, true, true) + } + menu.setGroupCheckable(R.id.source_group, true, true) + if (!hasSelectedGroup) { + allItem.isChecked = true + } } /** * 更新搜索历史 */ private fun upHistory(key: String? = null) { - bookData?.removeObservers(this) - if (key.isNullOrBlank()) { - tv_book_show.gone() - rv_bookshelf_search.gone() - } else { - bookData = App.db.bookDao().liveDataSearch(key) - bookData?.observe(this, { - if (it.isEmpty()) { - tv_book_show.gone() - rv_bookshelf_search.gone() - } else { - tv_book_show.visible() - rv_bookshelf_search.visible() - } - bookAdapter.setItems(it) - }) - } - historyData?.removeObservers(this) - historyData = + booksFlowJob?.cancel() + booksFlowJob = launch { if (key.isNullOrBlank()) { - App.db.searchKeywordDao().liveDataByUsage() + binding.tvBookShow.gone() + binding.rvBookshelfSearch.gone() } else { - App.db.searchKeywordDao().liveDataSearch(key) + val bookFlow = appDb.bookDao.flowSearch(key) + bookFlow.collect { + if (it.isEmpty()) { + binding.tvBookShow.gone() + binding.rvBookshelfSearch.gone() + } else { + binding.tvBookShow.visible() + binding.rvBookshelfSearch.visible() + } + bookAdapter.setItems(it) + } } - historyData?.observe(this, { - historyKeyAdapter.setItems(it) - if (it.isEmpty()) { - tv_clear_history.invisible() - } else { - tv_clear_history.visible() + } + historyFlowJob?.cancel() + historyFlowJob = launch { + when { + key.isNullOrBlank() -> appDb.searchKeywordDao.flowByUsage() + else -> appDb.searchKeywordDao.flowSearch(key) + }.collect { + historyKeyAdapter.setItems(it) + if (it.isEmpty()) { + binding.tvClearHistory.invisible() + } else { + binding.tvClearHistory.visible() + } } - }) + } } /** * 更新搜索结果 */ - @Synchronized private fun upSearchItems(items: List) { adapter.setItems(items) } @@ -294,17 +317,17 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se * 开始搜索 */ private fun startSearch() { - refresh_progress_bar.isAutoLoading = true - fb_stop.visible() + binding.refreshProgressBar.isAutoLoading = true + binding.fbStop.visible() } /** * 搜索结束 */ private fun searchFinally() { - refresh_progress_bar.isAutoLoading = false + binding.refreshProgressBar.isAutoLoading = false loadMoreView.startLoad() - fb_stop.invisible() + binding.fbStop.invisible() } /** @@ -313,10 +336,10 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se override fun showBookInfo(name: String, author: String) { viewModel.getSearchBook(name, author) { searchBook -> searchBook?.let { - startActivity( - Pair("name", it.name), - Pair("author", it.author) - ) + startActivity { + putExtra("name", it.name) + putExtra("author", it.author) + } } } } @@ -325,10 +348,10 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se * 显示书籍详情 */ override fun showBookInfo(book: Book) { - startActivity( - Pair("name", book.name), - Pair("author", book.author) - ) + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + } } /** @@ -337,16 +360,30 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se override fun searchHistory(key: String) { launch { when { - search_view.query.toString() == key -> { - search_view.setQuery(key, true) + searchView.query.toString() == key -> { + searchView.setQuery(key, true) } - withContext(IO) { App.db.bookDao().findByName(key).isEmpty() } -> { - search_view.setQuery(key, true) + withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> { + searchView.setQuery(key, true) } else -> { - search_view.setQuery(key, false) + searchView.setQuery(key, false) } } } } + + override fun deleteHistory(searchKeyword: SearchKeyword) { + viewModel.deleteHistory(searchKeyword) + } + + companion object { + + fun start(context: Context, key: String?) { + context.startActivity { + putExtra("key", key) + } + } + + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt index a96244113..e36c11fe1 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt @@ -2,66 +2,99 @@ package io.legado.app.ui.book.search import android.content.Context import android.os.Bundle -import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemSearchBinding import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover -import kotlinx.android.synthetic.main.item_bookshelf_list.view.tv_name -import kotlinx.android.synthetic.main.item_search.view.* -import org.jetbrains.anko.sdk27.listeners.onClick + class SearchAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_search) { + DiffRecyclerAdapter(context) { + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return when { + oldItem.name != newItem.name -> false + oldItem.author != newItem.author -> false + else -> true + } + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return false + } - override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + override fun getChangePayload(oldItem: SearchBook, newItem: SearchBook): Any { + val payload = Bundle() + payload.putInt("origins", newItem.origins.size) + if (oldItem.coverUrl != newItem.coverUrl) + payload.putString("cover", newItem.coverUrl) + if (oldItem.kind != newItem.kind) + payload.putString("kind", newItem.kind) + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) + payload.putString("last", newItem.latestChapterTitle) + if (oldItem.intro != newItem.intro) + payload.putString("intro", newItem.intro) + return payload + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemSearchBinding { + return ItemSearchBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchBinding, + item: SearchBook, + payloads: MutableList + ) { val bundle = payloads.getOrNull(0) as? Bundle if (bundle == null) { - bind(holder.itemView, item) + bind(binding, item) } else { - bindChange(holder.itemView, item, bundle) + bindChange(binding, item, bundle) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - onClick { - getItem(holder.layoutPosition)?.let { - callBack.showBookInfo(it.name, it.author) - } + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchBinding) { + binding.root.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it.name, it.author) } } } - private fun bind(itemView: View, searchBook: SearchBook) { - with(itemView) { - tv_name.text = searchBook.name - tv_author.text = context.getString(R.string.author_show, searchBook.author) - bv_originCount.setBadgeCount(searchBook.origins.size) - upLasted(itemView, searchBook.latestChapterTitle) - tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro) - upKind(itemView, searchBook.getKindList()) - iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author) + private fun bind(binding: ItemSearchBinding, searchBook: SearchBook) { + binding.run { + tvName.text = searchBook.name + tvAuthor.text = context.getString(R.string.author_show, searchBook.author) + bvOriginCount.setBadgeCount(searchBook.origins.size) + upLasted(binding, searchBook.latestChapterTitle) + tvIntroduce.text = searchBook.trimIntro(context) + upKind(binding, searchBook.getKindList()) + ivCover.load(searchBook.coverUrl, searchBook.name, searchBook.author) } } - private fun bindChange(itemView: View, searchBook: SearchBook, bundle: Bundle) { - with(itemView) { + private fun bindChange(binding: ItemSearchBinding, searchBook: SearchBook, bundle: Bundle) { + binding.run { bundle.keySet().map { when (it) { - "name" -> tv_name.text = searchBook.name - "author" -> tv_author.text = - context.getString(R.string.author_show, searchBook.author) - "origins" -> bv_originCount.setBadgeCount(searchBook.origins.size) - "last" -> upLasted(itemView, searchBook.latestChapterTitle) - "intro" -> tv_introduce.text = - context.getString(R.string.intro_show, searchBook.intro) - "kind" -> upKind(itemView, searchBook.getKindList()) - "cover" -> iv_cover.load( + "origins" -> bvOriginCount.setBadgeCount(searchBook.origins.size) + "last" -> upLasted(binding, searchBook.latestChapterTitle) + "intro" -> tvIntroduce.text = searchBook.trimIntro(context) + "kind" -> upKind(binding, searchBook.getKindList()) + "cover" -> ivCover.load( searchBook.coverUrl, searchBook.name, searchBook.author @@ -71,27 +104,24 @@ class SearchAdapter(context: Context, val callBack: CallBack) : } } - private fun upLasted(itemView: View, latestChapterTitle: String?) { - with(itemView) { + private fun upLasted(binding: ItemSearchBinding, latestChapterTitle: String?) { + binding.run { if (latestChapterTitle.isNullOrEmpty()) { - tv_lasted.gone() + tvLasted.gone() } else { - tv_lasted.text = - context.getString( - R.string.lasted_show, - latestChapterTitle - ) - tv_lasted.visible() + tvLasted.text = + context.getString(R.string.lasted_show, latestChapterTitle) + tvLasted.visible() } } } - private fun upKind(itemView: View, kinds: List) = with(itemView) { + private fun upKind(binding: ItemSearchBinding, kinds: List) = binding.run { if (kinds.isEmpty()) { - ll_kind.gone() + llKind.gone() } else { - ll_kind.visible() - ll_kind.setLabels(kinds) + llKind.visible() + llKind.setLabels(kinds) } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index 179b2cd9b..9b2f9999e 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -2,10 +2,12 @@ package io.legado.app.ui.book.search import android.app.Application import android.os.Handler +import android.os.Looper import androidx.lifecycle.MutableLiveData -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword import io.legado.app.model.webBook.SearchBookModel @@ -15,8 +17,8 @@ import kotlinx.coroutines.isActive class SearchViewModel(application: Application) : BaseViewModel(application), SearchBookModel.CallBack { - val handler = Handler() - private val searchBookModel = SearchBookModel(this, this) + val handler = Handler(Looper.getMainLooper()) + private val searchBookModel = SearchBookModel(viewModelScope, this) var isSearchLiveData = MutableLiveData() var searchBookLiveData = MutableLiveData>() var searchKey: String = "" @@ -61,12 +63,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application), } override fun onSearchSuccess(searchBooks: ArrayList) { - if (context.getPrefBoolean(PreferKey.precisionSearch)) { - precisionSearch(this, searchBooks) - } else { - App.db.searchBookDao().insert(*searchBooks.toTypedArray()) - mergeItems(this, searchBooks) - } + val precision = context.getPrefBoolean(PreferKey.precisionSearch) + appDb.searchBookDao.insert(*searchBooks.toTypedArray()) + mergeItems(viewModelScope, searchBooks, precision) } override fun onSearchFinish() { @@ -79,98 +78,73 @@ class SearchViewModel(application: Application) : BaseViewModel(application), isLoading = false } - /** - * 精确搜索处理 - */ - private fun precisionSearch(scope: CoroutineScope, searchBooks: List) { - val books = arrayListOf() - searchBooks.forEach { searchBook -> - if (searchBook.name.contains(searchKey, true) - || searchBook.author.contains(searchKey, true) - ) books.add(searchBook) - } - App.db.searchBookDao().insert(*books.toTypedArray()) - if (scope.isActive) { - mergeItems(scope, books) - } - } - /** * 合并搜索结果并排序 */ @Synchronized - private fun mergeItems(scope: CoroutineScope, newDataS: List) { + private fun mergeItems(scope: CoroutineScope, newDataS: List, precision: Boolean) { if (newDataS.isNotEmpty()) { - val copyDataS = ArrayList(searchBooks) - val searchBooksAdd = ArrayList() - if (copyDataS.size == 0) { - copyDataS.addAll(newDataS) - } else { - //存在 - newDataS.forEach { item -> + val copyData = ArrayList(searchBooks) + val equalData = arrayListOf() + val containsData = arrayListOf() + val otherData = arrayListOf() + copyData.forEach { + if (!scope.isActive) return + if (it.name == searchKey || it.author == searchKey) { + equalData.add(it) + } else if (it.name.contains(searchKey) || it.author.contains(searchKey)) { + containsData.add(it) + } else { + otherData.add(it) + } + } + newDataS.forEach { nBook -> + if (!scope.isActive) return + if (nBook.name == searchKey || nBook.author == searchKey) { var hasSame = false - for (searchBook in copyDataS) { - if (item.name == searchBook.name - && item.author == searchBook.author - ) { + equalData.forEach { pBook -> + if (!scope.isActive) return + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) hasSame = true - searchBook.addOrigin(item.origin) - break } } if (!hasSame) { - searchBooksAdd.add(item) + equalData.add(nBook) } - } - //添加 - searchBooksAdd.forEach { item -> - if (searchKey == item.name) { - for ((index, searchBook) in copyDataS.withIndex()) { - if (searchKey != searchBook.name) { - copyDataS.add(index, item) - break - } - } - } else if (searchKey == item.author) { - for ((index, searchBook) in copyDataS.withIndex()) { - if (searchKey != searchBook.name && searchKey == searchBook.author) { - copyDataS.add(index, item) - break - } + } else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) { + var hasSame = false + containsData.forEach { pBook -> + if (!scope.isActive) return + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) + hasSame = true } - } else { - copyDataS.add(item) } - } - } - if (!scope.isActive) return - searchBooks.sortWith(Comparator { o1, o2 -> - if (o1.name == searchKey && o2.name != searchKey) { - 1 - } else if (o1.name != searchKey && o2.name == searchKey) { - -1 - } else if (o1.author == searchKey && o2.author != searchKey) { - 1 - } else if (o1.author != searchKey && o2.author == searchKey) { - -1 - } else if (o1.name == o2.name) { - when { - o1.origins.size > o2.origins.size -> { - 1 - } - o1.origins.size < o2.origins.size -> { - -1 - } - else -> { - 0 + if (!hasSame) { + containsData.add(nBook) + } + } else if (!precision) { + var hasSame = false + otherData.forEach { pBook -> + if (!scope.isActive) return + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) + hasSame = true } } - } else { - 0 + if (!hasSame) { + otherData.add(nBook) + } } - }) + } if (!scope.isActive) return - searchBooks = copyDataS + equalData.sortByDescending { it.origins.size } + equalData.addAll(containsData.sortedByDescending { it.origins.size }) + if (!precision) { + equalData.addAll(otherData) + } + searchBooks = equalData upAdapter() } } @@ -187,7 +161,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application), */ fun getSearchBook(name: String, author: String, success: ((searchBook: SearchBook?) -> Unit)?) { execute { - val searchBook = App.db.searchBookDao().getFirstByNameAuthor(name, author) + val searchBook = appDb.searchBookDao.getFirstByNameAuthor(name, author) success?.invoke(searchBook) } } @@ -197,10 +171,10 @@ class SearchViewModel(application: Application) : BaseViewModel(application), */ fun saveSearchKey(key: String) { execute { - App.db.searchKeywordDao().get(key)?.let { + appDb.searchKeywordDao.get(key)?.let { it.usage = it.usage + 1 - App.db.searchKeywordDao().update(it) - } ?: App.db.searchKeywordDao().insert(SearchKeyword(key, 1)) + appDb.searchKeywordDao.update(it) + } ?: appDb.searchKeywordDao.insert(SearchKeyword(key, 1)) } } @@ -209,10 +183,14 @@ class SearchViewModel(application: Application) : BaseViewModel(application), */ fun clearHistory() { execute { - App.db.searchKeywordDao().deleteAll() + appDb.searchKeywordDao.deleteAll() } } + fun deleteHistory(searchKeyword: SearchKeyword) { + appDb.searchKeywordDao.delete(searchKeyword) + } + override fun onCleared() { super.onCleared() searchBookModel.close() diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt new file mode 100644 index 000000000..77382d7c5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt @@ -0,0 +1,262 @@ +package io.legado.app.ui.book.searchContent + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import com.github.liuyueyi.quick.transfer.ChineseUtils +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ActivitySearchContentBinding +import io.legado.app.help.AppConfig +import io.legado.app.help.BookHelp +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.observeEvent +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class SearchContentActivity : + VMBaseActivity(), + SearchContentAdapter.Callback { + + override val binding by viewBinding(ActivitySearchContentBinding::inflate) + override val viewModel by viewModels() + lateinit var adapter: SearchContentAdapter + private lateinit var mLayoutManager: UpLinearLayoutManager + private lateinit var searchView: SearchView + private var searchResultCounts = 0 + private var durChapterIndex = 0 + private var searchResultList: MutableList = mutableListOf() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView = binding.titleBar.findViewById(R.id.search_view) + val bbg = bottomBackground + val btc = getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + binding.llSearchBaseInfo.setBackgroundColor(bbg) + binding.tvCurrentSearchInfo.setTextColor(btc) + binding.ivSearchContentTop.setColorFilter(btc) + binding.ivSearchContentBottom.setColorFilter(btc) + initSearchView() + initRecyclerView() + initView() + intent.getStringExtra("bookUrl")?.let { + viewModel.initBook(it) { + initBook() + } + } + } + + private fun initSearchView() { + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search) + searchView.clearFocus() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + if (viewModel.lastQuery != query) { + startContentSearch(query) + } + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + }) + } + + private fun initRecyclerView() { + adapter = SearchContentAdapter(this, this) + mLayoutManager = UpLinearLayoutManager(this) + binding.recyclerView.layoutManager = mLayoutManager + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + } + + private fun initView() { + binding.ivSearchContentTop.setOnClickListener { + mLayoutManager.scrollToPositionWithOffset( + 0, + 0 + ) + } + binding.ivSearchContentBottom.setOnClickListener { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + } + + @SuppressLint("SetTextI18n") + private fun initBook() { + binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" + viewModel.book?.let { + initCacheFileNames(it) + durChapterIndex = it.durChapterIndex + intent.getStringExtra("searchWord")?.let { searchWord -> + searchView.setQuery(searchWord, true) + } + } + } + + private fun initCacheFileNames(book: Book) { + launch(Dispatchers.IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Dispatchers.Main) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SAVE_CONTENT) { chapter -> + viewModel.book?.bookUrl?.let { bookUrl -> + if (chapter.bookUrl == bookUrl) { + adapter.cacheFileNames.add(chapter.getFileName()) + adapter.notifyItemChanged(chapter.index, true) + } + } + } + } + + @SuppressLint("SetTextI18n") + fun startContentSearch(newText: String) { + // 按章节搜索内容 + if (newText.isNotBlank()) { + adapter.clearItems() + searchResultList.clear() + binding.refreshProgressBar.isAutoLoading = true + searchResultCounts = 0 + viewModel.lastQuery = newText + var searchResults = listOf() + launch(Dispatchers.Main) { + appDb.bookChapterDao.getChapterList(viewModel.bookUrl).map { chapter -> + withContext(Dispatchers.IO) { + if (isLocalBook + || adapter.cacheFileNames.contains(chapter.getFileName()) + ) { + searchResults = searchChapter(newText, chapter) + } + } + if (searchResults.isNotEmpty()) { + searchResultList.addAll(searchResults) + binding.refreshProgressBar.isAutoLoading = false + binding.tvCurrentSearchInfo.text = "搜索结果:$searchResultCounts" + adapter.addItems(searchResults) + searchResults = listOf() + } + } + } + } + } + + private suspend fun searchChapter(query: String, chapter: BookChapter?): List { + val searchResults: MutableList = mutableListOf() + var positions: List + var replaceContents: List? + var totalContents: String + if (chapter != null) { + viewModel.book?.let { book -> + val bookContent = BookHelp.getContent(book, chapter) + if (bookContent != null) { + //搜索替换后的正文 + withContext(Dispatchers.IO) { + chapter.title = when (AppConfig.chineseConverterType) { + 1 -> ChineseUtils.t2s(chapter.title) + 2 -> ChineseUtils.s2t(chapter.title) + else -> chapter.title + } + replaceContents = + viewModel.contentProcessor!!.getContent( + book, + chapter.title, + bookContent + ) + } + totalContents = replaceContents?.joinToString("") ?: bookContent + positions = searchPosition(totalContents, query) + var count = 1 + positions.map { + val construct = constructText(totalContents, it, query) + val result = SearchResult( + index = searchResultCounts, + indexWithinChapter = count, + text = construct[1] as String, + chapterTitle = chapter.title, + query = query, + chapterIndex = chapter.index, + newPosition = construct[0] as Int, + contentPosition = it + ) + count += 1 + searchResultCounts += 1 + searchResults.add(result) + } + } + } + } + return searchResults + } + + private fun searchPosition(content: String, pattern: String): List { + val position: MutableList = mutableListOf() + var index = content.indexOf(pattern) + while (index >= 0) { + position.add(index) + index = content.indexOf(pattern, index + 1) + } + return position + } + + private fun constructText(content: String, position: Int, query: String): Array { + // 构建关键词周边文字,在搜索结果里显示 + // todo: 判断段落,只在关键词所在段落内分割 + // todo: 利用标点符号分割完整的句 + // todo: length和设置结合,自由调整周边文字长度 + val length = 20 + var po1 = position - length + var po2 = position + query.length + length + if (po1 < 0) { + po1 = 0 + } + if (po2 > content.length) { + po2 = content.length + } + val newPosition = position - po1 + val newText = content.substring(po1, po2) + return arrayOf(newPosition, newText) + } + + val isLocalBook: Boolean + get() = viewModel.book?.isLocalBook() == true + + override fun openSearchResult(searchResult: SearchResult) { + val searchData = Intent() + searchData.putExtra("index", searchResult.chapterIndex) + searchData.putExtra("contentPosition", searchResult.contentPosition) + searchData.putExtra("query", searchResult.query) + searchData.putExtra("indexWithinChapter", searchResult.indexWithinChapter) + setResult(RESULT_OK, searchData) + finish() + } + + override fun durChapterIndex(): Int { + return durChapterIndex + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt new file mode 100644 index 000000000..364e7daaf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.book.searchContent + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemSearchListBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.hexString + + +class SearchContentAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + val cacheFileNames = hashSetOf() + val textColor = context.getCompatColor(R.color.primaryText).hexString.substring(2) + val accentColor = context.accentColor.hexString.substring(2) + + override fun getViewBinding(parent: ViewGroup): ItemSearchListBinding { + return ItemSearchListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchListBinding, + item: SearchResult, + payloads: MutableList + ) { + binding.run { + val isDur = callback.durChapterIndex() == item.chapterIndex + if (payloads.isEmpty()) { + tvSearchResult.text = item.getHtmlCompat(textColor, accentColor) + tvSearchResult.paint.isFakeBoldText = isDur + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchListBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callback.openSearchResult(it) + } + } + } + + interface Callback { + fun openSearchResult(searchResult: SearchResult) + fun durChapterIndex(): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt new file mode 100644 index 000000000..121248738 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -0,0 +1,28 @@ +package io.legado.app.ui.book.searchContent + + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.help.ContentProcessor + +class SearchContentViewModel(application: Application) : BaseViewModel(application) { + var bookUrl: String = "" + var book: Book? = null + var contentProcessor: ContentProcessor? = null + var lastQuery: String = "" + + fun initBook(bookUrl: String, success: () -> Unit) { + this.bookUrl = bookUrl + execute { + book = appDb.bookDao.getBook(bookUrl) + book?.let { + contentProcessor = ContentProcessor.get(it.name, it.origin) + } + }.onSuccess { + success.invoke() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt new file mode 100644 index 000000000..3dc4e9fc3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -0,0 +1,39 @@ +package io.legado.app.ui.book.searchContent + +import android.text.Spanned +import androidx.core.text.HtmlCompat + +data class SearchResult( + var index: Int = 0, + var indexWithinChapter: Int = 0, + var text: String = "", + var chapterTitle: String = "", + val query: String, + var pageSize: Int = 0, + var chapterIndex: Int = 0, + var pageIndex: Int = 0, + var newPosition: Int = 0, + var contentPosition: Int = 0 +) { + + fun getHtmlCompat(textColor: String, accentColor: String): Spanned { + val html = colorPresentText(newPosition, query, text, textColor, accentColor) + + "($chapterTitle)" + return HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + + private fun colorPresentText( + position: Int, + center: String, + targetText: String, + textColor: String, + accentColor: String + ): String { + val sub1 = text.substring(0, position) + val sub2 = text.substring(position + center.length, targetText.length) + return "$sub1" + + "$center" + + "$sub2" + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt index 75ac065e0..43c2ba1ae 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt @@ -1,62 +1,72 @@ package io.legado.app.ui.book.source.debug -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.widget.SearchView -import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivitySourceDebugBinding +import io.legado.app.help.LocalConfig import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor -import io.legado.app.ui.qrcode.QrCodeActivity -import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_source_debug.* -import kotlinx.android.synthetic.main.view_search.* +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.launch -import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.toast -class BookSourceDebugActivity : - VMBaseActivity(R.layout.activity_source_debug) { +class BookSourceDebugActivity : VMBaseActivity() { - override val viewModel: BookSourceDebugModel - get() = getViewModel(BookSourceDebugModel::class.java) + override val binding by viewBinding(ActivitySourceDebugBinding::inflate) + override val viewModel by viewModels() private lateinit var adapter: BookSourceDebugAdapter - private val qrRequestCode = 101 + private lateinit var searchView: SearchView + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it?.let { + startSearch(it) + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView = binding.titleBar.findViewById(R.id.search_view) viewModel.init(intent.getStringExtra("key")) initRecyclerView() initSearchView() - viewModel.observe{state, msg-> + viewModel.observe { state, msg -> launch { adapter.addItem(msg) if (state == -1 || state == 1000) { - rotate_loading.hide() + binding.rotateLoading.hide() } } } } + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!LocalConfig.debugHelpVersionIsLast) { + showHelp() + } + } + private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(binding.recyclerView) adapter = BookSourceDebugAdapter(this) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter - rotate_loading.loadingColor = accentColor + binding.recyclerView.adapter = adapter + binding.rotateLoading.loadingColor = accentColor } private fun initSearchView() { - search_view.onActionViewExpanded() - search_view.isSubmitButtonEnabled = true - search_view.queryHint = getString(R.string.search_book_key) - search_view.clearFocus() - search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search_book_key) + searchView.clearFocus() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { - search_view.clearFocus() + searchView.clearFocus() startSearch(query ?: "我的") return true } @@ -70,36 +80,36 @@ class BookSourceDebugActivity : private fun startSearch(key: String) { adapter.clearItems() viewModel.startDebug(key, { - rotate_loading.show() + binding.rotateLoading.show() }, { - toast("未获取到书源") + toastOnUi("未获取到书源") }) } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.source_debug, menu) + menuInflater.inflate(R.menu.book_source_debug, menu) return super.onCompatCreateOptionsMenu(menu) } override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_scan -> { - startActivityForResult(qrRequestCode) - } + R.id.menu_scan -> qrCodeResult.launch(null) + R.id.menu_search_src -> + TextDialog.show(supportFragmentManager, viewModel.searchSrc) + R.id.menu_book_src -> + TextDialog.show(supportFragmentManager, viewModel.bookSrc) + R.id.menu_toc_src -> + TextDialog.show(supportFragmentManager, viewModel.tocSrc) + R.id.menu_content_src -> + TextDialog.show(supportFragmentManager, viewModel.contentSrc) + R.id.menu_help -> showHelp() } return super.onCompatOptionsItemSelected(item) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - qrRequestCode -> { - if (resultCode == RESULT_OK) { - data?.getStringExtra("result")?.let { - startSearch(it) - } - } - } - } + private fun showHelp() { + val text = String(assets.open("help/debugHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt index 718b0a231..dd93b0a59 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt @@ -2,32 +2,43 @@ package io.legado.app.ui.book.source.debug import android.content.Context import android.view.View +import android.view.ViewGroup import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import kotlinx.android.synthetic.main.item_log.view.* +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemLogBinding class BookSourceDebugAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_log) { - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - holder.itemView.apply { - if (text_view.getTag(R.id.tag1) == null) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { val listener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { - text_view.isCursorVisible = false - text_view.isCursorVisible = true + textView.isCursorVisible = false + textView.isCursorVisible = true } override fun onViewDetachedFromWindow(v: View) {} } - text_view.addOnAttachStateChangeListener(listener) - text_view.setTag(R.id.tag1, listener) + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) } - text_view.text = item + textView.text = item } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { //nothing } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt index c7755401d..991591029 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt @@ -1,8 +1,8 @@ package io.legado.app.ui.book.source.debug import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.model.Debug import io.legado.app.model.webBook.WebBook @@ -10,33 +10,45 @@ class BookSourceDebugModel(application: Application) : BaseViewModel(application Debug.Callback { private var webBook: WebBook? = null - - private var callback: ((Int, String)-> Unit)? = null + private var callback: ((Int, String) -> Unit)? = null + var searchSrc: String? = null + var bookSrc: String? = null + var tocSrc: String? = null + var contentSrc: String? = null fun init(sourceUrl: String?) { sourceUrl?.let { //优先使用这个,不会抛出异常 execute { - val bookSource = App.db.bookSourceDao().getBookSource(sourceUrl) + val bookSource = appDb.bookSourceDao.getBookSource(sourceUrl) bookSource?.let { webBook = WebBook(it) } } } } - fun observe(callback: (Int, String)-> Unit){ + fun observe(callback: (Int, String) -> Unit) { this.callback = callback } fun startDebug(key: String, start: (() -> Unit)? = null, error: (() -> Unit)? = null) { - webBook?.let { + execute { + Debug.callback = this@BookSourceDebugModel + Debug.startDebug(this, webBook!!, key) + }.onStart { start?.invoke() - Debug.callback = this - Debug.startDebug(it, key) - } ?: error?.invoke() + }.onError { + error?.invoke() + } } override fun printLog(state: Int, msg: String) { - callback?.invoke(state, msg) + when (state) { + 10 -> searchSrc = msg + 20 -> bookSrc = msg + 30 -> tocSrc = msg + 40 -> contentSrc = msg + else -> callback?.invoke(state, msg) + } } override fun onCleared() { diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt index 27ed39d84..0ab548be7 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -1,9 +1,7 @@ package io.legado.app.ui.book.source.edit import android.app.Activity -import android.content.Intent import android.graphics.Rect -import android.net.Uri import android.os.Bundle import android.view.Gravity import android.view.Menu @@ -11,32 +9,39 @@ import android.view.MenuItem import android.view.ViewTreeObserver import android.widget.EditText import android.widget.PopupWindow +import androidx.activity.viewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.tabs.TabLayout +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.rule.* +import io.legado.app.databinding.ActivityBookSourceEditBinding +import io.legado.app.help.LocalConfig import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.backgroundColor import io.legado.app.ui.book.source.debug.BookSourceDebugActivity -import io.legado.app.ui.login.SourceLogin -import io.legado.app.ui.qrcode.QrCodeActivity +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.qrcode.QrCodeResult import io.legado.app.ui.widget.KeyboardToolPop +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_book_source_edit.* -import org.jetbrains.anko.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlin.math.abs class BookSourceEditActivity : - VMBaseActivity(R.layout.activity_book_source_edit, false), + VMBaseActivity(false), KeyboardToolPop.CallBack { - override val viewModel: BookSourceEditViewModel - get() = getViewModel(BookSourceEditViewModel::class.java) - private val qrRequestCode = 101 + override val binding by viewBinding(ActivityBookSourceEditBinding::inflate) + override val viewModel by viewModels() + private val adapter = BookSourceEditAdapter() private val sourceEntities: ArrayList = ArrayList() private val searchEntities: ArrayList = ArrayList() @@ -44,6 +49,20 @@ class BookSourceEditActivity : private val infoEntities: ArrayList = ArrayList() private val tocEntities: ArrayList = ArrayList() private val contentEntities: ArrayList = ArrayList() + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + viewModel.importSource(it) { source -> + upRecyclerView(source) + } + } + private val selectDoc = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + sendText(uri.toString()) + } else { + sendText(uri.path.toString()) + } + } private var mSoftKeyboardTool: PopupWindow? = null private var mIsSoftKeyBoardShowing = false @@ -55,6 +74,13 @@ class BookSourceEditActivity : } } + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!LocalConfig.ruleHelpVersionIsLast) { + showRuleHelp() + } + } + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.source_edit, menu) return super.onCompatCreateOptionsMenu(menu) @@ -63,6 +89,9 @@ class BookSourceEditActivity : override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_save -> getSource().let { source -> + if (!source.equal(viewModel.bookSource ?: BookSource())) { + source.lastUpdateTime = System.currentTimeMillis() + } if (checkSource(source)) { viewModel.save(source) { setResult(Activity.RESULT_OK); finish() } } @@ -70,33 +99,32 @@ class BookSourceEditActivity : R.id.menu_debug_source -> getSource().let { source -> if (checkSource(source)) { viewModel.save(source) { - startActivity(Pair("key", source.bookSourceUrl)) + startActivity { + putExtra("key", source.bookSourceUrl) + } } } } R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource())) R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } - R.id.menu_qr_code_camera -> startActivityForResult(qrRequestCode) + R.id.menu_qr_code_camera -> qrCodeResult.launch(null) R.id.menu_share_str -> share(GSON.toJson(getSource())) - R.id.menu_share_qr -> shareWithQr(getString(R.string.share_book_source), GSON.toJson(getSource())) - R.id.menu_rule_summary -> { - try { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(getString(R.string.source_rule_url)) - startActivity(intent) - } catch (e: Exception) { - toast(R.string.can_not_open) - } - } + R.id.menu_share_qr -> shareWithQr( + GSON.toJson(getSource()), + getString(R.string.share_book_source), + ErrorCorrectionLevel.L + ) + R.id.menu_help -> showRuleHelp() R.id.menu_login -> getSource().let { if (checkSource(it)) { if (it.loginUrl.isNullOrEmpty()) { - toast(R.string.source_no_login) + toastOnUi(R.string.source_no_login) } else { - startActivity( - Pair("sourceUrl", it.bookSourceUrl), - Pair("loginUrl", it.loginUrl) - ) + startActivity { + putExtra("sourceUrl", it.bookSourceUrl) + putExtra("loginUrl", it.loginUrl) + putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME]) + } } } } @@ -105,13 +133,13 @@ class BookSourceEditActivity : } private fun initView() { - ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(binding.recyclerView) mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter - tab_layout.setBackgroundColor(backgroundColor) - tab_layout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.tabLayout.setBackgroundColor(backgroundColor) + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { } @@ -130,12 +158,12 @@ class BookSourceEditActivity : val source = getSource() if (!source.equal(viewModel.bookSource ?: BookSource())) { alert(R.string.exit) { - messageResource = R.string.exit_no_save + setMessage(R.string.exit_no_save) positiveButton(R.string.yes) negativeButton(R.string.no) { super.finish() } - }.show().applyTint() + }.show() } else { super.finish() } @@ -155,14 +183,14 @@ class BookSourceEditActivity : 5 -> adapter.editEntities = contentEntities else -> adapter.editEntities = sourceEntities } - recycler_view.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) } private fun upRecyclerView(source: BookSource? = viewModel.bookSource) { source?.let { - cb_is_enable.isChecked = it.enabled - cb_is_enable_find.isChecked = it.enabledExplore - sp_type.setSelection(it.bookSourceType) + binding.cbIsEnable.isChecked = it.enabled + binding.cbIsEnableFind.isChecked = it.enabledExplore + binding.spType.setSelection(it.bookSourceType) } //基本信息 sourceEntities.clear() @@ -170,10 +198,11 @@ class BookSourceEditActivity : add(EditEntity("bookSourceUrl", source?.bookSourceUrl, R.string.source_url)) add(EditEntity("bookSourceName", source?.bookSourceName, R.string.source_name)) add(EditEntity("bookSourceGroup", source?.bookSourceGroup, R.string.source_group)) + add(EditEntity("bookSourceComment", source?.bookSourceComment, R.string.comment)) add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url)) add(EditEntity("bookUrlPattern", source?.bookUrlPattern, R.string.book_url_pattern)) add(EditEntity("header", source?.header, R.string.source_http_header)) - add(EditEntity("bookSourceComment", source?.bookSourceComment, R.string.comment)) + } //搜索 val sr = source?.getSearchRule() @@ -203,6 +232,7 @@ class BookSourceEditActivity : add(EditEntity("intro", ir?.intro, R.string.rule_book_intro)) add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url)) add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url)) + add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name)) } //目录页 val tr = source?.getTocRule() @@ -241,15 +271,15 @@ class BookSourceEditActivity : add(EditEntity("coverUrl", er?.coverUrl, R.string.rule_cover_url)) add(EditEntity("bookUrl", er?.bookUrl, R.string.r_book_url)) } - tab_layout.selectTab(tab_layout.getTabAt(0)) + binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0)) setEditEntities(0) } private fun getSource(): BookSource { val source = viewModel.bookSource?.copy() ?: BookSource() - source.enabled = cb_is_enable.isChecked - source.enabledExplore = cb_is_enable_find.isChecked - source.bookSourceType = sp_type.selectedItemPosition + source.enabled = binding.cbIsEnable.isChecked + source.enabledExplore = binding.cbIsEnableFind.isChecked + source.bookSourceType = binding.spType.selectedItemPosition val searchRule = SearchRule() val exploreRule = ExploreRule() val bookInfoRule = BookInfoRule() @@ -308,6 +338,7 @@ class BookSourceEditActivity : "lastChapter" -> bookInfoRule.lastChapter = it.value "coverUrl" -> bookInfoRule.coverUrl = it.value "tocUrl" -> bookInfoRule.tocUrl = it.value + "canReName" -> bookInfoRule.canReName = it.value } } tocEntities.forEach { @@ -340,7 +371,7 @@ class BookSourceEditActivity : private fun checkSource(source: BookSource): Boolean { if (source.bookSourceUrl.isBlank() || source.bookSourceName.isBlank()) { - toast("书源名称和URL不能为空") + toastOnUi(R.string.non_null_name_url) return false } return true @@ -363,17 +394,43 @@ class BookSourceEditActivity : override fun sendText(text: String) { if (text == AppConst.keyboardToolChars[0]) { - insertText(AppConst.urlOption) + showHelpDialog() } else { insertText(text) } } + private fun showHelpDialog() { + val items = arrayListOf("插入URL参数", "书源教程", "正则教程", "选择文件") + selector(getString(R.string.help), items) { _, index -> + when (index) { + 0 -> insertText(AppConst.urlOption) + 1 -> showRuleHelp() + 2 -> showRegexHelp() + 3 -> selectDoc.launch( + FilePickerParam( + mode = FilePicker.FILE + ) + ) + } + } + } + + private fun showRuleHelp() { + val mdText = String(assets.open("help/ruleHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + + private fun showRegexHelp() { + val mdText = String(assets.open("help/regexHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + private fun showKeyboardTopPopupWindow() { mSoftKeyboardTool?.let { if (it.isShowing) return if (!isFinishing) { - it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + it.showAtLocation(binding.root, Gravity.BOTTOM, 0, 0) } } } @@ -382,34 +439,21 @@ class BookSourceEditActivity : mSoftKeyboardTool?.dismiss() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - qrRequestCode -> if (resultCode == RESULT_OK) { - data?.getStringExtra("result")?.let { - viewModel.importSource(it) { source -> - upRecyclerView(source) - } - } - } - } - } - private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { val rect = Rect() // 获取当前页面窗口的显示范围 window.decorView.getWindowVisibleDisplayFrame(rect) - val screenHeight = this@BookSourceEditActivity.displayMetrics.heightPixels + val screenHeight = this@BookSourceEditActivity.getSize().heightPixels val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 val preShowing = mIsSoftKeyBoardShowing if (abs(keyboardHeight) > screenHeight / 5) { mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 - recycler_view.setPadding(0, 0, 0, 100) + binding.recyclerView.setPadding(0, 0, 0, 100) showKeyboardTopPopupWindow() } else { mIsSoftKeyBoardShowing = false - recycler_view.setPadding(0, 0, 0, 0) + binding.recyclerView.setPadding(0, 0, 0, 0) if (preShowing) { closePopupWindow() } diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt index 163d66584..b2b168dcc 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.source.edit +import android.annotation.SuppressLint import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater @@ -7,22 +8,21 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.legado.app.R -import kotlinx.android.synthetic.main.item_source_edit.view.* +import io.legado.app.databinding.ItemSourceEditBinding class BookSourceEditAdapter : RecyclerView.Adapter() { var editEntities: ArrayList = ArrayList() + @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - return MyViewHolder( - LayoutInflater.from( - parent.context - ).inflate(R.layout.item_source_edit, parent, false) - ) + val binding = ItemSourceEditBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + return MyViewHolder(binding) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { @@ -33,8 +33,9 @@ class BookSourceEditAdapter : RecyclerView.Adapter(R.layout.activity_book_source), +class BookSourceActivity : VMBaseActivity(), PopupMenu.OnMenuItemClickListener, BookSourceAdapter.CallBack, - FileChooserDialog.CallBack, + SelectActionBar.CallBack, SearchView.OnQueryTextListener { - override val viewModel: BookSourceViewModel - get() = getViewModel(BookSourceViewModel::class.java) + override val binding by viewBinding(ActivityBookSourceBinding::inflate) + override val viewModel by viewModels() private val importRecordKey = "bookSourceRecordKey" - private val qrRequestCode = 101 - private val importRequestCode = 132 - private val exportRequestCode = 65 private lateinit var adapter: BookSourceAdapter - private var bookSourceLiveDate: LiveData>? = null - private var groups = linkedSetOf() + private lateinit var searchView: SearchView + private var sourceFlowJob: Job? = null + private val groups = linkedSetOf() private var groupMenu: SubMenu? = null - private var sort = 0 + private var sort = Sort.Default + private var sortAscending = true + private var snackBar: Snackbar? = null + private val qrResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + ImportBookSourceDialog.start(supportFragmentManager, it) + } + private val importDoc = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + try { + uri.readText(this)?.let { + ImportBookSourceDialog.start(supportFragmentManager, it) + } + } catch (e: Exception) { + toastOnUi("readTextError:${e.localizedMessage}") + } + } + private val exportDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(this, uri)?.let { + viewModel.exportSelection(adapter.selection, it) + } + } else { + uri.path?.let { + viewModel.exportSelection(adapter.selection, File(it)) + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView = binding.titleBar.findViewById(R.id.search_view) initRecyclerView() initSearchView() - initLiveDataBookSource() + upBookSource() initLiveDataGroup() initSelectActionBar() + if (!LocalConfig.bookSourcesHelpVersionIsLast) { + showHelp() + } } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -85,153 +114,211 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_add_book_source -> startActivity() - R.id.menu_import_source_qr -> startActivityForResult(qrRequestCode) + R.id.menu_import_qr -> qrResult.launch(null) + R.id.menu_share_source -> viewModel.shareSelection(adapter.selection) { + startActivity(Intent.createChooser(it, getString(R.string.share_selected_source))) + } R.id.menu_group_manage -> GroupManageDialog().show(supportFragmentManager, "groupManage") - R.id.menu_import_source_local -> FilePicker - .selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) - R.id.menu_import_source_onLine -> showImportDialog() + R.id.menu_import_local -> importDoc.launch( + FilePickerParam( + mode = FilePicker.FILE, + allowExtensions = arrayOf("txt", "json") + ) + ) + R.id.menu_import_onLine -> showImportDialog() R.id.menu_sort_manual -> { item.isChecked = true - sort = 0 - initLiveDataBookSource(search_view.query?.toString()) + sortCheck(Sort.Default) + upBookSource(searchView.query?.toString()) } R.id.menu_sort_auto -> { item.isChecked = true - sort = 2 - initLiveDataBookSource(search_view.query?.toString()) + sortCheck(Sort.Weight) + upBookSource(searchView.query?.toString()) } - R.id.menu_sort_pin_yin -> { + R.id.menu_sort_name -> { item.isChecked = true - sort = 3 - initLiveDataBookSource(search_view.query?.toString()) + sortCheck(Sort.Name) + upBookSource(searchView.query?.toString()) } R.id.menu_sort_url -> { item.isChecked = true - sort = 4 - initLiveDataBookSource(search_view.query?.toString()) + sortCheck(Sort.Url) + upBookSource(searchView.query?.toString()) + } + R.id.menu_sort_time -> { + item.isChecked = true + sortCheck(Sort.Update) + upBookSource(searchView.query?.toString()) + } + R.id.menu_sort_enable -> { + item.isChecked = true + sortCheck(Sort.Enable) + upBookSource(searchView.query?.toString()) } R.id.menu_enabled_group -> { - search_view.setQuery(getString(R.string.enabled), true) + searchView.setQuery(getString(R.string.enabled), true) } R.id.menu_disabled_group -> { - search_view.setQuery(getString(R.string.disabled), true) + searchView.setQuery(getString(R.string.disabled), true) } + R.id.menu_help -> showHelp() } if (item.groupId == R.id.source_group) { - search_view.setQuery(item.title, true) + searchView.setQuery("group:${item.title}", true) } return super.onCompatOptionsItemSelected(item) } private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) + ATH.applyEdgeEffectColor(binding.recyclerView) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) adapter = BookSourceAdapter(this, this) - recycler_view.adapter = adapter - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter - itemTouchCallback.isCanDrag = true - val dragSelectTouchHelper: DragSelectTouchHelper = - DragSelectTouchHelper(adapter.initDragSelectTouchHelperCallback()).setSlideArea(16, 50) - dragSelectTouchHelper.attachToRecyclerView(recycler_view) + binding.recyclerView.adapter = adapter // When this page is opened, it is in selection mode + val dragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) dragSelectTouchHelper.activeSlideSelect() - // Note: need judge selection first, so add ItemTouchHelper after it. - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) } private fun initSearchView() { - ATH.setTint(search_view, primaryTextColor) - search_view.onActionViewExpanded() - search_view.queryHint = getString(R.string.search_book_source) - search_view.clearFocus() - search_view.setOnQueryTextListener(this) - } - - private fun initLiveDataBookSource(searchKey: String? = null) { - bookSourceLiveDate?.removeObservers(this) - bookSourceLiveDate = when { - searchKey.isNullOrEmpty() -> { - App.db.bookSourceDao().liveDataAll() - } - searchKey == getString(R.string.enabled) -> { - App.db.bookSourceDao().liveDataEnabled() - } - searchKey == getString(R.string.disabled) -> { - App.db.bookSourceDao().liveDataDisabled() - } - else -> { - App.db.bookSourceDao().liveDataSearch("%$searchKey%") + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.queryHint = getString(R.string.search_book_source) + searchView.clearFocus() + searchView.setOnQueryTextListener(this) + } + + private fun upBookSource(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = launch { + when { + searchKey.isNullOrEmpty() -> { + appDb.bookSourceDao.flowAll() + } + searchKey == getString(R.string.enabled) -> { + appDb.bookSourceDao.flowEnabled() + } + searchKey == getString(R.string.disabled) -> { + appDb.bookSourceDao.flowDisabled() + } + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.bookSourceDao.flowGroupSearch("%$key%") + } + else -> { + appDb.bookSourceDao.flowSearch("%$searchKey%") + } + }.collect { data -> + val sourceList = + if (sortAscending) when (sort) { + Sort.Weight -> data.sortedBy { it.weight } + Sort.Name -> data.sortedWith { o1, o2 -> + o1.bookSourceName.cnCompare(o2.bookSourceName) + } + Sort.Url -> data.sortedBy { it.bookSourceUrl } + Sort.Update -> data.sortedByDescending { it.lastUpdateTime } + Sort.Enable -> data.sortedWith { o1, o2 -> + var sort = -o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + else -> data + } + else when (sort) { + Sort.Weight -> data.sortedByDescending { it.weight } + Sort.Name -> data.sortedWith { o1, o2 -> + o2.bookSourceName.cnCompare(o1.bookSourceName) + } + Sort.Url -> data.sortedByDescending { it.bookSourceUrl } + Sort.Update -> data.sortedBy { it.lastUpdateTime } + Sort.Enable -> data.sortedWith { o1, o2 -> + var sort = o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + else -> data.reversed() + } + adapter.setItems(sourceList, adapter.diffItemCallback) } } - bookSourceLiveDate?.observe(this, { data -> - val sourceList = when (sort) { - 1 -> data.sortedBy { it.weight } - 2 -> data.sortedBy { it.bookSourceName } - 3 -> data.sortedBy { it.bookSourceUrl } - else -> data - } - val diffResult = DiffUtil - .calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), sourceList)) - adapter.setItems(sourceList, diffResult) - upCountView() - }) } - private fun initLiveDataGroup() { - App.db.bookSourceDao().liveGroup().observe(this, { - groups.clear() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) - } - upGroupMenu() - }) + private fun showHelp() { + val text = String(assets.open("help/SourceMBookHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) } - private fun initSelectActionBar() { - select_action_bar.setMainActionText(R.string.delete) - select_action_bar.inflateMenu(R.menu.book_source_sel) - select_action_bar.setOnMenuItemClickListener(this) - select_action_bar.setCallBack(object : SelectActionBar.CallBack { - override fun selectAll(selectAll: Boolean) { - if (selectAll) { - adapter.selectAll() - } else { - adapter.revertSelection() + private fun sortCheck(sort: Sort) { + if (this.sort == sort) { + sortAscending = !sortAscending + } else { + sortAscending = true + this.sort = sort + } + } + + private fun initLiveDataGroup() { + launch { + appDb.bookSourceDao.flowGroup() + .collect { + groups.clear() + it.forEach { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupMenu() } - } + } + } - override fun revertSelection() { - adapter.revertSelection() - } + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } - override fun onClickMainAction() { - this@BookSourceActivity - .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { - okButton { viewModel.delSelection(adapter.getSelection()) } - noButton { } - } - .show().applyTint() - } - }) + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.selection) } + noButton() + }.show() + } + private fun initSelectActionBar() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.book_source_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) } override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) - R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection()) - R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection()) - R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.selection) + R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.selection) R.id.menu_check_source -> checkSource() - R.id.menu_top_sel -> viewModel.topSource(*adapter.getSelection().toTypedArray()) - R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray()) + R.id.menu_top_sel -> viewModel.topSource(*adapter.selection.toTypedArray()) + R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray()) R.id.menu_add_group -> selectionAddToGroups() R.id.menu_remove_group -> selectionRemoveFromGroups() + R.id.menu_export_selection -> exportDir.launch(null) } return true } @@ -239,73 +326,69 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity @SuppressLint("InflateParams") private fun checkSource() { alert(titleResource = R.string.search_book_key) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setText(CheckSource.keyword) - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setText(CheckSource.keyword) } + customView { alertBinding.root } okButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotEmpty()) { CheckSource.keyword = it } } - CheckSource.start(this@BookSourceActivity, adapter.getSelection()) + CheckSource.start(this@BookSourceActivity, adapter.selection) } - noButton { } - }.show().applyTint() + noButton() + }.show() } @SuppressLint("InflateParams") private fun selectionAddToGroups() { alert(titleResource = R.string.add_group) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setHint(R.string.group_name) - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp } + customView { alertBinding.root } okButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotEmpty()) { - viewModel.selectionAddToGroups(adapter.getSelection(), it) + viewModel.selectionAddToGroups(adapter.selection, it) } } } - noButton { } - }.show().applyTint() + cancelButton() + }.show() } @SuppressLint("InflateParams") private fun selectionRemoveFromGroups() { alert(titleResource = R.string.remove_group) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setHint(R.string.group_name) - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp } + customView { alertBinding.root } okButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotEmpty()) { - viewModel.selectionRemoveFromGroups(adapter.getSelection(), it) + viewModel.selectionRemoveFromGroups(adapter.selection, it) } } } - noButton { } - }.show().applyTint() + cancelButton() + }.show() } - private fun upGroupMenu() { - groupMenu?.removeGroup(R.id.source_group) - groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE)) - .map { - groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it) - } + private fun upGroupMenu() = groupMenu?.let { menu -> + menu.removeGroup(R.id.source_group) + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.map { + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, it) + } } @SuppressLint("InflateParams") @@ -315,39 +398,59 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity .getAsString(importRecordKey) ?.splitNotBlank(",") ?.toMutableList() ?: mutableListOf() - alert(titleResource = R.string.import_book_source_on_line) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setFilterValues(cacheUrls) - edit_view.delCallBack = { - cacheUrls.remove(it) - aCache.put(importRecordKey, cacheUrls.joinToString(",")) - } + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } } + customView { alertBinding.root } okButton { - val text = editText?.text?.toString() + val text = alertBinding.editView.text?.toString() text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) aCache.put(importRecordKey, cacheUrls.joinToString(",")) } - startActivity(Pair("source", it)) + ImportBookSourceDialog.start(supportFragmentManager, it) } } cancelButton() - }.show().applyTint() + }.show() + } + + override fun observeLiveBus() { + observeEvent(EventBus.CHECK_SOURCE) { msg -> + snackBar?.setText(msg) ?: let { + snackBar = Snackbar + .make(binding.root, msg, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.cancel) { + CheckSource.stop(this) + }.apply { show() } + } + } + observeEvent(EventBus.CHECK_SOURCE_DONE) { + snackBar?.dismiss() + snackBar = null + groups.map { group -> + if (group.contains("失效")) { + searchView.setQuery("失效", true) + toastOnUi("发现有失效书源,已为您自动筛选!") + } + } + } } override fun upCountView() { - select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) + binding.selectActionBar + .upCountView(adapter.selection.size, adapter.itemCount) } override fun onQueryTextChange(newText: String?): Boolean { newText?.let { - initLiveDataBookSource(it) + upBookSource(it) } return false } @@ -365,7 +468,9 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity } override fun edit(bookSource: BookSource) { - startActivity(Pair("data", bookSource.bookSourceUrl)) + startActivity { + putExtra("data", bookSource.bookSourceUrl) + } } override fun upOrder() { @@ -380,59 +485,21 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity viewModel.bottomSource(bookSource) } - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - exportRequestCode -> viewModel.exportSelection( - adapter.getSelection(), - File(currentPath) - ) - importRequestCode -> { - startActivity(Pair("filePath", currentPath)) - } - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - qrRequestCode -> if (resultCode == RESULT_OK) { - data?.getStringExtra("result")?.let { - startActivity("source" to it) - } - } - importRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - try { - uri.readText(this)?.let { - val dataKey = IntentDataHelp.putData(it) - startActivity("dataKey" to dataKey) - } - } catch (e: Exception) { - toast("readTextError:${e.localizedMessage}") - } - } - } - exportRequestCode -> { - data?.data?.let { uri -> - if (uri.toString().isContentPath()) { - DocumentFile.fromTreeUri(this, uri)?.let { - viewModel.exportSelection(adapter.getSelection(), it) - } - } else { - uri.path?.let { - viewModel.exportSelection(adapter.getSelection(), File(it)) - } - } - } - } + override fun debug(bookSource: BookSource) { + startActivity { + putExtra("key", bookSource.bookSourceUrl) } } override fun finish() { - if (search_view.query.isNullOrEmpty()) { + if (searchView.query.isNullOrEmpty()) { super.finish() } else { - search_view.setQuery("", true) + searchView.setQuery("", true) } } + + enum class Sort { + Default, Name, Url, Weight, Update, Enable + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt index a5177f443..7669362fc 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt @@ -4,124 +4,161 @@ import android.content.Context import android.graphics.Color import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.ItemBookSourceBinding import io.legado.app.lib.theme.backgroundColor import io.legado.app.ui.widget.recycler.DragSelectTouchHelper -import io.legado.app.ui.widget.recycler.ItemTouchCallback.OnItemTouchCallbackListener +import io.legado.app.ui.widget.recycler.ItemTouchCallback.Callback +import io.legado.app.utils.ColorUtils import io.legado.app.utils.invisible import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_book_source.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import java.util.* + class BookSourceAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_book_source), - OnItemTouchCallbackListener { + RecyclerAdapter(context), + Callback { private val selected = linkedSetOf() - fun selectAll() { - getItems().forEach { - selected.add(it) + val selection: List + get() { + val selection = arrayListOf() + getItems().map { + if (selected.contains(it)) { + selection.add(it) + } + } + return selection.sortedBy { it.customOrder } } - notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) - callBack.upCountView() - } - fun revertSelection() { - getItems().forEach { - if (selected.contains(it)) { - selected.remove(it) - } else { - selected.add(it) + val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: BookSource, newItem: BookSource): Boolean { + return oldItem.bookSourceUrl == newItem.bookSourceUrl } - } - notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) - callBack.upCountView() - } - fun getSelection(): List { - val selection = arrayListOf() - getItems().map { - if (selected.contains(it)) { - selection.add(it) + override fun areContentsTheSame(oldItem: BookSource, newItem: BookSource): Boolean { + if (oldItem.bookSourceName != newItem.bookSourceName) + return false + if (oldItem.bookSourceGroup != newItem.bookSourceGroup) + return false + if (oldItem.enabled != newItem.enabled) + return false + if (oldItem.enabledExplore != newItem.enabledExplore + || oldItem.exploreUrl != newItem.exploreUrl + ) { + return false + } + return true + } + + override fun getChangePayload(oldItem: BookSource, newItem: BookSource): Any? { + val payload = Bundle() + if (oldItem.bookSourceName != newItem.bookSourceName) { + payload.putString("name", newItem.bookSourceName) + } + if (oldItem.bookSourceGroup != newItem.bookSourceGroup) { + payload.putString("group", newItem.bookSourceGroup) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (oldItem.enabledExplore != newItem.enabledExplore + || oldItem.exploreUrl != newItem.exploreUrl + ) { + payload.putBoolean("showExplore", true) + } + if (payload.isEmpty) { + return null + } + return payload } } - return selection.sortedBy { it.customOrder } + + override fun getViewBinding(parent: ViewGroup): ItemBookSourceBinding { + return ItemBookSourceBinding.inflate(inflater, parent, false) } - override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList) { - with(holder.itemView) { + override fun convert( + holder: ItemViewHolder, + binding: ItemBookSourceBinding, + item: BookSource, + payloads: MutableList + ) { + binding.run { val payload = payloads.getOrNull(0) as? Bundle if (payload == null) { - this.setBackgroundColor(context.backgroundColor) + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) if (item.bookSourceGroup.isNullOrEmpty()) { - cb_book_source.text = item.bookSourceName + cbBookSource.text = item.bookSourceName } else { - cb_book_source.text = + cbBookSource.text = String.format("%s (%s)", item.bookSourceName, item.bookSourceGroup) } - swt_enabled.isChecked = item.enabled - cb_book_source.isChecked = selected.contains(item) - upShowExplore(iv_explore, item) + swtEnabled.isChecked = item.enabled + cbBookSource.isChecked = selected.contains(item) + upShowExplore(ivExplore, item) } else { payload.keySet().map { when (it) { - "selected" -> cb_book_source.isChecked = selected.contains(item) - "name", "group" -> if (item.bookSourceGroup.isNullOrEmpty()) { - cb_book_source.text = item.bookSourceName - } else { - cb_book_source.text = - String.format("%s (%s)", item.bookSourceName, item.bookSourceGroup) - } - "enabled" -> swt_enabled.isChecked = payload.getBoolean(it) - "showExplore" -> upShowExplore(iv_explore, item) + "selected" -> cbBookSource.isChecked = selected.contains(item) } } } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - swt_enabled.setOnCheckedChangeListener { view, checked -> - getItem(holder.layoutPosition)?.let { - if (view.isPressed) { - it.enabled = checked - callBack.update(it) + override fun registerListener(holder: ItemViewHolder, binding: ItemBookSourceBinding) { + binding.apply { + swtEnabled.setOnCheckedChangeListener { view, checked -> + if (view.isPressed) { + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + it.enabled = checked + callBack.update(it) + } } } } - cb_book_source.setOnCheckedChangeListener { view, checked -> - getItem(holder.layoutPosition)?.let { - if (view.isPressed) { - if (checked) { - selected.add(it) - } else { - selected.remove(it) + cbBookSource.setOnCheckedChangeListener { view, checked -> + if (view.isPressed) { + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + if (checked) { + selected.add(it) + } else { + selected.remove(it) + } + callBack.upCountView() } - callBack.upCountView() } } } - iv_edit.onClick { + ivEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.edit(it) } } - iv_menu_more.onClick { - showMenu(iv_menu_more, holder.layoutPosition) + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) } } } + override fun onCurrentListChanged() { + callBack.upCountView() + } + private fun showMenu(view: View, position: Int) { val source = getItem(position) ?: return val popupMenu = PopupMenu(context, view) @@ -140,6 +177,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : when (menuItem.itemId) { R.id.menu_top -> callBack.toTop(source) R.id.menu_bottom -> callBack.toBottom(source) + R.id.menu_debug_source -> callBack.debug(source) R.id.menu_del -> callBack.del(source) R.id.menu_enable_explore -> { callBack.update(source.copy(enabledExplore = !source.enabledExplore)) @@ -166,7 +204,27 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : } } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { val srcItem = getItem(srcPosition) val targetItem = getItem(targetPosition) if (srcItem != null && targetItem != null) { @@ -180,8 +238,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : movedItems.add(targetItem) } } - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + swapItem(srcPosition, targetPosition) return true } @@ -194,8 +251,8 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : } } - fun initDragSelectTouchHelperCallback(): DragSelectTouchHelper.Callback { - return object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { override fun currentSelectedId(): MutableSet { return selected } @@ -218,7 +275,6 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : return false } } - } interface CallBack { fun del(bookSource: BookSource) @@ -226,6 +282,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : fun update(vararg bookSource: BookSource) fun toTop(bookSource: BookSource) fun toBottom(bookSource: BookSource) + fun debug(bookSource: BookSource) fun upOrder() fun upCountView() } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index e291ac625..6cc8ea56a 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -1,56 +1,55 @@ package io.legado.app.ui.book.source.manage import android.app.Application +import android.content.Intent import android.text.TextUtils +import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON -import io.legado.app.utils.splitNotBlank -import io.legado.app.utils.writeText -import org.jetbrains.anko.longToast +import io.legado.app.utils.* import java.io.File class BookSourceViewModel(application: Application) : BaseViewModel(application) { fun topSource(vararg sources: BookSource) { execute { - val minOrder = App.db.bookSourceDao().minOrder - 1 + val minOrder = appDb.bookSourceDao.minOrder - 1 sources.forEachIndexed { index, bookSource -> bookSource.customOrder = minOrder - index } - App.db.bookSourceDao().update(*sources) + appDb.bookSourceDao.update(*sources) } } fun bottomSource(vararg sources: BookSource) { execute { - val maxOrder = App.db.bookSourceDao().maxOrder + 1 + val maxOrder = appDb.bookSourceDao.maxOrder + 1 sources.forEachIndexed { index, bookSource -> bookSource.customOrder = maxOrder + index } - App.db.bookSourceDao().update(*sources) + appDb.bookSourceDao.update(*sources) } } fun del(bookSource: BookSource) { - execute { App.db.bookSourceDao().delete(bookSource) } + execute { appDb.bookSourceDao.delete(bookSource) } } fun update(vararg bookSource: BookSource) { - execute { App.db.bookSourceDao().update(*bookSource) } + execute { appDb.bookSourceDao.update(*bookSource) } } fun upOrder() { execute { - val sources = App.db.bookSourceDao().all + val sources = appDb.bookSourceDao.all for ((index: Int, source: BookSource) in sources.withIndex()) { source.customOrder = index + 1 } - App.db.bookSourceDao().update(*sources.toTypedArray()) + appDb.bookSourceDao.update(*sources.toTypedArray()) } } @@ -60,7 +59,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabled = true)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } @@ -70,7 +69,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabled = false)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } @@ -80,7 +79,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabledExplore = true)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } @@ -90,7 +89,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabledExplore = false)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } @@ -109,7 +108,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) val newGroup = ArrayList(lh).joinToString(separator = ",") list.add(source.copy(bookSourceGroup = newGroup)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } @@ -128,13 +127,13 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) val newGroup = ArrayList(lh).joinToString(separator = ",") list.add(source.copy(bookSourceGroup = newGroup)) } - App.db.bookSourceDao().update(*list.toTypedArray()) + appDb.bookSourceDao.update(*list.toTypedArray()) } } fun delSelection(sources: List) { execute { - App.db.bookSourceDao().delete(*sources.toTypedArray()) + appDb.bookSourceDao.delete(*sources.toTypedArray()) } } @@ -144,9 +143,9 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) FileUtils.createFileIfNotExist(file, "exportBookSource.json") .writeText(json) }.onSuccess { - context.longToast("成功导出至\n${file.absolutePath}") + context.longToastOnUi("成功导出至\n${file.absolutePath}") }.onError { - context.longToast("导出失败\n${it.localizedMessage}") + context.longToastOnUi("导出失败\n${it.localizedMessage}") } } @@ -157,25 +156,45 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) doc.createFile("", "exportBookSource.json") ?.writeText(context, json) }.onSuccess { - context.longToast("成功导出至\n${doc.uri.path}") + context.longToastOnUi("成功导出至\n${doc.uri.path}") }.onError { - context.longToast("导出失败\n${it.localizedMessage}") + context.longToastOnUi("导出失败\n${it.localizedMessage}") + } + } + + fun shareSelection(sources: List, success: ((intent: Intent) -> Unit)) { + execute { + val tmpSharePath = "${context.filesDir}/shareBookSource.json" + FileUtils.delete(tmpSharePath) + val intent = Intent(Intent.ACTION_SEND) + val file = FileUtils.createFileWithReplace(tmpSharePath) + file.writeText(GSON.toJson(sources)) + val fileUri = FileProvider.getUriForFile(context, AppConst.authority, file) + intent.type = "text/*" + intent.putExtra(Intent.EXTRA_STREAM, fileUri) + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.msg) } } fun addGroup(group: String) { execute { - val sources = App.db.bookSourceDao().noGroup + val sources = appDb.bookSourceDao.noGroup sources.map { source -> source.bookSourceGroup = group } - App.db.bookSourceDao().update(*sources.toTypedArray()) + appDb.bookSourceDao.update(*sources.toTypedArray()) } } fun upGroup(oldGroup: String, newGroup: String?) { execute { - val sources = App.db.bookSourceDao().getByGroup(oldGroup) + val sources = appDb.bookSourceDao.getByGroup(oldGroup) sources.map { source -> source.bookSourceGroup?.splitNotBlank(",")?.toHashSet()?.let { it.remove(oldGroup) @@ -184,18 +203,18 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) source.bookSourceGroup = TextUtils.join(",", it) } } - App.db.bookSourceDao().update(*sources.toTypedArray()) + appDb.bookSourceDao.update(*sources.toTypedArray()) } } fun delGroup(group: String) { execute { execute { - val sources = App.db.bookSourceDao().getByGroup(group) + val sources = appDb.bookSourceDao.getByGroup(group) sources.map { source -> source.removeGroup(group) } - App.db.bookSourceDao().update(*sources.toTypedArray()) + appDb.bookSourceDao.update(*sources.toTypedArray()) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt deleted file mode 100644 index 8810d2dd9..000000000 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.legado.app.ui.book.source.manage - -import android.os.Bundle -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.BookSource - -class DiffCallBack( - private val oldItems: List, - private val newItems: List -) : DiffUtil.Callback() { - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.bookSourceUrl == newItem.bookSourceUrl - } - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - if (oldItem.bookSourceName != newItem.bookSourceName) - return false - if (oldItem.bookSourceGroup != newItem.bookSourceGroup) - return false - if (oldItem.enabled != newItem.enabled) - return false - if (oldItem.enabledExplore != newItem.enabledExplore - || oldItem.exploreUrl != newItem.exploreUrl - ) { - return false - } - return true - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - val payload = Bundle() - if (oldItem.bookSourceName != newItem.bookSourceName) { - payload.putString("name", newItem.bookSourceName) - } - if (oldItem.bookSourceGroup != newItem.bookSourceGroup) { - payload.putString("group", newItem.bookSourceGroup) - } - if (oldItem.enabled != newItem.enabled) { - payload.putBoolean("enabled", newItem.enabled) - } - if (oldItem.enabledExplore != newItem.enabledExplore - || oldItem.exploreUrl != newItem.exploreUrl - ) { - payload.putBoolean("showExplore", true) - } - if (payload.isEmpty) { - return null - } - return payload - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt index 5c7136691..7e5337b98 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -3,44 +3,43 @@ package io.legado.app.ui.book.source.manage import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.EditText import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R +import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity +import io.legado.app.utils.getSize import io.legado.app.utils.requestInputMethod import io.legado.app.utils.splitNotBlank -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_group_manage.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch -class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { - private lateinit var viewModel: BookSourceViewModel + +class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + private val viewModel: BookSourceViewModel by activityViewModels() private lateinit var adapter: GroupAdapter + private val binding by viewBinding(DialogRecyclerViewBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -49,33 +48,33 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModelOfActivity(BookSourceViewModel::class.java) return inflater.inflate(R.layout.dialog_recycler_view, container) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { view.setBackgroundColor(backgroundColor) - tool_bar.setBackgroundColor(primaryColor) - tool_bar.title = getString(R.string.group_manage) - tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = getString(R.string.group_manage) + binding.toolBar.inflateMenu(R.menu.group_manage) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter initData() } private fun initData() { - App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, { - val groups = linkedSetOf() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + launch { + appDb.bookSourceDao.flowGroup().collect { + val groups = linkedSetOf() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + adapter.setItems(groups.toList()) } - adapter.setItems(groups.toList()) - }) + } } override fun onMenuItemClick(item: MenuItem?): Boolean { @@ -88,62 +87,61 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { @SuppressLint("InflateParams") private fun addGroup() { alert(title = getString(R.string.add_group)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - setHint(R.string.group_name) - } - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + alertBinding.editView.setHint(R.string.group_name) + customView { alertBinding.root } yesButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotBlank()) { viewModel.addGroup(it) } } } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } @SuppressLint("InflateParams") private fun editGroup(group: String) { alert(title = getString(R.string.group_edit)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - setHint(R.string.group_name) - setText(group) - } - } - } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + alertBinding.editView.setHint(R.string.group_name) + alertBinding.editView.setText(group) + customView { alertBinding.root } yesButton { - viewModel.upGroup(group, editText?.text?.toString()) + viewModel.upGroup(group, alertBinding.editView.text?.toString()) } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_manage) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - with(holder.itemView) { - setBackgroundColor(context.backgroundColor) - tv_group.text = item + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - tv_edit.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } - tv_del.onClick { + tvDel.setOnClickListener { getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt new file mode 100644 index 000000000..43a6ac72d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt @@ -0,0 +1,48 @@ +package io.legado.app.ui.book.toc + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ItemBookmarkBinding +import splitties.views.onLongClick + +class BookmarkAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookmarkBinding { + return ItemBookmarkBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookmarkBinding, + item: Bookmark, + payloads: MutableList + ) { + binding.tvChapterName.text = item.chapterName + binding.tvBookText.text = item.bookText + binding.tvContent.text = item.content + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) { + binding.root.setOnClickListener { + getItem(holder.layoutPosition)?.let { bookmark -> + callback.onClick(bookmark) + } + } + binding.root.onLongClick { + getItem(holder.layoutPosition)?.let { bookmark -> + callback.onLongClick(bookmark) + } + } + + } + + interface Callback { + fun onClick(bookmark: Bookmark) + fun onLongClick(bookmark: Bookmark) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/BookmarkDialog.kt b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkDialog.kt new file mode 100644 index 000000000..6602986ce --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkDialog.kt @@ -0,0 +1,87 @@ +package io.legado.app.ui.book.toc + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.appDb +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.DialogBookmarkBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BookmarkDialog : BaseDialogFragment() { + + companion object { + + fun start(fragmentManager: FragmentManager, bookmark: Bookmark) { + BookmarkDialog().apply { + arguments = Bundle().apply { + putParcelable("bookmark", bookmark) + } + }.show(fragmentManager, "bookMarkDialog") + } + + } + + private val binding by viewBinding(DialogBookmarkBinding::bind) + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_bookmark, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + val bookmark = arguments?.getParcelable("bookmark") + bookmark ?: let { + dismiss() + return + } + binding.run { + tvChapterName.text = bookmark.chapterName + editBookText.setText(bookmark.bookText) + editContent.setText(bookmark.content) + tvCancel.setOnClickListener { + dismiss() + } + tvOk.setOnClickListener { + bookmark.bookText = editBookText.text?.toString() ?: "" + bookmark.content = editContent.text?.toString() ?: "" + launch { + withContext(IO) { + appDb.bookmarkDao.insert(bookmark) + } + dismiss() + } + } + tvFooterLeft.setOnClickListener { + launch { + withContext(IO) { + appDb.bookmarkDao.delete(bookmark) + } + dismiss() + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt new file mode 100644 index 000000000..b9c140225 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt @@ -0,0 +1,73 @@ +package io.legado.app.ui.book.toc + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.data.appDb +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.FragmentBookmarkBinding +import io.legado.app.lib.theme.ATH +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + + +class BookmarkFragment : VMBaseFragment(R.layout.fragment_bookmark), + BookmarkAdapter.Callback, + TocViewModel.BookmarkCallBack { + override val viewModel by activityViewModels() + private val binding by viewBinding(FragmentBookmarkBinding::bind) + private lateinit var adapter: BookmarkAdapter + private var bookmarkFlowJob: Job? = null + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.bookMarkCallBack = this + initRecyclerView() + viewModel.bookData.observe(this) { + upBookmark(null) + } + } + + private fun initRecyclerView() { + ATH.applyEdgeEffectColor(binding.recyclerView) + adapter = BookmarkAdapter(requireContext(), this) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + } + + override fun upBookmark(searchKey: String?) { + val book = viewModel.bookData.value ?: return + bookmarkFlowJob?.cancel() + bookmarkFlowJob = launch { + when { + searchKey.isNullOrBlank() -> appDb.bookmarkDao.flowByBook(book.name, book.author) + else -> appDb.bookmarkDao.flowSearch(book.name, book.author, searchKey) + }.collect { + adapter.setItems(it) + } + } + } + + + override fun onClick(bookmark: Bookmark) { + activity?.run { + setResult(Activity.RESULT_OK, Intent().apply { + putExtra("index", bookmark.chapterIndex) + putExtra("chapterPos", bookmark.chapterPos) + }) + finish() + } + } + + override fun onLongClick(bookmark: Bookmark) { + BookmarkDialog.start(childFragmentManager, bookmark) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt new file mode 100644 index 000000000..15eebaeb6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt @@ -0,0 +1,75 @@ +package io.legado.app.ui.book.toc + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ItemChapterListBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.visible + + +class ChapterListAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + val cacheFileNames = hashSetOf() + + override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding { + return ItemChapterListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChapterListBinding, + item: BookChapter, + payloads: MutableList + ) { + binding.run { + val isDur = callback.durChapterIndex() == item.index + val cached = callback.isLocalBook || cacheFileNames.contains(item.getFileName()) + if (payloads.isEmpty()) { + if (isDur) { + tvChapterName.setTextColor(context.accentColor) + } else { + tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText)) + } + tvChapterName.text = item.title + if (!item.tag.isNullOrEmpty()) { + tvTag.text = item.tag + tvTag.visible() + } + upHasCache(binding, isDur, cached) + } else { + upHasCache(binding, isDur, cached) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callback.openChapter(it) + } + } + } + + private fun upHasCache(binding: ItemChapterListBinding, isDur: Boolean, cached: Boolean) = + binding.apply { + tvChapterName.paint.isFakeBoldText = cached + ivChecked.setImageResource(R.drawable.ic_outline_cloud_24) + ivChecked.visible(!cached) + if (isDur) { + ivChecked.setImageResource(R.drawable.ic_check) + ivChecked.visible() + } + } + + interface Callback { + val isLocalBook: Boolean + fun openChapter(bookChapter: BookChapter) + fun durChapterIndex(): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt new file mode 100644 index 000000000..fdf1a2183 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt @@ -0,0 +1,139 @@ +package io.legado.app.ui.book.toc + +import android.annotation.SuppressLint +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.FragmentChapterListBinding +import io.legado.app.help.BookHelp +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.observeEvent +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.min + +class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapter_list), + ChapterListAdapter.Callback, + TocViewModel.ChapterListCallBack { + override val viewModel by activityViewModels() + private val binding by viewBinding(FragmentChapterListBinding::bind) + lateinit var adapter: ChapterListAdapter + private var durChapterIndex = 0 + private lateinit var mLayoutManager: UpLinearLayoutManager + private var tocFlowJob: Job? = null + private var scrollToDurChapter = false + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + viewModel.chapterCallBack = this@ChapterListFragment + val bbg = bottomBackground + val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + llChapterBaseInfo.setBackgroundColor(bbg) + tvCurrentChapterInfo.setTextColor(btc) + ivChapterTop.setColorFilter(btc) + ivChapterBottom.setColorFilter(btc) + initRecyclerView() + initView() + viewModel.bookData.observe(this@ChapterListFragment) { + initBook(it) + } + } + + private fun initRecyclerView() { + adapter = ChapterListAdapter(requireContext(), this) + mLayoutManager = UpLinearLayoutManager(requireContext()) + binding.recyclerView.layoutManager = mLayoutManager + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + } + + private fun initView() = binding.run { + ivChapterTop.setOnClickListener { mLayoutManager.scrollToPositionWithOffset(0, 0) } + ivChapterBottom.setOnClickListener { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + tvCurrentChapterInfo.setOnClickListener { + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + } + } + + @SuppressLint("SetTextI18n") + private fun initBook(book: Book) { + launch { + upChapterList(null) + durChapterIndex = book.durChapterIndex + binding.tvCurrentChapterInfo.text = + "${book.durChapterTitle}(${book.durChapterIndex + 1}/${book.totalChapterNum})" + initCacheFileNames(book) + } + } + + private fun initCacheFileNames(book: Book) { + launch(IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Main) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SAVE_CONTENT) { chapter -> + viewModel.bookData.value?.bookUrl?.let { bookUrl -> + if (chapter.bookUrl == bookUrl) { + adapter.cacheFileNames.add(chapter.getFileName()) + adapter.notifyItemChanged(chapter.index, true) + } + } + } + } + + override fun upChapterList(searchKey: String?) { + tocFlowJob?.cancel() + tocFlowJob = launch { + when { + searchKey.isNullOrBlank() -> appDb.bookChapterDao.flowByBook(viewModel.bookUrl) + else -> appDb.bookChapterDao.flowSearch(viewModel.bookUrl, searchKey) + }.collect { + adapter.setItems(it) + if (searchKey.isNullOrBlank() && !scrollToDurChapter) { + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + scrollToDurChapter = true + } + } + } + } + + override val isLocalBook: Boolean + get() = viewModel.bookData.value?.isLocalBook() == true + + override fun durChapterIndex(): Int { + return min(durChapterIndex, adapter.itemCount) + } + + override fun openChapter(bookChapter: BookChapter) { + activity?.run { + setResult(RESULT_OK, Intent().putExtra("index", bookChapter.index)) + finish() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt similarity index 54% rename from app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListActivity.kt rename to app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt index 58f19a6b4..3bfd9b500 100644 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt @@ -1,60 +1,66 @@ -package io.legado.app.ui.book.chapterlist +@file:Suppress("DEPRECATION") +package io.legado.app.ui.book.toc + +import android.content.Intent import android.os.Bundle import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.widget.SearchView import androidx.core.view.isGone import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter +import com.google.android.material.tabs.TabLayout import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivityChapterListBinding import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.utils.getViewModel import io.legado.app.utils.gone +import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.activity_chapter_list.* -import kotlinx.android.synthetic.main.view_tab_layout.* -class ChapterListActivity : VMBaseActivity(R.layout.activity_chapter_list) { - override val viewModel: ChapterListViewModel - get() = getViewModel(ChapterListViewModel::class.java) +class TocActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivityChapterListBinding::inflate) + override val viewModel by viewModels() + private lateinit var tabLayout: TabLayout private var searchView: SearchView? = null override fun onActivityCreated(savedInstanceState: Bundle?) { - tab_layout.isTabIndicatorFullWidth = false - tab_layout.setSelectedTabIndicatorColor(accentColor) + tabLayout = binding.titleBar.findViewById(R.id.tab_layout) + tabLayout.isTabIndicatorFullWidth = false + tabLayout.setSelectedTabIndicatorColor(accentColor) + binding.viewPager.adapter = TabFragmentPageAdapter() + tabLayout.setupWithViewPager(binding.viewPager) intent.getStringExtra("bookUrl")?.let { - viewModel.initBook(it) { - view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager) - tab_layout.setupWithViewPager(view_pager) - } + viewModel.initBook(it) } } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.search_view, menu) + menuInflater.inflate(R.menu.book_toc, menu) val search = menu.findItem(R.id.menu_search) searchView = search.actionView as SearchView ATH.setTint(searchView!!, primaryTextColor) searchView?.maxWidth = resources.displayMetrics.widthPixels searchView?.onActionViewCollapsed() searchView?.setOnCloseListener { - tab_layout.visible() + tabLayout.visible() false } - searchView?.setOnSearchClickListener { tab_layout.gone() } + searchView?.setOnSearchClickListener { tabLayout.gone() } searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { return false } override fun onQueryTextChange(newText: String): Boolean { - if (tab_layout.selectedTabPosition == 1) { + if (tabLayout.selectedTabPosition == 1) { viewModel.startBookmarkSearch(newText) } else { viewModel.startChapterListSearch(newText) @@ -65,8 +71,31 @@ class ChapterListActivity : VMBaseActivity(R.layout.activi return super.onCompatCreateOptionsMenu(menu) } - private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : - FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_reverse_toc -> viewModel.reverseToc { + setResult(RESULT_OK, Intent().apply { + putExtra("index", it.durChapterIndex) + putExtra("chapterPos", it.durChapterPos) + }) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (tabLayout.isGone) { + searchView?.onActionViewCollapsed() + tabLayout.visible() + } else { + super.onBackPressed() + } + } + + @Suppress("DEPRECATION") + private inner class TabFragmentPageAdapter : + FragmentPagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + override fun getItem(position: Int): Fragment { return when (position) { 1 -> BookmarkFragment() @@ -78,7 +107,7 @@ class ChapterListActivity : VMBaseActivity(R.layout.activi return 2 } - override fun getPageTitle(position: Int): CharSequence? { + override fun getPageTitle(position: Int): CharSequence { return when (position) { 1 -> getString(R.string.bookmark) else -> getString(R.string.chapter_list) @@ -87,12 +116,4 @@ class ChapterListActivity : VMBaseActivity(R.layout.activi } - override fun onBackPressed() { - if (tab_layout.isGone) { - searchView?.onActionViewCollapsed() - tab_layout.visible() - } else { - super.onBackPressed() - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt new file mode 100644 index 000000000..e727de0b2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt @@ -0,0 +1,26 @@ +package io.legado.app.ui.book.toc + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +class TocActivityResult : ActivityResultContract?>() { + + override fun createIntent(context: Context, input: String?): Intent { + return Intent(context, TocActivity::class.java) + .putExtra("bookUrl", input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Pair? { + if (resultCode == RESULT_OK) { + intent?.let { + return Pair( + it.getIntExtra("index", 0), + it.getIntExtra("chapterPos", 0) + ) + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt new file mode 100644 index 000000000..1fdb66fd6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt @@ -0,0 +1,56 @@ +package io.legado.app.ui.book.toc + + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book + +class TocViewModel(application: Application) : BaseViewModel(application) { + var bookUrl: String = "" + var bookData = MutableLiveData() + var chapterCallBack: ChapterListCallBack? = null + var bookMarkCallBack: BookmarkCallBack? = null + + fun initBook(bookUrl: String) { + this.bookUrl = bookUrl + execute { + appDb.bookDao.getBook(bookUrl)?.let { + bookData.postValue(it) + } + } + } + + fun reverseToc(success: (book: Book) -> Unit) { + execute { + bookData.value?.apply { + setReverseToc(!getReverseToc()) + val toc = appDb.bookChapterDao.getChapterList(bookUrl) + val newToc = toc.reversed() + newToc.forEachIndexed { index, bookChapter -> + bookChapter.index = index + } + appDb.bookChapterDao.insert(*newToc.toTypedArray()) + } + }.onSuccess { + it?.let(success) + } + } + + fun startChapterListSearch(newText: String?) { + chapterCallBack?.upChapterList(newText) + } + + fun startBookmarkSearch(newText: String?) { + bookMarkCallBack?.upBookmark(newText) + } + + interface ChapterListCallBack { + fun upChapterList(searchKey: String?) + } + + interface BookmarkCallBack { + fun upBookmark(searchKey: String?) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt index 56d753eaf..18c274561 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -1,27 +1,94 @@ package io.legado.app.ui.config -import android.content.Intent import android.content.SharedPreferences +import android.net.Uri import android.os.Bundle import android.text.InputType +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem import android.view.View +import androidx.documentfile.provider.DocumentFile import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig +import io.legado.app.help.LocalConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.storage.Backup +import io.legado.app.help.storage.BookWebDav +import io.legado.app.help.storage.ImportOldData import io.legado.app.help.storage.Restore import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.utils.applyTint -import io.legado.app.utils.getPrefString +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.* +import kotlinx.coroutines.Dispatchers +import splitties.init.appCtx class BackupConfigFragment : BasePreferenceFragment(), - SharedPreferences.OnSharedPreferenceChangeListener, - FileChooserDialog.CallBack { + SharedPreferences.OnSharedPreferenceChangeListener { + + private val selectBackupPath = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + AppConfig.backupPath = uri.toString() + } else { + AppConfig.backupPath = uri.path + } + } + private val backupDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + AppConfig.backupPath = uri.toString() + Coroutine.async { + Backup.backup(appCtx, uri.toString()) + }.onSuccess { + appCtx.toastOnUi(R.string.backup_success) + }.onError { + appCtx.toastOnUi(R.string.backup_fail) + } + } else { + uri.path?.let { path -> + AppConfig.backupPath = path + Coroutine.async { + Backup.backup(appCtx, path) + }.onSuccess { + appCtx.toastOnUi(R.string.backup_success) + }.onError { + appCtx.toastOnUi(R.string.backup_fail) + } + } + } + } + private val restoreDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + AppConfig.backupPath = uri.toString() + Coroutine.async { + Restore.restore(appCtx, uri.toString()) + } + } else { + uri.path?.let { path -> + AppConfig.backupPath = path + Coroutine.async { + Restore.restore(appCtx, path) + } + } + } + } + private val restoreOld = registerForActivityResult(FilePicker()) { uri -> + uri?.let { + ImportOldData.importUri(appCtx, uri) + } + } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_backup) @@ -48,13 +115,35 @@ class BackupConfigFragment : BasePreferenceFragment(), upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) findPreference("web_dav_restore") - ?.onLongClick = { BackupRestoreUi.restoreByFolder(this) } + ?.onLongClick = { restoreDir.launch(null) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) ATH.applyEdgeEffectColor(listView) + setHasOptionsMenu(true) + if (!LocalConfig.backupHelpVersionIsLast) { + showHelp() + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.backup_restore, menu) + menu.applyTint(requireContext()) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_help -> showHelp() + } + return super.onOptionsItemSelected(item) + } + + private fun showHelp() { + val text = String(requireContext().assets.open("help/webDavHelp.md").readBytes()) + TextDialog.show(childFragmentManager, text, TextDialog.MD) } override fun onDestroy() { @@ -108,11 +197,11 @@ class BackupConfigFragment : BasePreferenceFragment(), override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { - PreferKey.backupPath -> BackupRestoreUi.selectBackupFolder(this) + PreferKey.backupPath -> selectBackupPath.launch(null) PreferKey.restoreIgnore -> restoreIgnore() - "web_dav_backup" -> BackupRestoreUi.backup(this) - "web_dav_restore" -> BackupRestoreUi.restore(this) - "import_old" -> BackupRestoreUi.importOldData(this) + "web_dav_backup" -> backup() + "web_dav_restore" -> restore() + "import_old" -> restoreOld.launch(null) } return super.onPreferenceTreeClick(preference) } @@ -126,19 +215,91 @@ class BackupConfigFragment : BasePreferenceFragment(), multiChoiceItems(Restore.ignoreTitle, checkedItems) { _, which, isChecked -> Restore.ignoreConfig[Restore.ignoreKeys[which]] = isChecked } - }.show() - .applyTint() - .setOnDismissListener { + onDismiss { Restore.saveIgnoreConfig() } + }.show() } - override fun onFilePicked(requestCode: Int, currentPath: String) { - BackupRestoreUi.onFilePicked(requestCode, currentPath) + + fun backup() { + val backupPath = AppConfig.backupPath + if (backupPath.isNullOrEmpty()) { + backupDir.launch(null) + } else { + if (backupPath.isContentScheme()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(requireContext(), uri) + if (doc?.canWrite() == true) { + Coroutine.async { + Backup.backup(requireContext(), backupPath) + }.onSuccess { + toastOnUi(R.string.backup_success) + }.onError { + toastOnUi(R.string.backup_fail) + } + } else { + backupDir.launch(null) + } + } else { + backupUsePermission(backupPath) + } + } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - BackupRestoreUi.onActivityResult(requestCode, resultCode, data) + private fun backupUsePermission(path: String) { + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = path + Backup.backup(requireContext(), path) + }.onSuccess { + toastOnUi(R.string.backup_success) + }.onError { + toastOnUi(R.string.backup_fail) + } + } + .request() } + + fun restore() { + Coroutine.async(context = Dispatchers.Main) { + BookWebDav.showRestoreDialog(requireContext()) + }.onError { + longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。") + val backupPath = getPrefString(PreferKey.backupPath) + if (backupPath?.isNotEmpty() == true) { + if (backupPath.isContentScheme()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(requireContext(), uri) + if (doc?.canWrite() == true) { + Restore.restore(requireContext(), backupPath) + } else { + restoreDir.launch(null) + } + } else { + restoreUsePermission(backupPath) + } + } else { + restoreDir.launch(null) + } + } + } + + private fun restoreUsePermission(path: String) { + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = path + Restore.restoreDatabase(path) + Restore.restoreConfig(path) + } + } + .request() + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt deleted file mode 100644 index e62030482..000000000 --- a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt +++ /dev/null @@ -1,198 +0,0 @@ -package io.legado.app.ui.config - -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.Fragment -import io.legado.app.App -import io.legado.app.R -import io.legado.app.constant.PreferKey -import io.legado.app.help.AppConfig -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.help.storage.Backup -import io.legado.app.help.storage.ImportOldData -import io.legado.app.help.storage.Restore -import io.legado.app.help.storage.WebDavHelp -import io.legado.app.ui.filechooser.FilePicker -import io.legado.app.utils.getPrefString -import io.legado.app.utils.isContentPath -import io.legado.app.utils.longToast -import io.legado.app.utils.toast -import kotlinx.coroutines.Dispatchers.Main -import org.jetbrains.anko.toast -import java.io.File - -object BackupRestoreUi { - private const val selectFolderRequestCode = 21 - private const val backupSelectRequestCode = 22 - private const val restoreSelectRequestCode = 33 - private const val oldDataRequestCode = 11 - - fun backup(fragment: Fragment) { - val backupPath = AppConfig.backupPath - if (backupPath.isNullOrEmpty()) { - selectBackupFolder(fragment, backupSelectRequestCode) - } else { - if (backupPath.isContentPath()) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) - if (doc?.canWrite() == true) { - Coroutine.async { - Backup.backup(fragment.requireContext(), backupPath) - }.onSuccess { - fragment.toast(R.string.backup_success) - } - } else { - selectBackupFolder(fragment, backupSelectRequestCode) - } - } else { - backupUsePermission(fragment, backupPath) - } - } - } - - private fun backupUsePermission( - fragment: Fragment, - path: String - ) { - PermissionsCompat.Builder(fragment) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - Coroutine.async { - AppConfig.backupPath = path - Backup.backup(fragment.requireContext(), path) - }.onSuccess { - fragment.toast(R.string.backup_success) - } - } - .request() - } - - fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) { - FilePicker.selectFolder(fragment, requestCode) - } - - fun restore(fragment: Fragment) { - Coroutine.async(context = Main) { - WebDavHelp.showRestoreDialog(fragment.requireContext()) - }.onError { - fragment.longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。") - val backupPath = fragment.getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - if (backupPath.isContentPath()) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) - if (doc?.canWrite() == true) { - Restore.restore(fragment.requireContext(), backupPath) - } else { - selectBackupFolder(fragment, restoreSelectRequestCode) - } - } else { - restoreUsePermission(fragment, backupPath) - } - } else { - selectBackupFolder(fragment, restoreSelectRequestCode) - } - } - } - - fun restoreByFolder(fragment: Fragment) { - selectBackupFolder(fragment, restoreSelectRequestCode) - } - - private fun restoreUsePermission(fragment: Fragment, path: String) { - PermissionsCompat.Builder(fragment) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - Coroutine.async { - AppConfig.backupPath = path - Restore.restoreDatabase(path) - Restore.restoreConfig(path) - } - } - .request() - } - - fun importOldData(fragment: Fragment) { - FilePicker.selectFolder(fragment, oldDataRequestCode) - } - - fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - backupSelectRequestCode -> { - AppConfig.backupPath = currentPath - Coroutine.async { - Backup.backup(App.INSTANCE, currentPath) - }.onSuccess { - App.INSTANCE.toast(R.string.backup_success) - } - } - restoreSelectRequestCode -> { - AppConfig.backupPath = currentPath - Coroutine.async { - Restore.restore(App.INSTANCE, currentPath) - } - } - selectFolderRequestCode -> { - AppConfig.backupPath = currentPath - } - oldDataRequestCode -> { - ImportOldData.import(App.INSTANCE, File(currentPath)) - } - } - } - - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - backupSelectRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - App.INSTANCE.contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - AppConfig.backupPath = uri.toString() - Coroutine.async { - Backup.backup(App.INSTANCE, uri.toString()) - }.onSuccess { - App.INSTANCE.toast(R.string.backup_success) - } - } - } - restoreSelectRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - App.INSTANCE.contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - AppConfig.backupPath = uri.toString() - Coroutine.async { - Restore.restore(App.INSTANCE, uri.toString()) - } - } - } - selectFolderRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - App.INSTANCE.contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - AppConfig.backupPath = uri.toString() - } - } - oldDataRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - ImportOldData.importUri(uri) - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt index 6d7bfcfe7..b022a2fe2 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt @@ -1,16 +1,19 @@ package io.legado.app.ui.config import android.os.Bundle +import androidx.activity.viewModels import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.EventBus -import io.legado.app.utils.getViewModel +import io.legado.app.databinding.ActivityConfigBinding + import io.legado.app.utils.observeEvent -import kotlinx.android.synthetic.main.activity_config.* +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class ConfigActivity : VMBaseActivity() { -class ConfigActivity : VMBaseActivity(R.layout.activity_config) { - override val viewModel: ConfigViewModel - get() = getViewModel(ConfigViewModel::class.java) + override val binding by viewBinding(ActivityConfigBinding::inflate) + override val viewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { intent.getIntExtra("configType", -1).let { @@ -19,7 +22,7 @@ class ConfigActivity : VMBaseActivity(R.layout.activity_config) when (viewModel.configType) { ConfigViewModel.TYPE_CONFIG -> { - title_bar.title = getString(R.string.other_setting) + binding.titleBar.title = getString(R.string.other_setting) val fTag = "otherConfigFragment" var configFragment = supportFragmentManager.findFragmentByTag(fTag) if (configFragment == null) configFragment = OtherConfigFragment() @@ -28,7 +31,7 @@ class ConfigActivity : VMBaseActivity(R.layout.activity_config) .commit() } ConfigViewModel.TYPE_THEME_CONFIG -> { - title_bar.title = getString(R.string.theme_setting) + binding.titleBar.title = getString(R.string.theme_setting) val fTag = "themeConfigFragment" var configFragment = supportFragmentManager.findFragmentByTag(fTag) if (configFragment == null) configFragment = ThemeConfigFragment() @@ -37,7 +40,7 @@ class ConfigActivity : VMBaseActivity(R.layout.activity_config) .commit() } ConfigViewModel.TYPE_WEB_DAV_CONFIG -> { - title_bar.title = getString(R.string.backup_restore) + binding.titleBar.title = getString(R.string.backup_restore) val fTag = "backupConfigFragment" var configFragment = supportFragmentManager.findFragmentByTag(fTag) if (configFragment == null) configFragment = BackupConfigFragment() diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index 1230bb2b5..93ec2b45b 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -1,46 +1,38 @@ package io.legado.app.ui.config -import android.app.Activity.RESULT_OK +import android.annotation.SuppressLint import android.content.ComponentName import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager -import android.net.Uri import android.os.Bundle +import android.os.Process import android.view.View -import androidx.documentfile.provider.DocumentFile import androidx.preference.ListPreference import androidx.preference.Preference -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.receiver.SharedReceiverActivity import io.legado.app.service.WebService -import io.legado.app.ui.widget.image.CoverImageView +import io.legado.app.ui.main.MainActivity import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.utils.* -import java.io.File +import splitties.init.appCtx class OtherConfigFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { - private val requestCodeCover = 231 - - private val packageManager = App.INSTANCE.packageManager + private val packageManager = appCtx.packageManager private val componentName = ComponentName( - App.INSTANCE, + appCtx, SharedReceiverActivity::class.java.name ) private val webPort get() = getPrefInt(PreferKey.webPort, 1122) @@ -48,9 +40,10 @@ class OtherConfigFragment : BasePreferenceFragment(), override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) addPreferencesFromResource(R.xml.pref_config_other) + upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent) + upPreferenceSummary(PreferKey.preDownloadNum, AppConfig.preDownloadNum.toString()) upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString()) upPreferenceSummary(PreferKey.webPort, webPort.toString()) - upPreferenceSummary(PreferKey.defaultCover, getPrefString(PreferKey.defaultCover)) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -66,6 +59,15 @@ class OtherConfigFragment : BasePreferenceFragment(), override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { + PreferKey.userAgent -> showUserAgentDialog() + PreferKey.preDownloadNum -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.pre_download)) + .setMaxValue(9999) + .setMinValue(1) + .setValue(AppConfig.preDownloadNum) + .show { + AppConfig.preDownloadNum = it + } PreferKey.threadCount -> NumberPickerDialog(requireContext()) .setTitle(getString(R.string.threads_num_title)) .setMaxValue(999) @@ -83,23 +85,16 @@ class OtherConfigFragment : BasePreferenceFragment(), putPrefInt(PreferKey.webPort, it) } PreferKey.cleanCache -> clearCache() - PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { - selectDefaultCover() - } else { - selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> - if (i == 0) { - removePref(PreferKey.defaultCover) - } else { - selectDefaultCover() - } - } - } + } return super.onPreferenceTreeClick(preference) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { + PreferKey.preDownloadNum -> { + upPreferenceSummary(key, AppConfig.preDownloadNum.toString()) + } PreferKey.threadCount -> { upPreferenceSummary(key, AppConfig.threadCount.toString()) postEvent(PreferKey.threadCount, "") @@ -115,16 +110,16 @@ class OtherConfigFragment : BasePreferenceFragment(), PreferKey.processText -> sharedPreferences?.let { setProcessTextEnable(it.getBoolean(key, true)) } - PreferKey.showRss -> postEvent(EventBus.SHOW_RSS, "") - PreferKey.defaultCover -> upPreferenceSummary( - key, - getPrefString(PreferKey.defaultCover) - ) - PreferKey.replaceEnableDefault -> AppConfig.replaceEnableDefault = - App.INSTANCE.getPrefBoolean(PreferKey.replaceEnableDefault, true) - PreferKey.language -> { - LanguageUtils.setConfigurationOld(App.INSTANCE) - postEvent(EventBus.RECREATE, "") + PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true) + PreferKey.language -> listView.postDelayed({ + LanguageUtils.setConfiguration(appCtx) + val intent = Intent(appCtx, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + appCtx.startActivity(intent) + Process.killProcess(Process.myPid()) + }, 1000) + PreferKey.userAgent -> listView.post { + upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent) } } } @@ -132,6 +127,8 @@ class OtherConfigFragment : BasePreferenceFragment(), private fun upPreferenceSummary(preferenceKey: String, value: String?) { val preference = findPreference(preferenceKey) ?: return when (preferenceKey) { + PreferKey.preDownloadNum -> preference.summary = + getString(R.string.pre_download_s, value) PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value) PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value) else -> if (preference is ListPreference) { @@ -144,23 +141,36 @@ class OtherConfigFragment : BasePreferenceFragment(), } } + @SuppressLint("InflateParams") + private fun showUserAgentDialog() { + alert("UserAgent") { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + alertBinding.editView.setText(AppConfig.userAgent) + customView { alertBinding.root } + okButton { + val userAgent = alertBinding.editView.text?.toString() + if (userAgent.isNullOrBlank()) { + removePref(PreferKey.userAgent) + } else { + putPrefString(PreferKey.userAgent, userAgent) + } + } + noButton() + }.show() + } + private fun clearCache() { - requireContext().alert(titleResource = R.string.clear_cache, - messageResource = R.string.sure_del) { + requireContext().alert( + titleResource = R.string.clear_cache, + messageResource = R.string.sure_del + ) { okButton { BookHelp.clearCache() FileUtils.deleteFile(requireActivity().cacheDir.absolutePath) - toast(R.string.clear_cache_success) + toastOnUi(R.string.clear_cache_success) } noButton() - }.show().applyTint() - } - - private fun selectDefaultCover() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "image/*" - startActivityForResult(intent, requestCodeCover) + }.show() } private fun isProcessTextEnabled(): Boolean { @@ -181,52 +191,4 @@ class OtherConfigFragment : BasePreferenceFragment(), } } - private fun setCoverFromUri(uri: Uri) { - if (uri.toString().isContentPath()) { - val doc = DocumentFile.fromSingleUri(requireContext(), uri) - doc?.name?.let { - var file = requireContext().externalFilesDir - file = FileUtils.createFileIfNotExist(file, "covers", it) - kotlin.runCatching { - DocumentUtils.readBytes(requireContext(), doc.uri) - }.getOrNull()?.let { byteArray -> - file.writeBytes(byteArray) - putPrefString(PreferKey.defaultCover, file.absolutePath) - CoverImageView.upDefaultCover() - } ?: toast("获取文件出错") - } - } else { - PermissionsCompat.Builder(this) - .addPermissions( - Permissions.READ_EXTERNAL_STORAGE, - Permissions.WRITE_EXTERNAL_STORAGE - ) - .rationale(R.string.bg_image_per) - .onGranted { - RealPathUtil.getPath(requireContext(), uri)?.let { path -> - val imgFile = File(path) - if (imgFile.exists()) { - var file = requireContext().externalFilesDir - file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) - file.writeBytes(imgFile.readBytes()) - putPrefString(PreferKey.defaultCover, file.absolutePath) - CoverImageView.upDefaultCover() - } - } - } - .request() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - requestCodeCover -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - setCoverFromUri(uri) - } - } - } - } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt index 4b76ebc3e..da4d47c6f 100644 --- a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt @@ -2,50 +2,71 @@ package io.legado.app.ui.config import android.annotation.SuppressLint import android.content.SharedPreferences +import android.net.Uri import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.documentfile.provider.DocumentFile import androidx.preference.Preference -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BasePreferenceFragment +import io.legado.app.constant.AppConst import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig import io.legado.app.help.LauncherIconHelp import io.legado.app.help.ThemeConfig import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.theme.ATH +import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.prefs.ColorPreference -import io.legado.app.ui.widget.prefs.IconListPreference -import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* +import java.io.File @Suppress("SameParameterValue") class ThemeConfigFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { + private val requestCodeCover = 111 + private val requestCodeCoverDark = 112 + private val requestCodeBgLight = 121 + private val requestCodeBgDark = 122 + private val selectImage = registerForActivityResult(ActivityResultContractUtils.SelectImage()) { + val uri = it?.second ?: return@registerForActivityResult + when (it.first) { + requestCodeCover -> setCoverFromUri(PreferKey.defaultCover, uri) + requestCodeCoverDark -> setCoverFromUri(PreferKey.defaultCoverDark, uri) + requestCodeBgLight -> setBgFromUri(uri, PreferKey.bgImage) { + upTheme(false) + } + requestCodeBgDark -> setBgFromUri(uri, PreferKey.bgImageN) { + upTheme(true) + } + } + } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_theme) if (Build.VERSION.SDK_INT < 26) { - findPreference(PreferKey.launcherIcon)?.let { - preferenceScreen.removePreference(it) - } + preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon) } + upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage)) + upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN)) upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString()) + upPreferenceSummary(PreferKey.defaultCover, getPrefString(PreferKey.defaultCover)) + upPreferenceSummary(PreferKey.defaultCoverDark, getPrefString(PreferKey.defaultCoverDark)) findPreference(PreferKey.cBackground)?.let { it.onSaveColor = { color -> if (!ColorUtils.isColorLight(color)) { - toast(R.string.day_background_too_dark) + toastOnUi(R.string.day_background_too_dark) true } else { false @@ -55,49 +76,13 @@ class ThemeConfigFragment : BasePreferenceFragment(), findPreference(PreferKey.cNBackground)?.let { it.onSaveColor = { color -> if (ColorUtils.isColorLight(color)) { - toast(R.string.night_background_too_light) + toastOnUi(R.string.night_background_too_light) true } else { false } } } - findPreference(PreferKey.cAccent)?.let { - it.onSaveColor = { color -> - val background = - getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100)) - val textColor = getCompatColor(R.color.primaryText) - when { - ColorUtils.getColorDifference(color, background) <= 60 -> { - toast(R.string.accent_background_diff) - true - } - ColorUtils.getColorDifference(color, textColor) <= 60 -> { - toast(R.string.accent_text_diff) - true - } - else -> false - } - } - } - findPreference(PreferKey.cNAccent)?.let { - it.onSaveColor = { color -> - val background = - getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900)) - val textColor = getCompatColor(R.color.primaryText) - when { - ColorUtils.getColorDifference(color, background) <= 60 -> { - toast(R.string.accent_background_diff) - true - } - ColorUtils.getColorDifference(color, textColor) <= 60 -> { - toast(R.string.accent_text_diff) - true - } - else -> false - } - } - } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -106,14 +91,14 @@ class ThemeConfigFragment : BasePreferenceFragment(), setHasOptionsMenu(true) } - override fun onResume() { - super.onResume() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) } - override fun onPause() { + override fun onDestroy() { + super.onDestroy() preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - super.onPause() } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -125,7 +110,7 @@ class ThemeConfigFragment : BasePreferenceFragment(), when (item.itemId) { R.id.menu_theme_mode -> { AppConfig.isNightTheme = !AppConfig.isNightTheme - App.INSTANCE.applyDayNight() + ThemeConfig.applyDayNight(requireContext()) } } return super.onOptionsItemSelected(item) @@ -136,6 +121,7 @@ class ThemeConfigFragment : BasePreferenceFragment(), when (key) { PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key)) PreferKey.transparentStatusBar -> recreateActivities() + PreferKey.immNavigationBar -> recreateActivities() PreferKey.cPrimary, PreferKey.cAccent, PreferKey.cBackground, @@ -148,6 +134,9 @@ class ThemeConfigFragment : BasePreferenceFragment(), PreferKey.cNBBackground -> { upTheme(true) } + PreferKey.defaultCover, PreferKey.defaultCoverDark -> { + upPreferenceSummary(key, getPrefString(key)) + } } } @@ -161,8 +150,7 @@ class ThemeConfigFragment : BasePreferenceFragment(), .setMinValue(0) .setValue(AppConfig.elevation) .setCustomButton((R.string.btn_default_s)) { - AppConfig.elevation = - App.INSTANCE.resources.getDimension(R.dimen.design_appbar_elevation).toInt() + AppConfig.elevation = AppConst.sysElevation recreateActivities() } .show { @@ -171,21 +159,64 @@ class ThemeConfigFragment : BasePreferenceFragment(), } "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) + PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { + selectImage.launch(requestCodeBgLight) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.bgImage) + upTheme(false) + } else { + selectImage.launch(requestCodeBgLight) + } + } + } + PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) { + selectImage.launch(requestCodeBgDark) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.bgImageN) + upTheme(true) + } else { + selectImage.launch(requestCodeBgDark) + } + } + } + PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { + selectImage.launch(requestCodeCover) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.defaultCover) + } else { + selectImage.launch(requestCodeCover) + } + } + } + PreferKey.defaultCoverDark -> + if (getPrefString(PreferKey.defaultCoverDark).isNullOrEmpty()) { + selectImage.launch(requestCodeCoverDark) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.defaultCoverDark) + } else { + selectImage.launch(requestCodeCoverDark) + } + } + } } return super.onPreferenceTreeClick(preference) } @SuppressLint("InflateParams") private fun saveThemeAlert(key: String) { - alert("主题名称") { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - } - } + alert(R.string.theme_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + customView { alertBinding.root } okButton { - editText?.text?.toString()?.let { themeName -> + alertBinding.editView.text?.toString()?.let { themeName -> when (key) { "saveDayTheme" -> { ThemeConfig.saveDayTheme(requireContext(), themeName) @@ -196,14 +227,14 @@ class ThemeConfigFragment : BasePreferenceFragment(), } } } - noButton { } - }.show().applyTint() + noButton() + }.show() } private fun upTheme(isNightTheme: Boolean) { if (AppConfig.isNightTheme == isNightTheme) { listView.post { - App.INSTANCE.applyTheme() + ThemeConfig.applyTheme(requireContext()) recreateActivities() } } @@ -218,6 +249,92 @@ class ThemeConfigFragment : BasePreferenceFragment(), when (preferenceKey) { PreferKey.barElevation -> preference.summary = getString(R.string.bar_elevation_s, value) + PreferKey.bgImage, + PreferKey.bgImageN, + PreferKey.defaultCover, + PreferKey.defaultCoverDark -> preference.summary = if (value.isNullOrBlank()) { + getString(R.string.select_image) + } else { + value + } + else -> preference.summary = value } } + + private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(requireContext(), uri) + doc?.name?.let { + var file = requireContext().externalFiles + file = FileUtils.createFileIfNotExist(file, preferenceKey, it) + kotlin.runCatching { + DocumentUtils.readBytes(requireContext(), doc.uri) + }.getOrNull()?.let { byteArray -> + file.writeBytes(byteArray) + putPrefString(preferenceKey, file.absolutePath) + upPreferenceSummary(preferenceKey, file.absolutePath) + success() + } ?: toastOnUi("获取文件出错") + } + } else { + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = requireContext().externalFiles + file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name) + file.writeBytes(imgFile.readBytes()) + putPrefString(preferenceKey, file.absolutePath) + upPreferenceSummary(preferenceKey, file.absolutePath) + success() + } + } + } + .request() + } + } + + private fun setCoverFromUri(preferenceKey: String, uri: Uri) { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(requireContext(), uri) + doc?.name?.let { + var file = requireContext().externalFiles + file = FileUtils.createFileIfNotExist(file, "covers", it) + kotlin.runCatching { + DocumentUtils.readBytes(requireContext(), doc.uri) + }.getOrNull()?.let { byteArray -> + file.writeBytes(byteArray) + putPrefString(preferenceKey, file.absolutePath) + CoverImageView.upDefaultCover() + } ?: toastOnUi("获取文件出错") + } + } else { + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = requireContext().externalFiles + file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name) + file.writeBytes(imgFile.readBytes()) + putPrefString(PreferKey.defaultCover, file.absolutePath) + CoverImageView.upDefaultCover() + } + } + } + .request() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt b/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt index a1b93262d..be118d3de 100644 --- a/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt +++ b/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.config import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -11,29 +10,23 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemThemeConfigBinding import io.legado.app.help.ThemeConfig import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.GSON -import io.legado.app.utils.applyTint -import io.legado.app.utils.getClipText -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_theme_config.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.share +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding class ThemeListDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { - + private val binding by viewBinding(DialogRecyclerViewBinding::bind) private lateinit var adapter: Adapter override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -46,24 +39,24 @@ class ThemeListDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.setTitle(R.string.theme_list) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.theme_list) initView() initMenu() initData() } - private fun initView() { + private fun initView() = binding.run { adapter = Adapter() - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter } - private fun initMenu() { - tool_bar.setOnMenuItemClickListener(this) - tool_bar.inflateMenu(R.menu.theme_list) - tool_bar.menu.applyTint(requireContext()) + private fun initMenu() = binding.run { + toolBar.setOnMenuItemClickListener(this@ThemeListDialog) + toolBar.inflateMenu(R.menu.theme_list) + toolBar.menu.applyTint(requireContext()) } fun initData() { @@ -74,8 +67,11 @@ class ThemeListDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { when (item?.itemId) { R.id.menu_import -> { requireContext().getClipText()?.let { - ThemeConfig.addConfig(it) - initData() + if (ThemeConfig.addConfig(it)) { + initData() + } else { + toastOnUi("格式不对,添加失败") + } } } } @@ -85,12 +81,11 @@ class ThemeListDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { fun delete(index: Int) { alert(R.string.delete, R.string.sure_del) { okButton { - ThemeConfig.configList.removeAt(index) - ThemeConfig.save() + ThemeConfig.delConfig(index) initData() } noButton() - }.show().applyTint() + }.show() } fun share(index: Int) { @@ -98,23 +93,33 @@ class ThemeListDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { requireContext().share(json, "主题分享") } - inner class Adapter : SimpleRecyclerAdapter(requireContext(), R.layout.item_theme_config) { + inner class Adapter : + RecyclerAdapter(requireContext()) { + + override fun getViewBinding(parent: ViewGroup): ItemThemeConfigBinding { + return ItemThemeConfigBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: ThemeConfig.Config, payloads: MutableList) { - holder.itemView.apply { - tv_name.text = item.themeName + override fun convert( + holder: ItemViewHolder, + binding: ItemThemeConfigBinding, + item: ThemeConfig.Config, + payloads: MutableList + ) { + binding.apply { + tvName.text = item.themeName } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemThemeConfigBinding) { + binding.apply { + root.setOnClickListener { ThemeConfig.applyConfig(context, ThemeConfig.configList[holder.layoutPosition]) } - iv_share.onClick { + ivShare.setOnClickListener { share(holder.layoutPosition) } - iv_delete.onClick { + ivDelete.setOnClickListener { delete(holder.layoutPosition) } } diff --git a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt new file mode 100644 index 000000000..78c093ba4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt @@ -0,0 +1,74 @@ +package io.legado.app.ui.dict + +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogDictBinding +import io.legado.app.utils.invisible +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 词典 + */ +class DictDialog : BaseDialogFragment() { + + companion object { + + fun dict(manager: FragmentManager, word: String) { + DictDialog().apply { + val bundle = Bundle() + bundle.putString("word", word) + arguments = bundle + }.show(manager, word) + } + + } + + private val viewModel by viewModels() + private val binding by viewBinding(DialogDictBinding::bind) + + override fun onStart() { + super.onStart() + dialog?.window + ?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + return inflater.inflate(R.layout.dialog_dict, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.tvDict.movementMethod = LinkMovementMethod() + val word = arguments?.getString("word") + if (word.isNullOrEmpty()) { + toastOnUi(R.string.cannot_empty) + dismiss() + return + } + viewModel.dictHtmlData.observe(viewLifecycleOwner) { + binding.rotateLoading.invisible() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.tvDict.text = Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY) + } else { + binding.tvDict.text = Html.fromHtml(it) + } + } + viewModel.dict(word) + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt new file mode 100644 index 000000000..27f2724a2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt @@ -0,0 +1,31 @@ +package io.legado.app.ui.dict + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.help.http.get +import io.legado.app.help.http.newCallStrResponse +import io.legado.app.help.http.okHttpClient +import io.legado.app.utils.toastOnUi +import org.jsoup.Jsoup + +class DictViewModel(application: Application) : BaseViewModel(application) { + + var dictHtmlData: MutableLiveData = MutableLiveData() + + fun dict(word: String) { + execute { + val body = okHttpClient.newCallStrResponse { + get("https://apii.dict.cn/mini.php", mapOf(Pair("q", word))) + }.body + val jsoup = Jsoup.parse(body!!) + jsoup.body() + }.onSuccess { + dictHtmlData.postValue(it.html()) + }.onError { + context.toastOnUi(it.localizedMessage) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/document/FilePicker.kt b/app/src/main/java/io/legado/app/ui/document/FilePicker.kt new file mode 100644 index 000000000..dfc0d5789 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/document/FilePicker.kt @@ -0,0 +1,43 @@ +package io.legado.app.ui.document + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +@Suppress("unused") +class FilePicker : ActivityResultContract() { + + companion object { + const val DIRECTORY = 0 + const val FILE = 1 + } + + override fun createIntent(context: Context, input: FilePickerParam?): Intent { + val intent = Intent(context, FilePickerActivity::class.java) + input?.let { + intent.putExtra("mode", it.mode) + intent.putExtra("title", it.title) + intent.putExtra("allowExtensions", it.allowExtensions) + intent.putExtra("otherActions", it.otherActions) + } + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + if (resultCode == RESULT_OK) { + return intent?.data + } + return null + } + +} + +@Suppress("ArrayInDataClass") +data class FilePickerParam( + var mode: Int = 0, + var title: String? = null, + var allowExtensions: Array = arrayOf(), + var otherActions: Array? = null, +) diff --git a/app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt b/app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt new file mode 100644 index 000000000..08bc51275 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt @@ -0,0 +1,147 @@ +package io.legado.app.ui.document + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.webkit.MimeTypeMap +import androidx.activity.result.contract.ActivityResultContracts +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.constant.Theme +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.io.File + +class FilePickerActivity : + BaseActivity( + theme = Theme.Transparent + ), FilePickerDialog.CallBack { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + + private val selectDocTree = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { + it ?: let { + finish() + return@registerForActivityResult + } + if (it.isContentScheme()) { + contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + onResult(Intent().setData(it)) + } + + private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) { + it ?: return@registerForActivityResult + onResult(Intent().setData(it)) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + val mode = intent.getIntExtra("mode", 0) + val allowExtensions = intent.getStringArrayExtra("allowExtensions") + val selectList = if (mode == FilePicker.DIRECTORY) { + arrayListOf(getString(R.string.sys_folder_picker)) + } else { + arrayListOf(getString(R.string.sys_file_picker)) + } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + selectList.add(getString(R.string.app_folder_picker)) + } + intent.getStringArrayListExtra("otherActions")?.let { + selectList.addAll(it) + } + val title = intent.getStringExtra("title") ?: let { + if (mode == FilePicker.DIRECTORY) { + return@let getString(R.string.select_folder) + } else { + return@let getString(R.string.select_file) + } + } + alert(title) { + items(selectList) { _, index -> + when (index) { + 0 -> if (mode == FilePicker.DIRECTORY) { + selectDocTree.launch(null) + } else { + selectDoc.launch(typesOfExtensions(allowExtensions)) + } + 1 -> if (mode == FilePicker.DIRECTORY) { + checkPermissions { + FilePickerDialog.show( + supportFragmentManager, + mode = FilePicker.DIRECTORY + ) + } + } else { + checkPermissions { + FilePickerDialog.show( + supportFragmentManager, + mode = FilePicker.FILE, + allowExtensions = allowExtensions + ) + } + } + else -> { + val path = selectList[index] + val uri = if (path.isContentScheme()) { + Uri.fromFile(File(path)) + } else { + Uri.parse(path) + } + onResult(Intent().setData(uri)) + } + } + } + onCancelled { + finish() + } + }.show() + } + + private fun checkPermissions(success: (() -> Unit)? = null) { + PermissionsCompat.Builder(this) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + success?.invoke() + } + .request() + } + + private fun typesOfExtensions(allowExtensions: Array?): Array { + val types = hashSetOf() + if (allowExtensions.isNullOrEmpty()) { + types.add("*/*") + } else { + allowExtensions.forEach { + when (it) { + "*" -> types.add("*/*") + "txt", "xml" -> types.add("text/*") + else -> { + val mime = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(it) + ?: "application/octet-stream" + types.add(mime) + } + } + } + } + return types.toTypedArray() + } + + override fun onResult(data: Intent) { + if (data.data != null) { + setResult(RESULT_OK, data) + } + finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt b/app/src/main/java/io/legado/app/ui/document/FilePickerDialog.kt similarity index 69% rename from app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt rename to app/src/main/java/io/legado/app/ui/document/FilePickerDialog.kt index d7e6b05d6..af573efb0 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt +++ b/app/src/main/java/io/legado/app/ui/document/FilePickerDialog.kt @@ -1,7 +1,9 @@ -package io.legado.app.ui.filechooser +package io.legado.app.ui.document +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -12,27 +14,28 @@ import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R +import io.legado.app.databinding.DialogFileChooserBinding import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.filechooser.adapter.FileAdapter -import io.legado.app.ui.filechooser.adapter.PathAdapter +import io.legado.app.ui.document.FilePicker.Companion.DIRECTORY +import io.legado.app.ui.document.FilePicker.Companion.FILE +import io.legado.app.ui.document.adapter.FileAdapter +import io.legado.app.ui.document.adapter.PathAdapter import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_file_chooser.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.io.File -class FileChooserDialog : DialogFragment(), +class FilePickerDialog : DialogFragment(), Toolbar.OnMenuItemClickListener, FileAdapter.CallBack, PathAdapter.CallBack { companion object { const val tag = "FileChooserDialog" - const val DIRECTORY = 0 - const val FILE = 1 fun show( manager: FragmentManager, - requestCode: Int, mode: Int = FILE, title: String? = null, initPath: String? = null, @@ -42,10 +45,9 @@ class FileChooserDialog : DialogFragment(), allowExtensions: Array? = null, menus: Array? = null ) { - FileChooserDialog().apply { + FilePickerDialog().apply { val bundle = Bundle() bundle.putInt("mode", mode) - bundle.putInt("requestCode", requestCode) bundle.putString("title", title) bundle.putBoolean("isShowHomeDir", isShowHomeDir) bundle.putBoolean("isShowUpDir", isShowUpDir) @@ -58,14 +60,13 @@ class FileChooserDialog : DialogFragment(), } } + private val binding by viewBinding(DialogFileChooserBinding::bind) override var allowExtensions: Array? = null override val isSelectDir: Boolean get() = mode == DIRECTORY override var isShowHomeDir: Boolean = false override var isShowUpDir: Boolean = true override var isShowHideDir: Boolean = false - - private var requestCode: Int = 0 var title: String? = null private var initPath = FileUtils.getSdCardPath() private var mode: Int = FILE @@ -75,8 +76,7 @@ class FileChooserDialog : DialogFragment(), override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt()) } @@ -90,10 +90,9 @@ class FileChooserDialog : DialogFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - tool_bar.setBackgroundColor(primaryColor) + binding.toolBar.setBackgroundColor(primaryColor) view.setBackgroundResource(R.color.background_card) arguments?.let { - requestCode = it.getInt("requestCode") mode = it.getInt("mode", FILE) title = it.getString("title") isShowHomeDir = it.getBoolean("isShowHomeDir") @@ -105,7 +104,7 @@ class FileChooserDialog : DialogFragment(), allowExtensions = it.getStringArray("allowExtensions") menus = it.getStringArray("menus") } - tool_bar.title = title ?: let { + binding.toolBar.title = title ?: let { if (isSelectDir) { getString(R.string.folder_chooser) } else { @@ -118,43 +117,37 @@ class FileChooserDialog : DialogFragment(), } private fun initMenu() { - tool_bar.inflateMenu(R.menu.file_chooser) + binding.toolBar.inflateMenu(R.menu.file_chooser) if (isSelectDir) { - tool_bar.menu.findItem(R.id.menu_ok).isVisible = true + binding.toolBar.menu.findItem(R.id.menu_ok).isVisible = true } menus?.let { it.forEach { menuTitle -> - tool_bar.menu.add(menuTitle) + binding.toolBar.menu.add(menuTitle) } } - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) } private fun initContentView() { fileAdapter = FileAdapter(requireContext(), this) pathAdapter = PathAdapter(requireContext(), this) - rv_file.addItemDecoration(VerticalDivider(requireContext())) - rv_file.layoutManager = LinearLayoutManager(activity) - rv_file.adapter = fileAdapter + binding.rvFile.addItemDecoration(VerticalDivider(requireContext())) + binding.rvFile.layoutManager = LinearLayoutManager(activity) + binding.rvFile.adapter = fileAdapter - rv_path.layoutManager = LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false) - rv_path.adapter = pathAdapter + binding.rvPath.layoutManager = LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false) + binding.rvPath.adapter = pathAdapter } override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_ok -> fileAdapter.currentPath?.let { - (parentFragment as? CallBack)?.onFilePicked(requestCode, it) - (activity as? CallBack)?.onFilePicked(requestCode, it) - dismiss() - } - else -> item?.title?.let { - (parentFragment as? CallBack)?.onMenuClick(it.toString()) - (activity as? CallBack)?.onMenuClick(it.toString()) - dismiss() + setData(it) + dismissAllowingStateLoss() } } return true @@ -167,15 +160,14 @@ class FileChooserDialog : DialogFragment(), } else { fileItem?.path?.let { path -> if (mode == DIRECTORY) { - toast("这是文件夹选择,不能选择文件,点击右上角的确定选择文件夹") - } else if (allowExtensions == null || + toastOnUi("这是文件夹选择,不能选择文件,点击右上角的确定选择文件夹") + } else if (allowExtensions.isNullOrEmpty() || allowExtensions?.contains(FileUtils.getExtension(path)) == true ) { - (parentFragment as? CallBack)?.onFilePicked(requestCode, path) - (activity as? CallBack)?.onFilePicked(requestCode, path) - dismiss() + setData(path) + dismissAllowingStateLoss() } else { - toast("不能打开此文件") + toastOnUi("不能打开此文件") } } } @@ -200,15 +192,25 @@ class FileChooserDialog : DialogFragment(), adapterCount-- } if (adapterCount < 1) { - tv_empty.visible() - tv_empty.setText(R.string.empty) + binding.tvEmpty.visible() + binding.tvEmpty.setText(R.string.empty) } else { - tv_empty.gone() + binding.tvEmpty.gone() } } + private fun setData(path: String) { + val data = Intent().setData(Uri.fromFile(File(path))) + (parentFragment as? CallBack)?.onResult(data) + (activity as? CallBack)?.onResult(data) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + activity?.finish() + } + interface CallBack { - fun onFilePicked(requestCode: Int, currentPath: String) - fun onMenuClick(menu: String) {} + fun onResult(data: Intent) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt b/app/src/main/java/io/legado/app/ui/document/adapter/FileAdapter.kt similarity index 71% rename from app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt rename to app/src/main/java/io/legado/app/ui/document/adapter/FileAdapter.kt index 4458b692e..324e627b8 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/document/adapter/FileAdapter.kt @@ -1,32 +1,32 @@ -package io.legado.app.ui.filechooser.adapter +package io.legado.app.ui.document.adapter import android.content.Context -import io.legado.app.R +import android.view.ViewGroup import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemFileFilepickerBinding import io.legado.app.help.AppConfig import io.legado.app.lib.theme.getPrimaryDisabledTextColor import io.legado.app.lib.theme.getPrimaryTextColor -import io.legado.app.ui.filechooser.entity.FileItem -import io.legado.app.ui.filechooser.utils.ConvertUtils -import io.legado.app.ui.filechooser.utils.FilePickerIcon +import io.legado.app.ui.document.entity.FileItem +import io.legado.app.ui.document.utils.ConvertUtils +import io.legado.app.ui.document.utils.FilePickerIcon import io.legado.app.utils.FileUtils -import kotlinx.android.synthetic.main.item_path_filepicker.view.* -import org.jetbrains.anko.sdk27.listeners.onClick + import java.io.File import java.util.* class FileAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_file_filepicker) { + RecyclerAdapter(context) { private var rootPath: String? = null var currentPath: String? = null private set - private val homeIcon = ConvertUtils.toDrawable(FilePickerIcon.getHOME()) - private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUPDIR()) - private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFOLDER()) - private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFILE()) + private val homeIcon = ConvertUtils.toDrawable(FilePickerIcon.getHome()) + private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir()) + private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder()) + private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile()) private val primaryTextColor = context.getPrimaryTextColor(!AppConfig.isNightTheme) private val disabledTextColor = context.getPrimaryDisabledTextColor(!AppConfig.isNightTheme) @@ -49,7 +49,7 @@ class FileAdapter(context: Context, val callBack: CallBack) : fileRoot.path = rootPath ?: path data.add(fileRoot) } - if (callBack.isShowUpDir && path != "/") { + if (callBack.isShowUpDir && path != PathAdapter.sdCardDirectory) { //添加“返回上一级目录” val fileParent = FileItem() fileParent.isDirectory = true @@ -86,30 +86,39 @@ class FileAdapter(context: Context, val callBack: CallBack) : } - override fun convert(holder: ItemViewHolder, item: FileItem, payloads: MutableList) { - holder.itemView.apply { - image_view.setImageDrawable(item.icon) - text_view.text = item.name + override fun getViewBinding(parent: ViewGroup): ItemFileFilepickerBinding { + return ItemFileFilepickerBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFileFilepickerBinding, + item: FileItem, + payloads: MutableList + ) { + binding.apply { + imageView.setImageDrawable(item.icon) + textView.text = item.name if (item.isDirectory) { - text_view.setTextColor(primaryTextColor) + textView.setTextColor(primaryTextColor) } else { if (callBack.isSelectDir) { - text_view.setTextColor(disabledTextColor) + textView.setTextColor(disabledTextColor) } else { callBack.allowExtensions?.let { - if (it.contains(FileUtils.getExtension(item.path))) { - text_view.setTextColor(primaryTextColor) + if (it.isEmpty() || it.contains(FileUtils.getExtension(item.path))) { + textView.setTextColor(primaryTextColor) } else { - text_view.setTextColor(disabledTextColor) + textView.setTextColor(disabledTextColor) } - } ?: text_view.setTextColor(primaryTextColor) + } ?: textView.setTextColor(primaryTextColor) } } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemFileFilepickerBinding) { + holder.itemView.setOnClickListener { callBack.onFileClick(holder.layoutPosition) } } @@ -129,10 +138,12 @@ class FileAdapter(context: Context, val callBack: CallBack) : * 是否显示返回主目录 */ var isShowHomeDir: Boolean + /** * 是否显示返回上一级 */ var isShowUpDir: Boolean + /** * 是否显示隐藏的目录(以“.”开头) */ diff --git a/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt b/app/src/main/java/io/legado/app/ui/document/adapter/PathAdapter.kt similarity index 54% rename from app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt rename to app/src/main/java/io/legado/app/ui/document/adapter/PathAdapter.kt index 86ccf67c0..65311c13c 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/document/adapter/PathAdapter.kt @@ -1,23 +1,21 @@ -package io.legado.app.ui.filechooser.adapter +package io.legado.app.ui.document.adapter import android.content.Context import android.os.Environment -import io.legado.app.R +import android.view.ViewGroup import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.ui.filechooser.utils.ConvertUtils -import io.legado.app.ui.filechooser.utils.FilePickerIcon -import kotlinx.android.synthetic.main.item_path_filepicker.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemPathFilepickerBinding +import io.legado.app.ui.document.utils.ConvertUtils +import io.legado.app.ui.document.utils.FilePickerIcon + import java.util.* class PathAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_path_filepicker) { + RecyclerAdapter(context) { private val paths = LinkedList() - @Suppress("DEPRECATION") - private val sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath - private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW()) + private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getArrow()) fun getPath(position: Int): String { val tmp = StringBuilder("$sdCardDirectory/") @@ -37,7 +35,7 @@ class PathAdapter(context: Context, val callBack: CallBack) : paths.clear() if (path1 != "/" && path1 != "") { val subDirs = path1.substring(path1.indexOf("/") + 1) - .split("/".toRegex()) + .split("/") .dropLastWhile { it.isEmpty() } .toTypedArray() Collections.addAll(paths, *subDirs) @@ -46,15 +44,24 @@ class PathAdapter(context: Context, val callBack: CallBack) : setItems(paths) } - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - holder.itemView.apply { - text_view.text = item - image_view.setImageDrawable(arrowIcon) + override fun getViewBinding(parent: ViewGroup): ItemPathFilepickerBinding { + return ItemPathFilepickerBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemPathFilepickerBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + textView.text = item + imageView.setImageDrawable(arrowIcon) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemPathFilepickerBinding) { + holder.itemView.setOnClickListener { callBack.onPathClick(holder.layoutPosition) } } @@ -65,5 +72,8 @@ class PathAdapter(context: Context, val callBack: CallBack) : companion object { private const val ROOT_HINT = "SD" + + @Suppress("DEPRECATION") + val sdCardDirectory: String = Environment.getExternalStorageDirectory().absolutePath } } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/entity/FileItem.kt b/app/src/main/java/io/legado/app/ui/document/entity/FileItem.kt similarity index 87% rename from app/src/main/java/io/legado/app/ui/filechooser/entity/FileItem.kt rename to app/src/main/java/io/legado/app/ui/document/entity/FileItem.kt index bf4a00229..88cd6b6b2 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/entity/FileItem.kt +++ b/app/src/main/java/io/legado/app/ui/document/entity/FileItem.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.filechooser.entity +package io.legado.app.ui.document.entity import android.graphics.drawable.Drawable diff --git a/app/src/main/java/io/legado/app/ui/filechooser/entity/JavaBean.kt b/app/src/main/java/io/legado/app/ui/document/entity/JavaBean.kt similarity index 92% rename from app/src/main/java/io/legado/app/ui/filechooser/entity/JavaBean.kt rename to app/src/main/java/io/legado/app/ui/document/entity/JavaBean.kt index 33760dce6..eb81f1c71 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/entity/JavaBean.kt +++ b/app/src/main/java/io/legado/app/ui/document/entity/JavaBean.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.filechooser.entity +package io.legado.app.ui.document.entity import java.io.Serializable import java.lang.reflect.Field @@ -34,15 +34,13 @@ open class JavaBean : Serializable { val fields = list.toTypedArray() for (field in fields) { val fieldName = field.name - try { + kotlin.runCatching { val obj = field.get(this) sb.append(fieldName) sb.append("=") sb.append(obj) sb.append("\n") - } catch (ignored: IllegalAccessException) { } - } return sb.toString() } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt b/app/src/main/java/io/legado/app/ui/document/utils/ConvertUtils.kt similarity index 89% rename from app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt rename to app/src/main/java/io/legado/app/ui/document/utils/ConvertUtils.kt index 17d2f15e7..8844b3f3e 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt +++ b/app/src/main/java/io/legado/app/ui/document/utils/ConvertUtils.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.filechooser.utils +package io.legado.app.ui.document.utils import android.content.res.Resources import android.graphics.Bitmap @@ -6,7 +6,6 @@ import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import java.io.BufferedReader -import java.io.IOException import java.io.InputStream import java.io.InputStreamReader import java.text.DecimalFormat @@ -17,17 +16,16 @@ import java.text.DecimalFormat * @author 李玉江[QQ:1023694760] * @since 2014-4-18 */ +@Suppress("MemberVisibilityCanBePrivate") object ConvertUtils { const val GB: Long = 1073741824 const val MB: Long = 1048576 const val KB: Long = 1024 fun toInt(obj: Any): Int { - return try { + return kotlin.runCatching { Integer.parseInt(obj.toString()) - } catch (e: NumberFormatException) { - -1 - } + }.getOrDefault(-1) } fun toInt(bytes: ByteArray): Int { @@ -41,11 +39,9 @@ object ConvertUtils { } fun toFloat(obj: Any): Float { - return try { + return kotlin.runCatching { java.lang.Float.parseFloat(obj.toString()) - } catch (e: NumberFormatException) { - -1f - } + }.getOrDefault(-1f) } fun toString(objects: Array, tag: String): String { @@ -61,7 +57,7 @@ object ConvertUtils { fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? { var bitmap: Bitmap? = null if (bytes.isNotEmpty()) { - try { + kotlin.runCatching { val options = BitmapFactory.Options() // 设置让解码器以最佳方式解码 options.inPreferredConfig = null @@ -71,7 +67,6 @@ object ConvertUtils { } bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) bitmap!!.density = 96// 96 dpi - } catch (e: Exception) { } } return bitmap @@ -100,7 +95,7 @@ object ConvertUtils { @JvmOverloads fun toString(`is`: InputStream, charset: String = "utf-8"): String { val sb = StringBuilder() - try { + kotlin.runCatching { val reader = BufferedReader(InputStreamReader(`is`, charset)) while (true) { val line = reader.readLine() @@ -112,9 +107,7 @@ object ConvertUtils { } reader.close() `is`.close() - } catch (e: IOException) { } - return sb.toString() } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/utils/FilePickerIcon.java b/app/src/main/java/io/legado/app/ui/document/utils/FilePickerIcon.java similarity index 99% rename from app/src/main/java/io/legado/app/ui/filechooser/utils/FilePickerIcon.java rename to app/src/main/java/io/legado/app/ui/document/utils/FilePickerIcon.java index 6732626e4..0f3447f6a 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/utils/FilePickerIcon.java +++ b/app/src/main/java/io/legado/app/ui/document/utils/FilePickerIcon.java @@ -1,4 +1,4 @@ -package io.legado.app.ui.filechooser.utils; +package io.legado.app.ui.document.utils; /** * Generated by https://github.com/gzu-liyujiang/Image2ByteVar @@ -8,23 +8,23 @@ package io.legado.app.ui.filechooser.utils; */ public class FilePickerIcon { - public static byte[] getFILE() { + public static byte[] getFile() { return FILE; } - public static byte[] getFOLDER() { + public static byte[] getFolder() { return FOLDER; } - public static byte[] getHOME() { + public static byte[] getHome() { return HOME; } - public static byte[] getUPDIR() { + public static byte[] getUpDir() { return UPDIR; } - public static byte[] getARROW() { + public static byte[] getArrow() { return ARROW; } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/FilePicker.kt b/app/src/main/java/io/legado/app/ui/filechooser/FilePicker.kt deleted file mode 100644 index 07ed5c6f1..000000000 --- a/app/src/main/java/io/legado/app/ui/filechooser/FilePicker.kt +++ /dev/null @@ -1,224 +0,0 @@ -package io.legado.app.ui.filechooser - -import android.content.Intent -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import io.legado.app.R -import io.legado.app.base.BaseActivity -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.lib.dialogs.alert -import io.legado.app.utils.applyTint -import io.legado.app.utils.toast -import org.jetbrains.anko.toast - -@Suppress("unused") -object FilePicker { - - fun selectFolder( - activity: AppCompatActivity, - requestCode: Int, - title: String = activity.getString(R.string.select_folder), - otherActions: List? = null, - otherFun: ((action: String) -> Unit)? = null - ) { - activity.alert(title = title) { - val selectList = - activity.resources.getStringArray(R.array.select_folder).toMutableList() - otherActions?.let { - selectList.addAll(otherActions) - } - items(selectList) { _, index -> - when (index) { - 0 -> { - try { - val intent = createSelectDirIntent() - activity.startActivityForResult(intent, requestCode) - } catch (e: java.lang.Exception) { - e.printStackTrace() - activity.toast(e.localizedMessage ?: "ERROR") - } - } - 1 -> checkPermissions(activity) { - FileChooserDialog.show( - activity.supportFragmentManager, - requestCode, - mode = FileChooserDialog.DIRECTORY - ) - } - else -> otherFun?.invoke(selectList[index]) - } - } - }.show().applyTint() - } - - fun selectFolder( - fragment: Fragment, - requestCode: Int, - title: String = fragment.getString(R.string.select_folder), - otherActions: List? = null, - otherFun: ((action: String) -> Unit)? = null - ) { - fragment.requireContext() - .alert(title = title) { - val selectList = - fragment.resources.getStringArray(R.array.select_folder).toMutableList() - otherActions?.let { - selectList.addAll(otherActions) - } - items(selectList) { _, index -> - when (index) { - 0 -> { - try { - val intent = createSelectDirIntent() - fragment.startActivityForResult(intent, requestCode) - } catch (e: java.lang.Exception) { - e.printStackTrace() - fragment.toast(e.localizedMessage ?: "ERROR") - } - } - 1 -> checkPermissions(fragment) { - FileChooserDialog.show( - fragment.childFragmentManager, - requestCode, - mode = FileChooserDialog.DIRECTORY - ) - } - else -> otherFun?.invoke(selectList[index]) - } - } - }.show().applyTint() - } - - fun selectFile( - activity: BaseActivity, - requestCode: Int, - title: String = activity.getString(R.string.select_file), - allowExtensions: Array, - otherActions: List? = null, - otherFun: ((action: String) -> Unit)? = null - ) { - activity.alert(title = title) { - val selectList = - activity.resources.getStringArray(R.array.select_folder).toMutableList() - otherActions?.let { - selectList.addAll(otherActions) - } - items(selectList) { _, index -> - when (index) { - 0 -> { - try { - val intent = createSelectFileIntent() - intent.putExtra( - Intent.EXTRA_MIME_TYPES, - typesOfExtensions(allowExtensions) - ) - activity.startActivityForResult(intent, requestCode) - } catch (e: java.lang.Exception) { - e.printStackTrace() - activity.toast(e.localizedMessage ?: "ERROR") - } - } - 1 -> checkPermissions(activity) { - FileChooserDialog.show( - activity.supportFragmentManager, - requestCode, - mode = FileChooserDialog.FILE, - allowExtensions = allowExtensions - ) - } - else -> otherFun?.invoke(selectList[index]) - } - } - }.show().applyTint() - } - - fun selectFile( - fragment: Fragment, - requestCode: Int, - title: String = fragment.getString(R.string.select_file), - allowExtensions: Array, - otherActions: List? = null, - otherFun: ((action: String) -> Unit)? = null - ) { - fragment.requireContext() - .alert(title = title) { - val selectList = - fragment.resources.getStringArray(R.array.select_folder).toMutableList() - otherActions?.let { - selectList.addAll(otherActions) - } - items(selectList) { _, index -> - when (index) { - 0 -> { - try { - val intent = createSelectFileIntent() - intent.putExtra( - Intent.EXTRA_MIME_TYPES, - typesOfExtensions(allowExtensions) - ) - fragment.startActivityForResult(intent, requestCode) - } catch (e: java.lang.Exception) { - e.printStackTrace() - fragment.toast(e.localizedMessage ?: "ERROR") - } - } - 1 -> checkPermissions(fragment) { - FileChooserDialog.show( - fragment.childFragmentManager, - requestCode, - mode = FileChooserDialog.FILE, - allowExtensions = allowExtensions - ) - } - else -> otherFun?.invoke(selectList[index]) - } - } - }.show().applyTint() - } - - private fun createSelectFileIntent(): Intent { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.type = "*/*" - return intent - } - - private fun createSelectDirIntent(): Intent { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - return intent - } - - private fun checkPermissions(fragment: Fragment, success: (() -> Unit)? = null) { - PermissionsCompat.Builder(fragment) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - success?.invoke() - } - .request() - } - - private fun checkPermissions(activity: AppCompatActivity, success: (() -> Unit)? = null) { - PermissionsCompat.Builder(activity) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - success?.invoke() - } - .request() - } - - private fun typesOfExtensions(allowExtensions: Array): Array { - val types = hashSetOf() - allowExtensions.forEach { - when (it) { - "txt", "xml" -> types.add("text/*") - else -> types.add("application/$it") - } - } - return types.toTypedArray() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLogin.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt similarity index 73% rename from app/src/main/java/io/legado/app/ui/login/SourceLogin.kt rename to app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt index 767545d38..af9522ec7 100644 --- a/app/src/main/java/io/legado/app/ui/login/SourceLogin.kt +++ b/app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt @@ -10,32 +10,39 @@ import android.webkit.WebView import android.webkit.WebViewClient import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.databinding.ActivitySourceLoginBinding import io.legado.app.help.http.CookieStore import io.legado.app.utils.snackbar -import kotlinx.android.synthetic.main.activity_source_login.* +import io.legado.app.utils.viewbindingdelegate.viewBinding -class SourceLogin : BaseActivity(R.layout.activity_source_login) { +class SourceLoginActivity : BaseActivity() { + override val binding by viewBinding(ActivitySourceLoginBinding::inflate) var sourceUrl: String? = null var loginUrl: String? = null + var userAgent: String? = null var checking = false override fun onActivityCreated(savedInstanceState: Bundle?) { sourceUrl = intent.getStringExtra("sourceUrl") loginUrl = intent.getStringExtra("loginUrl") + userAgent = intent.getStringExtra("userAgent") title = getString(R.string.login_source, sourceUrl) initWebView() } @SuppressLint("SetJavaScriptEnabled") private fun initWebView() { - val settings = web_view.settings + val settings = binding.webView.settings settings.setSupportZoom(true) settings.builtInZoomControls = true settings.javaScriptEnabled = true + userAgent?.let { + settings.userAgentString = it + } val cookieManager = CookieManager.getInstance() - web_view.webViewClient = object : WebViewClient() { + binding.webView.webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { val cookie = cookieManager.getCookie(url) sourceUrl?.let { @@ -55,7 +62,9 @@ class SourceLogin : BaseActivity(R.layout.activity_source_login) { super.onPageFinished(view, url) } } - web_view.loadUrl(loginUrl) + loginUrl?.let { + binding.webView.loadUrl(it) + } } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -68,8 +77,10 @@ class SourceLogin : BaseActivity(R.layout.activity_source_login) { R.id.menu_success -> { if (!checking) { checking = true - title_bar.snackbar(R.string.check_host_cookie) - web_view.loadUrl(sourceUrl) + binding.titleBar.snackbar(R.string.check_host_cookie) + loginUrl?.let { + binding.webView.loadUrl(it) + } } } } @@ -78,6 +89,6 @@ class SourceLogin : BaseActivity(R.layout.activity_source_login) { override fun onDestroy() { super.onDestroy() - web_view.destroy() + binding.webView.destroy() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 934bb56f8..e2baa8fbd 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -1,59 +1,70 @@ +@file:Suppress("DEPRECATION") + package io.legado.app.ui.main import android.os.Bundle import android.view.KeyEvent import android.view.MenuItem +import android.view.ViewGroup +import androidx.activity.viewModels import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView -import io.legado.app.App import io.legado.app.BuildConfig import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst.appInfo import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ActivityMainBinding import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp +import io.legado.app.help.LocalConfig import io.legado.app.help.storage.Backup import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.elevation import io.legado.app.service.BaseReadAloudService -import io.legado.app.ui.main.bookshelf.BookshelfFragment +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.ui.main.bookshelf.style1.BookshelfFragment1 +import io.legado.app.ui.main.bookshelf.style2.BookshelfFragment2 import io.legado.app.ui.main.explore.ExploreFragment import io.legado.app.ui.main.my.MyFragment import io.legado.app.ui.main.rss.RssFragment import io.legado.app.ui.widget.dialog.TextDialog -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_main.* -import org.jetbrains.anko.toast +import io.legado.app.utils.observeEvent +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + -class MainActivity : VMBaseActivity(R.layout.activity_main), +class MainActivity : VMBaseActivity(), BottomNavigationView.OnNavigationItemSelectedListener, - BottomNavigationView.OnNavigationItemReselectedListener, - ViewPager.OnPageChangeListener by ViewPager.SimpleOnPageChangeListener() { - override val viewModel: MainViewModel - get() = getViewModel(MainViewModel::class.java) + BottomNavigationView.OnNavigationItemReselectedListener { + + override val binding by viewBinding(ActivityMainBinding::inflate) + override val viewModel by viewModels() private var exitTime: Long = 0 private var bookshelfReselected: Long = 0 + private var exploreReselected: Long = 0 private var pagePosition = 0 - private val fragmentId = arrayOf(0, 1, 2, 3) - private val fragmentMap = mapOf( - Pair(fragmentId[0], BookshelfFragment()), - Pair(fragmentId[1], ExploreFragment()), - Pair(fragmentId[2], RssFragment()), - Pair(fragmentId[3], MyFragment()) - ) + private val fragmentMap = hashMapOf() + private var bottomMenuCount = 2 + private val realPositions = arrayOf(0, 1, 2, 3) override fun onActivityCreated(savedInstanceState: Bundle?) { - ATH.applyEdgeEffectColor(view_pager_main) - ATH.applyBottomNavigationColor(bottom_navigation_view) - view_pager_main.offscreenPageLimit = 3 - view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) - view_pager_main.addOnPageChangeListener(this) - bottom_navigation_view.setOnNavigationItemSelectedListener(this) - bottom_navigation_view.setOnNavigationItemReselectedListener(this) - bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS + upBottomMenu() + binding.apply { + ATH.applyEdgeEffectColor(viewPagerMain) + ATH.applyBottomNavigationColor(bottomNavigationView) + viewPagerMain.offscreenPageLimit = 3 + viewPagerMain.adapter = TabFragmentPageAdapter(supportFragmentManager) + viewPagerMain.addOnPageChangeListener(PageChangeCallback()) + bottomNavigationView.elevation = + if (AppConfig.elevation < 0) elevation else AppConfig.elevation.toFloat() + bottomNavigationView.setOnNavigationItemSelectedListener(this@MainActivity) + bottomNavigationView.setOnNavigationItemReselectedListener(this@MainActivity) + } } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -61,21 +72,25 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), upVersion() //自动更新书籍 if (AppConfig.autoRefreshBook) { - view_pager_main.postDelayed({ + binding.viewPagerMain.postDelayed({ viewModel.upAllBookToc() }, 1000) } - view_pager_main.postDelayed({ + binding.viewPagerMain.postDelayed({ viewModel.postLoad() }, 3000) } - override fun onNavigationItemSelected(item: MenuItem): Boolean { + override fun onNavigationItemSelected(item: MenuItem): Boolean = binding.run { when (item.itemId) { - R.id.menu_bookshelf -> view_pager_main.setCurrentItem(0, false) - R.id.menu_find_book -> view_pager_main.setCurrentItem(1, false) - R.id.menu_rss -> view_pager_main.setCurrentItem(2, false) - R.id.menu_my_config -> view_pager_main.setCurrentItem(3, false) + R.id.menu_bookshelf -> viewPagerMain.setCurrentItem(0, false) + R.id.menu_discovery -> viewPagerMain.setCurrentItem(1, false) + R.id.menu_rss -> if (AppConfig.showDiscovery) { + viewPagerMain.setCurrentItem(2, false) + } else { + viewPagerMain.setCurrentItem(1, false) + } + R.id.menu_my_config -> viewPagerMain.setCurrentItem(bottomMenuCount - 1, false) } return false } @@ -86,32 +101,30 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), if (System.currentTimeMillis() - bookshelfReselected > 300) { bookshelfReselected = System.currentTimeMillis() } else { - (fragmentMap[0] as? BookshelfFragment)?.gotoTop() + (fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.gotoTop() + } + } + R.id.menu_discovery -> { + if (System.currentTimeMillis() - exploreReselected > 300) { + exploreReselected = System.currentTimeMillis() + } else { + (fragmentMap[1] as? ExploreFragment)?.compressExplore() } } } } private fun upVersion() { - if (getPrefInt(PreferKey.versionCode) != App.versionCode) { - putPrefInt(PreferKey.versionCode, App.versionCode) - if (!BuildConfig.DEBUG) { + if (LocalConfig.versionCode != appInfo.versionCode) { + LocalConfig.versionCode = appInfo.versionCode + if (LocalConfig.isFirstOpenApp) { + val text = String(assets.open("help/appHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) + } else if (!BuildConfig.DEBUG) { val log = String(assets.open("updateLog.md").readBytes()) TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000, true) } - } - } - - override fun onPageSelected(position: Int) { - view_pager_main.hideSoftInput() - pagePosition = position - when (position) { - 0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true - 2 -> if (AppConfig.isShowRSS) { - bottom_navigation_view.menu.getItem(position).isChecked = true - } else { - bottom_navigation_view.menu.getItem(3).isChecked = true - } + viewModel.upVersion() } } @@ -120,11 +133,16 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), when (keyCode) { KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled) { if (pagePosition != 0) { - view_pager_main.currentItem = 0 + binding.viewPagerMain.currentItem = 0 return true } + (fragmentMap[getFragmentId(0)] as? BookshelfFragment2)?.let { + if (it.back()) { + return true + } + } if (System.currentTimeMillis() - exitTime > 2000) { - toast(R.string.double_click_exit) + toastOnUi(R.string.double_click_exit) exitTime = System.currentTimeMillis() } else { if (BaseReadAloudService.pause) { @@ -156,11 +174,13 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), observeEvent(EventBus.RECREATE) { recreate() } - observeEvent(EventBus.SHOW_RSS) { - bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS - view_pager_main.adapter?.notifyDataSetChanged() - if (AppConfig.isShowRSS) { - view_pager_main.setCurrentItem(3, false) + observeEvent(EventBus.NOTIFY_MAIN) { + binding.apply { + upBottomMenu() + viewPagerMain.adapter?.notifyDataSetChanged() + if (it) { + viewPagerMain.setCurrentItem(bottomMenuCount - 1, false) + } } } observeEvent(PreferKey.threadCount) { @@ -168,28 +188,82 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), } } + private fun upBottomMenu() { + val showDiscovery = AppConfig.showDiscovery + val showRss = AppConfig.showRSS + binding.bottomNavigationView.menu.let { menu -> + menu.findItem(R.id.menu_discovery).isVisible = showDiscovery + menu.findItem(R.id.menu_rss).isVisible = showRss + } + bottomMenuCount = 2 + realPositions[1] = 1 + realPositions[2] = 2 + when { + showDiscovery -> bottomMenuCount++ + showRss -> { + realPositions[1] = 2 + realPositions[2] = 3 + } + else -> { + realPositions[1] = 3 + realPositions[2] = 3 + } + } + if (showRss) { + bottomMenuCount++ + } else { + realPositions[2] = 3 + } + } + + private fun getFragmentId(position: Int): Int { + val p = realPositions[position] + if (p == 0) { + return if (AppConfig.bookGroupStyle == 1) 11 else 0 + } + return p + } + + private inner class PageChangeCallback : ViewPager.SimpleOnPageChangeListener() { + + override fun onPageSelected(position: Int) { + pagePosition = position + binding.bottomNavigationView.menu + .getItem(realPositions[position]).isChecked = true + } + + } + + @Suppress("DEPRECATION") private inner class TabFragmentPageAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private fun getId(position: Int): Int { + return getFragmentId(position) + } + override fun getItemPosition(`object`: Any): Int { return POSITION_NONE } override fun getItem(position: Int): Fragment { - return when (position) { - 0 -> fragmentMap.getValue(fragmentId[0]) - 1 -> fragmentMap.getValue(fragmentId[1]) - 2 -> if (AppConfig.isShowRSS) { - fragmentMap.getValue(fragmentId[2]) - } else { - fragmentMap.getValue(fragmentId[3]) - } - else -> fragmentMap.getValue(fragmentId[3]) + return when (getId(position)) { + 0 -> BookshelfFragment1() + 11 -> BookshelfFragment2() + 1 -> ExploreFragment() + 2 -> RssFragment() + else -> MyFragment() } } override fun getCount(): Int { - return if (AppConfig.isShowRSS) 4 else 3 + return bottomMenuCount + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val fragment = super.instantiateItem(container, position) as Fragment + fragmentMap[getId(position)] = fragment + return fragment } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index c9fc9066a..b78d15239 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -1,30 +1,28 @@ package io.legado.app.ui.main import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.constant.BookType import io.legado.app.constant.EventBus +import io.legado.app.data.appDb import io.legado.app.data.entities.Book -import io.legado.app.data.entities.RssSource import io.legado.app.help.AppConfig -import io.legado.app.help.DefaultValueHelp -import io.legado.app.help.http.HttpHelper -import io.legado.app.help.storage.Restore +import io.legado.app.help.BookHelp +import io.legado.app.help.DefaultData +import io.legado.app.help.LocalConfig import io.legado.app.model.webBook.WebBook -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonObject +import io.legado.app.service.help.CacheBook import io.legado.app.utils.postEvent -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.Executors +import kotlin.math.min class MainViewModel(application: Application) : BaseViewModel(application) { private var threadCount = AppConfig.threadCount - private var upTocPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + private var upTocPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() val updateList = CopyOnWriteArraySet() private val bookMap = ConcurrentHashMap() @@ -39,17 +37,17 @@ class MainViewModel(application: Application) : BaseViewModel(application) { fun upPool() { threadCount = AppConfig.threadCount upTocPool.close() - upTocPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + upTocPool = Executors.newFixedThreadPool(min(threadCount,8)).asCoroutineDispatcher() } fun upAllBookToc() { execute { - upToc(App.db.bookDao().hasUpdateBooks) + upToc(appDb.bookDao.hasUpdateBooks) } } fun upToc(books: List) { - execute { + execute(context = upTocPool) { books.filter { it.origin != BookType.local && it.canUpdate }.forEach { @@ -64,41 +62,71 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } } + @Synchronized private fun updateToc() { - synchronized(this) { - bookMap.forEach { bookEntry -> - if (!updateList.contains(bookEntry.key)) { - val book = bookEntry.value - synchronized(this) { - updateList.add(book.bookUrl) - postEvent(EventBus.UP_BOOK, book.bookUrl) + var update = false + bookMap.forEach { bookEntry -> + if (!updateList.contains(bookEntry.key)) { + update = true + val book = bookEntry.value + synchronized(this) { + updateList.add(book.bookUrl) + postEvent(EventBus.UP_BOOKSHELF, book.bookUrl) + } + appDb.bookSourceDao.getBookSource(book.origin)?.let { bookSource -> + execute(context = upTocPool) { + val webBook = WebBook(bookSource) + if (book.tocUrl.isBlank()) { + webBook.getBookInfoAwait(this, book) + } + val toc = webBook.getChapterListAwait(this, book) + appDb.bookDao.update(book) + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + cacheBook(webBook, book) + }.onError(upTocPool) { + it.printStackTrace() + }.onFinally(upTocPool) { + synchronized(this) { + bookMap.remove(bookEntry.key) + updateList.remove(book.bookUrl) + postEvent(EventBus.UP_BOOKSHELF, book.bookUrl) + upNext() + } } - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getChapterList(book, context = upTocPool) - .timeout(300000) - .onSuccess(IO) { - App.db.bookDao().update(book) - App.db.bookChapterDao().delByBook(book.bookUrl) - App.db.bookChapterDao().insert(*it.toTypedArray()) - } - .onError { - it.printStackTrace() - } - .onFinally { - synchronized(this) { - bookMap.remove(bookEntry.key) - updateList.remove(book.bookUrl) - postEvent(EventBus.UP_BOOK, book.bookUrl) - upNext() + } ?: synchronized(this) { + bookMap.remove(bookEntry.key) + updateList.remove(book.bookUrl) + postEvent(EventBus.UP_BOOKSHELF, book.bookUrl) + upNext() + } + return + } + } + if (!update) { + usePoolCount-- + } + } + + private fun cacheBook(webBook: WebBook, book: Book) { + execute { + if (book.totalChapterNum > book.durChapterIndex) { + val downloadToIndex = + min(book.totalChapterNum, book.durChapterIndex.plus(AppConfig.preDownloadNum)) + for (i in book.durChapterIndex until downloadToIndex) { + appDb.bookChapterDao.getChapter(book.bookUrl, i)?.let { chapter -> + if (!BookHelp.hasContent(book, chapter)) { + var addToCache = false + while (!addToCache) { + if (CacheBook.downloadCount() < 10) { + CacheBook.download(this, webBook, book, chapter) + addToCache = true + } else { + delay(100) } } - } ?: synchronized(this) { - bookMap.remove(bookEntry.key) - updateList.remove(book.bookUrl) - postEvent(EventBus.UP_BOOK, book.bookUrl) - upNext() + } } - return } } } @@ -112,28 +140,26 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } } - fun initRss() { + fun postLoad() { execute { - val url = "https://gitee.com/alanskycn/yuedu/raw/master/JS/RSS/rssSource" - HttpHelper.simpleGet(url)?.let { body -> - val sources = mutableListOf() - val items: List> = Restore.jsonPath.parse(body).read("$") - for (item in items) { - val jsonItem = Restore.jsonPath.parse(item) - GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> - sources.add(source) - } + if (appDb.httpTTSDao.count == 0) { + DefaultData.httpTTS.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) } - App.db.rssSourceDao().insert(*sources.toTypedArray()) } } } - fun postLoad() { + fun upVersion() { execute { - FileUtils.deleteFile(FileUtils.getPath(context.cacheDir, "Fonts")) - if (App.db.httpTTSDao().count == 0) { - DefaultValueHelp.initHttpTTS() + if (LocalConfig.needUpHttpTTS) { + DefaultData.importDefaultHttpTTS() + } + if (LocalConfig.needUpTxtTocRule) { + DefaultData.importDefaultTocRules() + } + if (LocalConfig.needUpRssSources) { + DefaultData.importDefaultRssSources() } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt new file mode 100644 index 000000000..d9a5acbfe --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt @@ -0,0 +1,145 @@ +package io.legado.app.ui.main.bookshelf + +import android.annotation.SuppressLint +import android.view.MenuItem +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Book +import io.legado.app.databinding.DialogBookshelfConfigBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.book.arrange.ArrangeBookActivity +import io.legado.app.ui.book.cache.CacheActivity +import io.legado.app.ui.book.group.GroupManageDialog +import io.legado.app.ui.book.local.ImportBookActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.main.MainViewModel +import io.legado.app.utils.* + +abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment(layoutId) { + + val activityViewModel by activityViewModels() + override val viewModel by viewModels() + + private val importBookshelf = registerForActivityResult(FilePicker()) { + it?.readText(requireContext())?.let { text -> + viewModel.importBookshelf(text, groupId) + } + } + + abstract val groupId: Long + abstract val books: List + + abstract fun gotoTop() + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + when (item.itemId) { + R.id.menu_search -> startActivity() + R.id.menu_update_toc -> activityViewModel.upToc(books) + R.id.menu_bookshelf_layout -> configBookshelf() + R.id.menu_group_manage -> GroupManageDialog() + .show(childFragmentManager, "groupManageDialog") + R.id.menu_add_local -> startActivity() + R.id.menu_add_url -> addBookByUrl() + R.id.menu_arrange_bookshelf -> startActivity { + putExtra("groupId", groupId) + } + R.id.menu_download -> startActivity { + putExtra("groupId", groupId) + } + R.id.menu_export_bookshelf -> viewModel.exportBookshelf(books) { + activity?.share(it) + } + R.id.menu_import_bookshelf -> importBookshelfAlert(groupId) + } + } + + @SuppressLint("InflateParams") + fun addBookByUrl() { + alert(titleResource = R.string.add_book_url) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + viewModel.addBookByUrl(it) + } + } + noButton() + }.show() + } + + @SuppressLint("InflateParams") + fun configBookshelf() { + alert(titleResource = R.string.bookshelf_layout) { + val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) + val bookshelfSort = getPrefInt(PreferKey.bookshelfSort) + val alertBinding = + DialogBookshelfConfigBinding.inflate(layoutInflater) + .apply { + spGroupStyle.setSelection(AppConfig.bookGroupStyle) + swShowUnread.isChecked = AppConfig.showUnread + rgLayout.checkByIndex(bookshelfLayout) + rgSort.checkByIndex(bookshelfSort) + } + customView { alertBinding.root } + okButton { + alertBinding.apply { + if (AppConfig.bookGroupStyle != spGroupStyle.selectedItemPosition) { + AppConfig.bookGroupStyle = spGroupStyle.selectedItemPosition + postEvent(EventBus.NOTIFY_MAIN, false) + } + if (AppConfig.showUnread != swShowUnread.isChecked) { + AppConfig.showUnread = swShowUnread.isChecked + postEvent(EventBus.BOOKSHELF_REFRESH, "") + } + var changed = false + if (bookshelfLayout != rgLayout.getCheckedIndex()) { + putPrefInt(PreferKey.bookshelfLayout, rgLayout.getCheckedIndex()) + changed = true + } + if (bookshelfSort != rgSort.getCheckedIndex()) { + putPrefInt(PreferKey.bookshelfSort, rgSort.getCheckedIndex()) + changed = true + } + if (changed) { + postEvent(EventBus.RECREATE, "") + } + } + } + noButton() + }.show() + } + + + private fun importBookshelfAlert(groupId: Long) { + alert(titleResource = R.string.import_bookshelf) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url/json" + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + viewModel.importBookshelf(it, groupId) + } + } + noButton() + neutralButton(R.string.select_file) { + importBookshelf.launch( + FilePickerParam( + mode = FilePicker.FILE, + allowExtensions = arrayOf("txt", "json") + ) + ) + } + }.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt deleted file mode 100644 index 4444f33d7..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt +++ /dev/null @@ -1,298 +0,0 @@ -package io.legado.app.ui.main.bookshelf - -import android.annotation.SuppressLint -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentStatePagerAdapter -import androidx.lifecycle.LiveData -import com.google.android.material.tabs.TabLayout -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseFragment -import io.legado.app.constant.AppConst -import io.legado.app.constant.PreferKey -import io.legado.app.data.entities.BookGroup -import io.legado.app.help.AppConfig -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.okButton -import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.accentColor -import io.legado.app.ui.book.arrange.ArrangeBookActivity -import io.legado.app.ui.book.download.DownloadActivity -import io.legado.app.ui.book.group.GroupManageDialog -import io.legado.app.ui.book.local.ImportBookActivity -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.main.MainViewModel -import io.legado.app.ui.main.bookshelf.books.BooksFragment -import io.legado.app.ui.widget.text.AutoCompleteTextView -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.fragment_bookshelf.* -import kotlinx.android.synthetic.main.view_tab_layout.* -import kotlinx.android.synthetic.main.view_title_bar.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jetbrains.anko.startActivity - - -class BookshelfFragment : VMBaseFragment(R.layout.fragment_bookshelf), - TabLayout.OnTabSelectedListener, - SearchView.OnQueryTextListener, - GroupManageDialog.CallBack { - - override val viewModel: BookshelfViewModel - get() = getViewModel(BookshelfViewModel::class.java) - private val activityViewModel: MainViewModel - get() = getViewModelOfActivity(MainViewModel::class.java) - private var bookGroupLiveData: LiveData>? = null - private var noGroupLiveData: LiveData? = null - private val bookGroups = mutableListOf() - private val fragmentMap = hashMapOf() - private var showGroupNone = false - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(toolbar) - initView() - initBookGroupData() - } - - override fun onCompatCreateOptionsMenu(menu: Menu) { - menuInflater.inflate(R.menu.main_bookshelf, menu) - } - - override fun onCompatOptionsItemSelected(item: MenuItem) { - super.onCompatOptionsItemSelected(item) - when (item.itemId) { - R.id.menu_search -> startActivity() - R.id.menu_update_toc -> { - val group = bookGroups[tab_layout.selectedTabPosition] - val fragment = fragmentMap[group.groupId] - fragment?.getBooks()?.let { - activityViewModel.upToc(it) - } - } - R.id.menu_bookshelf_layout -> configBookshelf() - R.id.menu_group_manage -> GroupManageDialog() - .show(childFragmentManager, "groupManageDialog") - R.id.menu_add_local -> startActivity() - R.id.menu_add_url -> addBookByUrl() - R.id.menu_arrange_bookshelf -> startActivity( - Pair("groupId", selectedGroup?.groupId ?: 0), - Pair("groupName", selectedGroup?.groupName ?: 0) - ) - R.id.menu_download -> startActivity( - Pair("groupId", selectedGroup?.groupId ?: 0), - Pair("groupName", selectedGroup?.groupName ?: 0) - ) - } - } - - private val selectedGroup: BookGroup? - get() = bookGroups.getOrNull(view_pager_bookshelf?.currentItem ?: 0) - - private fun initView() { - ATH.applyEdgeEffectColor(view_pager_bookshelf) - tab_layout.isTabIndicatorFullWidth = false - tab_layout.tabMode = TabLayout.MODE_SCROLLABLE - tab_layout.setSelectedTabIndicatorColor(requireContext().accentColor) - tab_layout.setupWithViewPager(view_pager_bookshelf) - view_pager_bookshelf.offscreenPageLimit = 1 - view_pager_bookshelf.adapter = TabFragmentPageAdapter(childFragmentManager) - } - - private fun initBookGroupData() { - bookGroupLiveData?.removeObservers(viewLifecycleOwner) - bookGroupLiveData = App.db.bookGroupDao().liveDataAll() - bookGroupLiveData?.observe(viewLifecycleOwner, { - viewModel.checkGroup(it) - launch { - synchronized(this) { - tab_layout.removeOnTabSelectedListener(this@BookshelfFragment) - } - var noGroupSize = 0 - withContext(IO) { - if (AppConfig.bookGroupNoneShow) { - noGroupSize = App.db.bookDao().noGroupSize - } - } - synchronized(this@BookshelfFragment) { - bookGroups.clear() - if (AppConfig.bookGroupAllShow) { - bookGroups.add(AppConst.bookGroupAll) - } - if (AppConfig.bookGroupLocalShow) { - bookGroups.add(AppConst.bookGroupLocal) - } - if (AppConfig.bookGroupAudioShow) { - bookGroups.add(AppConst.bookGroupAudio) - } - showGroupNone = if (noGroupSize > 0 && it.isNotEmpty()) { - bookGroups.add(AppConst.bookGroupNone) - true - } else { - false - } - bookGroups.addAll(it) - view_pager_bookshelf.adapter?.notifyDataSetChanged() - tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() - tab_layout.addOnTabSelectedListener(this@BookshelfFragment) - } - } - }) - noGroupLiveData?.removeObservers(viewLifecycleOwner) - noGroupLiveData = App.db.bookDao().observeNoGroupSize() - noGroupLiveData?.observe(viewLifecycleOwner, { - if (it > 0 && !showGroupNone && AppConfig.bookGroupNoneShow) { - showGroupNone = true - upGroup() - } else if (it == 0 && showGroupNone) { - showGroupNone = false - upGroup() - } - }) - } - - override fun onQueryTextSubmit(query: String?): Boolean { - context?.startActivity(Pair("key", query)) - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - return false - } - - override fun upGroup() { - launch { - var noGroupSize = 0 - withContext(IO) { - if (AppConfig.bookGroupNoneShow) { - noGroupSize = App.db.bookDao().noGroupSize - } - } - synchronized(this@BookshelfFragment) { - bookGroups.remove(AppConst.bookGroupAll) - bookGroups.remove(AppConst.bookGroupLocal) - bookGroups.remove(AppConst.bookGroupAudio) - bookGroups.remove(AppConst.bookGroupNone) - showGroupNone = - if (noGroupSize > 0 && bookGroups.isNotEmpty()) { - bookGroups.add(0, AppConst.bookGroupNone) - true - } else { - false - } - if (AppConfig.bookGroupAudioShow) { - bookGroups.add(0, AppConst.bookGroupAudio) - } - if (AppConfig.bookGroupLocalShow) { - bookGroups.add(0, AppConst.bookGroupLocal) - } - if (AppConfig.bookGroupAllShow) { - bookGroups.add(0, AppConst.bookGroupAll) - } - view_pager_bookshelf.adapter?.notifyDataSetChanged() - } - } - } - - @SuppressLint("InflateParams") - private fun configBookshelf() { - requireContext().alert(titleResource = R.string.bookshelf_layout) { - val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) - val bookshelfSort = getPrefInt(PreferKey.bookshelfSort) - val root = LayoutInflater.from(requireContext()) - .inflate(R.layout.dialog_bookshelf_config, null).apply { - rg_layout.checkByIndex(bookshelfLayout) - rg_sort.checkByIndex(bookshelfSort) - } - customView = root - okButton { - root.apply { - var changed = false - if (bookshelfLayout != rg_layout.getCheckedIndex()) { - putPrefInt(PreferKey.bookshelfLayout, rg_layout.getCheckedIndex()) - changed = true - } - if (bookshelfSort != rg_sort.getCheckedIndex()) { - putPrefInt(PreferKey.bookshelfSort, rg_sort.getCheckedIndex()) - changed = true - } - if (changed) { - activity?.recreate() - } - } - } - noButton() - }.show().applyTint() - } - - @SuppressLint("InflateParams") - private fun addBookByUrl() { - requireContext() - .alert(titleResource = R.string.add_book_url) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - } - } - okButton { - editText?.text?.toString()?.let { - viewModel.addBookByUrl(it) - } - } - noButton { } - }.show().applyTint() - } - - override fun onTabReselected(tab: TabLayout.Tab?) = Unit - - override fun onTabUnselected(tab: TabLayout.Tab?) = Unit - - override fun onTabSelected(tab: TabLayout.Tab?) { - tab?.position?.let { - putPrefInt(PreferKey.saveTabPosition, it) - } - } - - fun gotoTop() { - fragmentMap[selectedGroup?.groupId]?.gotoTop() - } - - private inner class TabFragmentPageAdapter(fm: FragmentManager) : - FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - - override fun getPageTitle(position: Int): CharSequence? { - return bookGroups[position].groupName - } - - override fun getItemPosition(`object`: Any): Int { - return POSITION_NONE - } - - override fun getItem(position: Int): Fragment { - val group = bookGroups[position] - var fragment = fragmentMap[group.groupId] - if (fragment == null) { - fragment = BooksFragment.newInstance(position, group.groupId) - fragmentMap[group.groupId] = fragment - } - return fragment - } - - override fun getCount(): Int { - return bookGroups.size - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index ae1287842..7e8519cac 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -1,15 +1,20 @@ package io.legado.app.ui.main.bookshelf import android.app.Application -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookSource +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.model.webBook.PreciseSearch import io.legado.app.model.webBook.WebBook -import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.isActive class BookshelfViewModel(application: Application) : BaseViewModel(application) { @@ -21,12 +26,12 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) for (url in urls) { val bookUrl = url.trim() if (bookUrl.isEmpty()) continue - if (App.db.bookDao().getBook(bookUrl) != null) continue + if (appDb.bookDao.getBook(bookUrl) != null) continue val baseUrl = NetworkUtils.getBaseUrl(bookUrl) ?: continue - var source = App.db.bookSourceDao().getBookSource(baseUrl) + var source = appDb.bookSourceDao.getBookSource(baseUrl) if (source == null) { if (hasBookUrlPattern == null) { - hasBookUrlPattern = App.db.bookSourceDao().hasBookUrlPattern + hasBookUrlPattern = appDb.bookSourceDao.hasBookUrlPattern } hasBookUrlPattern.forEach { bookSource -> if (bookUrl.matches(bookSource.bookUrlPattern!!.toRegex())) { @@ -41,10 +46,10 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) origin = bookSource.bookSourceUrl, originName = bookSource.bookSourceName ) - WebBook(bookSource).getBookInfo(book, this) + WebBook(bookSource).getBookInfo(this, book) .onSuccess(IO) { - it.order = App.db.bookDao().maxOrder + 1 - App.db.bookDao().insert(it) + it.order = appDb.bookDao.maxOrder + 1 + it.save() successCount++ }.onError { throw Exception(it.localizedMessage) @@ -53,29 +58,89 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) } }.onSuccess { if (successCount > 0) { - toast(R.string.success) + context.toastOnUi(R.string.success) } else { - toast("ERROR") + context.toastOnUi("ERROR") } }.onError { - toast(it.localizedMessage ?: "ERROR") + context.toastOnUi(it.localizedMessage ?: "ERROR") } } - fun checkGroup(groups: List) { + fun exportBookshelf(books: List?, success: (json: String) -> Unit) { + execute { + val exportList = arrayListOf>() + books?.forEach { + val bookMap = hashMapOf() + bookMap["name"] = it.name + bookMap["author"] = it.author + bookMap["intro"] = it.getDisplayIntro() + exportList.add(bookMap) + } + GSON.toJson(exportList) + }.onSuccess { + success(it) + } + } + + fun importBookshelf(str: String, groupId: Long) { + execute { + val text = str.trim() + when { + text.isAbsUrl() -> { + okHttpClient.newCall { + url(text) + }.text().let { + importBookshelf(it, groupId) + } + } + text.isJsonArray() -> { + importBookshelfByJson(text, groupId) + } + else -> { + throw Exception("格式不对") + } + } + }.onError { + context.toastOnUi(it.localizedMessage ?: "ERROR") + } + } + + private fun importBookshelfByJson(json: String, groupId: Long) { execute { - groups.forEach { group -> - if (group.groupId and (group.groupId - 1) != 0) { - var id = 1 - val idsSum = App.db.bookGroupDao().idsSum - while (id and idsSum != 0) { - id = id.shl(1) + val bookSources = appDb.bookSourceDao.allEnabled + GSON.fromJsonArray>(json)?.forEach { + if (!isActive) return@execute + val name = it["name"] ?: "" + val author = it["author"] ?: "" + if (name.isNotEmpty() && appDb.bookDao.getBook(name, author) == null) { + val book = PreciseSearch + .searchFirstBook(this, bookSources, name, author) + book?.let { + if (groupId > 0) { + book.group = groupId + } + book.save() } - App.db.bookGroupDao().delete(group) - App.db.bookGroupDao().insert(group.copy(groupId = id)) - App.db.bookDao().upGroup(group.groupId, id) } } + }.onFinally { + context.toastOnUi(R.string.success) + } + } + + fun checkGroup(groups: List) { + groups.forEach { group -> + if (group.groupId >= 0 && group.groupId and (group.groupId - 1) != 0L) { + var id = 1L + val idsSum = appDb.bookGroupDao.idsSum + while (id and idsSum != 0L) { + id = id.shl(1) + } + appDb.bookGroupDao.delete(group) + appDb.bookGroupDao.insert(group.copy(groupId = id)) + appDb.bookDao.upGroup(group.groupId, id) + } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt deleted file mode 100644 index 1e8fda88b..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import android.content.Context -import androidx.core.os.bundleOf -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.data.entities.Book - -abstract class BaseBooksAdapter(context: Context, layoutId: Int) : - SimpleRecyclerAdapter(context, layoutId) { - - fun notification(bookUrl: String) { - for (i in 0 until itemCount) { - getItem(i)?.let { - if (it.bookUrl == bookUrl) { - notifyItemChanged(i, bundleOf(Pair("refresh", null))) - return - } - } - } - } - - interface CallBack { - fun open(book: Book) - fun openBookInfo(book: Book) - fun isUpdate(bookUrl: String): Boolean - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt deleted file mode 100644 index 7967b3843..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import android.content.Context -import android.os.Bundle -import android.view.View -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.constant.BookType -import io.legado.app.data.entities.Book -import io.legado.app.lib.theme.ATH -import io.legado.app.utils.invisible -import kotlinx.android.synthetic.main.item_bookshelf_grid.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - -class BooksAdapterGrid(context: Context, private val callBack: CallBack) : - BaseBooksAdapter(context, R.layout.item_bookshelf_grid) { - - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { - val bundle = payloads.getOrNull(0) as? Bundle - with(holder.itemView) { - if (bundle == null) { - ATH.applyBackgroundTint(this) - tv_name.text = item.name - iv_cover.load(item.getDisplayCover(), item.name, item.author) - upRefresh(this, item) - } else { - bundle.keySet().forEach { - when (it) { - "name" -> tv_name.text = item.name - "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) - "refresh" -> upRefresh(this, item) - } - } - } - } - } - - private fun upRefresh(itemView: View, item: Book) = with(itemView) { - if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { - bv_unread.invisible() - rl_loading.show() - } else { - rl_loading.hide() - bv_unread.setBadgeCount(item.getUnreadChapterNum()) - bv_unread.setHighlight(item.lastCheckCount > 0) - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - onClick { - getItem(holder.layoutPosition)?.let { - callBack.open(it) - } - } - - onLongClick { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it) - } - true - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt deleted file mode 100644 index f0f15c27d..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import android.content.Context -import android.os.Bundle -import android.view.View -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.constant.BookType -import io.legado.app.data.entities.Book -import io.legado.app.lib.theme.ATH -import io.legado.app.utils.invisible -import kotlinx.android.synthetic.main.item_bookshelf_list.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - -class BooksAdapterList(context: Context, private val callBack: CallBack) : - BaseBooksAdapter(context, R.layout.item_bookshelf_list) { - - override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { - val bundle = payloads.getOrNull(0) as? Bundle - with(holder.itemView) { - if (bundle == null) { - ATH.applyBackgroundTint(this) - tv_name.text = item.name - tv_author.text = item.author - tv_read.text = item.durChapterTitle - tv_last.text = item.latestChapterTitle - iv_cover.load(item.getDisplayCover(), item.name, item.author) - upRefresh(this, item) - } else { - tv_read.text = item.durChapterTitle - tv_last.text = item.latestChapterTitle - bundle.keySet().forEach { - when (it) { - "name" -> tv_name.text = item.name - "author" -> tv_author.text = item.author - "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) - "refresh" -> upRefresh(this, item) - } - } - } - } - } - - private fun upRefresh(itemView: View, item: Book) = with(itemView) { - if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { - bv_unread.invisible() - rl_loading.show() - } else { - rl_loading.hide() - bv_unread.setHighlight(item.lastCheckCount > 0) - bv_unread.setBadgeCount(item.getUnreadChapterNum()) - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - onClick { - getItem(holder.layoutPosition)?.let { - callBack.open(it) - } - } - - onLongClick { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it) - } - true - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt deleted file mode 100644 index 8498b77a3..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt +++ /dev/null @@ -1,71 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import androidx.core.os.bundleOf -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.Book - -class BooksDiffCallBack(private val oldItems: List, private val newItems: List) : - DiffUtil.Callback() { - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.name == newItem.name - && oldItem.author == newItem.author - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return when { - oldItem.durChapterTime != newItem.durChapterTime -> false - oldItem.name != newItem.name -> false - oldItem.author != newItem.author -> false - oldItem.durChapterTitle != newItem.durChapterTitle -> false - oldItem.latestChapterTitle != newItem.latestChapterTitle -> false - oldItem.lastCheckCount != newItem.lastCheckCount -> false - oldItem.getDisplayCover() != newItem.getDisplayCover() -> false - oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false - else -> true - } - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - val bundle = bundleOf() - if (oldItem.name != newItem.name) { - bundle.putString("name", newItem.name) - } - if (oldItem.author != newItem.author) { - bundle.putString("author", newItem.author) - } - if (oldItem.durChapterTitle != newItem.durChapterTitle) { - bundle.putString("dur", newItem.durChapterTitle) - } - if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { - bundle.putString("last", newItem.latestChapterTitle) - } - if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { - bundle.putString("cover", newItem.getDisplayCover()) - } - if (oldItem.lastCheckCount != newItem.lastCheckCount - || oldItem.durChapterTime != newItem.durChapterTime - || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() - || oldItem.lastCheckCount != newItem.lastCheckCount - ) { - bundle.putBoolean("refresh", true) - } - if (bundle.isEmpty) return null - return bundle - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt deleted file mode 100644 index ebbe310e8..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt +++ /dev/null @@ -1,161 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.LiveData -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.BaseFragment -import io.legado.app.constant.AppConst -import io.legado.app.constant.BookType -import io.legado.app.constant.EventBus -import io.legado.app.constant.PreferKey -import io.legado.app.data.entities.Book -import io.legado.app.help.AppConfig -import io.legado.app.help.IntentDataHelp -import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.accentColor -import io.legado.app.ui.audio.AudioPlayActivity -import io.legado.app.ui.book.info.BookInfoActivity -import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.main.MainViewModel -import io.legado.app.utils.getPrefInt -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.observeEvent -import kotlinx.android.synthetic.main.fragment_books.* -import org.jetbrains.anko.startActivity -import kotlin.math.max - - -class BooksFragment : BaseFragment(R.layout.fragment_books), - BaseBooksAdapter.CallBack { - - companion object { - fun newInstance(position: Int, groupId: Int): BooksFragment { - return BooksFragment().apply { - val bundle = Bundle() - bundle.putInt("position", position) - bundle.putInt("groupId", groupId) - arguments = bundle - } - } - } - - private val activityViewModel: MainViewModel - get() = getViewModelOfActivity(MainViewModel::class.java) - private lateinit var booksAdapter: BaseBooksAdapter - private var bookshelfLiveData: LiveData>? = null - private var position = 0 - private var groupId = -1 - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - arguments?.let { - position = it.getInt("position", 0) - groupId = it.getInt("groupId", -1) - } - initRecyclerView() - upRecyclerData() - } - - private fun initRecyclerView() { - ATH.applyEdgeEffectColor(rv_bookshelf) - refresh_layout.setColorSchemeColors(accentColor) - refresh_layout.setOnRefreshListener { - refresh_layout.isRefreshing = false - activityViewModel.upToc(booksAdapter.getItems()) - } - val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) - if (bookshelfLayout == 0) { - rv_bookshelf.layoutManager = LinearLayoutManager(context) - booksAdapter = BooksAdapterList(requireContext(), this) - } else { - rv_bookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) - booksAdapter = BooksAdapterGrid(requireContext(), this) - } - rv_bookshelf.adapter = booksAdapter - booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - val layoutManager = rv_bookshelf.layoutManager - if (positionStart == 0 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - rv_bookshelf.scrollToPosition(max(0, scrollTo)) - } - } - - override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { - val layoutManager = rv_bookshelf.layoutManager - if (toPosition == 0 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - rv_bookshelf.scrollToPosition(max(0, scrollTo)) - } - } - }) - } - - private fun upRecyclerData() { - bookshelfLiveData?.removeObservers(this) - bookshelfLiveData = when (groupId) { - AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll() - AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal() - AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio() - AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() - else -> App.db.bookDao().observeByGroup(groupId) - } - bookshelfLiveData?.observe(this, { list -> - val books = when (getPrefInt(PreferKey.bookshelfSort)) { - 1 -> list.sortedByDescending { it.latestChapterTime } - 2 -> list.sortedBy { it.name } - 3 -> list.sortedBy { it.order } - else -> list.sortedByDescending { it.durChapterTime } - } - val diffResult = DiffUtil - .calculateDiff(BooksDiffCallBack(booksAdapter.getItems(), books)) - booksAdapter.setItems(books, diffResult) - }) - } - - fun getBooks(): List { - return booksAdapter.getItems() - } - - fun gotoTop() { - if (AppConfig.isEInkMode) { - rv_bookshelf.scrollToPosition(0) - } else { - rv_bookshelf.smoothScrollToPosition(0) - } - } - - override fun open(book: Book) { - when (book.type) { - BookType.audio -> - context?.startActivity(Pair("bookUrl", book.bookUrl)) - else -> context?.startActivity( - Pair("bookUrl", book.bookUrl), - Pair("key", IntentDataHelp.putData(book)) - ) - } - } - - override fun openBookInfo(book: Book) { - context?.startActivity( - Pair("name", book.name), - Pair("author", book.author) - ) - } - - override fun isUpdate(bookUrl: String): Boolean { - return bookUrl in activityViewModel.updateList - } - - override fun observeLiveBus() { - super.observeLiveBus() - observeEvent(EventBus.UP_BOOK) { - booksAdapter.notification(it) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt new file mode 100644 index 000000000..bb818c303 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt @@ -0,0 +1,162 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.main.bookshelf.style1 + +import android.os.Bundle +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import com.google.android.material.tabs.TabLayout +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.FragmentBookshelfBinding +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.ui.main.bookshelf.style1.books.BooksFragment +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * 书架界面 + */ +class BookshelfFragment1 : BaseBookshelfFragment(R.layout.fragment_bookshelf), + TabLayout.OnTabSelectedListener, + SearchView.OnQueryTextListener { + + private val binding by viewBinding(FragmentBookshelfBinding::bind) + private lateinit var adapter: FragmentStatePagerAdapter + private lateinit var tabLayout: TabLayout + private val bookGroups = mutableListOf() + private val fragmentMap = hashMapOf() + + override val groupId: Long get() = selectedGroup.groupId + + override val books: List + get() { + val fragment = fragmentMap[selectedGroup.groupId] + return fragment?.getBooks() ?: emptyList() + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + tabLayout = binding.titleBar.findViewById(R.id.tab_layout) + setSupportToolbar(binding.titleBar.toolbar) + initView() + initBookGroupData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_bookshelf, menu) + } + + private val selectedGroup: BookGroup + get() = bookGroups[tabLayout.selectedTabPosition] + + private fun initView() { + ATH.applyEdgeEffectColor(binding.viewPagerBookshelf) + tabLayout.isTabIndicatorFullWidth = false + tabLayout.tabMode = TabLayout.MODE_SCROLLABLE + tabLayout.setSelectedTabIndicatorColor(requireContext().accentColor) + tabLayout.setupWithViewPager(binding.viewPagerBookshelf) + binding.viewPagerBookshelf.offscreenPageLimit = 1 + adapter = TabFragmentPageAdapter(childFragmentManager) + binding.viewPagerBookshelf.adapter = adapter + } + + private fun initBookGroupData() { + launch { + appDb.bookGroupDao.flowShow().collect { + viewModel.checkGroup(it) + upGroup(it) + } + } + } + + override fun onQueryTextSubmit(query: String?): Boolean { + SearchActivity.start(requireContext(), query) + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + + @Synchronized + private fun upGroup(data: List) { + if (data.isEmpty()) { + appDb.bookGroupDao.enableGroup(AppConst.bookGroupAllId) + } else { + if (data != bookGroups) { + bookGroups.clear() + bookGroups.addAll(data) + adapter.notifyDataSetChanged() + selectLastTab() + } + } + } + + @Synchronized + private fun selectLastTab() { + tabLayout.removeOnTabSelectedListener(this) + tabLayout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() + tabLayout.addOnTabSelectedListener(this) + } + + override fun onTabReselected(tab: TabLayout.Tab) { + fragmentMap[selectedGroup.groupId]?.let { + toastOnUi("${selectedGroup.groupName}(${it.getBooksCount()})") + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) = Unit + + override fun onTabSelected(tab: TabLayout.Tab) { + putPrefInt(PreferKey.saveTabPosition, tab.position) + } + + override fun gotoTop() { + fragmentMap[selectedGroup.groupId]?.gotoTop() + } + + private inner class TabFragmentPageAdapter(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getPageTitle(position: Int): CharSequence { + return bookGroups[position].groupName + } + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } + + override fun getItem(position: Int): Fragment { + val group = bookGroups[position] + return BooksFragment.newInstance(position, group.groupId) + } + + override fun getCount(): Int { + return bookGroups.size + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val fragment = super.instantiateItem(container, position) as BooksFragment + val group = bookGroups[position] + fragmentMap[group.groupId] = fragment + return fragment + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt new file mode 100644 index 000000000..b2dbb3863 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt @@ -0,0 +1,81 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.data.entities.Book + +abstract class BaseBooksAdapter(context: Context) : + DiffRecyclerAdapter(context) { + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean { + return oldItem.name == newItem.name + && oldItem.author == newItem.author + } + + override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean { + return when { + oldItem.durChapterTime != newItem.durChapterTime -> false + oldItem.name != newItem.name -> false + oldItem.author != newItem.author -> false + oldItem.durChapterTitle != newItem.durChapterTitle -> false + oldItem.latestChapterTitle != newItem.latestChapterTitle -> false + oldItem.lastCheckCount != newItem.lastCheckCount -> false + oldItem.getDisplayCover() != newItem.getDisplayCover() -> false + oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false + else -> true + } + } + + override fun getChangePayload(oldItem: Book, newItem: Book): Any? { + val bundle = bundleOf() + if (oldItem.name != newItem.name) { + bundle.putString("name", newItem.name) + } + if (oldItem.author != newItem.author) { + bundle.putString("author", newItem.author) + } + if (oldItem.durChapterTitle != newItem.durChapterTitle) { + bundle.putString("dur", newItem.durChapterTitle) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + bundle.putString("last", newItem.latestChapterTitle) + } + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { + bundle.putString("cover", newItem.getDisplayCover()) + } + if (oldItem.lastCheckCount != newItem.lastCheckCount + || oldItem.durChapterTime != newItem.durChapterTime + || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() + || oldItem.lastCheckCount != newItem.lastCheckCount + ) { + bundle.putBoolean("refresh", true) + } + if (bundle.isEmpty) return null + return bundle + } + + } + + fun notification(bookUrl: String) { + for (i in 0 until itemCount) { + getItem(i)?.let { + if (it.bookUrl == bookUrl) { + notifyItemChanged(i, bundleOf(Pair("refresh", null))) + return + } + } + } + } + + interface CallBack { + fun open(book: Book) + fun openBookInfo(book: Book) + fun isUpdate(bookUrl: String): Boolean + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt new file mode 100644 index 000000000..b029e5eed --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt @@ -0,0 +1,73 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemBookshelfGridBinding +import io.legado.app.help.AppConfig +import io.legado.app.utils.invisible +import splitties.views.onLongClick + +class BooksAdapterGrid(context: Context, private val callBack: CallBack) : + BaseBooksAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridBinding { + return ItemBookshelfGridBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookshelfGridBinding, + item: Book, + payloads: MutableList + ) = binding.run { + val bundle = payloads.getOrNull(0) as? Bundle + if (bundle == null) { + tvName.text = item.name + ivCover.load(item.getDisplayCover(), item.name, item.author) + upRefresh(binding, item) + } else { + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "cover" -> ivCover.load(item.getDisplayCover(), item.name, item.author) + "refresh" -> upRefresh(binding, item) + } + } + } + } + + private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.show() + } else { + binding.rlLoading.hide() + if (AppConfig.showUnread) { + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + } else { + binding.bvUnread.invisible() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfGridBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt new file mode 100644 index 000000000..813ead01b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt @@ -0,0 +1,79 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemBookshelfListBinding +import io.legado.app.help.AppConfig +import io.legado.app.utils.invisible +import splitties.views.onLongClick + +class BooksAdapterList(context: Context, private val callBack: CallBack) : + BaseBooksAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookshelfListBinding { + return ItemBookshelfListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookshelfListBinding, + item: Book, + payloads: MutableList + ) = binding.run { + val bundle = payloads.getOrNull(0) as? Bundle + if (bundle == null) { + tvName.text = item.name + tvAuthor.text = item.author + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + ivCover.load(item.getDisplayCover(), item.name, item.author) + upRefresh(binding, item) + } else { + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "author" -> tvAuthor.text = item.author + "cover" -> ivCover.load(item.getDisplayCover(), item.name, item.author) + "refresh" -> upRefresh(binding, item) + } + } + } + } + + private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.show() + } else { + binding.rlLoading.hide() + if (AppConfig.showUnread) { + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + } else { + binding.bvUnread.invisible() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt new file mode 100644 index 000000000..0ae3e207b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt @@ -0,0 +1,179 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.core.view.isGone +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseFragment +import io.legado.app.constant.AppConst +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.databinding.FragmentBooksBinding +import io.legado.app.help.AppConfig +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.main.MainViewModel +import io.legado.app.utils.cnCompare +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.observeEvent +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlin.math.max + +/** + * 书架界面 + */ +class BooksFragment : BaseFragment(R.layout.fragment_books), + BaseBooksAdapter.CallBack { + + companion object { + fun newInstance(position: Int, groupId: Long): BooksFragment { + return BooksFragment().apply { + val bundle = Bundle() + bundle.putInt("position", position) + bundle.putLong("groupId", groupId) + arguments = bundle + } + } + } + + private val binding by viewBinding(FragmentBooksBinding::bind) + private val activityViewModel: MainViewModel + by activityViewModels() + private lateinit var booksAdapter: BaseBooksAdapter<*> + private var booksFlowJob: Job? = null + private var position = 0 + private var groupId = -1L + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + arguments?.let { + position = it.getInt("position", 0) + groupId = it.getLong("groupId", -1) + } + initRecyclerView() + upRecyclerData() + } + + private fun initRecyclerView() { + ATH.applyEdgeEffectColor(binding.rvBookshelf) + binding.refreshLayout.setColorSchemeColors(accentColor) + binding.refreshLayout.setOnRefreshListener { + binding.refreshLayout.isRefreshing = false + activityViewModel.upToc(booksAdapter.getItems()) + } + val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) + if (bookshelfLayout == 0) { + binding.rvBookshelf.layoutManager = LinearLayoutManager(context) + booksAdapter = BooksAdapterList(requireContext(), this) + } else { + binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) + booksAdapter = BooksAdapterGrid(requireContext(), this) + } + binding.rvBookshelf.adapter = booksAdapter + booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (positionStart == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (toPosition == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + }) + } + + private fun upRecyclerData() { + booksFlowJob?.cancel() + booksFlowJob = launch { + when (groupId) { + AppConst.bookGroupAllId -> appDb.bookDao.flowAll() + AppConst.bookGroupLocalId -> appDb.bookDao.flowLocal() + AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() + AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup() + else -> appDb.bookDao.flowByGroup(groupId) + }.collect { list -> + binding.tvEmptyMsg.isGone = list.isNotEmpty() + val books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> list.sortedByDescending { it.latestChapterTime } + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + 3 -> list.sortedBy { it.order } + else -> list.sortedByDescending { it.durChapterTime } + } + booksAdapter.setItems(books) + } + } + } + + fun getBooks(): List { + return booksAdapter.getItems() + } + + fun gotoTop() { + if (AppConfig.isEInkMode) { + binding.rvBookshelf.scrollToPosition(0) + } else { + binding.rvBookshelf.smoothScrollToPosition(0) + } + } + + fun getBooksCount(): Int { + return booksAdapter.itemCount + } + + override fun open(book: Book) { + when (book.type) { + BookType.audio -> + startActivity { + putExtra("bookUrl", book.bookUrl) + } + else -> startActivity { + putExtra("bookUrl", book.bookUrl) + } + } + } + + override fun openBookInfo(book: Book) { + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + } + } + + override fun isUpdate(bookUrl: String): Boolean { + return bookUrl in activityViewModel.updateList + } + + @SuppressLint("NotifyDataSetChanged") + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.UP_BOOKSHELF) { + booksAdapter.notification(it) + } + observeEvent(EventBus.BOOKSHELF_REFRESH) { + booksAdapter.notifyDataSetChanged() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt new file mode 100644 index 000000000..e5a0428c5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt @@ -0,0 +1,86 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.data.entities.Book + +abstract class BaseBooksAdapter( + val context: Context, + val callBack: CallBack +) : RecyclerView.Adapter() { + + val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean { + return oldItem.name == newItem.name + && oldItem.author == newItem.author + } + + override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean { + return when { + oldItem.durChapterTime != newItem.durChapterTime -> false + oldItem.name != newItem.name -> false + oldItem.author != newItem.author -> false + oldItem.durChapterTitle != newItem.durChapterTitle -> false + oldItem.latestChapterTitle != newItem.latestChapterTitle -> false + oldItem.lastCheckCount != newItem.lastCheckCount -> false + oldItem.getDisplayCover() != newItem.getDisplayCover() -> false + oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false + else -> true + } + } + + override fun getChangePayload(oldItem: Book, newItem: Book): Any? { + val bundle = bundleOf() + if (oldItem.name != newItem.name) { + bundle.putString("name", newItem.name) + } + if (oldItem.author != newItem.author) { + bundle.putString("author", newItem.author) + } + if (oldItem.durChapterTitle != newItem.durChapterTitle) { + bundle.putString("dur", newItem.durChapterTitle) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + bundle.putString("last", newItem.latestChapterTitle) + } + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { + bundle.putString("cover", newItem.getDisplayCover()) + } + if (oldItem.lastCheckCount != newItem.lastCheckCount + || oldItem.durChapterTime != newItem.durChapterTime + || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() + || oldItem.lastCheckCount != newItem.lastCheckCount + ) { + bundle.putBoolean("refresh", true) + } + if (bundle.isEmpty) return null + return bundle + } + + } + + fun notification(bookUrl: String) { + for (i in 0 until itemCount) { + callBack.getItem(i).let { + if (it is Book && it.bookUrl == bookUrl) { + notifyItemChanged(i, bundleOf(Pair("refresh", null))) + return + } + } + } + } + + + interface CallBack { + fun onItemClick(position: Int) + fun onItemLongClick(position: Int) + fun isUpdate(bookUrl: String): Boolean + fun getItemCount(): Int + fun getItemType(position: Int): Int + fun getItem(position: Int): Any + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt new file mode 100644 index 000000000..fa1957ba5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt @@ -0,0 +1,139 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemBookshelfGridBinding +import io.legado.app.databinding.ItemBookshelfGridGroupBinding +import io.legado.app.help.AppConfig +import io.legado.app.utils.invisible +import splitties.views.onLongClick + +class BooksAdapterGrid(context: Context, callBack: CallBack) : + BaseBooksAdapter(context, callBack) { + + override fun getItemCount(): Int { + return callBack.getItemCount() + } + + override fun getItemViewType(position: Int): Int { + return callBack.getItemType(position) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + 1 -> GroupViewHolder( + ItemBookshelfGridGroupBinding.inflate(LayoutInflater.from(context), parent, false) + ) + else -> BookViewHolder( + ItemBookshelfGridBinding.inflate(LayoutInflater.from(context), parent, false) + ) + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + val bundle = payloads.getOrNull(0) as? Bundle + when { + bundle == null -> super.onBindViewHolder(holder, position, payloads) + holder is BookViewHolder -> onBindBook(holder.binding, position, bundle) + holder is GroupViewHolder -> onBindGroup(holder.binding, position, bundle) + } + } + + private fun onBindGroup(binding: ItemBookshelfGridGroupBinding, position: Int, bundle: Bundle) { + binding.run { + val item = callBack.getItem(position) as BookGroup + tvName.text = item.groupName + } + } + + private fun onBindBook(binding: ItemBookshelfGridBinding, position: Int, bundle: Bundle) { + binding.run { + val item = callBack.getItem(position) as Book + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "cover" -> ivCover.load( + item.getDisplayCover(), + item.name, + item.author + ) + "refresh" -> upRefresh(this, item) + } + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is BookViewHolder -> onBindBook(holder.binding, position) + is GroupViewHolder -> onBindGroup(holder.binding, position) + } + } + + private fun onBindGroup(binding: ItemBookshelfGridGroupBinding, position: Int) { + binding.run { + val item = callBack.getItem(position) + if (item is BookGroup) { + tvName.text = item.groupName + } + root.setOnClickListener { + callBack.onItemClick(position) + } + root.onLongClick { + callBack.onItemLongClick(position) + } + } + } + + private fun onBindBook(binding: ItemBookshelfGridBinding, position: Int) { + binding.run { + val item = callBack.getItem(position) + if (item is Book) { + tvName.text = item.name + ivCover.load(item.getDisplayCover(), item.name, item.author) + upRefresh(this, item) + } + root.setOnClickListener { + callBack.onItemClick(position) + } + root.onLongClick { + callBack.onItemLongClick(position) + } + } + } + + private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.show() + } else { + binding.rlLoading.hide() + if (AppConfig.showUnread) { + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + } else { + binding.bvUnread.invisible() + } + } + } + + class BookViewHolder(val binding: ItemBookshelfGridBinding) : + RecyclerView.ViewHolder(binding.root) + + class GroupViewHolder(val binding: ItemBookshelfGridGroupBinding) : + RecyclerView.ViewHolder(binding.root) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt new file mode 100644 index 000000000..962bfd0e9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt @@ -0,0 +1,155 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemBookshelfListBinding +import io.legado.app.databinding.ItemBookshelfListGroupBinding +import io.legado.app.help.AppConfig +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible +import splitties.views.onLongClick + +class BooksAdapterList(context: Context, callBack: CallBack) : + BaseBooksAdapter(context, callBack) { + + override fun getItemCount(): Int { + return callBack.getItemCount() + } + + override fun getItemViewType(position: Int): Int { + return callBack.getItemType(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 1 -> GroupViewHolder( + ItemBookshelfListGroupBinding.inflate(LayoutInflater.from(context), parent, false) + ) + else -> BookViewHolder( + ItemBookshelfListBinding.inflate(LayoutInflater.from(context), parent, false) + ) + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + val bundle = payloads.getOrNull(0) as? Bundle + when { + bundle == null -> super.onBindViewHolder(holder, position, payloads) + holder is BookViewHolder -> onBindBook(holder.binding, position, bundle) + holder is GroupViewHolder -> onBindGroup(holder.binding, position, bundle) + } + } + + private fun onBindGroup(binding: ItemBookshelfListGroupBinding, position: Int, bundle: Bundle) { + binding.run { + val item = callBack.getItem(position) as BookGroup + tvName.text = item.groupName + } + } + + private fun onBindBook(binding: ItemBookshelfListBinding, position: Int, bundle: Bundle) { + binding.run { + val item = callBack.getItem(position) as Book + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "author" -> tvAuthor.text = item.author + "cover" -> ivCover.load( + item.getDisplayCover(), + item.name, + item.author + ) + "refresh" -> upRefresh(this, item) + } + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is BookViewHolder -> onBindBook(holder.binding, position) + is GroupViewHolder -> onBindGroup(holder.binding, position) + } + } + + private fun onBindGroup(binding: ItemBookshelfListGroupBinding, position: Int) { + binding.run { + val item = callBack.getItem(position) + if (item is BookGroup) { + tvName.text = item.groupName + flHasNew.gone() + ivAuthor.gone() + ivLast.gone() + ivRead.gone() + tvAuthor.gone() + tvLast.gone() + tvRead.gone() + } + root.setOnClickListener { + callBack.onItemClick(position) + } + root.onLongClick { + callBack.onItemLongClick(position) + } + } + } + + private fun onBindBook(binding: ItemBookshelfListBinding, position: Int) { + binding.run { + val item = callBack.getItem(position) + if (item is Book) { + tvName.text = item.name + tvAuthor.text = item.author + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + ivCover.load(item.getDisplayCover(), item.name, item.author) + flHasNew.visible() + ivAuthor.visible() + ivLast.visible() + ivRead.visible() + upRefresh(this, item) + } + root.setOnClickListener { + callBack.onItemClick(position) + } + root.onLongClick { + callBack.onItemLongClick(position) + } + } + } + + private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { + if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.show() + } else { + binding.rlLoading.hide() + if (AppConfig.showUnread) { + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + } else { + binding.bvUnread.invisible() + } + } + } + + class BookViewHolder(val binding: ItemBookshelfListBinding) : + RecyclerView.ViewHolder(binding.root) + + class GroupViewHolder(val binding: ItemBookshelfListGroupBinding) : + RecyclerView.ViewHolder(binding.root) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt new file mode 100644 index 000000000..68d7a4bb4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt @@ -0,0 +1,239 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.View +import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.FragmentBookshelf1Binding +import io.legado.app.help.AppConfig +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.group.GroupEdit +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.utils.cnCompare +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.observeEvent +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlin.math.max + +/** + * 书架界面 + */ +class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1), + SearchView.OnQueryTextListener, + BaseBooksAdapter.CallBack { + + private val binding by viewBinding(FragmentBookshelf1Binding::bind) + private lateinit var booksAdapter: BaseBooksAdapter<*> + override var groupId = AppConst.bookGroupNoneId + private var booksFlowJob: Job? = null + private var bookGroups: List = emptyList() + override var books: List = emptyList() + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + initRecyclerView() + initGroupData() + initBooksData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_bookshelf, menu) + } + + private fun initRecyclerView() { + ATH.applyEdgeEffectColor(binding.rvBookshelf) + binding.refreshLayout.setColorSchemeColors(accentColor) + binding.refreshLayout.setOnRefreshListener { + binding.refreshLayout.isRefreshing = false + activityViewModel.upToc(books) + } + val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout) + if (bookshelfLayout == 0) { + binding.rvBookshelf.layoutManager = LinearLayoutManager(context) + booksAdapter = BooksAdapterList(requireContext(), this) + } else { + binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) + booksAdapter = BooksAdapterGrid(requireContext(), this) + } + binding.rvBookshelf.adapter = booksAdapter + booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (positionStart == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (toPosition == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + }) + } + + @SuppressLint("NotifyDataSetChanged") + private fun initGroupData() { + launch { + appDb.bookGroupDao.flowShow().collect { + if (it != bookGroups) { + bookGroups = it + booksAdapter.notifyDataSetChanged() + } + } + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun initBooksData() { + booksFlowJob?.cancel() + booksFlowJob = launch { + when (groupId) { + AppConst.bookGroupAllId -> appDb.bookDao.flowAll() + AppConst.bookGroupLocalId -> appDb.bookDao.flowLocal() + AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio() + AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup() + else -> appDb.bookDao.flowByGroup(groupId) + }.collect { list -> + binding.tvEmptyMsg.isGone = list.isNotEmpty() + books = when (getPrefInt(PreferKey.bookshelfSort)) { + 1 -> list.sortedByDescending { + it.latestChapterTime + } + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + 3 -> list.sortedBy { + it.order + } + else -> list.sortedByDescending { + it.durChapterTime + } + } + booksAdapter.notifyDataSetChanged() + } + } + } + + fun back(): Boolean { + if (groupId != AppConst.bookGroupNoneId) { + groupId = AppConst.bookGroupNoneId + initBooksData() + return true + } + return false + } + + override fun onQueryTextSubmit(query: String?): Boolean { + SearchActivity.start(requireContext(), query) + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + + override fun gotoTop() { + if (AppConfig.isEInkMode) { + binding.rvBookshelf.scrollToPosition(0) + } else { + binding.rvBookshelf.smoothScrollToPosition(0) + } + } + + override fun onItemClick(position: Int) { + when (val item = getItem(position)) { + is Book -> when (item.type) { + BookType.audio -> + startActivity { + putExtra("bookUrl", item.bookUrl) + } + else -> startActivity { + putExtra("bookUrl", item.bookUrl) + } + } + is BookGroup -> { + groupId = item.groupId + initBooksData() + } + } + } + + override fun onItemLongClick(position: Int) { + when (val item = getItem(position)) { + is Book -> startActivity { + putExtra("name", item.name) + putExtra("author", item.author) + } + is BookGroup -> GroupEdit.show(requireContext(), layoutInflater, item) + } + } + + override fun isUpdate(bookUrl: String): Boolean { + return bookUrl in activityViewModel.updateList + } + + override fun getItemCount(): Int { + return if (groupId == AppConst.bookGroupNoneId) { + bookGroups.size + books.size + } else { + books.size + } + } + + override fun getItemType(position: Int): Int { + return if (groupId == AppConst.bookGroupNoneId) { + if (position < bookGroups.size) 1 else 0 + } else { + 0 + } + } + + override fun getItem(position: Int): Any { + return if (groupId == AppConst.bookGroupNoneId) { + if (position < bookGroups.size) { + bookGroups[position] + } else { + books[position - bookGroups.size] + } + } else { + books[position] + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.UP_BOOKSHELF) { + booksAdapter.notification(it) + } + observeEvent(EventBus.BOOKSHELF_REFRESH) { + booksAdapter.notifyDataSetChanged() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt index ccfa5bf82..fe2eab534 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt @@ -1,89 +1,129 @@ package io.legado.app.ui.main.explore import android.content.Context -import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.PopupMenu -import io.legado.app.App +import android.widget.TextView +import androidx.core.view.children +import com.google.android.flexbox.FlexboxLayout import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.ExploreKind +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.databinding.ItemFindBookBinding import io.legado.app.help.coroutine.Coroutine import io.legado.app.lib.theme.accentColor import io.legado.app.utils.ACache import io.legado.app.utils.dp import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_fillet_text.view.* -import kotlinx.android.synthetic.main.item_find_book.view.* import kotlinx.coroutines.CoroutineScope -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick - +import splitties.views.onLongClick class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_find_book) { + RecyclerAdapter(context) { + + private val recycler = arrayListOf() private var exIndex = -1 private var scrollTo = -1 - override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList) { - with(holder.itemView) { - if (holder.layoutPosition == getActualItemCount() - 1) { - setPadding(16.dp, 12.dp, 16.dp, 12.dp) + override fun getViewBinding(parent: ViewGroup): ItemFindBookBinding { + return ItemFindBookBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFindBookBinding, + item: BookSource, + payloads: MutableList + ) { + binding.run { + if (holder.layoutPosition == itemCount - 1) { + root.setPadding(16.dp, 12.dp, 16.dp, 12.dp) } else { - setPadding(16.dp, 12.dp, 16.dp, 0) + root.setPadding(16.dp, 12.dp, 16.dp, 0) } if (payloads.isEmpty()) { - tv_name.text = item.bookSourceName + tvName.text = item.bookSourceName } if (exIndex == holder.layoutPosition) { - iv_status.setImageResource(R.drawable.ic_arrow_down) - rotate_loading.loadingColor = context.accentColor - rotate_loading.show() + ivStatus.setImageResource(R.drawable.ic_arrow_down) + rotateLoading.loadingColor = context.accentColor + rotateLoading.show() if (scrollTo >= 0) { callBack.scrollTo(scrollTo) } Coroutine.async(scope) { - item.getExploreKinds() + item.exploreKinds }.onSuccess { kindList -> - if (!kindList.isNullOrEmpty()) { - gl_child.visible() - gl_child.removeAllViews() - kindList.map { kind -> - val tv = LayoutInflater.from(context) - .inflate(R.layout.item_fillet_text, gl_child, false) - gl_child.addView(tv) - tv.text_view.text = kind.title - tv.text_view.onClick { - kind.url?.let { kindUrl -> - callBack.openExplore( - item.bookSourceUrl, - kind.title, - kindUrl - ) - } - } - } - } + upKindList(flexbox, item.bookSourceUrl, kindList) }.onFinally { - rotate_loading.hide() + rotateLoading.hide() if (scrollTo >= 0) { callBack.scrollTo(scrollTo) scrollTo = -1 } } } else { - iv_status.setImageResource(R.drawable.ic_arrow_right) - rotate_loading.hide() - gl_child.gone() + ivStatus.setImageResource(R.drawable.ic_arrow_right) + rotateLoading.hide() + recyclerFlexbox(flexbox) + flexbox.gone() } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - ll_title.onClick { + private fun upKindList(flexbox: FlexboxLayout, sourceUrl: String, kinds: List) { + if (!kinds.isNullOrEmpty()) { + recyclerFlexbox(flexbox) + flexbox.visible() + kinds.forEach { kind -> + val tv = getFlexboxChild(flexbox) + flexbox.addView(tv) + tv.text = kind.title + val lp = tv.layoutParams as FlexboxLayout.LayoutParams + kind.style().let { style -> + lp.flexGrow = style.layout_flexGrow + lp.flexShrink = style.layout_flexShrink + lp.alignSelf = style.alignSelf() + lp.flexBasisPercent = style.layout_flexBasisPercent + lp.isWrapBefore = style.layout_wrapBefore + } + if (kind.url.isNullOrBlank()) { + tv.setOnClickListener(null) + } else { + tv.setOnClickListener { + callBack.openExplore(sourceUrl, kind.title, kind.url) + } + } + } + } + } + + @Synchronized + private fun getFlexboxChild(flexbox: FlexboxLayout): TextView { + return if (recycler.isEmpty()) { + ItemFilletTextBinding.inflate(inflater, flexbox, false).root + } else { + recycler.last().also { + recycler.removeLast() + } as TextView + } + } + + @Synchronized + private fun recyclerFlexbox(flexbox: FlexboxLayout) { + recycler.addAll(flexbox.children) + flexbox.removeAllViews() + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFindBookBinding) { + binding.apply { + llTitle.setOnClickListener { val position = holder.layoutPosition val oldEx = exIndex exIndex = if (exIndex == position) -1 else position @@ -94,12 +134,23 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca notifyItemChanged(position, false) } } - ll_title.onLongClick { - showMenu(ll_title, holder.layoutPosition) + llTitle.onLongClick { + showMenu(llTitle, holder.layoutPosition) } } } + fun compressExplore(): Boolean { + return if (exIndex < 0) { + false + } else { + val oldExIndex = exIndex + exIndex = -1 + notifyItemChanged(oldExIndex) + true + } + } + private fun showMenu(view: View, position: Int): Boolean { val source = getItem(position) ?: return true val popupMenu = PopupMenu(context, view) @@ -108,12 +159,13 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca when (it.itemId) { R.id.menu_edit -> callBack.editSource(source.bookSourceUrl) R.id.menu_top -> callBack.toTop(source) - R.id.menu_refresh -> { + R.id.menu_refresh -> Coroutine.async(scope) { ACache.get(context, "explore").remove(source.bookSourceUrl) - notifyItemChanged(position) + }.onSuccess { + callBack.refreshData() } R.id.menu_del -> Coroutine.async(scope) { - App.db.bookSourceDao().delete(source) + appDb.bookSourceDao.delete(source) } } true @@ -123,8 +175,9 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca } interface CallBack { + fun refreshData() fun scrollTo(pos: Int) - fun openExplore(sourceUrl: String, title: String, exploreUrl: String) + fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) fun editSource(sourceUrl: String) fun toTop(source: BookSource) } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index 6aa870ee7..6d797a30d 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -6,46 +6,52 @@ import android.view.MenuItem import android.view.SubMenu import android.view.View import androidx.appcompat.widget.SearchView -import androidx.lifecycle.LiveData +import androidx.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.FragmentExploreBinding +import io.legado.app.help.AppConfig import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.book.explore.ExploreShowActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity -import io.legado.app.utils.getViewModel +import io.legado.app.utils.cnCompare import io.legado.app.utils.splitNotBlank import io.legado.app.utils.startActivity -import kotlinx.android.synthetic.main.fragment_find_book.* -import kotlinx.android.synthetic.main.view_search.* -import kotlinx.android.synthetic.main.view_title_bar.* -import java.text.Collator - - -class ExploreFragment : VMBaseFragment(R.layout.fragment_find_book), +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * 发现界面 + */ +class ExploreFragment : VMBaseFragment(R.layout.fragment_explore), ExploreAdapter.CallBack { - override val viewModel: ExploreViewModel - get() = getViewModel(ExploreViewModel::class.java) - + override val viewModel by viewModels() + private val binding by viewBinding(FragmentExploreBinding::bind) private lateinit var adapter: ExploreAdapter private lateinit var linearLayoutManager: LinearLayoutManager + private lateinit var searchView: SearchView private val groups = linkedSetOf() - private var liveGroup: LiveData>? = null - private var liveExplore: LiveData>? = null + private var exploreFlowJob: Job? = null private var groupsMenu: SubMenu? = null override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(toolbar) + searchView = binding.titleBar.findViewById(R.id.search_view) + setSupportToolbar(binding.titleBar.toolbar) initSearchView() initRecyclerView() initGroupData() - initExploreData() + upExploreData() } override fun onCompatCreateOptionsMenu(menu: Menu) { @@ -55,103 +61,135 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ upGroupsMenu() } + override fun onPause() { + super.onPause() + searchView.clearFocus() + } + private fun initSearchView() { - ATH.setTint(search_view, primaryTextColor) - search_view.onActionViewExpanded() - search_view.isSubmitButtonEnabled = true - search_view.queryHint = getString(R.string.screen_find) - search_view.clearFocus() - search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.screen_find) + searchView.clearFocus() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { return false } override fun onQueryTextChange(newText: String?): Boolean { - initExploreData(newText) + upExploreData(newText) return false } }) } private fun initRecyclerView() { - ATH.applyEdgeEffectColor(rv_find) + ATH.applyEdgeEffectColor(binding.rvFind) linearLayoutManager = LinearLayoutManager(context) - rv_find.layoutManager = linearLayoutManager - adapter = ExploreAdapter(requireContext(), this, this) - rv_find.adapter = adapter + binding.rvFind.layoutManager = linearLayoutManager + adapter = ExploreAdapter(requireContext(), lifecycleScope, this) + binding.rvFind.adapter = adapter adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { super.onItemRangeInserted(positionStart, itemCount) if (positionStart == 0) { - rv_find.scrollToPosition(0) + binding.rvFind.scrollToPosition(0) } } }) } private fun initGroupData() { - liveGroup?.removeObservers(viewLifecycleOwner) - liveGroup = App.db.bookSourceDao().liveGroupExplore() - liveGroup?.observe(viewLifecycleOwner, { - groups.clear() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) - } - upGroupsMenu() - }) + launch { + appDb.bookSourceDao.flowExploreGroup() + .collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupsMenu() + } + } } - private fun initExploreData(key: String? = null) { - liveExplore?.removeObservers(viewLifecycleOwner) - liveExplore = if (key.isNullOrBlank()) { - App.db.bookSourceDao().liveExplore() - } else { - App.db.bookSourceDao().liveExplore("%$key%") + private fun upExploreData(searchKey: String? = null) { + exploreFlowJob?.cancel() + exploreFlowJob = launch { + val exploreFlow = when { + searchKey.isNullOrBlank() -> { + appDb.bookSourceDao.flowExplore() + } + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.bookSourceDao.flowGroupExplore("%$key%") + } + else -> { + appDb.bookSourceDao.flowExplore("%$searchKey%") + } + } + exploreFlow.collect { + binding.tvEmptyMsg.isGone = it.isNotEmpty() || searchView.query.isNotEmpty() + val diffResult = DiffUtil + .calculateDiff(ExploreDiffCallBack(ArrayList(adapter.getItems()), it)) + adapter.setItems(it) + diffResult.dispatchUpdatesTo(adapter) + } } - liveExplore?.observe(viewLifecycleOwner, { - val diffResult = DiffUtil - .calculateDiff(ExploreDiffCallBack(ArrayList(adapter.getItems()), it)) - adapter.setItems(it) - diffResult.dispatchUpdatesTo(adapter) - }) } - private fun upGroupsMenu() { - groupsMenu?.let { subMenu -> - subMenu.removeGroup(R.id.menu_group_text) - groups.sortedWith(Collator.getInstance(java.util.Locale.CHINESE)) - .forEach { - subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) - } + private fun upGroupsMenu() = groupsMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group_text) + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.forEach { + subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) } } override fun onCompatOptionsItemSelected(item: MenuItem) { super.onCompatOptionsItemSelected(item) if (item.groupId == R.id.menu_group_text) { - search_view.setQuery(item.title, true) + searchView.setQuery("group:${item.title}", true) } } + override fun refreshData() { + upExploreData(searchView.query?.toString()) + } + override fun scrollTo(pos: Int) { - (rv_find.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0) + (binding.rvFind.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0) } - override fun openExplore(sourceUrl: String, title: String, exploreUrl: String) { - startActivity( - Pair("exploreName", title), - Pair("sourceUrl", sourceUrl), - Pair("exploreUrl", exploreUrl) - ) + override fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) { + if (exploreUrl.isNullOrBlank()) return + startActivity { + putExtra("exploreName", title) + putExtra("sourceUrl", sourceUrl) + putExtra("exploreUrl", exploreUrl) + } } override fun editSource(sourceUrl: String) { - startActivity(Pair("data", sourceUrl)) + startActivity { + putExtra("data", sourceUrl) + } } override fun toTop(source: BookSource) { viewModel.topSource(source) } + fun compressExplore() { + if (!adapter.compressExplore()) { + if (AppConfig.isEInkMode) { + binding.rvFind.scrollToPosition(0) + } else { + binding.rvFind.smoothScrollToPosition(0) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt index a3716b592..e6a2cc569 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt @@ -1,17 +1,17 @@ package io.legado.app.ui.main.explore import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.BookSource class ExploreViewModel(application: Application) : BaseViewModel(application) { fun topSource(bookSource: BookSource) { execute { - val minXh = App.db.bookSourceDao().minOrder + val minXh = appDb.bookSourceDao.minOrder bookSource.customOrder = minXh - 1 - App.db.bookSourceDao().insert(bookSource) + appDb.bookSourceDao.insert(bookSource) } } diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt index e4bc131ba..bf09a6171 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt @@ -1,42 +1,41 @@ package io.legado.app.ui.main.my -import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View import androidx.preference.Preference -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment import io.legado.app.base.BasePreferenceFragment import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.databinding.FragmentMyConfigBinding import io.legado.app.help.AppConfig +import io.legado.app.help.ThemeConfig import io.legado.app.lib.theme.ATH import io.legado.app.service.WebService import io.legado.app.ui.about.AboutActivity import io.legado.app.ui.about.DonateActivity import io.legado.app.ui.about.ReadRecordActivity import io.legado.app.ui.book.source.manage.BookSourceActivity -import io.legado.app.ui.config.BackupRestoreUi import io.legado.app.ui.config.ConfigActivity import io.legado.app.ui.config.ConfigViewModel -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.replacerule.ReplaceRuleActivity +import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.prefs.NameListPreference import io.legado.app.ui.widget.prefs.PreferenceCategory import io.legado.app.ui.widget.prefs.SwitchPreference import io.legado.app.utils.* -import kotlinx.android.synthetic.main.view_title_bar.* -import org.jetbrains.anko.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding -class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.CallBack { +class MyFragment : BaseFragment(R.layout.fragment_my_config) { + + private val binding by viewBinding(FragmentMyConfigBinding::bind) override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(toolbar) + setSupportToolbar(binding.titleBar.toolbar) val fragmentTag = "prefFragment" var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag) if (preferenceFragment == null) preferenceFragment = PreferenceFragment() @@ -51,21 +50,12 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog. override fun onCompatOptionsItemSelected(item: MenuItem) { when (item.itemId) { R.id.menu_help -> { - val text = String(requireContext().assets.open("help.md").readBytes()) + val text = String(requireContext().assets.open("help/appHelp.md").readBytes()) TextDialog.show(childFragmentManager, text, TextDialog.MD) } } } - override fun onFilePicked(requestCode: Int, currentPath: String) { - BackupRestoreUi.onFilePicked(requestCode, currentPath) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - BackupRestoreUi.onActivityResult(requestCode, resultCode, data) - } - /** * 配置 */ @@ -80,18 +70,25 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog. } addPreferencesFromResource(R.xml.pref_main) val webServicePre = findPreference(PreferKey.webService) - observeEvent(EventBus.WEB_SERVICE_STOP) { - webServicePre?.isChecked = false + observeEventSticky(EventBus.WEB_SERVICE) { + webServicePre?.let { + it.isChecked = WebService.isRun + it.summary = if (WebService.isRun) { + WebService.hostAddress + } else { + getString(R.string.web_service_desc) + } + } } findPreference(PreferKey.themeMode)?.let { it.setOnPreferenceChangeListener { _, _ -> - view?.post { App.INSTANCE.applyDayNight() } + view?.post { ThemeConfig.applyDayNight(requireContext()) } true } } if (AppConfig.isGooglePlay) { findPreference("aboutCategory") - ?.removePreference(findPreference("donate")) + ?.removePreferenceRecursively("donate") } } @@ -118,10 +115,8 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog. PreferKey.webService -> { if (requireContext().getPrefBoolean("webService")) { WebService.start(requireContext()) - toast(R.string.service_start) } else { WebService.stop(requireContext()) - toast(R.string.service_stop) } } "recordLog" -> LogUtils.upLevel() @@ -130,20 +125,20 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog. override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { - "bookSourceManage" -> context?.startActivity() - "replaceManage" -> context?.startActivity() - "setting" -> context?.startActivity( - Pair("configType", ConfigViewModel.TYPE_CONFIG) - ) - "web_dav_setting" -> context?.startActivity( - Pair("configType", ConfigViewModel.TYPE_WEB_DAV_CONFIG) - ) - "theme_setting" -> context?.startActivity( - Pair("configType", ConfigViewModel.TYPE_THEME_CONFIG) - ) - "readRecord" -> context?.startActivity() - "donate" -> context?.startActivity() - "about" -> context?.startActivity() + "bookSourceManage" -> startActivity() + "replaceManage" -> startActivity() + "setting" -> startActivity { + putExtra("configType", ConfigViewModel.TYPE_CONFIG) + } + "web_dav_setting" -> startActivity { + putExtra("configType", ConfigViewModel.TYPE_WEB_DAV_CONFIG) + } + "theme_setting" -> startActivity { + putExtra("configType", ConfigViewModel.TYPE_THEME_CONFIG) + } + "readRecord" -> startActivity() + "donate" -> startActivity() + "about" -> startActivity() } return super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt index 955c5ded3..1af9bdb9b 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt @@ -2,41 +2,51 @@ package io.legado.app.ui.main.rss import android.content.Context import android.view.View +import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ItemRssBinding import io.legado.app.help.ImageLoader -import kotlinx.android.synthetic.main.item_rss.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.sdk27.listeners.onLongClick +import splitties.views.onLongClick class RssAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_rss) { + RecyclerAdapter(context) { - override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { - with(holder.itemView) { - tv_name.text = item.sourceName + override fun getViewBinding(parent: ViewGroup): ItemRssBinding { + return ItemRssBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssBinding, + item: RssSource, + payloads: MutableList + ) { + binding.apply { + tvName.text = item.sourceName ImageLoader.load(context, item.sourceIcon) .centerCrop() .placeholder(R.drawable.image_rss) .error(R.drawable.image_rss) - .into(iv_icon) + .into(ivIcon) } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { - getItem(holder.layoutPosition)?.let { - callBack.openRss(it) + override fun registerListener(holder: ItemViewHolder, binding: ItemRssBinding) { + binding.apply { + root.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + callBack.openRss(it) + } } - } - holder.itemView.onLongClick { - getItem(holder.layoutPosition)?.let { - showMenu(holder.itemView.iv_icon, it) + root.onLongClick { + getItemByLayoutPosition(holder.layoutPosition)?.let { + showMenu(ivIcon, it) + } } - true } } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index 82ccf867f..b140c4638 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -3,40 +3,62 @@ package io.legado.app.ui.main.rss import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.SubMenu import android.view.View -import androidx.recyclerview.widget.GridLayoutManager -import io.legado.app.App +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.viewModels import io.legado.app.R import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.FragmentRssBinding +import io.legado.app.databinding.ItemRssBinding import io.legado.app.lib.theme.ATH -import io.legado.app.ui.main.MainViewModel +import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.rss.article.RssSortActivity import io.legado.app.ui.rss.favorites.RssFavoritesActivity +import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.ui.rss.source.manage.RssSourceViewModel -import io.legado.app.utils.getViewModel -import io.legado.app.utils.getViewModelOfActivity +import io.legado.app.ui.rss.subscription.RuleSubActivity +import io.legado.app.utils.cnCompare +import io.legado.app.utils.openUrl +import io.legado.app.utils.splitNotBlank import io.legado.app.utils.startActivity -import kotlinx.android.synthetic.main.fragment_rss.* -import kotlinx.android.synthetic.main.view_title_bar.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * 订阅界面 + */ class RssFragment : VMBaseFragment(R.layout.fragment_rss), RssAdapter.CallBack { - + private val binding by viewBinding(FragmentRssBinding::bind) + override val viewModel by viewModels() private lateinit var adapter: RssAdapter - override val viewModel: RssSourceViewModel - get() = getViewModel(RssSourceViewModel::class.java) + private lateinit var searchView: SearchView + private var rssFlowJob: Job? = null + private val groups = linkedSetOf() + private var groupsMenu: SubMenu? = null override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(toolbar) + searchView = binding.titleBar.findViewById(R.id.search_view) + setSupportToolbar(binding.titleBar.toolbar) + initSearchView() initRecyclerView() - initData() + initGroupData() + upRssFlowJob() } override fun onCompatCreateOptionsMenu(menu: Menu) { menuInflater.inflate(R.menu.main_rss, menu) + groupsMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupsMenu() } override fun onCompatOptionsItemSelected(item: MenuItem) { @@ -44,27 +66,102 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), when (item.itemId) { R.id.menu_rss_config -> startActivity() R.id.menu_rss_star -> startActivity() + else -> if (item.groupId == R.id.menu_group_text) { + searchView.setQuery(item.title, true) + } } } + override fun onPause() { + super.onPause() + searchView.clearFocus() + } + + private fun upGroupsMenu() = groupsMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group_text) + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.forEach { + subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) + } + } + + private fun initSearchView() { + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.rss) + searchView.clearFocus() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + upRssFlowJob(newText) + return false + } + }) + } + private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(binding.recyclerView) adapter = RssAdapter(requireContext(), this) - recycler_view.layoutManager = GridLayoutManager(requireContext(), 4) - recycler_view.adapter = adapter + binding.recyclerView.adapter = adapter + adapter.addHeaderView { + ItemRssBinding.inflate(layoutInflater, it, false).apply { + tvName.setText(R.string.rule_subscription) + ivIcon.setImageResource(R.drawable.image_legado) + root.setOnClickListener { + startActivity() + } + } + } } - private fun initData() { - App.db.rssSourceDao().liveEnabled().observe(viewLifecycleOwner, { - if (it.isEmpty()) { - getViewModelOfActivity(MainViewModel::class.java).initRss() + private fun initGroupData() { + launch { + appDb.rssSourceDao.flowGroup().collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupsMenu() } - adapter.setItems(it) - }) + } + } + + private fun upRssFlowJob(searchKey: String? = null) { + rssFlowJob?.cancel() + rssFlowJob = launch { + when { + searchKey.isNullOrEmpty() -> appDb.rssSourceDao.flowEnabled() + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.rssSourceDao.flowEnabledByGroup("%$key%") + } + else -> appDb.rssSourceDao.flowEnabled("%$searchKey%") + }.collect { + adapter.setItems(it) + } + } } override fun openRss(rssSource: RssSource) { - startActivity(Pair("url", rssSource.sourceUrl)) + if (rssSource.singleUrl) { + if (rssSource.sourceUrl.startsWith("http", true)) { + startActivity { + putExtra("title", rssSource.sourceName) + putExtra("origin", rssSource.sourceUrl) + } + } else { + context?.openUrl(rssSource.sourceUrl) + } + } else { + startActivity { + putExtra("url", rssSource.sourceUrl) + } + } } override fun toTop(rssSource: RssSource) { @@ -72,7 +169,9 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), } override fun edit(rssSource: RssSource) { - startActivity(Pair("data", rssSource.sourceUrl)) + startActivity { + putExtra("data", rssSource.sourceUrl) + } } override fun del(rssSource: RssSource) { diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt index 6ea1d85bd..dcabe5b8c 100644 --- a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt @@ -1,41 +1,39 @@ package io.legado.app.ui.qrcode -import android.app.Activity import android.content.Intent import android.graphics.BitmapFactory import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View -import cn.bingoogolapple.qrcode.core.QRCodeView +import androidx.activity.result.contract.ActivityResultContracts +import com.google.zxing.Result +import com.king.zxing.CameraScan.OnScanResultCallback import io.legado.app.R import io.legado.app.base.BaseActivity -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.databinding.ActivityQrcodeCaptureBinding +import io.legado.app.utils.QRCodeUtils import io.legado.app.utils.readBytes -import kotlinx.android.synthetic.main.activity_qrcode_capture.* -import kotlinx.android.synthetic.main.view_title_bar.* -import org.jetbrains.anko.toast +import io.legado.app.utils.viewbindingdelegate.viewBinding -class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeView.Delegate { +class QrCodeActivity : BaseActivity(), OnScanResultCallback { - private val requestQrImage = 202 - private var flashlightIsOpen: Boolean = false + override val binding by viewBinding(ActivityQrcodeCaptureBinding::inflate) - override fun onActivityCreated(savedInstanceState: Bundle?) { - setSupportActionBar(toolbar) - zxingview.setDelegate(this) - fab_flashlight.setOnClickListener { - if (flashlightIsOpen) { - flashlightIsOpen = false - zxingview.closeFlashlight() - } else { - flashlightIsOpen = true - zxingview.openFlashlight() - } + private val selectQrImage = registerForActivityResult(ActivityResultContracts.GetContent()) { + it.readBytes(this)?.let { bytes -> + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + onScanResultCallback(QRCodeUtils.parseCodeResult(bitmap)) } } + override fun onActivityCreated(savedInstanceState: Bundle?) { + val fTag = "qrCodeFragment" + val qrCodeFragment = QrCodeFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.fl_content, qrCodeFragment, fTag) + .commit() + } + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.qr_code_scan, menu) return super.onCompatCreateOptionsMenu(menu) @@ -43,69 +41,17 @@ class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeVie override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.action_choose_from_gallery -> { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "image/*" - startActivityForResult(intent, requestQrImage) - } + R.id.action_choose_from_gallery -> selectQrImage.launch("image/*") } return super.onCompatOptionsItemSelected(item) } - override fun onStart() { - super.onStart() - startCamera() - } - - private fun startCamera() { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.CAMERA) - .rationale(R.string.qr_per) - .onGranted { - zxingview.visibility = View.VISIBLE - zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别 - }.request() - } - - override fun onStop() { - zxingview.stopCamera() // 关闭摄像头预览,并且隐藏扫描框 - super.onStop() - } - - override fun onDestroy() { - zxingview.onDestroy() // 销毁二维码扫描控件 - super.onDestroy() - } - - override fun onScanQRCodeSuccess(result: String) { + override fun onScanResultCallback(result: Result?): Boolean { val intent = Intent() - intent.putExtra("result", result) + intent.putExtra("result", result?.text) setResult(RESULT_OK, intent) finish() - } - - override fun onCameraAmbientBrightnessChanged(isDark: Boolean) { - - } - - override fun onScanQRCodeOpenCameraError() { - toast("打开相机失败") - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - data?.data?.let { - zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别 - - if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) { - // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调 - it.readBytes(this)?.let { bytes -> - val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - zxingview.decodeQRCode(bitmap) - } - } - } + return true } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt new file mode 100644 index 000000000..ba5d4269d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt @@ -0,0 +1,32 @@ +package io.legado.app.ui.qrcode + +import com.google.zxing.Result +import com.king.zxing.CaptureFragment +import com.king.zxing.DecodeConfig +import com.king.zxing.DecodeFormatManager +import com.king.zxing.analyze.MultiFormatAnalyzer + + +class QrCodeFragment : CaptureFragment() { + + override fun initCameraScan() { + super.initCameraScan() + //初始化解码配置 + val decodeConfig = DecodeConfig() + //如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS + decodeConfig.hints = DecodeFormatManager.QR_CODE_HINTS + //设置是否全区域识别,默认false + decodeConfig.isFullAreaScan = true + //设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别 + decodeConfig.areaRectRatio = 0.8f + + //在启动预览之前,设置分析器,只识别二维码 + cameraScan.setAnalyzer(MultiFormatAnalyzer(decodeConfig)) + } + + override fun onScanResultCallback(result: Result?): Boolean { + (activity as? QrCodeActivity)?.onScanResultCallback(result) + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt new file mode 100644 index 000000000..b39f09791 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt @@ -0,0 +1,23 @@ +package io.legado.app.ui.qrcode + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +class QrCodeResult : ActivityResultContract() { + + override fun createIntent(context: Context, input: Unit?): Intent { + return Intent(context, QrCodeActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?): String? { + if (resultCode == RESULT_OK) { + intent?.getStringExtra("result")?.let { + return it + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt new file mode 100644 index 000000000..b108b918e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt @@ -0,0 +1,157 @@ +package io.legado.app.ui.replace + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + + +class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + private val viewModel: ReplaceRuleViewModel by activityViewModels() + private lateinit var adapter: GroupAdapter + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + + override fun onStart() { + super.onStart() + val dm = requireActivity().getSize() + dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_recycler_view, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + view.setBackgroundColor(backgroundColor) + binding.toolBar.setBackgroundColor(primaryColor) + initView() + initData() + } + + private fun initView() = binding.run { + toolBar.title = getString(R.string.group_manage) + toolBar.inflateMenu(R.menu.group_manage) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@GroupManageDialog) + adapter = GroupAdapter(requireContext()) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + } + + private fun initData() { + launch { + appDb.replaceRuleDao.flowGroup().collect { + val groups = linkedSetOf() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + adapter.setItems(groups.toList()) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> addGroup() + } + return true + } + + @SuppressLint("InflateParams") + private fun addGroup() { + alert(title = getString(R.string.add_group)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + customView { + alertBinding.apply { + editView.hint = "分组名称" + }.root + } + yesButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotBlank()) { + viewModel.addGroup(it) + } + } + } + noButton() + }.show().requestInputMethod() + } + + @SuppressLint("InflateParams") + private fun editGroup(group: String) { + alert(title = getString(R.string.group_edit)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "分组名称" + editView.setText(group) + } + customView { alertBinding.root } + yesButton { + viewModel.upGroup(group, alertBinding.editView.text?.toString()) + } + noButton() + }.show().requestInputMethod() + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + + tvDel.setOnClickListener { + getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt new file mode 100644 index 000000000..8a90440ff --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt @@ -0,0 +1,334 @@ +package io.legado.app.ui.replace + +import android.annotation.SuppressLint +import android.app.Activity +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.documentfile.provider.DocumentFile +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ActivityReplaceRuleBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.ContentProcessor +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.association.ImportReplaceRuleDialog +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.replace.edit.ReplaceEditActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import java.io.File + +/** + * 替换规则管理 + */ +class ReplaceRuleActivity : VMBaseActivity(), + SearchView.OnQueryTextListener, + PopupMenu.OnMenuItemClickListener, + SelectActionBar.CallBack, + ReplaceRuleAdapter.CallBack { + override val binding by viewBinding(ActivityReplaceRuleBinding::inflate) + override val viewModel by viewModels() + private val importRecordKey = "replaceRuleRecordKey" + private lateinit var adapter: ReplaceRuleAdapter + private lateinit var searchView: SearchView + private var groups = hashSetOf() + private var groupMenu: SubMenu? = null + private var replaceRuleFlowJob: Job? = null + private var dataInit = false + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + ImportReplaceRuleDialog.start(supportFragmentManager, it) + } + private val editActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_OK) + } + } + private val importDoc = registerForActivityResult(FilePicker()) { uri -> + kotlin.runCatching { + uri?.readText(this)?.let { + ImportReplaceRuleDialog.start(supportFragmentManager, it) + } + }.onFailure { + toastOnUi("readTextError:${it.localizedMessage}") + } + } + private val exportDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(this, uri)?.let { + viewModel.exportSelection(adapter.selection, it) + } + } else { + uri.path?.let { + viewModel.exportSelection(adapter.selection, File(it)) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView = binding.titleBar.findViewById(R.id.search_view) + initRecyclerView() + initSearchView() + initSelectActionView() + observeReplaceRuleData() + observeGroupData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.replace_rule, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + groupMenu = menu?.findItem(R.id.menu_group)?.subMenu + upGroupMenu() + return super.onPrepareOptionsMenu(menu) + } + + private fun initRecyclerView() { + ATH.applyEdgeEffectColor(binding.recyclerView) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + adapter = ReplaceRuleAdapter(this, this) + binding.recyclerView.adapter = adapter + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initSearchView() { + ATH.setTint(searchView, primaryTextColor) + searchView.onActionViewExpanded() + searchView.queryHint = getString(R.string.replace_purify_search) + searchView.clearFocus() + searchView.setOnQueryTextListener(this) + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + delSourceDialog() + } + + private fun initSelectActionView() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.replace_rule_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun delSourceDialog() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.selection) } + noButton() + }.show() + } + + private fun observeReplaceRuleData(searchKey: String? = null) { + dataInit = false + replaceRuleFlowJob?.cancel() + replaceRuleFlowJob = launch { + when { + searchKey.isNullOrEmpty() -> { + appDb.replaceRuleDao.flowAll() + } + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.replaceRuleDao.flowGroupSearch("%$key%") + } + else -> { + appDb.replaceRuleDao.flowSearch("%$searchKey%") + } + }.collect { + if (dataInit) { + setResult(Activity.RESULT_OK) + } + adapter.setItems(it, adapter.diffItemCallBack) + dataInit = true + } + } + } + + private fun observeGroupData() { + launch { + appDb.replaceRuleDao.flowGroup().collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupMenu() + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_replace_rule -> + editActivity.launch(ReplaceEditActivity.startIntent(this)) + R.id.menu_group_manage -> + GroupManageDialog().show(supportFragmentManager, "groupManage") + + R.id.menu_del_selection -> viewModel.delSelection(adapter.selection) + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_local -> importDoc.launch( + FilePickerParam( + mode = FilePicker.FILE, + allowExtensions = arrayOf("txt", "json") + ) + ) + R.id.menu_import_qr -> qrCodeResult.launch(null) + R.id.menu_help -> showHelp() + else -> if (item.groupId == R.id.replace_group) { + searchView.setQuery("group:${item.title}", true) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_top_sel -> viewModel.topSelect(adapter.selection) + R.id.menu_bottom_sel -> viewModel.bottomSelect(adapter.selection) + R.id.menu_export_selection -> exportDir.launch(null) + } + return false + } + + private fun upGroupMenu() { + groupMenu?.removeGroup(R.id.replace_group) + groups.map { + groupMenu?.add(R.id.replace_group, Menu.NONE, Menu.NONE, it) + } + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(this, cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_replace_rule_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (!cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + ImportReplaceRuleDialog.start(supportFragmentManager, it) + } + } + cancelButton() + }.show() + } + + private fun showHelp() { + val text = String(assets.open("help/replaceRuleHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) + } + + override fun onQueryTextChange(newText: String?): Boolean { + observeReplaceRuleData(newText) + return false + } + + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onDestroy() { + super.onDestroy() + Coroutine.async { ContentProcessor.upReplaceRules() } + } + + override fun upCountView() { + binding.selectActionBar.upCountView( + adapter.selection.size, + adapter.itemCount + ) + } + + override fun update(vararg rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.update(*rule) + } + + override fun delete(rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.delete(rule) + } + + override fun edit(rule: ReplaceRule) { + setResult(RESULT_OK) + editActivity.launch(ReplaceEditActivity.startIntent(this, rule.id)) + } + + override fun toTop(rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.toTop(rule) + } + + override fun toBottom(rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.toBottom(rule) + } + + override fun upOrder() { + setResult(RESULT_OK) + viewModel.upOrder() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt similarity index 54% rename from app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt rename to app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt index bcb29e87d..99bdddb64 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt @@ -1,29 +1,80 @@ -package io.legado.app.ui.replacerule +package io.legado.app.ui.replace import android.content.Context import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.widget.PopupMenu import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ItemReplaceRuleBinding import io.legado.app.lib.theme.backgroundColor import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback -import kotlinx.android.synthetic.main.item_replace_rule.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.ColorUtils + import java.util.* class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_replace_rule), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + ItemTouchCallback.Callback { private val selected = linkedSetOf() + val selection: LinkedHashSet + get() { + val selection = linkedSetOf() + getItems().map { + if (selected.contains(it)) { + selection.add(it) + } + } + return selection + } + + val diffItemCallBack = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean { + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.group != newItem.group) { + return false + } + if (oldItem.isEnabled != newItem.isEnabled) { + return false + } + return true + } + + override fun getChangePayload(oldItem: ReplaceRule, newItem: ReplaceRule): Any? { + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putString("name", newItem.name) + } + if (oldItem.group != newItem.group) { + payload.putString("group", newItem.group) + } + if (oldItem.isEnabled != newItem.isEnabled) { + payload.putBoolean("enabled", newItem.isEnabled) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + fun selectAll() { getItems().forEach { selected.add(it) @@ -44,63 +95,68 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : callBack.upCountView() } - fun getSelection(): LinkedHashSet { - val selection = linkedSetOf() - getItems().map { - if (selected.contains(it)) { - selection.add(it) - } - } - return selection + override fun getViewBinding(parent: ViewGroup): ItemReplaceRuleBinding { + return ItemReplaceRuleBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + callBack.upCountView() } - override fun convert(holder: ItemViewHolder, item: ReplaceRule, payloads: MutableList) { - with(holder.itemView) { + override fun convert( + holder: ItemViewHolder, + binding: ItemReplaceRuleBinding, + item: ReplaceRule, + payloads: MutableList + ) { + binding.run { val bundle = payloads.getOrNull(0) as? Bundle if (bundle == null) { - this.setBackgroundColor(context.backgroundColor) + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) if (item.group.isNullOrEmpty()) { - cb_name.text = item.name + cbName.text = item.name } else { - cb_name.text = + cbName.text = String.format("%s (%s)", item.name, item.group) } - swt_enabled.isChecked = item.isEnabled - cb_name.isChecked = selected.contains(item) + swtEnabled.isChecked = item.isEnabled + cbName.isChecked = selected.contains(item) } else { bundle.keySet().map { when (it) { - "selected" -> cb_name.isChecked = selected.contains(item) + "selected" -> cbName.isChecked = selected.contains(item) "name", "group" -> if (item.group.isNullOrEmpty()) { - cb_name.text = item.name + cbName.text = item.name } else { - cb_name.text = + cbName.text = String.format("%s (%s)", item.name, item.group) } - "enabled" -> swt_enabled.isChecked = item.isEnabled + "enabled" -> swtEnabled.isChecked = item.isEnabled } } } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - swt_enabled.setOnCheckedChangeListener { _, isChecked -> - getItem(holder.layoutPosition)?.let { - it.isEnabled = isChecked - callBack.update(it) + override fun registerListener(holder: ItemViewHolder, binding: ItemReplaceRuleBinding) { + binding.apply { + swtEnabled.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + getItem(holder.layoutPosition)?.let { + it.isEnabled = isChecked + callBack.update(it) + } } } - iv_edit.onClick { + ivEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.edit(it) } } - cb_name.onClick { + cbName.setOnClickListener { getItem(holder.layoutPosition)?.let { - if (cb_name.isChecked) { + if (cbName.isChecked) { selected.add(it) } else { selected.remove(it) @@ -108,8 +164,8 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : } callBack.upCountView() } - iv_menu_more.onClick { - showMenu(iv_menu_more, holder.layoutPosition) + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) } } } @@ -121,6 +177,7 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.menu_top -> callBack.toTop(item) + R.id.menu_bottom -> callBack.toBottom(item) R.id.menu_del -> callBack.delete(item) } true @@ -128,7 +185,7 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : popupMenu.show() } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { val srcItem = getItem(srcPosition) val targetItem = getItem(targetPosition) if (srcItem != null && targetItem != null) { @@ -142,8 +199,7 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : movedItems.add(targetItem) } } - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + swapItem(srcPosition, targetPosition) return true } @@ -156,8 +212,8 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : } } - fun initDragSelectTouchHelperCallback(): DragSelectTouchHelper.Callback { - return object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { override fun currentSelectedId(): MutableSet { return selected } @@ -180,13 +236,13 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : return false } } - } interface CallBack { fun update(vararg rule: ReplaceRule) fun delete(rule: ReplaceRule) fun edit(rule: ReplaceRule) fun toTop(rule: ReplaceRule) + fun toBottom(rule: ReplaceRule) fun upOrder() fun upCountView() } diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt similarity index 58% rename from app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt rename to app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt index afbbcdcf6..48a9abc2e 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt @@ -1,46 +1,69 @@ -package io.legado.app.ui.replacerule +package io.legado.app.ui.replace import android.app.Application import android.text.TextUtils import androidx.documentfile.provider.DocumentFile -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.ReplaceRule -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON -import io.legado.app.utils.splitNotBlank -import io.legado.app.utils.writeText -import org.jetbrains.anko.toast +import io.legado.app.utils.* import java.io.File class ReplaceRuleViewModel(application: Application) : BaseViewModel(application) { fun update(vararg rule: ReplaceRule) { execute { - App.db.replaceRuleDao().update(*rule) + appDb.replaceRuleDao.update(*rule) } } fun delete(rule: ReplaceRule) { execute { - App.db.replaceRuleDao().delete(rule) + appDb.replaceRuleDao.delete(rule) } } fun toTop(rule: ReplaceRule) { execute { - rule.order = App.db.replaceRuleDao().minOrder - 1 - App.db.replaceRuleDao().update(rule) + rule.order = appDb.replaceRuleDao.minOrder - 1 + appDb.replaceRuleDao.update(rule) + } + } + + fun topSelect(rules: LinkedHashSet) { + execute { + var minOrder = appDb.replaceRuleDao.minOrder - rules.size + rules.forEach { + it.order = ++minOrder + } + appDb.replaceRuleDao.update(*rules.toTypedArray()) + } + } + + fun toBottom(rule: ReplaceRule) { + execute { + rule.order = appDb.replaceRuleDao.maxOrder + 1 + appDb.replaceRuleDao.update(rule) + } + } + + fun bottomSelect(rules: LinkedHashSet) { + execute { + var maxOrder = appDb.replaceRuleDao.maxOrder + rules.forEach { + it.order = maxOrder++ + } + appDb.replaceRuleDao.update(*rules.toTypedArray()) } } fun upOrder() { execute { - val rules = App.db.replaceRuleDao().all + val rules = appDb.replaceRuleDao.all for ((index, rule) in rules.withIndex()) { rule.order = index + 1 } - App.db.replaceRuleDao().update(*rules.toTypedArray()) + appDb.replaceRuleDao.update(*rules.toTypedArray()) } } @@ -50,7 +73,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application rules.forEach { list.add(it.copy(isEnabled = true)) } - App.db.replaceRuleDao().update(*list.toTypedArray()) + appDb.replaceRuleDao.update(*list.toTypedArray()) } } @@ -60,13 +83,13 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application rules.forEach { list.add(it.copy(isEnabled = false)) } - App.db.replaceRuleDao().update(*list.toTypedArray()) + appDb.replaceRuleDao.update(*list.toTypedArray()) } } fun delSelection(rules: LinkedHashSet) { execute { - App.db.replaceRuleDao().delete(*rules.toTypedArray()) + appDb.replaceRuleDao.delete(*rules.toTypedArray()) } } @@ -76,9 +99,9 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application FileUtils.createFileIfNotExist(file, "exportReplaceRule.json") .writeText(json) }.onSuccess { - context.toast("成功导出至\n${file.absolutePath}") + context.toastOnUi("成功导出至\n${file.absolutePath}") }.onError { - context.toast("导出失败\n${it.localizedMessage}") + context.toastOnUi("导出失败\n${it.localizedMessage}") } } @@ -89,25 +112,25 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application doc.createFile("", "exportReplaceRule.json") ?.writeText(context, json) }.onSuccess { - context.toast("成功导出至\n${doc.uri.path}") + context.toastOnUi("成功导出至\n${doc.uri.path}") }.onError { - context.toast("导出失败\n${it.localizedMessage}") + context.toastOnUi("导出失败\n${it.localizedMessage}") } } fun addGroup(group: String) { execute { - val sources = App.db.replaceRuleDao().noGroup + val sources = appDb.replaceRuleDao.noGroup sources.map { source -> source.group = group } - App.db.replaceRuleDao().update(*sources.toTypedArray()) + appDb.replaceRuleDao.update(*sources.toTypedArray()) } } fun upGroup(oldGroup: String, newGroup: String?) { execute { - val sources = App.db.replaceRuleDao().getByGroup(oldGroup) + val sources = appDb.replaceRuleDao.getByGroup(oldGroup) sources.map { source -> source.group?.splitNotBlank(",")?.toHashSet()?.let { it.remove(oldGroup) @@ -116,21 +139,21 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application source.group = TextUtils.join(",", it) } } - App.db.replaceRuleDao().update(*sources.toTypedArray()) + appDb.replaceRuleDao.update(*sources.toTypedArray()) } } fun delGroup(group: String) { execute { execute { - val sources = App.db.replaceRuleDao().getByGroup(group) + val sources = appDb.replaceRuleDao.getByGroup(group) sources.map { source -> source.group?.splitNotBlank(",")?.toHashSet()?.let { it.remove(group) source.group = TextUtils.join(",", it) } } - App.db.replaceRuleDao().update(*sources.toTypedArray()) + appDb.replaceRuleDao.update(*sources.toTypedArray()) } } } diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt new file mode 100644 index 000000000..8fd22551a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt @@ -0,0 +1,185 @@ +package io.legado.app.ui.replace.edit + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.view.ViewTreeObserver +import android.widget.EditText +import android.widget.PopupWindow +import androidx.activity.viewModels +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ActivityReplaceEditBinding +import io.legado.app.lib.dialogs.selector +import io.legado.app.ui.widget.KeyboardToolPop +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.getSize +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlin.math.abs + +/** + * 编辑替换规则 + */ +class ReplaceEditActivity : + VMBaseActivity(false), + ViewTreeObserver.OnGlobalLayoutListener, + KeyboardToolPop.CallBack { + + companion object { + + fun startIntent( + context: Context, + id: Long = -1, + pattern: String? = null, + isRegex: Boolean = false, + scope: String? = null + ): Intent { + val intent = Intent(context, ReplaceEditActivity::class.java) + intent.putExtra("id", id) + intent.putExtra("pattern", pattern) + intent.putExtra("isRegex", isRegex) + intent.putExtra("scope", scope) + return intent + } + + } + + override val binding by viewBinding(ActivityReplaceEditBinding::inflate) + override val viewModel by viewModels() + + private var mSoftKeyboardTool: PopupWindow? = null + private var mIsSoftKeyBoardShowing = false + + override fun onActivityCreated(savedInstanceState: Bundle?) { + mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) + window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this) + viewModel.initData(intent) { + upReplaceView(it) + } + binding.ivHelp.setOnClickListener { + showRegexHelp() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.replace_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> { + val rule = getReplaceRule() + if (!rule.isValid()) { + toastOnUi(R.string.replace_rule_invalid) + } else { + viewModel.save(rule) { + setResult(RESULT_OK) + finish() + } + } + } + } + return true + } + + private fun upReplaceView(replaceRule: ReplaceRule) = binding.run { + etName.setText(replaceRule.name) + etGroup.setText(replaceRule.group) + etReplaceRule.setText(replaceRule.pattern) + cbUseRegex.isChecked = replaceRule.isRegex + etReplaceTo.setText(replaceRule.replacement) + etScope.setText(replaceRule.scope) + } + + private fun getReplaceRule(): ReplaceRule = binding.run { + val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule() + replaceRule.name = etName.text.toString() + replaceRule.group = etGroup.text.toString() + replaceRule.pattern = etReplaceRule.text.toString() + replaceRule.isRegex = cbUseRegex.isChecked + replaceRule.replacement = etReplaceTo.text.toString() + replaceRule.scope = etScope.text.toString() + return replaceRule + } + + private fun insertText(text: String) { + if (text.isBlank()) return + val view = window?.decorView?.findFocus() + if (view is EditText) { + val start = view.selectionStart + val end = view.selectionEnd + //TODO 获取EditText的文字 + val edit = view.editableText + if (start < 0 || start >= edit.length) { + edit.append(text) + } else { + //TODO 光标所在位置插入文字 + edit.replace(start, end, text) + } + } + } + + override fun sendText(text: String) { + if (text == AppConst.keyboardToolChars[0]) { + showHelpDialog() + } else { + insertText(text) + } + } + + private fun showHelpDialog() { + val items = arrayListOf("正则教程") + selector(getString(R.string.help), items) { _, index -> + when (index) { + 0 -> showRegexHelp() + } + } + } + + private fun showRegexHelp() { + val mdText = String(assets.open("help/regexHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + + private fun showKeyboardTopPopupWindow() { + mSoftKeyboardTool?.let { + if (it.isShowing) return + if (!isFinishing) { + it.showAtLocation(binding.llContent, Gravity.BOTTOM, 0, 0) + } + } + } + + private fun closePopupWindow() { + mSoftKeyboardTool?.dismiss() + } + + override fun onGlobalLayout() { + val rect = Rect() + // 获取当前页面窗口的显示范围 + window.decorView.getWindowVisibleDisplayFrame(rect) + val screenHeight = this.getSize().heightPixels + val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 + val preShowing = mIsSoftKeyBoardShowing + if (abs(keyboardHeight) > screenHeight / 5) { + mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 + binding.rootView.setPadding(0, 0, 0, 100) + showKeyboardTopPopupWindow() + } else { + mIsSoftKeyBoardShowing = false + binding.rootView.setPadding(0, 0, 0, 0) + if (preShowing) { + closePopupWindow() + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt new file mode 100644 index 000000000..fd37bb36d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt @@ -0,0 +1,47 @@ +package io.legado.app.ui.replace.edit + +import android.app.Application +import android.content.Intent +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule + +class ReplaceEditViewModel(application: Application) : BaseViewModel(application) { + + var replaceRule: ReplaceRule? = null + + fun initData(intent: Intent, finally: (replaceRule: ReplaceRule) -> Unit) { + execute { + val id = intent.getLongExtra("id", -1) + if (id > 0) { + replaceRule = appDb.replaceRuleDao.findById(id) + } else { + val pattern = intent.getStringExtra("pattern") ?: "" + val isRegex = intent.getBooleanExtra("isRegex", false) + val scope = intent.getStringExtra("scope") + replaceRule = ReplaceRule( + name = pattern, + pattern = pattern, + isRegex = isRegex, + scope = scope + ) + } + }.onFinally { + replaceRule?.let { + finally(it) + } + } + } + + fun save(replaceRule: ReplaceRule, success: () -> Unit) { + execute { + if (replaceRule.order == 0) { + replaceRule.order = appDb.replaceRuleDao.maxOrder + 1 + } + appDb.replaceRuleDao.insert(replaceRule) + }.onSuccess { + success() + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt deleted file mode 100644 index 62fe0cec3..000000000 --- a/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.legado.app.ui.replacerule - -import android.os.Bundle -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.ReplaceRule - -class DiffCallBack( - private val oldItems: List, - private val newItems: List -) : DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.id == newItem.id - } - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - if (oldItem.name != newItem.name) { - return false - } - if (oldItem.group != newItem.group) { - return false - } - if (oldItem.isEnabled != newItem.isEnabled) { - return false - } - return true - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - val payload = Bundle() - if (oldItem.name != newItem.name) { - payload.putString("name", newItem.name) - } - if (oldItem.group != newItem.group) { - payload.putString("group", newItem.group) - } - if (oldItem.isEnabled != newItem.isEnabled) { - payload.putBoolean("enabled", newItem.isEnabled) - } - if (payload.isEmpty) { - return null - } - return payload - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt deleted file mode 100644 index bf9cb5409..000000000 --- a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt +++ /dev/null @@ -1,154 +0,0 @@ -package io.legado.app.ui.replacerule - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.DialogFragment -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.AppPattern -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton -import io.legado.app.lib.theme.backgroundColor -import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.requestInputMethod -import io.legado.app.utils.splitNotBlank -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_group_manage.view.* -import org.jetbrains.anko.sdk27.listeners.onClick - -class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { - private lateinit var viewModel: ReplaceRuleViewModel - private lateinit var adapter: GroupAdapter - - override fun onStart() { - super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) - dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - viewModel = getViewModelOfActivity(ReplaceRuleViewModel::class.java) - return inflater.inflate(R.layout.dialog_recycler_view, container) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.setBackgroundColor(backgroundColor) - tool_bar.setBackgroundColor(primaryColor) - initData() - } - - private fun initData() { - tool_bar.title = getString(R.string.group_manage) - tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) - adapter = GroupAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, { - val groups = linkedSetOf() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) - } - adapter.setItems(groups.toList()) - }) - } - - override fun onMenuItemClick(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.menu_add -> addGroup() - } - return true - } - - @SuppressLint("InflateParams") - private fun addGroup() { - alert(title = getString(R.string.add_group)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - } - } - } - yesButton { - editText?.text?.toString()?.let { - if (it.isNotBlank()) { - viewModel.addGroup(it) - } - } - } - noButton() - }.show().applyTint().requestInputMethod() - } - - @SuppressLint("InflateParams") - private fun editGroup(group: String) { - alert(title = getString(R.string.group_edit)) { - var editText: EditText? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - setText(group) - } - } - } - yesButton { - viewModel.upGroup(group, editText?.text?.toString()) - } - noButton() - }.show().applyTint().requestInputMethod() - } - - private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_manage) { - - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - with(holder.itemView) { - setBackgroundColor(context.backgroundColor) - tv_group.text = item - } - } - - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - tv_edit.onClick { - getItem(holder.layoutPosition)?.let { - editGroup(it) - } - } - - tv_del.onClick { - getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt deleted file mode 100644 index 1f65877f8..000000000 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt +++ /dev/null @@ -1,318 +0,0 @@ -package io.legado.app.ui.replacerule - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.SubMenu -import androidx.appcompat.widget.PopupMenu -import androidx.appcompat.widget.SearchView -import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LiveData -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App -import io.legado.app.R -import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.AppPattern -import io.legado.app.data.entities.ReplaceRule -import io.legado.app.help.BookHelp -import io.legado.app.help.IntentDataHelp -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.lib.dialogs.* -import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.ui.association.ImportReplaceRuleActivity -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker -import io.legado.app.ui.replacerule.edit.ReplaceEditDialog -import io.legado.app.ui.widget.SelectActionBar -import io.legado.app.ui.widget.recycler.DragSelectTouchHelper -import io.legado.app.ui.widget.recycler.ItemTouchCallback -import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.ui.widget.text.AutoCompleteTextView -import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_replace_rule.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.view_search.* -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.toast -import java.io.File - - -class ReplaceRuleActivity : VMBaseActivity(R.layout.activity_replace_rule), - SearchView.OnQueryTextListener, - PopupMenu.OnMenuItemClickListener, - FileChooserDialog.CallBack, - ReplaceRuleAdapter.CallBack { - override val viewModel: ReplaceRuleViewModel - get() = getViewModel(ReplaceRuleViewModel::class.java) - private val importRecordKey = "replaceRuleRecordKey" - private val importRequestCode = 132 - private val exportRequestCode = 65 - private lateinit var adapter: ReplaceRuleAdapter - private var groups = hashSetOf() - private var groupMenu: SubMenu? = null - private var replaceRuleLiveData: LiveData>? = null - private var dataInit = false - - override fun onActivityCreated(savedInstanceState: Bundle?) { - initRecyclerView() - initSearchView() - initSelectActionView() - observeReplaceRuleData() - observeGroupData() - } - - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.replace_rule, menu) - return super.onCompatCreateOptionsMenu(menu) - } - - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - groupMenu = menu?.findItem(R.id.menu_group)?.subMenu - upGroupMenu() - return super.onPrepareOptionsMenu(menu) - } - - private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - adapter = ReplaceRuleAdapter(this, this) - recycler_view.adapter = adapter - recycler_view.addItemDecoration(VerticalDivider(this)) - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter - itemTouchCallback.isCanDrag = true - val dragSelectTouchHelper: DragSelectTouchHelper = - DragSelectTouchHelper(adapter.initDragSelectTouchHelperCallback()).setSlideArea(16, 50) - dragSelectTouchHelper.attachToRecyclerView(recycler_view) - // When this page is opened, it is in selection mode - dragSelectTouchHelper.activeSlideSelect() - - // Note: need judge selection first, so add ItemTouchHelper after it. - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) - } - - private fun initSearchView() { - ATH.setTint(search_view, primaryTextColor) - search_view.onActionViewExpanded() - search_view.queryHint = getString(R.string.replace_purify_search) - search_view.clearFocus() - search_view.setOnQueryTextListener(this) - } - - private fun initSelectActionView() { - select_action_bar.setMainActionText(R.string.delete) - select_action_bar.inflateMenu(R.menu.replace_rule_sel) - select_action_bar.setOnMenuItemClickListener(this) - select_action_bar.setCallBack(object : SelectActionBar.CallBack { - override fun selectAll(selectAll: Boolean) { - if (selectAll) { - adapter.selectAll() - } else { - adapter.revertSelection() - } - } - - override fun revertSelection() { - adapter.revertSelection() - } - - override fun onClickMainAction() { - this@ReplaceRuleActivity - .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { - okButton { viewModel.delSelection(adapter.getSelection()) } - noButton { } - } - .show().applyTint() - } - }) - } - - private fun observeReplaceRuleData(key: String? = null) { - dataInit = false - replaceRuleLiveData?.removeObservers(this) - replaceRuleLiveData = if (key.isNullOrEmpty()) { - App.db.replaceRuleDao().liveDataAll() - } else { - App.db.replaceRuleDao().liveDataSearch(key) - } - replaceRuleLiveData?.observe(this, { - if (dataInit) { - setResult(Activity.RESULT_OK) - } - val diffResult = - DiffUtil.calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), it)) - adapter.setItems(it, diffResult) - dataInit = true - upCountView() - }) - } - - private fun observeGroupData() { - App.db.replaceRuleDao().liveGroup().observe(this, { - groups.clear() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) - } - upGroupMenu() - }) - } - - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_add_replace_rule -> - ReplaceEditDialog().show(supportFragmentManager, "replaceNew") - R.id.menu_group_manage -> - GroupManageDialog().show(supportFragmentManager, "groupManage") - - R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) - R.id.menu_import_source_onLine -> showImportDialog() - R.id.menu_import_source_local -> FilePicker - .selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) - else -> if (item.groupId == R.id.replace_group) { - search_view.setQuery(item.title, true) - } - } - return super.onCompatOptionsItemSelected(item) - } - - override fun onMenuItemClick(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) - R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) - } - return false - } - - private fun upGroupMenu() { - groupMenu?.removeGroup(R.id.replace_group) - groups.map { - groupMenu?.add(R.id.replace_group, Menu.NONE, Menu.NONE, it) - } - } - - @SuppressLint("InflateParams") - private fun showImportDialog() { - val aCache = ACache.get(this, cacheDir = false) - val cacheUrls: MutableList = aCache - .getAsString(importRecordKey) - ?.splitNotBlank(",") - ?.toMutableList() ?: mutableListOf() - alert(titleResource = R.string.import_replace_rule_on_line) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setFilterValues(cacheUrls) - edit_view.delCallBack = { - cacheUrls.remove(it) - aCache.put(importRecordKey, cacheUrls.joinToString(",")) - } - } - } - okButton { - val text = editText?.text?.toString() - text?.let { - if (!cacheUrls.contains(it)) { - cacheUrls.add(0, it) - aCache.put(importRecordKey, cacheUrls.joinToString(",")) - } - startActivity("source" to it) - } - } - cancelButton() - }.show().applyTint() - } - - override fun onQueryTextChange(newText: String?): Boolean { - observeReplaceRuleData("%$newText%") - return false - } - - override fun onQueryTextSubmit(query: String?): Boolean { - return false - } - - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - importRequestCode -> { - startActivity("filePath" to currentPath) - } - exportRequestCode -> viewModel.exportSelection( - adapter.getSelection(), - File(currentPath) - ) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - importRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - try { - uri.readText(this)?.let { - val dataKey = IntentDataHelp.putData(it) - startActivity("dataKey" to dataKey) - } - } catch (e: Exception) { - toast("readTextError:${e.localizedMessage}") - } - } - } - exportRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - if (uri.toString().isContentPath()) { - DocumentFile.fromTreeUri(this, uri)?.let { - viewModel.exportSelection(adapter.getSelection(), it) - } - } else { - uri.path?.let { - viewModel.exportSelection(adapter.getSelection(), File(it)) - } - } - } - } - } - } - - override fun onDestroy() { - super.onDestroy() - Coroutine.async { BookHelp.upReplaceRules() } - } - - override fun upCountView() { - select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) - } - - override fun update(vararg rule: ReplaceRule) { - setResult(Activity.RESULT_OK) - viewModel.update(*rule) - } - - override fun delete(rule: ReplaceRule) { - setResult(Activity.RESULT_OK) - viewModel.delete(rule) - } - - override fun edit(rule: ReplaceRule) { - setResult(Activity.RESULT_OK) - ReplaceEditDialog.show(supportFragmentManager, rule.id) - } - - override fun toTop(rule: ReplaceRule) { - setResult(Activity.RESULT_OK) - viewModel.toTop(rule) - } - - override fun upOrder() { - setResult(Activity.RESULT_OK) - viewModel.upOrder() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt deleted file mode 100644 index 4bbfb44be..000000000 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt +++ /dev/null @@ -1,156 +0,0 @@ -package io.legado.app.ui.replacerule.edit - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.EditText -import android.widget.PopupWindow -import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.FragmentManager -import io.legado.app.R -import io.legado.app.base.BaseDialogFragment -import io.legado.app.constant.AppConst -import io.legado.app.data.entities.ReplaceRule -import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.widget.KeyboardToolPop -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModel -import io.legado.app.utils.toast -import kotlinx.android.synthetic.main.dialog_replace_edit.* -import org.jetbrains.anko.sdk27.listeners.onFocusChange - -class ReplaceEditDialog : BaseDialogFragment(), - Toolbar.OnMenuItemClickListener, - KeyboardToolPop.CallBack { - - companion object { - - fun show( - fragmentManager: FragmentManager, - id: Long = -1, - pattern: String? = null, - isRegex: Boolean = false, - scope: String? = null - ) { - val dialog = ReplaceEditDialog() - val bundle = Bundle() - bundle.putLong("id", id) - bundle.putString("pattern", pattern) - bundle.putBoolean("isRegex", isRegex) - bundle.putString("scope", scope) - dialog.arguments = bundle - dialog.show(fragmentManager, this::class.simpleName) - } - } - - private lateinit var viewModel: ReplaceEditViewModel - private lateinit var mSoftKeyboardTool: PopupWindow - - override fun onStart() { - super.onStart() - dialog?.window?.setLayout(MATCH_PARENT, WRAP_CONTENT) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - viewModel = getViewModel(ReplaceEditViewModel::class.java) - return inflater.inflate(R.layout.dialog_replace_edit, container) - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - mSoftKeyboardTool = KeyboardToolPop(requireContext(), AppConst.keyboardToolChars, this) - tool_bar.inflateMenu(R.menu.replace_edit) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) - viewModel.replaceRuleData.observe(viewLifecycleOwner, { - upReplaceView(it) - }) - arguments?.let { - viewModel.initData(it) - } - et_replace_rule.onFocusChange { v, hasFocus -> - if (hasFocus) { - mSoftKeyboardTool.width = v.width - mSoftKeyboardTool.showAsDropDown(v) - } else { - mSoftKeyboardTool.dismiss() - } - } - } - - override fun onMenuItemClick(item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.menu_save -> { - val rule = getReplaceRule() - if (!rule.isValid()){ - toast(R.string.replace_rule_invalid) - } - else{ - viewModel.save(rule) { - callBack?.onReplaceRuleSave() - dismiss() - } - } - } - } - return true - } - - private fun upReplaceView(replaceRule: ReplaceRule) { - et_name.setText(replaceRule.name) - et_group.setText(replaceRule.group) - et_replace_rule.setText(replaceRule.pattern) - cb_use_regex.isChecked = replaceRule.isRegex - et_replace_to.setText(replaceRule.replacement) - et_scope.setText(replaceRule.scope) - } - - private fun getReplaceRule(): ReplaceRule { - val replaceRule: ReplaceRule = viewModel.replaceRuleData.value ?: ReplaceRule() - replaceRule.name = et_name.text.toString() - replaceRule.group = et_group.text.toString() - replaceRule.pattern = et_replace_rule.text.toString() - replaceRule.isRegex = cb_use_regex.isChecked - replaceRule.replacement = et_replace_to.text.toString() - replaceRule.scope = et_scope.text.toString() - return replaceRule - } - - val callBack get() = activity as? CallBack - - private fun insertText(text: String) { - if (text.isBlank()) return - val view = dialog?.window?.decorView?.findFocus() - if (view is EditText) { - val start = view.selectionStart - val end = view.selectionEnd - val edit = view.editableText//获取EditText的文字 - if (start < 0 || start >= edit.length) { - edit.append(text) - } else { - edit.replace(start, end, text)//光标所在位置插入文字 - } - } - } - - override fun sendText(text: String) { - if (text == AppConst.keyboardToolChars[0]) { - val view = dialog?.window?.decorView?.findFocus() - view?.clearFocus() - } else { - insertText(text) - } - } - - interface CallBack { - fun onReplaceRuleSave() - } -} diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt deleted file mode 100644 index 1308b64ec..000000000 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.legado.app.ui.replacerule.edit - -import android.app.Application -import android.os.Bundle -import androidx.lifecycle.MutableLiveData -import io.legado.app.App -import io.legado.app.base.BaseViewModel -import io.legado.app.data.entities.ReplaceRule - -class ReplaceEditViewModel(application: Application) : BaseViewModel(application) { - - val replaceRuleData = MutableLiveData() - - fun initData(bundle: Bundle) { - execute { - replaceRuleData.value ?: let { - val id = bundle.getLong("id") - if (id > 0) { - App.db.replaceRuleDao().findById(id)?.let { - replaceRuleData.postValue(it) - } - } else { - val pattern = bundle.getString("pattern") ?: "" - val isRegex = bundle.getBoolean("isRegex") - val scope = bundle.getString("scope") - val rule = ReplaceRule( - name = pattern, - pattern = pattern, - isRegex = isRegex, - scope = scope - ) - replaceRuleData.postValue(rule) - } - } - } - } - - fun save(replaceRule: ReplaceRule, success: () -> Unit) { - execute { - if (replaceRule.order == 0) { - replaceRule.order = App.db.replaceRuleDao().maxOrder + 1 - } - App.db.replaceRuleDao().insert(replaceRule) - }.onSuccess { - success() - } - } - -} diff --git a/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt new file mode 100644 index 000000000..69eb4ccd6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt @@ -0,0 +1,16 @@ +package io.legado.app.ui.rss.article + +import android.content.Context +import androidx.viewbinding.ViewBinding +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssArticle + + +abstract class BaseRssArticlesAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + interface CallBack { + val isGridLayout: Boolean + fun readRss(rssArticle: RssArticle) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt index 13f3e59cb..cb5605bc9 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt @@ -1,34 +1,40 @@ package io.legado.app.ui.rss.article -import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable +import android.view.ViewGroup import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticleBinding import io.legado.app.help.ImageLoader +import io.legado.app.utils.getCompatColor import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_rss_article.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.textColorResource -class RssArticlesAdapter(context: Context, layoutId: Int, val callBack: CallBack) : - SimpleRecyclerAdapter(context, layoutId) { +class RssArticlesAdapter(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { - @SuppressLint("CheckResult") - override fun convert(holder: ItemViewHolder, item: RssArticle, payloads: MutableList) { - with(holder.itemView) { - tv_title.text = item.title - tv_pub_date.text = item.pubDate + override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding { + return ItemRssArticleBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticleBinding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate if (item.image.isNullOrBlank() && !callBack.isGridLayout) { - image_view.gone() + imageView.gone() } else { ImageLoader.load(context, item.image).apply { if (callBack.isGridLayout) { @@ -41,7 +47,7 @@ class RssArticlesAdapter(context: Context, layoutId: Int, val callBack: CallBack target: Target?, isFirstResource: Boolean ): Boolean { - image_view.gone() + imageView.gone() return false } @@ -52,32 +58,28 @@ class RssArticlesAdapter(context: Context, layoutId: Int, val callBack: CallBack dataSource: DataSource?, isFirstResource: Boolean ): Boolean { - image_view.visible() + imageView.visible() return false } }) } - }.into(image_view) + }.into(imageView) } if (item.read) { - tv_title.textColorResource = R.color.tv_text_summary + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) } else { - tv_title.textColorResource = R.color.primaryText + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.readRss(it) } } } - interface CallBack { - val isGridLayout: Boolean - fun readRss(rssArticle: RssArticle) - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt new file mode 100644 index 000000000..574825624 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt @@ -0,0 +1,85 @@ +package io.legado.app.ui.rss.article + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticle1Binding +import io.legado.app.help.ImageLoader +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssArticlesAdapter1(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticle1Binding { + return ItemRssArticle1Binding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticle1Binding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank() && !callBack.isGridLayout) { + imageView.gone() + } else { + ImageLoader.load(context, item.image).apply { + if (callBack.isGridLayout) { + placeholder(R.drawable.image_rss_article) + } else { + addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + } + }.into(imageView) + } + if (item.read) { + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) + } else { + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle1Binding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt new file mode 100644 index 000000000..c4fef291e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt @@ -0,0 +1,85 @@ +package io.legado.app.ui.rss.article + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticle2Binding +import io.legado.app.help.ImageLoader +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssArticlesAdapter2(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticle2Binding { + return ItemRssArticle2Binding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticle2Binding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank() && !callBack.isGridLayout) { + imageView.gone() + } else { + ImageLoader.load(context, item.image).apply { + if (callBack.isGridLayout) { + placeholder(R.drawable.image_rss_article) + } else { + addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + } + }.into(imageView) + } + if (item.read) { + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) + } else { + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle2Binding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt index f9b684592..e1e8175db 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt @@ -1,28 +1,32 @@ package io.legado.app.ui.rss.article + import android.os.Bundle import android.view.View -import androidx.lifecycle.LiveData +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment +import io.legado.app.data.appDb import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.FragmentRssArticlesBinding +import io.legado.app.databinding.ViewLoadMoreBinding import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.accentColor import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.utils.getViewModel -import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.startActivity -import kotlinx.android.synthetic.main.fragment_rss_articles.* -import kotlinx.android.synthetic.main.view_load_more.view.* -import kotlinx.android.synthetic.main.view_refresh_recycler.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch class RssArticlesFragment : VMBaseFragment(R.layout.fragment_rss_articles), - RssArticlesAdapter.CallBack { + BaseRssArticlesAdapter.CallBack { companion object { fun create(sortName: String, sortUrl: String): RssArticlesFragment { @@ -35,44 +39,47 @@ class RssArticlesFragment : VMBaseFragment(R.layout.fragme } } - private val activityViewModel: RssSortViewModel - get() = getViewModelOfActivity(RssSortViewModel::class.java) - override val viewModel: RssArticlesViewModel - get() = getViewModel(RssArticlesViewModel::class.java) - lateinit var adapter: RssArticlesAdapter + private val binding by viewBinding(FragmentRssArticlesBinding::bind) + private val activityViewModel by activityViewModels() + override val viewModel by viewModels() + lateinit var adapter: BaseRssArticlesAdapter<*> private lateinit var loadMoreView: LoadMoreView - private var rssArticlesData: LiveData>? = null + private var articlesFlowJob: Job? = null override val isGridLayout: Boolean get() = activityViewModel.isGridLayout override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { viewModel.init(arguments) initView() - refresh_recycler_view.startLoading() initView() initData() } - private fun initView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = if (activityViewModel.isGridLayout) { - recycler_view.setPadding(8, 0, 8, 0) + private fun initView() = binding.run { + refreshLayout.setColorSchemeColors(accentColor) + ATH.applyEdgeEffectColor(recyclerView) + recyclerView.layoutManager = if (activityViewModel.isGridLayout) { + recyclerView.setPadding(8, 0, 8, 0) GridLayoutManager(requireContext(), 2) } else { - recycler_view.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) LinearLayoutManager(requireContext()) } - adapter = RssArticlesAdapter(requireContext(), activityViewModel.layoutId, this) - recycler_view.adapter = adapter + adapter = when (activityViewModel.rssSource?.articleStyle) { + 1 -> RssArticlesAdapter1(requireContext(), this@RssArticlesFragment) + 2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment) + else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment) + } + recyclerView.adapter = adapter loadMoreView = LoadMoreView(requireContext()) - adapter.addFooterView(loadMoreView) - refresh_recycler_view.onRefreshStart = { - activityViewModel.rssSource?.let { - viewModel.loadContent(it) - } + adapter.addFooterView { + ViewLoadMoreBinding.bind(loadMoreView) } - recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { + refreshLayout.setOnRefreshListener { + loadArticles() + } + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) if (!recyclerView.canScrollVertically(1)) { @@ -80,22 +87,32 @@ class RssArticlesFragment : VMBaseFragment(R.layout.fragme } } }) + refreshLayout.post { + refreshLayout.isRefreshing = true + loadArticles() + } } private fun initData() { - activityViewModel.url?.let { - rssArticlesData?.removeObservers(this) - rssArticlesData = App.db.rssArticleDao().liveByOriginSort(it, viewModel.sortName) - rssArticlesData?.observe(viewLifecycleOwner, { list -> - adapter.setItems(list) - }) + val rssUrl = activityViewModel.url ?: return + articlesFlowJob?.cancel() + articlesFlowJob = launch { + appDb.rssArticleDao.flowByOriginSort(rssUrl, viewModel.sortName).collect { + adapter.setItems(it) + } + } + } + + private fun loadArticles() { + activityViewModel.rssSource?.let { + viewModel.loadContent(it) } } private fun scrollToBottom() { if (viewModel.isLoading) return if (loadMoreView.hasMore && adapter.getActualItemCount() > 0) { - loadMoreView.rotate_loading.show() + loadMoreView.startLoad() activityViewModel.rssSource?.let { viewModel.loadMore(it) } @@ -103,22 +120,22 @@ class RssArticlesFragment : VMBaseFragment(R.layout.fragme } override fun observeLiveBus() { - viewModel.loadFinally.observe(viewLifecycleOwner, { - refresh_recycler_view.stopLoading() + viewModel.loadFinally.observe(viewLifecycleOwner) { + binding.refreshLayout.isRefreshing = false if (it) { loadMoreView.startLoad() } else { loadMoreView.noMore() } - }) + } } override fun readRss(rssArticle: RssArticle) { activityViewModel.read(rssArticle) - startActivity( - Pair("title", rssArticle.title), - Pair("origin", rssArticle.origin), - Pair("link", rssArticle.link) - ) + startActivity { + putExtra("title", rssArticle.title) + putExtra("origin", rssArticle.origin) + putExtra("link", rssArticle.link) + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt index 44f125ef2..b30579356 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -3,11 +3,13 @@ package io.legado.app.ui.rss.article import android.app.Application import android.os.Bundle import androidx.lifecycle.MutableLiveData -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.model.rss.Rss +import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -30,16 +32,16 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application fun loadContent(rssSource: RssSource) { isLoading = true page = 1 - Rss.getArticles(sortName, sortUrl, rssSource, page) + Rss.getArticles(viewModelScope, sortName, sortUrl, rssSource, page) .onSuccess(Dispatchers.IO) { nextPageUrl = it.nextPageUrl it.articles.let { list -> list.forEach { rssArticle -> rssArticle.order = order-- } - App.db.rssArticleDao().insert(*list.toTypedArray()) + appDb.rssArticleDao.insert(*list.toTypedArray()) if (!rssSource.ruleNextPage.isNullOrEmpty()) { - App.db.rssArticleDao().clearOld(rssSource.sourceUrl, sortName, order) + appDb.rssArticleDao.clearOld(rssSource.sourceUrl, sortName, order) loadFinally.postValue(true) } else { withContext(Dispatchers.Main) { @@ -49,8 +51,9 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application isLoading = false } }.onError { + loadFinally.postValue(false) it.printStackTrace() - toast(it.localizedMessage) + context.toastOnUi(it.localizedMessage) } } @@ -59,7 +62,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application page++ val pageUrl = nextPageUrl if (!pageUrl.isNullOrEmpty()) { - Rss.getArticles(sortName, pageUrl, rssSource, page) + Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page) .onSuccess(Dispatchers.IO) { nextPageUrl = it.nextPageUrl loadMoreSuccess(it.articles) @@ -80,7 +83,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application return@let } val firstArticle = list.first() - val dbArticle = App.db.rssArticleDao() + val dbArticle = appDb.rssArticleDao .get(firstArticle.origin, firstArticle.link) if (dbArticle != null) { loadFinally.postValue(false) @@ -88,7 +91,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application list.forEach { rssArticle -> rssArticle.order = order-- } - App.db.rssArticleDao().insert(*list.toTypedArray()) + appDb.rssArticleDao.insert(*list.toTypedArray()) } } isLoading = false diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt index 307374f9f..ec7e366db 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt @@ -1,36 +1,45 @@ +@file:Suppress("DEPRECATION") + package io.legado.app.ui.rss.article -import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivityRssArtivlesBinding import io.legado.app.ui.rss.source.edit.RssSourceEditActivity -import io.legado.app.utils.getViewModel import io.legado.app.utils.gone +import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.activity_rss_artivles.* -import org.jetbrains.anko.startActivityForResult -class RssSortActivity : VMBaseActivity(R.layout.activity_rss_artivles) { +class RssSortActivity : VMBaseActivity() { - override val viewModel: RssSortViewModel - get() = getViewModel(RssSortViewModel::class.java) - private val editSource = 12319 - private val fragments = linkedMapOf() + override val binding by viewBinding(ActivityRssArtivlesBinding::inflate) + override val viewModel by viewModels() private lateinit var adapter: TabFragmentPageAdapter + private val fragments = linkedMapOf() + private val upSourceResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == RESULT_OK) { + viewModel.initData(intent) { + upFragments() + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { - adapter = TabFragmentPageAdapter(supportFragmentManager) - tab_layout.setupWithViewPager(view_pager) - view_pager.adapter = adapter + adapter = TabFragmentPageAdapter() + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) viewModel.titleLiveData.observe(this, { - title_bar.title = it + binding.titleBar.title = it }) viewModel.initData(intent) { upFragments() @@ -45,7 +54,10 @@ class RssSortActivity : VMBaseActivity(R.layout.activity_rss_a override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let { - startActivityForResult(editSource, Pair("data", it)) + upSourceResult.launch( + Intent(this, RssSourceEditActivity::class.java) + .putExtra("data", it) + ) } R.id.menu_clear -> { viewModel.url?.let { @@ -66,32 +78,21 @@ class RssSortActivity : VMBaseActivity(R.layout.activity_rss_a fragments[it.key] = RssArticlesFragment.create(it.key, it.value) } if (fragments.size == 1) { - tab_layout.gone() + binding.tabLayout.gone() } else { - tab_layout.visible() + binding.tabLayout.visible() } adapter.notifyDataSetChanged() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - editSource -> if (resultCode == Activity.RESULT_OK) { - viewModel.initData(intent) { - upFragments() - } - } - } - } - - private inner class TabFragmentPageAdapter(fm: FragmentManager) : - FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private inner class TabFragmentPageAdapter : + FragmentStatePagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { override fun getItemPosition(`object`: Any): Int { return POSITION_NONE } - override fun getPageTitle(position: Int): CharSequence? { + override fun getPageTitle(position: Int): CharSequence { return fragments.keys.elementAt(position) } @@ -102,6 +103,7 @@ class RssSortActivity : VMBaseActivity(R.layout.activity_rss_a override fun getCount(): Int { return fragments.size } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt index 135064bce..9a3b4be72 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt @@ -3,9 +3,8 @@ package io.legado.app.ui.rss.article import android.app.Application import android.content.Intent import androidx.lifecycle.MutableLiveData -import io.legado.app.App -import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssReadRecord import io.legado.app.data.entities.RssSource @@ -17,18 +16,12 @@ class RssSortViewModel(application: Application) : BaseViewModel(application) { val titleLiveData = MutableLiveData() var order = System.currentTimeMillis() val isGridLayout get() = rssSource?.articleStyle == 2 - val layoutId - get() = when (rssSource?.articleStyle) { - 1 -> R.layout.item_rss_article_1 - 2 -> R.layout.item_rss_article_2 - else -> R.layout.item_rss_article - } fun initData(intent: Intent, finally: () -> Unit) { execute { url = intent.getStringExtra("url") url?.let { url -> - rssSource = App.db.rssSourceDao().getByKey(url) + rssSource = appDb.rssSourceDao.getByKey(url) rssSource?.let { titleLiveData.postValue(it.sourceName) } ?: let { @@ -48,21 +41,21 @@ class RssSortViewModel(application: Application) : BaseViewModel(application) { it.articleStyle = 0 } execute { - App.db.rssSourceDao().update(it) + appDb.rssSourceDao.update(it) } } } fun read(rssArticle: RssArticle) { execute { - App.db.rssArticleDao().insertRecord(RssReadRecord(rssArticle.link)) + appDb.rssArticleDao.insertRecord(RssReadRecord(rssArticle.link)) } } fun clearArticles() { execute { url?.let { - App.db.rssArticleDao().delete(it) + appDb.rssArticleDao.delete(it) } order = System.currentTimeMillis() }.onSuccess { diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt index 09f6d1549..74710a7f2 100644 --- a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt @@ -1,23 +1,23 @@ package io.legado.app.ui.rss.favorites import android.os.Bundle -import androidx.lifecycle.LiveData import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App -import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.data.appDb import io.legado.app.data.entities.RssStar -import io.legado.app.lib.theme.ATH +import io.legado.app.databinding.ActivityRssFavoritesBinding import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.widget.recycler.VerticalDivider -import kotlinx.android.synthetic.main.view_refresh_recycler.* -import org.jetbrains.anko.startActivity +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch -class RssFavoritesActivity : BaseActivity(R.layout.activity_rss_favorites), +class RssFavoritesActivity : BaseActivity(), RssFavoritesAdapter.CallBack { - private var liveData: LiveData>? = null + override val binding by viewBinding(ActivityRssFavoritesBinding::inflate) private lateinit var adapter: RssFavoritesAdapter override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -26,26 +26,27 @@ class RssFavoritesActivity : BaseActivity(R.layout.activity_rss_favorites), } private fun initView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) - adapter = RssFavoritesAdapter(this, this) - recycler_view.adapter = adapter + binding.recyclerView.let { + it.layoutManager = LinearLayoutManager(this) + it.addItemDecoration(VerticalDivider(this)) + adapter = RssFavoritesAdapter(this, this) + it.adapter = adapter + } } private fun initData() { - liveData?.removeObservers(this) - liveData = App.db.rssStarDao().liveAll() - liveData?.observe(this, { - adapter.setItems(it) - }) + launch { + appDb.rssStarDao.liveAll().collect { + adapter.setItems(it) + } + } } override fun readRss(rssStar: RssStar) { - startActivity( - Pair("title", rssStar.title), - Pair("origin", rssStar.origin), - Pair("link", rssStar.link) - ) + startActivity { + putExtra("title", rssStar.title) + putExtra("origin", rssStar.origin) + putExtra("link", rssStar.link) + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt index 6077d5db9..71feb0e4d 100644 --- a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt @@ -2,29 +2,38 @@ package io.legado.app.ui.rss.favorites import android.content.Context import android.graphics.drawable.Drawable +import android.view.ViewGroup import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target -import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.RssStar +import io.legado.app.databinding.ItemRssArticleBinding import io.legado.app.help.ImageLoader import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_rss_article.view.* -import org.jetbrains.anko.sdk27.listeners.onClick + class RssFavoritesAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_rss_article) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding { + return ItemRssArticleBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: RssStar, payloads: MutableList) { - with(holder.itemView) { - tv_title.text = item.title - tv_pub_date.text = item.pubDate + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticleBinding, + item: RssStar, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate if (item.image.isNullOrBlank()) { - image_view.gone() + imageView.gone() } else { ImageLoader.load(context, item.image) .addListener(object : RequestListener { @@ -34,7 +43,7 @@ class RssFavoritesAdapter(context: Context, val callBack: CallBack) : target: Target?, isFirstResource: Boolean ): Boolean { - image_view.gone() + imageView.gone() return false } @@ -45,18 +54,18 @@ class RssFavoritesAdapter(context: Context, val callBack: CallBack) : dataSource: DataSource?, isFirstResource: Boolean ): Boolean { - image_view.visible() + imageView.visible() return false } }) - .into(image_view) + .into(imageView) } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.readRss(it) } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index 3e48c3ebf..1728f7320 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.rss.read import android.annotation.SuppressLint import android.app.DownloadManager -import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration import android.net.Uri @@ -10,42 +9,54 @@ import android.os.Bundle import android.os.Environment import android.view.* import android.webkit.* +import androidx.activity.viewModels import androidx.core.view.size +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.databinding.ActivityRssReadBinding +import io.legado.app.help.AppConfig import io.legado.app.lib.theme.DrawableUtils import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker +import io.legado.app.service.help.Download +import io.legado.app.ui.association.OnLineImportActivity +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_rss_read.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.launch import org.apache.commons.text.StringEscapeUtils -import org.jetbrains.anko.share import org.jsoup.Jsoup +import splitties.systemservices.downloadManager -class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_read, false), - FileChooserDialog.CallBack, +class ReadRssActivity : VMBaseActivity(false), ReadRssViewModel.CallBack { - override val viewModel: ReadRssViewModel - get() = getViewModel(ReadRssViewModel::class.java) - private val savePathRequestCode = 132 - private val imagePathKey = "" + override val binding by viewBinding(ActivityRssReadBinding::inflate) + override val viewModel by viewModels() + private val imagePathKey = "imagePath" private var starMenuItem: MenuItem? = null private var ttsMenuItem: MenuItem? = null private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null private var webPic: String? = null + private val saveImage = registerForActivityResult(FilePicker()) { + ACache.get(this).put(imagePathKey, it.toString()) + viewModel.saveImage(webPic, it.toString()) + } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel.callBack = this - title_bar.title = intent.getStringExtra("title") + binding.titleBar.title = intent.getStringExtra("title") initWebView() initLiveData() viewModel.initData(intent) } + @Suppress("DEPRECATION") + @SuppressLint("SwitchIntDef") override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) when (newConfig.orientation) { @@ -74,62 +85,34 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.menu_rss_refresh -> viewModel.refresh() R.id.menu_rss_star -> viewModel.favorite() R.id.menu_share_it -> viewModel.rssArticle?.let { share(it.link) - } + } ?: toastOnUi(R.string.null_url) R.id.menu_aloud -> readAloud() } return super.onCompatOptionsItemSelected(item) } - private fun initWebView() { - web_view.webChromeClient = object : WebChromeClient() { - override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR - ll_view.invisible() - custom_web_view.addView(view) - customWebViewCallback = callback - } - - override fun onHideCustomView() { - custom_web_view.removeAllViews() - ll_view.visible() - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - web_view.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading( - view: WebView?, - request: WebResourceRequest? - ): Boolean { - if (request?.url?.scheme == "http" || request?.url?.scheme == "https") { - return false - } - request?.url?.let { - openUrl(it) - } - return true - } + @JavascriptInterface + fun isNightTheme(): Boolean { + return AppConfig.isNightTheme(this) + } - @Suppress("DEPRECATION") - override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { - if (url?.startsWith("http", true) == true) { - return false - } - url?.let { - openUrl(it) - } - return true - } - } - web_view.settings.apply { + @SuppressLint("SetJavaScriptEnabled") + private fun initWebView() { + binding.webView.webChromeClient = RssWebChromeClient() + binding.webView.webViewClient = RssWebViewClient() + binding.webView.settings.apply { mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW domStorageEnabled = true allowContentAccess = true } - web_view.setOnLongClickListener { - val hitTestResult = web_view.hitTestResult + binding.webView.addJavascriptInterface(this, "app") + upWebViewTheme() + binding.webView.setOnLongClickListener { + val hitTestResult = binding.webView.hitTestResult if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE ) { @@ -141,9 +124,9 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r } return@setOnLongClickListener false } - web_view.setDownloadListener { url, _, contentDisposition, _, _ -> + binding.webView.setDownloadListener { url, _, contentDisposition, _, _ -> val fileName = URLUtil.guessFileName(url, contentDisposition, null) - ll_view.longSnackbar(fileName, getString(R.string.action_download)) { + binding.llView.longSnackbar(fileName, getString(R.string.action_download)) { // 指定下载地址 val request = DownloadManager.Request(Uri.parse(url)) // 允许媒体扫描,根据下载的文件类型被加入相册、音乐等媒体库 @@ -160,68 +143,85 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r request.setAllowedOverRoaming(true) // 允许下载的网路类型 request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI) + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) // 设置下载文件保存的路径和文件名 request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName) - val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager // 添加一个下载任务 val downloadId = downloadManager.enqueue(request) - print(downloadId) + Download.start(this, downloadId, fileName) } } + } private fun saveImage() { + val path = ACache.get(this@ReadRssActivity).getAsString(imagePathKey) + if (path.isNullOrEmpty()) { + selectSaveFolder() + } else { + viewModel.saveImage(webPic, path) + } + } + + private fun selectSaveFolder() { val default = arrayListOf() - val path = ACache.get(this).getAsString(imagePathKey) + val path = ACache.get(this@ReadRssActivity).getAsString(imagePathKey) if (!path.isNullOrEmpty()) { default.add(path) } - FilePicker.selectFolder( - this, - savePathRequestCode, - getString(R.string.save_image), - default - ) { - viewModel.saveImage(webPic, it) - } + saveImage.launch( + FilePickerParam( + otherActions = default.toTypedArray() + ) + ) } @SuppressLint("SetJavaScriptEnabled") private fun initLiveData() { - viewModel.contentLiveData.observe(this, { content -> + viewModel.contentLiveData.observe(this) { content -> viewModel.rssArticle?.let { upJavaScriptEnable() val url = NetworkUtils.getAbsoluteURL(it.origin, it.link) val html = viewModel.clHtml(content) if (viewModel.rssSource?.loadWithBaseUrl == true) { - web_view.loadDataWithBaseURL( - url, - html, - "text/html", - "utf-8", - url - )//不想用baseUrl进else + binding.webView + .loadDataWithBaseURL(url, html, "text/html", "utf-8", url)//不想用baseUrl进else } else { - web_view.loadDataWithBaseURL( - null, - html, - "text/html;charset=utf-8", - "utf-8", - url - ) + binding.webView + .loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url) } } - }) - viewModel.urlLiveData.observe(this, { + } + viewModel.urlLiveData.observe(this) { upJavaScriptEnable() - web_view.loadUrl(it.url, it.headerMap) - }) + binding.webView.loadUrl(it.url, it.headerMap) + } } @SuppressLint("SetJavaScriptEnabled") private fun upJavaScriptEnable() { if (viewModel.rssSource?.enableJs == true) { - web_view.settings.javaScriptEnabled = true + binding.webView.settings.javaScriptEnabled = true + } + } + + private fun upWebViewTheme() { + if (AppConfig.isNightTheme) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy( + binding.webView.settings, + WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING + ) + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + WebSettingsCompat.setForceDark( + binding.webView.settings, + WebSettingsCompat.FORCE_DARK_ON + ) + } else { + binding.webView + .evaluateJavascript(AppConst.darkWebViewJs, null) + } } } @@ -262,12 +262,12 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { event?.let { when (keyCode) { - KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled && web_view.canGoBack()) { - if (custom_web_view.size > 0) { + KeyEvent.KEYCODE_BACK -> if (event.isTracking && !event.isCanceled && binding.webView.canGoBack()) { + if (binding.customWebView.size > 0) { customWebViewCallback?.onCustomViewHidden() return true - } else if (web_view.copyBackForwardList().size > 1) { - web_view.goBack() + } else if (binding.webView.copyBackForwardList().size > 1) { + binding.webView.goBack() return true } } @@ -282,8 +282,8 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r viewModel.textToSpeech?.stop() upTtsMenu(false) } else { - web_view.settings.javaScriptEnabled = true - web_view.evaluateJavascript("document.documentElement.outerHTML") { + binding.webView.settings.javaScriptEnabled = true + binding.webView.evaluateJavascript("document.documentElement.outerHTML") { val html = StringEscapeUtils.unescapeJson(it) .replace("^\"|\"$".toRegex(), "") Jsoup.parse(html).text() @@ -292,27 +292,70 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r } } - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - savePathRequestCode -> { - ACache.get(this).put(imagePathKey, currentPath) - viewModel.saveImage(webPic, currentPath) - } + override fun onDestroy() { + super.onDestroy() + binding.webView.destroy() + } + + inner class RssWebChromeClient : WebChromeClient() { + override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + binding.llView.invisible() + binding.customWebView.addView(view) + customWebViewCallback = callback + } + + override fun onHideCustomView() { + binding.customWebView.removeAllViews() + binding.llView.visible() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - savePathRequestCode -> data?.data?.let { - onFilePicked(requestCode, it.toString()) + inner class RssWebViewClient : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + request?.let { + return shouldOverrideUrlLoading(it.url) + } + return true + } + + @Suppress("DEPRECATION") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + url?.let { + return shouldOverrideUrlLoading(Uri.parse(it)) + } + return true + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + upWebViewTheme() + } + + private fun shouldOverrideUrlLoading(url: Uri): Boolean { + when (url.scheme) { + "http", "https" -> { + return false + } + "legado", "yuedu" -> { + startActivity { + data = url + } + return true + } + else -> { + binding.root.longSnackbar("跳转其它应用", "确认") { + openUrl(url) + } + return true + } } } - } - override fun onDestroy() { - super.onDestroy() - web_view.destroy() } } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt index 11b4ba940..1badb5706 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -9,22 +9,20 @@ import android.util.Base64 import android.webkit.URLUtil import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.MutableLiveData -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppConst +import io.legado.app.data.appDb import io.legado.app.data.entities.RssArticle import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssStar -import io.legado.app.help.http.HttpHelper -import io.legado.app.model.rss.Rss +import io.legado.app.help.http.newCall +import io.legado.app.help.http.okHttpClient import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.utils.DocumentUtils -import io.legado.app.utils.FileUtils -import io.legado.app.utils.isContentPath -import io.legado.app.utils.writeBytes +import io.legado.app.model.rss.Rss +import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.launch import java.io.File import java.util.* @@ -45,22 +43,35 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), execute { val origin = intent.getStringExtra("origin") val link = intent.getStringExtra("link") - if (origin != null && link != null) { - rssSource = App.db.rssSourceDao().getByKey(origin) - rssStar = App.db.rssStarDao().get(origin, link) - rssArticle = rssStar?.toRssArticle() ?: App.db.rssArticleDao().get(origin, link) - rssArticle?.let { rssArticle -> - if (!rssArticle.description.isNullOrBlank()) { - contentLiveData.postValue(rssArticle.description) + origin?.let { + rssSource = appDb.rssSourceDao.getByKey(origin) + if (link != null) { + rssStar = appDb.rssStarDao.get(origin, link) + rssArticle = rssStar?.toRssArticle() ?: appDb.rssArticleDao.get(origin, link) + rssArticle?.let { rssArticle -> + if (!rssArticle.description.isNullOrBlank()) { + contentLiveData.postValue(rssArticle.description!!) + } else { + rssSource?.let { + val ruleContent = it.ruleContent + if (!ruleContent.isNullOrBlank()) { + loadContent(rssArticle, ruleContent) + } else { + loadUrl(rssArticle.link, rssArticle.origin) + } + } ?: loadUrl(rssArticle.link, rssArticle.origin) + } + } + } else { + val ruleContent = rssSource?.ruleContent + if (ruleContent.isNullOrBlank()) { + loadUrl(origin, origin) } else { - rssSource?.let { - val ruleContent = it.ruleContent - if (!ruleContent.isNullOrBlank()) { - loadContent(rssArticle, ruleContent) - } else { - loadUrl(rssArticle) - } - } ?: loadUrl(rssArticle) + val rssArticle = RssArticle() + rssArticle.origin = origin + rssArticle.link = origin + rssArticle.title = rssSource!!.sourceName + loadContent(rssArticle, ruleContent) } } } @@ -69,10 +80,10 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } } - private fun loadUrl(rssArticle: RssArticle) { + private fun loadUrl(url: String, baseUrl: String) { val analyzeUrl = AnalyzeUrl( - rssArticle.link, - baseUrl = rssArticle.origin, + ruleUrl = url, + baseUrl = baseUrl, useWebView = true, headerMapF = rssSource?.getHeaderMap() ) @@ -80,25 +91,40 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } private fun loadContent(rssArticle: RssArticle, ruleContent: String) { - Rss.getContent(rssArticle, ruleContent, rssSource, this) - .onSuccess(IO) { body -> - rssArticle.description = body - App.db.rssArticleDao().insert(rssArticle) - rssStar?.let { - it.description = body - App.db.rssStarDao().insert(it) + rssSource?.let { source -> + Rss.getContent(viewModelScope, rssArticle, ruleContent, source) + .onSuccess(IO) { body -> + rssArticle.description = body + appDb.rssArticleDao.insert(rssArticle) + rssStar?.let { + it.description = body + appDb.rssStarDao.insert(it) + } + contentLiveData.postValue(body) } - contentLiveData.postValue(body) - } + } + } + + fun refresh() { + rssArticle?.let { rssArticle -> + rssSource?.let { + val ruleContent = it.ruleContent + if (!ruleContent.isNullOrBlank()) { + loadContent(rssArticle, ruleContent) + } else { + loadUrl(rssArticle.link, rssArticle.origin) + } + } ?: loadUrl(rssArticle.link, rssArticle.origin) + } } fun favorite() { execute { rssStar?.let { - App.db.rssStarDao().delete(it.origin, it.link) + appDb.rssStarDao.delete(it.origin, it.link) rssStar = null } ?: rssArticle?.toStar()?.let { - App.db.rssStarDao().insert(it) + appDb.rssStarDao.insert(it) rssStar = it } }.onSuccess { @@ -111,7 +137,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), execute { val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg" webData2bitmap(webPic)?.let { biteArray -> - if (path.isContentPath()) { + if (path.isContentScheme()) { val uri = Uri.parse(path) DocumentFile.fromTreeUri(context, uri)?.let { doc -> DocumentUtils.createFileIfNotExist(doc, fileName) @@ -123,15 +149,18 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } } ?: throw Throwable("NULL") }.onError { - toast("保存图片失败:${it.localizedMessage}") + context.toastOnUi("保存图片失败:${it.localizedMessage}") }.onSuccess { - toast("保存成功") + context.toastOnUi("保存成功") } } private suspend fun webData2bitmap(data: String): ByteArray? { return if (URLUtil.isValidUrl(data)) { - HttpHelper.simpleGetBytesAsync(data) + @Suppress("BlockingMethodInNonBlockingContext") + okHttpClient.newCall { + url(data) + }.bytes() } else { Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) } @@ -171,9 +200,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), ttsInitFinish = true play() } else { - launch { - toast(R.string.tts_init_failed) - } + context.toastOnUi(R.string.tts_init_failed) } } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt index b97bae3ea..d755c70fa 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt @@ -1,23 +1,26 @@ package io.legado.app.ui.rss.source.debug import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager +import android.view.Menu +import android.view.MenuItem +import android.widget.SearchView +import androidx.activity.viewModels import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivitySourceDebugBinding import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor -import io.legado.app.utils.getViewModel +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.gone -import kotlinx.android.synthetic.main.activity_source_debug.* -import kotlinx.android.synthetic.main.view_search.* +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.launch -import org.jetbrains.anko.toast -class RssSourceDebugActivity : VMBaseActivity(R.layout.activity_source_debug) { +class RssSourceDebugActivity : VMBaseActivity() { - override val viewModel: RssSourceDebugModel - get() = getViewModel(RssSourceDebugModel::class.java) + override val binding by viewBinding(ActivitySourceDebugBinding::inflate) + override val viewModel by viewModels() private lateinit var adapter: RssSourceDebugAdapter @@ -28,7 +31,7 @@ class RssSourceDebugActivity : VMBaseActivity(R.layout.acti launch { adapter.addItem(msg) if (state == -1 || state == 1000) { - rotate_loading.hide() + binding.rotateLoading.hide() } } } @@ -37,24 +40,38 @@ class RssSourceDebugActivity : VMBaseActivity(R.layout.acti } } + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_source_debug, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_list_src -> + TextDialog.show(supportFragmentManager, viewModel.listSrc) + R.id.menu_content_src -> + TextDialog.show(supportFragmentManager, viewModel.contentSrc) + } + return super.onCompatOptionsItemSelected(item) + } + private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(binding.recyclerView) adapter = RssSourceDebugAdapter(this) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter - rotate_loading.loadingColor = accentColor + binding.recyclerView.adapter = adapter + binding.rotateLoading.loadingColor = accentColor } private fun initSearchView() { - search_view.gone() + binding.titleBar.findViewById(R.id.search_view).gone() } private fun startSearch() { adapter.clearItems() viewModel.startDebug({ - rotate_loading.show() + binding.rotateLoading.show() }, { - toast("未获取到源") + toastOnUi("未获取到源") }) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt index 3d5262db4..dc92e12d6 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt @@ -2,32 +2,43 @@ package io.legado.app.ui.rss.source.debug import android.content.Context import android.view.View +import android.view.ViewGroup import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import kotlinx.android.synthetic.main.item_log.view.* +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemLogBinding class RssSourceDebugAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_log) { - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - holder.itemView.apply { - if (text_view.getTag(R.id.tag1) == null) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { val listener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { - text_view.isCursorVisible = false - text_view.isCursorVisible = true + textView.isCursorVisible = false + textView.isCursorVisible = true } override fun onViewDetachedFromWindow(v: View) {} } - text_view.addOnAttachStateChangeListener(listener) - text_view.setTag(R.id.tag1, listener) + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) } - text_view.text = item + textView.text = item } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { //nothing } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt index c383d72d6..23892dc6f 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt @@ -1,22 +1,23 @@ package io.legado.app.ui.rss.source.debug import android.app.Application -import io.legado.app.App +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb import io.legado.app.data.entities.RssSource import io.legado.app.model.Debug class RssSourceDebugModel(application: Application) : BaseViewModel(application), Debug.Callback { - private var rssSource: RssSource? = null - private var callback: ((Int, String) -> Unit)? = null + var listSrc: String? = null + var contentSrc: String? = null fun initData(sourceUrl: String?, finally: () -> Unit) { sourceUrl?.let { execute { - rssSource = App.db.rssSourceDao().getByKey(sourceUrl) + rssSource = appDb.rssSourceDao.getByKey(sourceUrl) }.onFinally { finally() } @@ -31,12 +32,16 @@ class RssSourceDebugModel(application: Application) : BaseViewModel(application) rssSource?.let { start?.invoke() Debug.callback = this - Debug.startDebug(it) + Debug.startDebug(viewModelScope, it) } ?: error?.invoke() } override fun printLog(state: Int, msg: String) { - callback?.invoke(state, msg) + when (state) { + 10 -> listSrc = msg + 20 -> contentSrc = msg + else -> callback?.invoke(state, msg) + } } override fun onCleared() { diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt index 34b795a2b..cd941c206 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.rss.source.edit import android.app.Activity -import android.content.Intent import android.graphics.Rect import android.os.Bundle import android.view.Gravity @@ -10,35 +9,43 @@ import android.view.MenuItem import android.view.ViewTreeObserver import android.widget.EditText import android.widget.PopupWindow -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.activity.viewModels +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ActivityRssSourceEditBinding +import io.legado.app.help.LocalConfig import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH -import io.legado.app.ui.qrcode.QrCodeActivity +import io.legado.app.ui.qrcode.QrCodeResult import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity import io.legado.app.ui.widget.KeyboardToolPop +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_rss_source_edit.* -import org.jetbrains.anko.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlin.math.abs class RssSourceEditActivity : - VMBaseActivity(R.layout.activity_rss_source_edit, false), + VMBaseActivity(false), ViewTreeObserver.OnGlobalLayoutListener, KeyboardToolPop.CallBack { + override val binding by viewBinding(ActivityRssSourceEditBinding::inflate) + override val viewModel by viewModels() private var mSoftKeyboardTool: PopupWindow? = null private var mIsSoftKeyBoardShowing = false - private val qrRequestCode = 101 private val adapter = RssSourceEditAdapter() private val sourceEntities: ArrayList = ArrayList() - - override val viewModel: RssSourceEditViewModel - get() = getViewModel(RssSourceEditViewModel::class.java) - + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it?.let { + viewModel.importSource(it) { source: RssSource -> + upRecyclerView(source) + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { initView() @@ -47,16 +54,23 @@ class RssSourceEditActivity : } } + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!LocalConfig.ruleHelpVersionIsLast) { + showRuleHelp() + } + } + override fun finish() { val source = getRssSource() - if (!source.equal(viewModel.rssSource ?: RssSource())) { + if (!source.equal(viewModel.rssSource)) { alert(R.string.exit) { - messageResource = R.string.exit_no_save + setMessage(R.string.exit_no_save) positiveButton(R.string.yes) negativeButton(R.string.no) { super.finish() } - }.show().applyTint() + }.show() } else { super.finish() } @@ -69,6 +83,7 @@ class RssSourceEditActivity : override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.source_edit, menu) + menu.findItem(R.id.menu_login).isVisible = false return super.onCompatCreateOptionsMenu(menu) } @@ -87,32 +102,39 @@ class RssSourceEditActivity : val source = getRssSource() if (checkSource(source)) { viewModel.save(source) { - startActivity(Pair("key", source.sourceUrl)) + startActivity { + putExtra("key", source.sourceUrl) + } } } } R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource())) - R.id.menu_qr_code_camera -> startActivityForResult(qrRequestCode) + R.id.menu_qr_code_camera -> qrCodeResult.launch(null) R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_share_str -> share(GSON.toJson(getRssSource())) - R.id.menu_share_qr -> shareWithQr(getString(R.string.share_rss_source), GSON.toJson(getRssSource())) + R.id.menu_share_qr -> shareWithQr( + GSON.toJson(getRssSource()), + getString(R.string.share_rss_source), + ErrorCorrectionLevel.L + ) + R.id.menu_help -> showRuleHelp() } return super.onCompatOptionsItemSelected(item) } private fun initView() { - ATH.applyEdgeEffectColor(recycler_view) + ATH.applyEdgeEffectColor(binding.recyclerView) mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.adapter = adapter + binding.recyclerView.adapter = adapter } private fun upRecyclerView(rssSource: RssSource? = viewModel.rssSource) { rssSource?.let { - cb_is_enable.isChecked = rssSource.enabled - cb_enable_js.isChecked = rssSource.enableJs - cb_enable_base_url.isChecked = rssSource.loadWithBaseUrl + binding.cbIsEnable.isChecked = rssSource.enabled + binding.cbSingleUrl.isChecked = rssSource.singleUrl + binding.cbEnableJs.isChecked = rssSource.enableJs + binding.cbEnableBaseUrl.isChecked = rssSource.loadWithBaseUrl } sourceEntities.clear() sourceEntities.apply { @@ -120,6 +142,7 @@ class RssSourceEditActivity : add(EditEntity("sourceUrl", rssSource?.sourceUrl, R.string.source_url)) add(EditEntity("sourceIcon", rssSource?.sourceIcon, R.string.source_icon)) add(EditEntity("sourceGroup", rssSource?.sourceGroup, R.string.source_group)) + add(EditEntity("sourceComment", rssSource?.sourceComment, R.string.comment)) add(EditEntity("sortUrl", rssSource?.sortUrl, R.string.sort_url)) add(EditEntity("ruleArticles", rssSource?.ruleArticles, R.string.r_articles)) add(EditEntity("ruleNextPage", rssSource?.ruleNextPage, R.string.r_next)) @@ -136,16 +159,18 @@ class RssSourceEditActivity : } private fun getRssSource(): RssSource { - val source = viewModel.rssSource?.copy() ?: RssSource() - source.enabled = cb_is_enable.isChecked - source.enableJs = cb_enable_js.isChecked - source.loadWithBaseUrl = cb_enable_base_url.isChecked + val source = viewModel.rssSource + source.enabled = binding.cbIsEnable.isChecked + source.singleUrl = binding.cbSingleUrl.isChecked + source.enableJs = binding.cbEnableJs.isChecked + source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked sourceEntities.forEach { when (it.key) { "sourceName" -> source.sourceName = it.value ?: "" "sourceUrl" -> source.sourceUrl = it.value ?: "" "sourceIcon" -> source.sourceIcon = it.value ?: "" "sourceGroup" -> source.sourceGroup = it.value + "sourceComment" -> source.sourceComment = it.value "sortUrl" -> source.sortUrl = it.value "ruleArticles" -> source.ruleArticles = it.value "ruleNextPage" -> source.ruleNextPage = it.value @@ -164,7 +189,7 @@ class RssSourceEditActivity : private fun checkSource(source: RssSource): Boolean { if (source.sourceName.isBlank() || source.sourceName.isBlank()) { - toast("名称或url不能为空") + toastOnUi("名称或url不能为空") return false } return true @@ -187,17 +212,38 @@ class RssSourceEditActivity : override fun sendText(text: String) { if (text == AppConst.keyboardToolChars[0]) { - insertText(AppConst.urlOption) + showHelpDialog() } else { insertText(text) } } + private fun showHelpDialog() { + val items = arrayListOf("插入URL参数", "订阅源教程", "正则教程") + selector(getString(R.string.help), items) { _, index -> + when (index) { + 0 -> insertText(AppConst.urlOption) + 1 -> showRuleHelp() + 2 -> showRegexHelp() + } + } + } + + private fun showRuleHelp() { + val mdText = String(assets.open("help/ruleHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + + private fun showRegexHelp() { + val mdText = String(assets.open("help/regexHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, mdText, TextDialog.MD) + } + private fun showKeyboardTopPopupWindow() { mSoftKeyboardTool?.let { if (it.isShowing) return if (!isFinishing) { - it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + it.showAtLocation(binding.root, Gravity.BOTTOM, 0, 0) } } } @@ -210,32 +256,20 @@ class RssSourceEditActivity : val rect = Rect() // 获取当前页面窗口的显示范围 window.decorView.getWindowVisibleDisplayFrame(rect) - val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels + val screenHeight = this@RssSourceEditActivity.getSize().heightPixels val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 val preShowing = mIsSoftKeyBoardShowing if (abs(keyboardHeight) > screenHeight / 5) { mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 - recycler_view.setPadding(0, 0, 0, 100) + binding.recyclerView.setPadding(0, 0, 0, 100) showKeyboardTopPopupWindow() } else { mIsSoftKeyBoardShowing = false - recycler_view.setPadding(0, 0, 0, 0) + binding.recyclerView.setPadding(0, 0, 0, 0) if (preShowing) { closePopupWindow() } } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - qrRequestCode -> if (resultCode == RESULT_OK) { - data?.getStringExtra("result")?.let { - viewModel.importSource(it) { source: RssSource -> - upRecyclerView(source) - } - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt index b467c9b48..9bccbcf04 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.rss.source.edit +import android.annotation.SuppressLint import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater @@ -7,22 +8,21 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.legado.app.R -import kotlinx.android.synthetic.main.item_source_edit.view.* +import io.legado.app.databinding.ItemSourceEditBinding class RssSourceEditAdapter : RecyclerView.Adapter() { var editEntities: ArrayList = ArrayList() + @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { - return MyViewHolder( - LayoutInflater.from( - parent.context - ).inflate(R.layout.item_source_edit, parent, false) - ) + val binding = ItemSourceEditBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + return MyViewHolder(binding) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { @@ -33,8 +33,9 @@ class RssSourceEditAdapter : RecyclerView.Adapter Unit) { execute { val key = intent.getStringExtra("data") - var source: RssSource? = null if (key != null) { - source = App.db.rssSourceDao().getByKey(key) - } - source?.let { - oldSourceUrl = it.sourceUrl - rssSource = it + appDb.rssSourceDao.getByKey(key)?.let { + rssSource = it + } } + oldSourceUrl = rssSource.sourceUrl }.onFinally { onFinally() } @@ -33,18 +29,15 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati fun save(source: RssSource, success: (() -> Unit)) { execute { - oldSourceUrl?.let { - if (oldSourceUrl != source.sourceUrl) { - App.db.rssSourceDao().delete(it) - } + if (oldSourceUrl != source.sourceUrl) { + appDb.rssSourceDao.delete(oldSourceUrl) + oldSourceUrl = source.sourceUrl } - oldSourceUrl = source.sourceUrl - App.db.rssSourceDao().insert(source) - rssSource = source + appDb.rssSourceDao.insert(source) }.onSuccess { success() }.onError { - toast(it.localizedMessage) + context.toastOnUi(it.localizedMessage) it.printStackTrace() } } @@ -57,12 +50,12 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati } source }.onError { - toast(it.localizedMessage) + context.toastOnUi(it.localizedMessage) }.onSuccess { if (it != null) { onSuccess(it) } else { - toast("格式不对") + context.toastOnUi("格式不对") } } } @@ -74,7 +67,7 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati finally.invoke(it) } }.onError { - toast(it.localizedMessage ?: "Error") + context.toastOnUi(it.msg) } } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt deleted file mode 100644 index cb539e803..000000000 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/DiffCallBack.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.legado.app.ui.rss.source.manage - -import android.os.Bundle -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.RssSource - -class DiffCallBack( - private val oldItems: List, - private val newItems: List -) : DiffUtil.Callback() { - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.sourceUrl == newItem.sourceUrl - } - - override fun getOldListSize(): Int { - return oldItems.size - } - - override fun getNewListSize(): Int { - return newItems.size - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - return oldItem.sourceName == newItem.sourceName - && oldItem.sourceGroup == newItem.sourceGroup - && oldItem.enabled == newItem.enabled - } - - override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { - val oldItem = oldItems[oldItemPosition] - val newItem = newItems[newItemPosition] - val payload = Bundle() - if (oldItem.sourceName != newItem.sourceName) { - payload.putString("name", newItem.sourceName) - } - if (oldItem.sourceGroup != newItem.sourceGroup) { - payload.putString("group", newItem.sourceGroup) - } - if (oldItem.enabled != newItem.enabled) { - payload.putBoolean("enabled", newItem.enabled) - } - if (payload.isEmpty) { - return null - } - return payload - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt index 48180951f..5fb2d2bad 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt @@ -3,41 +3,41 @@ package io.legado.app.ui.rss.source.manage import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.EditText import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.primaryColor import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_group_manage.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { - private lateinit var viewModel: RssSourceViewModel + private val viewModel: RssSourceViewModel by activityViewModels() private lateinit var adapter: GroupAdapter + private val binding by viewBinding(DialogRecyclerViewBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -46,30 +46,37 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModelOfActivity(RssSourceViewModel::class.java) return inflater.inflate(R.layout.dialog_recycler_view, container) } - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.title = getString(R.string.group_manage) - tool_bar.inflateMenu(R.menu.group_manage) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.title = getString(R.string.group_manage) + toolBar.inflateMenu(R.menu.group_manage) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@GroupManageDialog) adapter = GroupAdapter(requireContext()) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration(VerticalDivider(requireContext())) - recycler_view.adapter = adapter - tv_ok.setTextColor(requireContext().accentColor) - tv_ok.visible() - tv_ok.onClick { dismiss() } - App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, { - val groups = linkedSetOf() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + tvOk.setTextColor(requireContext().accentColor) + tvOk.visible() + tvOk.setOnClickListener { + dismissAllowingStateLoss() + } + initData() + } + + private fun initData() { + launch { + appDb.rssSourceDao.flowGroup().collect { + val groups = linkedSetOf() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + adapter.setItems(groups.toList()) } - adapter.setItems(groups.toList()) - }) + } } override fun onMenuItemClick(item: MenuItem?): Boolean { @@ -82,62 +89,68 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener @SuppressLint("InflateParams") private fun addGroup() { alert(title = getString(R.string.add_group)) { - var editText: EditText? = null + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - } - } + alertBinding.apply { + editView.setHint(R.string.group_name) + }.root } yesButton { - editText?.text?.toString()?.let { + alertBinding.editView.text?.toString()?.let { if (it.isNotBlank()) { viewModel.addGroup(it) } } } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } @SuppressLint("InflateParams") private fun editGroup(group: String) { alert(title = getString(R.string.group_edit)) { - var editText: EditText? = null + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "分组名称" - setText(group) - } - } + alertBinding.apply { + editView.setHint(R.string.group_name) + editView.setText(group) + }.root } yesButton { - viewModel.upGroup(group, editText?.text?.toString()) + viewModel.upGroup(group, alertBinding.editView.text?.toString()) } noButton() - }.show().applyTint().requestInputMethod() + }.show().requestInputMethod() } private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_manage) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - with(holder.itemView) { - tv_group.text = item + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - tv_edit.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } - tv_del.onClick { + tvDel.setOnClickListener { getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt index 05885d183..cf3e97d96 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt @@ -1,71 +1,89 @@ package io.legado.app.ui.rss.source.manage import android.annotation.SuppressLint -import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.SubMenu +import androidx.activity.viewModels import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LiveData -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb import io.legado.app.data.entities.RssSource -import io.legado.app.help.IntentDataHelp -import io.legado.app.lib.dialogs.* +import io.legado.app.databinding.ActivityRssSourceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.ui.association.ImportRssSourceActivity -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker -import io.legado.app.ui.qrcode.QrCodeActivity +import io.legado.app.ui.association.ImportRssSourceDialog +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam +import io.legado.app.ui.qrcode.QrCodeResult import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.VerticalDivider -import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* -import kotlinx.android.synthetic.main.activity_rss_source.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.view_search.* -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.toast +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import java.io.File -import java.text.Collator -import java.util.* - -class RssSourceActivity : VMBaseActivity(R.layout.activity_rss_source), +/** + * 订阅源管理 + */ +class RssSourceActivity : VMBaseActivity(), PopupMenu.OnMenuItemClickListener, - FileChooserDialog.CallBack, + SelectActionBar.CallBack, RssSourceAdapter.CallBack { - override val viewModel: RssSourceViewModel - get() = getViewModel(RssSourceViewModel::class.java) + override val binding by viewBinding(ActivityRssSourceBinding::inflate) + override val viewModel by viewModels() private val importRecordKey = "rssSourceRecordKey" - private val qrRequestCode = 101 - private val importRequestCode = 124 - private val exportRequestCode = 65 private lateinit var adapter: RssSourceAdapter - private var sourceLiveData: LiveData>? = null + private var sourceFlowJob: Job? = null private var groups = hashSetOf() private var groupMenu: SubMenu? = null + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + ImportRssSourceDialog.start(supportFragmentManager, it) + } + private val importDoc = registerForActivityResult(FilePicker()) { uri -> + kotlin.runCatching { + uri?.readText(this)?.let { + ImportRssSourceDialog.start(supportFragmentManager, it) + } + }.onFailure { + toastOnUi("readTextError:${it.localizedMessage}") + } + } + private val exportDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(this, uri)?.let { + viewModel.exportSelection(adapter.selection, it) + } + } else { + uri.path?.let { + viewModel.exportSelection(adapter.selection, File(it)) + } + } + } override fun onActivityCreated(savedInstanceState: Bundle?) { initRecyclerView() initSearchView() - initLiveDataGroup() - initLiveDataSource() + initGroupFlow() + upSourceFlow() initViewEvent() } @@ -83,14 +101,24 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_add -> startActivity() - R.id.menu_import_source_local -> FilePicker - .selectFile(this, importRequestCode, allowExtensions = arrayOf("txt", "json")) - R.id.menu_import_source_onLine -> showImportDialog() - R.id.menu_import_source_qr -> startActivityForResult(qrRequestCode) + R.id.menu_import_local -> importDoc.launch( + FilePickerParam( + mode = FilePicker.FILE, + allowExtensions = arrayOf("txt", "json") + ) + ) + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_qr -> qrCodeResult.launch(null) R.id.menu_group_manage -> GroupManageDialog() .show(supportFragmentManager, "rssGroupManage") + R.id.menu_share_source -> viewModel.shareSelection(adapter.selection) { + startActivity(Intent.createChooser(it, getString(R.string.share_selected_source))) + } + R.id.menu_import_default -> viewModel.importDefault() + R.id.menu_help -> showHelp() else -> if (item.groupId == R.id.source_group) { - search_view.setQuery(item.title, true) + binding.titleBar.findViewById(R.id.search_view) + .setQuery("group:${item.title}", true) } } return super.onCompatOptionsItemSelected(item) @@ -98,116 +126,132 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) - R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) - R.id.menu_export_selection -> FilePicker.selectFolder(this, exportRequestCode) - R.id.menu_top_sel -> viewModel.topSource(*adapter.getSelection().toTypedArray()) - R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.getSelection().toTypedArray()) + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_del_selection -> viewModel.delSelection(adapter.selection) + R.id.menu_export_selection -> exportDir.launch(null) + R.id.menu_top_sel -> viewModel.topSource(*adapter.selection.toTypedArray()) + R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray()) } return true } private fun initRecyclerView() { - ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) + ATH.applyEdgeEffectColor(binding.recyclerView) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) adapter = RssSourceAdapter(this, this) - recycler_view.adapter = adapter - val itemTouchCallback = ItemTouchCallback() - itemTouchCallback.onItemTouchCallbackListener = adapter - itemTouchCallback.isCanDrag = true - val dragSelectTouchHelper: DragSelectTouchHelper = - DragSelectTouchHelper(adapter.initDragSelectTouchHelperCallback()).setSlideArea(16, 50) - dragSelectTouchHelper.attachToRecyclerView(recycler_view) + binding.recyclerView.adapter = adapter // When this page is opened, it is in selection mode + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) dragSelectTouchHelper.activeSlideSelect() - // Note: need judge selection first, so add ItemTouchHelper after it. - ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) } private fun initSearchView() { - ATH.setTint(search_view, primaryTextColor) - search_view.onActionViewExpanded() - search_view.queryHint = getString(R.string.search_rss_source) - search_view.clearFocus() - search_view.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String?): Boolean { - return false - } + binding.titleBar.findViewById(R.id.search_view).let { + ATH.setTint(it, primaryTextColor) + it.onActionViewExpanded() + it.queryHint = getString(R.string.search_rss_source) + it.clearFocus() + it.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } - override fun onQueryTextChange(newText: String?): Boolean { - initLiveDataSource(newText) - return false - } - }) + override fun onQueryTextChange(newText: String?): Boolean { + upSourceFlow(newText) + return false + } + }) + } } - private fun initLiveDataGroup() { - App.db.rssSourceDao().liveGroup().observe(this, { - groups.clear() - it.map { group -> - groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + private fun initGroupFlow() { + launch { + appDb.rssSourceDao.flowGroup().collect { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + upGroupMenu() } - upGroupMenu() - }) + } + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + delSourceDialog() } private fun initViewEvent() { - select_action_bar.setMainActionText(R.string.delete) - select_action_bar.inflateMenu(R.menu.rss_source_sel) - select_action_bar.setOnMenuItemClickListener(this) - select_action_bar.setCallBack(object : SelectActionBar.CallBack { - override fun selectAll(selectAll: Boolean) { - if (selectAll) { - adapter.selectAll() - } else { - adapter.revertSelection() - } - } + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.rss_source_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } - override fun revertSelection() { - adapter.revertSelection() - } + private fun delSourceDialog() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.selection) } + noButton() + }.show() + } - override fun onClickMainAction() { - this@RssSourceActivity - .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { - okButton { viewModel.delSelection(adapter.getSelection()) } - noButton { } - } - .show().applyTint() - } - }) + private fun upGroupMenu() = groupMenu?.let { menu -> + menu.removeGroup(R.id.source_group) + groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + }.map { + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, it) + } } - private fun upGroupMenu() { - groupMenu?.removeGroup(R.id.source_group) - groups.sortedWith(Collator.getInstance(Locale.CHINESE)) - .map { - groupMenu?.add(R.id.source_group, Menu.NONE, Menu.NONE, it) + private fun upSourceFlow(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = launch { + when { + searchKey.isNullOrBlank() -> { + appDb.rssSourceDao.flowAll() + } + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.rssSourceDao.flowGroupSearch("%$key%") + } + else -> { + appDb.rssSourceDao.flowSearch("%$searchKey%") + } + }.collect { + adapter.setItems(it, adapter.diffItemCallback) } + } } - private fun initLiveDataSource(key: String? = null) { - sourceLiveData?.removeObservers(this) - sourceLiveData = - if (key.isNullOrBlank()) { - App.db.rssSourceDao().liveAll() - } else { - App.db.rssSourceDao().liveSearch("%$key%") - } - sourceLiveData?.observe(this, { - val diffResult = DiffUtil - .calculateDiff(DiffCallBack(adapter.getItems(), it)) - adapter.setItems(it, diffResult) - upCountView() - }) + private fun showHelp() { + val text = String(assets.open("help/SourceMRssHelp.md").readBytes()) + TextDialog.show(supportFragmentManager, text, TextDialog.MD) } override fun upCountView() { - select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) + binding.selectActionBar.upCountView( + adapter.selection.size, + adapter.itemCount + ) } @SuppressLint("InflateParams") @@ -217,78 +261,27 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r .getAsString(importRecordKey) ?.splitNotBlank(",") ?.toMutableList() ?: mutableListOf() - alert(titleResource = R.string.import_book_source_on_line) { - var editText: AutoCompleteTextView? = null - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view - edit_view.setFilterValues(cacheUrls) - edit_view.delCallBack = { - cacheUrls.remove(it) - aCache.put(importRecordKey, cacheUrls.joinToString(",")) - } + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } } + customView { alertBinding.root } okButton { - val text = editText?.text?.toString() + val text = alertBinding.editView.text?.toString() text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) aCache.put(importRecordKey, cacheUrls.joinToString(",")) } - startActivity("source" to it) + ImportRssSourceDialog.start(supportFragmentManager, it) } } cancelButton() - }.show().applyTint() - } - - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - importRequestCode -> { - startActivity("filePath" to currentPath) - } - exportRequestCode -> viewModel.exportSelection( - adapter.getSelection(), - File(currentPath) - ) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - importRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - try { - uri.readText(this)?.let { - val dataKey = IntentDataHelp.putData(it) - startActivity("dataKey" to dataKey) - } - } catch (e: Exception) { - toast("readTextError:${e.localizedMessage}") - } - } - } - qrRequestCode -> if (resultCode == RESULT_OK) { - data?.getStringExtra("result")?.let { - startActivity("source" to it) - } - } - exportRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - if (uri.toString().isContentPath()) { - DocumentFile.fromTreeUri(this, uri)?.let { - viewModel.exportSelection(adapter.getSelection(), it) - } - } else { - uri.path?.let { - viewModel.exportSelection(adapter.getSelection(), File(it)) - } - } - } - } - } + }.show() } override fun del(source: RssSource) { @@ -296,7 +289,9 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r } override fun edit(source: RssSource) { - startActivity(Pair("data", source.sourceUrl)) + startActivity { + putExtra("data", source.sourceUrl) + } } override fun update(vararg source: RssSource) { diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt index 2652503b3..42f7a4c67 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt @@ -3,118 +3,161 @@ package io.legado.app.ui.rss.source.manage import android.content.Context import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.widget.PopupMenu import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ItemRssSourceBinding import io.legado.app.lib.theme.backgroundColor import io.legado.app.ui.widget.recycler.DragSelectTouchHelper import io.legado.app.ui.widget.recycler.ItemTouchCallback -import kotlinx.android.synthetic.main.item_rss_source.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import java.util.* +import io.legado.app.utils.ColorUtils + class RssSourceAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_rss_source), - ItemTouchCallback.OnItemTouchCallbackListener { + RecyclerAdapter(context), + ItemTouchCallback.Callback { private val selected = linkedSetOf() - fun selectAll() { - getItems().forEach { - selected.add(it) + val selection: List + get() { + val selection = arrayListOf() + getItems().forEach { + if (selected.contains(it)) { + selection.add(it) + } + } + return selection.sortedBy { it.customOrder } } - notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) - callBack.upCountView() - } - fun revertSelection() { - getItems().forEach { - if (selected.contains(it)) { - selected.remove(it) - } else { - selected.add(it) + val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: RssSource, newItem: RssSource): Boolean { + return oldItem.sourceUrl == newItem.sourceUrl } - } - notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) - callBack.upCountView() - } - fun getSelection(): List { - val selection = arrayListOf() - getItems().forEach { - if (selected.contains(it)) { - selection.add(it) + override fun areContentsTheSame(oldItem: RssSource, newItem: RssSource): Boolean { + return oldItem.sourceName == newItem.sourceName + && oldItem.sourceGroup == newItem.sourceGroup + && oldItem.enabled == newItem.enabled + } + + override fun getChangePayload(oldItem: RssSource, newItem: RssSource): Any? { + val payload = Bundle() + if (oldItem.sourceName != newItem.sourceName) { + payload.putString("name", newItem.sourceName) + } + if (oldItem.sourceGroup != newItem.sourceGroup) { + payload.putString("group", newItem.sourceGroup) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (payload.isEmpty) { + return null + } + return payload } } - return selection.sortedBy { it.customOrder } + + override fun getViewBinding(parent: ViewGroup): ItemRssSourceBinding { + return ItemRssSourceBinding.inflate(inflater, parent, false) } - override fun convert(holder: ItemViewHolder, item: RssSource, payloads: MutableList) { - with(holder.itemView) { + override fun convert( + holder: ItemViewHolder, + binding: ItemRssSourceBinding, + item: RssSource, + payloads: MutableList + ) { + binding.run { val bundle = payloads.getOrNull(0) as? Bundle if (bundle == null) { - this.setBackgroundColor(context.backgroundColor) + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) if (item.sourceGroup.isNullOrEmpty()) { - cb_source.text = item.sourceName + cbSource.text = item.sourceName } else { - cb_source.text = + cbSource.text = String.format("%s (%s)", item.sourceName, item.sourceGroup) } - swt_enabled.isChecked = item.enabled - cb_source.isChecked = selected.contains(item) + swtEnabled.isChecked = item.enabled + cbSource.isChecked = selected.contains(item) } else { bundle.keySet().map { when (it) { - "name", "group" -> - if (item.sourceGroup.isNullOrEmpty()) { - cb_source.text = item.sourceName - } else { - cb_source.text = - String.format("%s (%s)", item.sourceName, item.sourceGroup) - } - "selected" -> cb_source.isChecked = selected.contains(item) - "enabled" -> swt_enabled.isChecked = item.enabled + "selected" -> cbSource.isChecked = selected.contains(item) } } } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.apply { - swt_enabled.setOnCheckedChangeListener { view, checked -> - getItem(holder.layoutPosition)?.let { - if (view.isPressed) { - it.enabled = checked - callBack.update(it) + override fun registerListener(holder: ItemViewHolder, binding: ItemRssSourceBinding) { + binding.apply { + swtEnabled.setOnCheckedChangeListener { view, checked -> + if (view.isPressed) { + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + it.enabled = checked + callBack.update(it) + } } } } - cb_source.setOnCheckedChangeListener { view, checked -> - getItem(holder.layoutPosition)?.let { - if (view.isPressed) { - if (checked) { - selected.add(it) - } else { - selected.remove(it) + cbSource.setOnCheckedChangeListener { view, checked -> + if (view.isPressed) { + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + if (checked) { + selected.add(it) + } else { + selected.remove(it) + } + callBack.upCountView() } - callBack.upCountView() } } } - iv_edit.onClick { + ivEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.edit(it) } } - iv_menu_more.onClick { - showMenu(iv_menu_more, holder.layoutPosition) + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) + } + } + } + + override fun onCurrentListChanged() { + callBack.upCountView() + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) } } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() } private fun showMenu(view: View, position: Int) { @@ -132,7 +175,7 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : popupMenu.show() } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { val srcItem = getItem(srcPosition) val targetItem = getItem(targetPosition) if (srcItem != null && targetItem != null) { @@ -146,8 +189,7 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : movedItems.add(targetItem) } } - Collections.swap(getItems(), srcPosition, targetPosition) - notifyItemMoved(srcPosition, targetPosition) + swapItem(srcPosition, targetPosition) return true } @@ -160,8 +202,8 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : } } - fun initDragSelectTouchHelperCallback(): DragSelectTouchHelper.Callback { - return object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { override fun currentSelectedId(): MutableSet { return selected } @@ -184,7 +226,6 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : return false } } - } interface CallBack { fun del(source: RssSource) diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt index 5c72b503d..76b0ac1fe 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt @@ -1,55 +1,55 @@ package io.legado.app.ui.rss.source.manage import android.app.Application +import android.content.Intent import android.text.TextUtils +import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile -import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.data.appDb import io.legado.app.data.entities.RssSource -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON -import io.legado.app.utils.splitNotBlank -import io.legado.app.utils.writeText -import org.jetbrains.anko.toast +import io.legado.app.help.DefaultData +import io.legado.app.utils.* import java.io.File class RssSourceViewModel(application: Application) : BaseViewModel(application) { fun topSource(vararg sources: RssSource) { execute { - val minOrder = App.db.rssSourceDao().minOrder - 1 + val minOrder = appDb.rssSourceDao.minOrder - 1 sources.forEachIndexed { index, rssSource -> rssSource.customOrder = minOrder - index } - App.db.rssSourceDao().update(*sources) + appDb.rssSourceDao.update(*sources) } } fun bottomSource(vararg sources: RssSource) { execute { - val maxOrder = App.db.rssSourceDao().maxOrder + 1 + val maxOrder = appDb.rssSourceDao.maxOrder + 1 sources.forEachIndexed { index, rssSource -> rssSource.customOrder = maxOrder + index } - App.db.rssSourceDao().update(*sources) + appDb.rssSourceDao.update(*sources) } } fun del(rssSource: RssSource) { - execute { App.db.rssSourceDao().delete(rssSource) } + execute { appDb.rssSourceDao.delete(rssSource) } } fun update(vararg rssSource: RssSource) { - execute { App.db.rssSourceDao().update(*rssSource) } + execute { appDb.rssSourceDao.update(*rssSource) } } fun upOrder() { execute { - val sources = App.db.rssSourceDao().all + val sources = appDb.rssSourceDao.all for ((index: Int, source: RssSource) in sources.withIndex()) { source.customOrder = index + 1 } - App.db.rssSourceDao().update(*sources.toTypedArray()) + appDb.rssSourceDao.update(*sources.toTypedArray()) } } @@ -59,7 +59,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabled = true)) } - App.db.rssSourceDao().update(*list.toTypedArray()) + appDb.rssSourceDao.update(*list.toTypedArray()) } } @@ -69,13 +69,13 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) sources.forEach { list.add(it.copy(enabled = false)) } - App.db.rssSourceDao().update(*list.toTypedArray()) + appDb.rssSourceDao.update(*list.toTypedArray()) } } fun delSelection(sources: List) { execute { - App.db.rssSourceDao().delete(*sources.toTypedArray()) + appDb.rssSourceDao.delete(*sources.toTypedArray()) } } @@ -85,9 +85,9 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) FileUtils.createFileIfNotExist(file, "exportRssSource.json") .writeText(json) }.onSuccess { - context.toast("成功导出至\n${file.absolutePath}") + context.toastOnUi("成功导出至\n${file.absolutePath}") }.onError { - context.toast("导出失败\n${it.localizedMessage}") + context.toastOnUi("导出失败\n${it.localizedMessage}") } } @@ -98,25 +98,45 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) doc.createFile("", "exportRssSource.json") ?.writeText(context, json) }.onSuccess { - context.toast("成功导出至\n${doc.uri.path}") + context.toastOnUi("成功导出至\n${doc.uri.path}") }.onError { - context.toast("导出失败\n${it.localizedMessage}") + context.toastOnUi("导出失败\n${it.localizedMessage}") + } + } + + fun shareSelection(sources: List, success: ((intent: Intent) -> Unit)) { + execute { + val tmpSharePath = "${context.filesDir}/shareRssSource.json" + FileUtils.delete(tmpSharePath) + val intent = Intent(Intent.ACTION_SEND) + val file = FileUtils.createFileWithReplace(tmpSharePath) + file.writeText(GSON.toJson(sources)) + val fileUri = FileProvider.getUriForFile(context, AppConst.authority, file) + intent.type = "text/*" + intent.putExtra(Intent.EXTRA_STREAM, fileUri) + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.msg) } } fun addGroup(group: String) { execute { - val sources = App.db.rssSourceDao().noGroup + val sources = appDb.rssSourceDao.noGroup sources.map { source -> source.sourceGroup = group } - App.db.rssSourceDao().update(*sources.toTypedArray()) + appDb.rssSourceDao.update(*sources.toTypedArray()) } } fun upGroup(oldGroup: String, newGroup: String?) { execute { - val sources = App.db.rssSourceDao().getByGroup(oldGroup) + val sources = appDb.rssSourceDao.getByGroup(oldGroup) sources.map { source -> source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let { it.remove(oldGroup) @@ -125,23 +145,29 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) source.sourceGroup = TextUtils.join(",", it) } } - App.db.rssSourceDao().update(*sources.toTypedArray()) + appDb.rssSourceDao.update(*sources.toTypedArray()) } } fun delGroup(group: String) { execute { execute { - val sources = App.db.rssSourceDao().getByGroup(group) + val sources = appDb.rssSourceDao.getByGroup(group) sources.map { source -> source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let { it.remove(group) source.sourceGroup = TextUtils.join(",", it) } } - App.db.rssSourceDao().update(*sources.toTypedArray()) + appDb.rssSourceDao.update(*sources.toTypedArray()) } } } + fun importDefault() { + execute { + DefaultData.importDefaultRssSources() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt new file mode 100644 index 000000000..50ab2871f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt @@ -0,0 +1,137 @@ +package io.legado.app.ui.rss.subscription + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.core.view.isGone +import androidx.recyclerview.widget.ItemTouchHelper +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.data.appDb +import io.legado.app.data.entities.RuleSub +import io.legado.app.databinding.ActivityRuleSubBinding +import io.legado.app.databinding.DialogRuleSubEditBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.association.ImportBookSourceDialog +import io.legado.app.ui.association.ImportReplaceRuleDialog +import io.legado.app.ui.association.ImportRssSourceDialog +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 规则订阅界面 + */ +class RuleSubActivity : BaseActivity(), + RuleSubAdapter.Callback { + + override val binding by viewBinding(ActivityRuleSubBinding::inflate) + private lateinit var adapter: RuleSubAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.source_subscription, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> { + val order = appDb.ruleSubDao.maxOrder + 1 + editSubscription(RuleSub(customOrder = order)) + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + adapter = RuleSubAdapter(this, this) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initData() { + launch { + appDb.ruleSubDao.flowAll().collect { + binding.tvEmptyMsg.isGone = it.isNotEmpty() + adapter.setItems(it) + } + } + } + + override fun openSubscription(ruleSub: RuleSub) { + when (ruleSub.type) { + 0 -> { + ImportBookSourceDialog.start(supportFragmentManager, ruleSub.url) + } + 1 -> { + ImportRssSourceDialog.start(supportFragmentManager, ruleSub.url) + } + 2 -> { + ImportReplaceRuleDialog.start(supportFragmentManager, ruleSub.url) + } + } + } + + override fun editSubscription(ruleSub: RuleSub) { + alert(R.string.rule_subscription) { + val alertBinding = DialogRuleSubEditBinding.inflate(layoutInflater).apply { + spType.setSelection(ruleSub.type) + etName.setText(ruleSub.name) + etUrl.setText(ruleSub.url) + } + customView { alertBinding.root } + okButton { + launch { + ruleSub.type = alertBinding.spType.selectedItemPosition + ruleSub.name = alertBinding.etName.text?.toString() ?: "" + ruleSub.url = alertBinding.etUrl.text?.toString() ?: "" + val rs = withContext(IO) { + appDb.ruleSubDao.findByUrl(ruleSub.url) + } + if (rs != null && rs.id != ruleSub.id) { + toastOnUi("${getString(R.string.url_already)}(${rs.name})") + return@launch + } + withContext(IO) { + appDb.ruleSubDao.insert(ruleSub) + } + } + } + cancelButton() + }.show() + } + + override fun delSubscription(ruleSub: RuleSub) { + launch(IO) { + appDb.ruleSubDao.delete(ruleSub) + } + } + + override fun updateSourceSub(vararg ruleSub: RuleSub) { + launch(IO) { + appDb.ruleSubDao.update(*ruleSub) + } + } + + override fun upOrder() { + launch(IO) { + val sourceSubs = appDb.ruleSubDao.all + for ((index: Int, ruleSub: RuleSub) in sourceSubs.withIndex()) { + ruleSub.customOrder = index + 1 + } + appDb.ruleSubDao.update(*sourceSubs.toTypedArray()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubAdapter.kt new file mode 100644 index 000000000..2bb5560be --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubAdapter.kt @@ -0,0 +1,97 @@ +package io.legado.app.ui.rss.subscription + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RuleSub +import io.legado.app.databinding.ItemRuleSubBinding +import io.legado.app.ui.widget.recycler.ItemTouchCallback + + +class RuleSubAdapter(context: Context, val callBack: Callback) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val typeArray = context.resources.getStringArray(R.array.rule_type) + + override fun convert( + holder: ItemViewHolder, + binding: ItemRuleSubBinding, + item: RuleSub, + payloads: MutableList + ) { + binding.tvType.text = typeArray[item.type] + binding.tvName.text = item.name + binding.tvUrl.text = item.url + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRuleSubBinding) { + binding.root.setOnClickListener { + callBack.openSubscription(getItem(holder.layoutPosition)!!) + } + binding.ivEdit.setOnClickListener { + callBack.editSubscription(getItem(holder.layoutPosition)!!) + } + binding.ivMenuMore.setOnClickListener { + showMenu(binding.ivMenuMore, holder.layoutPosition) + } + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.source_sub_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_del -> callBack.delSubscription(source) + } + true + } + popupMenu.show() + } + + override fun getViewBinding(parent: ViewGroup): ItemRuleSubBinding { + return ItemRuleSubBinding.inflate(inflater, parent, false) + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.customOrder == targetItem.customOrder) { + callBack.upOrder() + } else { + val srcOrder = srcItem.customOrder + srcItem.customOrder = targetItem.customOrder + targetItem.customOrder = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.updateSourceSub(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + interface Callback { + fun openSubscription(ruleSub: RuleSub) + fun editSubscription(ruleSub: RuleSub) + fun delSubscription(ruleSub: RuleSub) + fun updateSourceSub(vararg ruleSub: RuleSub) + fun upOrder() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt index 4b6d4a159..75e9adc12 100644 --- a/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt +++ b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt @@ -2,26 +2,29 @@ package io.legado.app.ui.welcome import android.content.Intent import android.os.Bundle -import com.hankcs.hanlp.HanLP -import io.legado.app.App -import io.legado.app.R +import com.github.liuyueyi.quick.transfer.ChineseUtils import io.legado.app.base.BaseActivity +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.databinding.ActivityWelcomeBinding import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.Coroutine -import io.legado.app.help.storage.SyncBookProgress +import io.legado.app.help.storage.BookWebDav import io.legado.app.lib.theme.accentColor import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.main.MainActivity import io.legado.app.utils.getPrefBoolean -import kotlinx.android.synthetic.main.activity_welcome.* -import org.jetbrains.anko.startActivity +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding import java.util.concurrent.TimeUnit -open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { +open class WelcomeActivity : BaseActivity() { + + override val binding by viewBinding(ActivityWelcomeBinding::inflate) override fun onActivityCreated(savedInstanceState: Bundle?) { - iv_book.setColorFilter(accentColor) - vw_title_line.setBackgroundColor(accentColor) + binding.ivBook.setColorFilter(accentColor) + binding.vwTitleLine.setBackgroundColor(accentColor) // 避免从桌面启动程序后,会重新实例化入口类的activity if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) { finish() @@ -32,23 +35,42 @@ open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { private fun init() { Coroutine.async { - //清楚过期数据 - App.db.searchBookDao() - .clearExpired(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + val books = appDb.bookDao.all + books.forEach { book -> + BookWebDav.getBookProgress(book)?.let { bookProgress -> + if (bookProgress.durChapterIndex > book.durChapterIndex || + (bookProgress.durChapterIndex == book.durChapterIndex && + bookProgress.durChapterPos > book.durChapterPos) + ) { + book.durChapterIndex = bookProgress.durChapterIndex + book.durChapterPos = bookProgress.durChapterPos + book.durChapterTitle = bookProgress.durChapterTitle + book.durChapterTime = bookProgress.durChapterTime + appDb.bookDao.update(book) + } + } + } + } + Coroutine.async { + appDb.cacheDao.clearDeadline(System.currentTimeMillis()) + //清除过期数据 + if (getPrefBoolean(PreferKey.autoClearExpired, true)) { + appDb.searchBookDao + .clearExpired(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + } //初始化简繁转换引擎 when (AppConfig.chineseConverterType) { - 1 -> HanLP.convertToSimplifiedChinese("初始化") - 2 -> HanLP.convertToTraditionalChinese("初始化") + 1 -> ChineseUtils.t2s("初始化") + 2 -> ChineseUtils.s2t("初始化") else -> null } } - SyncBookProgress.downloadBookProgress() - root_view.postDelayed({ startMainActivity() }, 500) + binding.root.postDelayed({ startMainActivity() }, 500) } private fun startMainActivity() { startActivity() - if (getPrefBoolean(R.string.pk_default_read)) { + if (getPrefBoolean(PreferKey.defaultToRead)) { startActivity() } finish() diff --git a/app/src/main/java/io/legado/app/ui/widget/ArcView.kt b/app/src/main/java/io/legado/app/ui/widget/ArcView.kt index 6338c6638..b92d552af 100644 --- a/app/src/main/java/io/legado/app/ui/widget/ArcView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/ArcView.kt @@ -8,9 +8,8 @@ import io.legado.app.R class ArcView @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { + attrs: AttributeSet? = null +) : View(context, attrs) { private var mWidth = 0 private var mHeight = 0 diff --git a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt index 3141c5a86..1c4aa8a6c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt @@ -11,18 +11,36 @@ import androidx.annotation.ColorInt import androidx.appcompat.widget.AppCompatTextView import io.legado.app.utils.dp -class BatteryView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { +class BatteryView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + private val batteryTypeface by lazy { + Typeface.createFromAsset(context.assets, "font/number.ttf") + } private val batteryPaint = Paint() private val outFrame = Rect() private val polar = Rect() var isBattery = false + set(value) { + field = value + if (value) { + super.setTypeface(batteryTypeface) + postInvalidate() + } + } init { - setPadding(4.dp, 0, 6.dp, 0) + setPadding(4.dp, 2.dp, 6.dp, 2.dp) batteryPaint.strokeWidth = 1.dp.toFloat() batteryPaint.isAntiAlias = true batteryPaint.color = paint.color - typeface = Typeface.createFromAsset(context.assets, "number.ttf") + } + + override fun setTypeface(tf: Typeface?) { + if (!isBattery) { + super.setTypeface(tf) + } } fun setColor(@ColorInt color: Int) { @@ -41,9 +59,9 @@ class BatteryView(context: Context, attrs: AttributeSet?) : AppCompatTextView(co if (!isBattery) return outFrame.set( 1.dp, - layout.getLineBaseline(0) + layout.getLineAscent(0) + 2.dp, + 1.dp, width - 3.dp, - layout.getLineBaseline(0) + 2.dp + height - 1.dp ) val dj = (outFrame.bottom - outFrame.top) / 3 polar.set( diff --git a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt index 5198599e4..441a1853b 100644 --- a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt @@ -2,79 +2,84 @@ package io.legado.app.ui.widget import android.content.Context import android.util.AttributeSet -import android.view.View +import android.view.LayoutInflater import android.widget.FrameLayout import android.widget.SeekBar import io.legado.app.R +import io.legado.app.databinding.ViewDetailSeekBarBinding import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.utils.ColorUtils import io.legado.app.utils.progressAdd -import kotlinx.android.synthetic.main.view_detail_seek_bar.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -class DetailSeekBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs), - SeekBar.OnSeekBarChangeListener { + +class DetailSeekBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs), + SeekBarChangeListener { + private var binding: ViewDetailSeekBarBinding = + ViewDetailSeekBarBinding.inflate(LayoutInflater.from(context), this, true) private val isBottomBackground: Boolean + var valueFormat: ((progress: Int) -> String)? = null var onChanged: ((progress: Int) -> Unit)? = null var progress: Int - get() = seek_bar.progress + get() = binding.seekBar.progress set(value) { - seek_bar.progress = value + binding.seekBar.progress = value } var max: Int - get() = seek_bar.max + get() = binding.seekBar.max set(value) { - seek_bar.max = value + binding.seekBar.max = value } init { - View.inflate(context, R.layout.view_detail_seek_bar, this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailSeekBar) isBottomBackground = typedArray.getBoolean(R.styleable.DetailSeekBar_isBottomBackground, false) - tv_seek_title.text = typedArray.getText(R.styleable.DetailSeekBar_title) - seek_bar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0) + binding.tvSeekTitle.text = typedArray.getText(R.styleable.DetailSeekBar_title) + binding.seekBar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0) typedArray.recycle() - if (isBottomBackground) { + if (isBottomBackground && !isInEditMode) { val isLight = ColorUtils.isColorLight(context.bottomBackground) val textColor = context.getPrimaryTextColor(isLight) - tv_seek_title.setTextColor(textColor) - iv_seek_plus.setColorFilter(textColor) - iv_seek_reduce.setColorFilter(textColor) - tv_seek_value.setTextColor(textColor) + binding.tvSeekTitle.setTextColor(textColor) + binding.ivSeekPlus.setColorFilter(textColor) + binding.ivSeekReduce.setColorFilter(textColor) + binding.tvSeekValue.setTextColor(textColor) } - iv_seek_plus.onClick { - seek_bar.progressAdd(1) - onChanged?.invoke(seek_bar.progress) + binding.ivSeekPlus.setOnClickListener { + binding.seekBar.progressAdd(1) + onChanged?.invoke(binding.seekBar.progress) } - iv_seek_reduce.onClick { - seek_bar.progressAdd(-1) - onChanged?.invoke(seek_bar.progress) + binding.ivSeekReduce.setOnClickListener { + binding.seekBar.progressAdd(-1) + onChanged?.invoke(binding.seekBar.progress) } - seek_bar.setOnSeekBarChangeListener(this) + binding.seekBar.setOnSeekBarChangeListener(this) } - private fun upValue(progress: Int = seek_bar.progress) { + private fun upValue(progress: Int = binding.seekBar.progress) { valueFormat?.let { - tv_seek_value.text = it.invoke(progress) + binding.tvSeekValue.text = it.invoke(progress) } ?: let { - tv_seek_value.text = progress.toString() + binding.tvSeekValue.text = progress.toString() } } - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { upValue(progress) } - override fun onStartTrackingTouch(seekBar: SeekBar?) { + override fun onStartTrackingTouch(seekBar: SeekBar) { } - override fun onStopTrackingTouch(seekBar: SeekBar?) { - onChanged?.invoke(seek_bar.progress) + override fun onStopTrackingTouch(seekBar: SeekBar) { + onChanged?.invoke(binding.seekBar.progress) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt index 56dff107c..276ce4704 100644 --- a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt +++ b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt @@ -1,18 +1,15 @@ package io.legado.app.ui.widget -import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import android.widget.PopupWindow import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import kotlinx.android.synthetic.main.item_fillet_text.view.* -import kotlinx.android.synthetic.main.popup_keyboard_tool.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.databinding.PopupKeyboardToolBinding class KeyboardToolPop( @@ -21,9 +18,10 @@ class KeyboardToolPop( val callBack: CallBack? ) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { + private val binding = PopupKeyboardToolBinding.inflate(LayoutInflater.from(context)) + init { - @SuppressLint("InflateParams") - contentView = LayoutInflater.from(context).inflate(R.layout.popup_keyboard_tool, null) + contentView = binding.root isTouchable = true isOutsideTouchable = false @@ -34,23 +32,33 @@ class KeyboardToolPop( private fun initRecyclerView() = with(contentView) { val adapter = Adapter(context) - recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) - recycler_view.adapter = adapter + binding.recyclerView.layoutManager = + LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + binding.recyclerView.adapter = adapter adapter.setItems(chars) } inner class Adapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_fillet_text) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - with(holder.itemView) { - text_view.text = item + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: String, + payloads: MutableList + ) { + binding.run { + textView.text = item } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { holder.itemView.apply { - onClick { + setOnClickListener { getItem(holder.layoutPosition)?.let { callBack?.sendText(it) } diff --git a/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt index a2f354772..01c4957db 100644 --- a/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt @@ -7,8 +7,11 @@ import android.widget.TextView import io.legado.app.ui.widget.text.AccentBgTextView import io.legado.app.utils.dp -@Suppress("unused") -class LabelsBar(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { +@Suppress("unused", "MemberVisibilityCanBePrivate") +class LabelsBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { private val unUsedViews = arrayListOf() private val usedViews = arrayListOf() diff --git a/app/src/main/java/io/legado/app/ui/widget/SearchView.kt b/app/src/main/java/io/legado/app/ui/widget/SearchView.kt index 6d340c6d7..975fe9d57 100644 --- a/app/src/main/java/io/legado/app/ui/widget/SearchView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/SearchView.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.widget +import android.annotation.SuppressLint import android.app.SearchableInfo import android.content.Context import android.graphics.Canvas @@ -15,21 +16,14 @@ import android.widget.TextView import androidx.appcompat.widget.SearchView import io.legado.app.R -class SearchView : SearchView { +class SearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : SearchView(context, attrs) { private var mSearchHintIcon: Drawable? = null private var textView: TextView? = null - constructor( - context: Context, - attrs: AttributeSet? = null - ) : super(context, attrs) - - constructor( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int - ) : super(context, attrs, defStyleAttr) - + @SuppressLint("UseCompatLoadingForDrawables") override fun onLayout( changed: Boolean, left: Int, diff --git a/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt index 4bef4c53d..57b926137 100644 --- a/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt @@ -2,58 +2,64 @@ package io.legado.app.ui.widget import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.view.Menu -import android.view.View import android.widget.FrameLayout import androidx.annotation.MenuRes import androidx.annotation.StringRes import androidx.appcompat.widget.PopupMenu import io.legado.app.R +import io.legado.app.databinding.ViewSelectActionBarBinding +import io.legado.app.help.AppConfig import io.legado.app.lib.theme.* import io.legado.app.utils.ColorUtils -import io.legado.app.utils.dp import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.view_select_action_bar.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -class SelectActionBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + +@Suppress("unused") +class SelectActionBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { private var callBack: CallBack? = null private var selMenu: PopupMenu? = null + private val binding = + ViewSelectActionBarBinding.inflate(LayoutInflater.from(context), this, true) init { setBackgroundColor(context.bottomBackground) - elevation = 10.dp.toFloat() - View.inflate(context, R.layout.view_select_action_bar, this) + elevation = + if (AppConfig.elevation < 0) context.elevation else AppConfig.elevation.toFloat() val textIsDark = ColorUtils.isColorLight(context.bottomBackground) val primaryTextColor = context.getPrimaryTextColor(textIsDark) val secondaryTextColor = context.getSecondaryTextColor(textIsDark) - cb_selected_all.setTextColor(primaryTextColor) - TintHelper.setTint(cb_selected_all, context.accentColor, !textIsDark) - iv_menu_more.setColorFilter(secondaryTextColor) - cb_selected_all.setOnCheckedChangeListener { buttonView, isChecked -> + binding.cbSelectedAll.setTextColor(primaryTextColor) + TintHelper.setTint(binding.cbSelectedAll, context.accentColor, !textIsDark) + binding.ivMenuMore.setColorFilter(secondaryTextColor) + binding.cbSelectedAll.setOnCheckedChangeListener { buttonView, isChecked -> if (buttonView.isPressed) { callBack?.selectAll(isChecked) } } - btn_revert_selection.onClick { callBack?.revertSelection() } - btn_select_action_main.onClick { callBack?.onClickMainAction() } - iv_menu_more.onClick { selMenu?.show() } + binding.btnRevertSelection.setOnClickListener { callBack?.revertSelection() } + binding.btnSelectActionMain.setOnClickListener { callBack?.onClickMainAction() } + binding.ivMenuMore.setOnClickListener { selMenu?.show() } } - fun setMainActionText(text: String) { - btn_select_action_main.text = text - btn_select_action_main.visible() + fun setMainActionText(text: String) = binding.run { + btnSelectActionMain.text = text + btnSelectActionMain.visible() } - fun setMainActionText(@StringRes id: Int) { - btn_select_action_main.setText(id) - btn_select_action_main.visible() + fun setMainActionText(@StringRes id: Int) = binding.run { + btnSelectActionMain.setText(id) + btnSelectActionMain.visible() } fun inflateMenu(@MenuRes resId: Int): Menu? { - selMenu = PopupMenu(context, iv_menu_more) + selMenu = PopupMenu(context, binding.ivMenuMore) selMenu?.inflate(resId) - iv_menu_more.visible() + binding.ivMenuMore.visible() return selMenu?.menu } @@ -65,22 +71,22 @@ class SelectActionBar(context: Context, attrs: AttributeSet?) : FrameLayout(cont selMenu?.setOnMenuItemClickListener(listener) } - fun upCountView(selectCount: Int, allCount: Int) { + fun upCountView(selectCount: Int, allCount: Int) = binding.run { if (selectCount == 0) { - cb_selected_all.isChecked = false + cbSelectedAll.isChecked = false } else { - cb_selected_all.isChecked = selectCount >= allCount + cbSelectedAll.isChecked = selectCount >= allCount } //重置全选的文字 - if (cb_selected_all.isChecked) { - cb_selected_all.text = context.getString( + if (cbSelectedAll.isChecked) { + cbSelectedAll.text = context.getString( R.string.select_cancel_count, selectCount, allCount ) } else { - cb_selected_all.text = context.getString( + cbSelectedAll.text = context.getString( R.string.select_all_count, selectCount, allCount @@ -89,11 +95,11 @@ class SelectActionBar(context: Context, attrs: AttributeSet?) : FrameLayout(cont setMenuClickable(selectCount > 0) } - private fun setMenuClickable(isClickable: Boolean) { - btn_revert_selection.isEnabled = isClickable - btn_revert_selection.isClickable = isClickable - btn_select_action_main.isEnabled = isClickable - btn_select_action_main.isClickable = isClickable + private fun setMenuClickable(isClickable: Boolean) = binding.run { + btnRevertSelection.isEnabled = isClickable + btnRevertSelection.isClickable = isClickable + btnSelectActionMain.isEnabled = isClickable + btnSelectActionMain.isClickable = isClickable } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt b/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt index 0f6ad3200..95e54ac63 100644 --- a/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt +++ b/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt @@ -14,10 +14,10 @@ import io.legado.app.utils.getCompatColor /** * ShadowLayout.java * - * * Created by lijiankun on 17/8/11. */ -class ShadowLayout( +@Suppress("unused") +class ShadowLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : RelativeLayout(context, attrs) { diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index 39e1cd441..593ee4c4c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -17,11 +17,12 @@ import io.legado.app.lib.theme.primaryColor import io.legado.app.utils.activity import io.legado.app.utils.navigationBarHeight import io.legado.app.utils.statusBarHeight -import org.jetbrains.anko.backgroundColor -import org.jetbrains.anko.bottomPadding -import org.jetbrains.anko.topPadding -class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, attrs) { +@Suppress("unused") +class TitleBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppBarLayout(context, attrs) { val toolbar: Toolbar val menu: Menu @@ -138,23 +139,24 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a } } - if (a.getBoolean(R.styleable.TitleBar_fitStatusBar, true)) { - topPadding = context.statusBarHeight - } + if (!isInEditMode) { + if (a.getBoolean(R.styleable.TitleBar_fitStatusBar, true)) { + setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom) + } - if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) { - bottomPadding = context.navigationBarHeight - } + if (a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false)) { + setPadding(paddingLeft, paddingTop, paddingRight, context.navigationBarHeight) + } - backgroundColor = context.primaryColor + setBackgroundColor(context.primaryColor) - stateListAnimator = null - elevation = if (AppConfig.elevation < 0) { - context.elevation - } else { - AppConfig.elevation.toFloat() + stateListAnimator = null + elevation = if (AppConfig.elevation < 0) { + context.elevation + } else { + AppConfig.elevation.toFloat() + } } - a.recycle() } @@ -193,11 +195,12 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a fun transparent() { elevation = 0f - backgroundColor = Color.TRANSPARENT + setBackgroundColor(Color.TRANSPARENT) } fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, fullScreen: Boolean) { - topPadding = if (!isInMultiWindowMode && fullScreen) context.statusBarHeight else 0 + val topPadding = if (!isInMultiWindowMode && fullScreen) context.statusBarHeight else 0 + setPadding(paddingLeft, topPadding, paddingRight, paddingBottom) } private fun attachToActivity() { diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt index df123bfcd..3a0af5b03 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt @@ -11,11 +11,11 @@ import android.view.View import io.legado.app.R +@Suppress("unused", "MemberVisibilityCanBePrivate") class RefreshProgressBar @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { + attrs: AttributeSet? = null +) : View(context, attrs) { private var a = 1 private var durProgress = 0 private var secondDurProgress = 0 diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt index 7a5e436c3..2ff870766 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt @@ -17,9 +17,13 @@ import io.legado.app.utils.dp * RotateLoading * Created by Victor on 2015/4/28. */ -class RotateLoading : View { +@Suppress("MemberVisibilityCanBePrivate") +class RotateLoading @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { - private lateinit var mPaint: Paint + private var mPaint: Paint private var loadingRectF: RectF? = null private var shadowRectF: RectF? = null @@ -54,19 +58,7 @@ class RotateLoading : View { private val hidden = Runnable { this.stopInternal() } - constructor(context: Context) : super(context) { - initView(context, null) - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - initView(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - initView(context, attrs) - } - - private fun initView(context: Context, attrs: AttributeSet?) { + init { loadingColor = context.accentColor thisWidth = DEFAULT_WIDTH.dp shadowPosition = DEFAULT_SHADOW_POSITION.dp @@ -80,8 +72,12 @@ class RotateLoading : View { R.styleable.RotateLoading_loading_width, DEFAULT_WIDTH.dp ) - shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION) - speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) + shadowPosition = typedArray.getInt( + R.styleable.RotateLoading_shadow_position, + DEFAULT_SHADOW_POSITION + ) + speedOfDegree = + typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) hideMode = when (typedArray.getInt(R.styleable.RotateLoading_hide_mode, 2)) { 1 -> INVISIBLE else -> GONE diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt index 9b80b1825..6542ea55f 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt @@ -20,6 +20,7 @@ import android.graphics.* import android.view.View import android.view.animation.AccelerateInterpolator import java.util.* +import kotlin.math.pow class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rect) : ValueAnimator() { @@ -99,20 +100,20 @@ class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rec } private inner class Particle { - internal var alpha: Float = 0.toFloat() - internal var color: Int = 0 - internal var cx: Float = 0.toFloat() - internal var cy: Float = 0.toFloat() - internal var radius: Float = 0.toFloat() - internal var baseCx: Float = 0.toFloat() - internal var baseCy: Float = 0.toFloat() - internal var baseRadius: Float = 0.toFloat() - internal var top: Float = 0.toFloat() - internal var bottom: Float = 0.toFloat() - internal var mag: Float = 0.toFloat() - internal var neg: Float = 0.toFloat() - internal var life: Float = 0.toFloat() - internal var overflow: Float = 0.toFloat() + var alpha: Float = 0.toFloat() + var color: Int = 0 + var cx: Float = 0.toFloat() + var cy: Float = 0.toFloat() + var radius: Float = 0.toFloat() + var baseCx: Float = 0.toFloat() + var baseCy: Float = 0.toFloat() + var baseRadius: Float = 0.toFloat() + var top: Float = 0.toFloat() + var bottom: Float = 0.toFloat() + var mag: Float = 0.toFloat() + var neg: Float = 0.toFloat() + var life: Float = 0.toFloat() + var overflow: Float = 0.toFloat() fun advance(factor: Float) { @@ -130,7 +131,7 @@ class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rec alpha = 1f - f f = bottom * f2 cx = baseCx + f - cy = (baseCy - this.neg * Math.pow(f.toDouble(), 2.0)).toFloat() - f * mag + cy = (baseCy - this.neg * f.toDouble().pow(2.0)).toFloat() - f * mag radius = V + (baseRadius - V) * f2 } } diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt index c79695a51..7345e7ed9 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt @@ -29,7 +29,9 @@ import android.view.View import java.util.* -class ExplosionView : View { +@Suppress("unused") +class ExplosionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + View(context, attrs) { private var customDuration = ExplosionAnimator.DEFAULT_DURATION private var idPlayAnimationEffect = 0 @@ -39,24 +41,7 @@ class ExplosionView : View { private val mExplosions = ArrayList() private val mExpandInset = IntArray(2) - constructor(context: Context) : super(context) { - init() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - private fun init() { - + init { Arrays.fill(mExpandInset, Utils.dp2Px(32)) } @@ -75,8 +60,8 @@ class ExplosionView : View { this.customDuration = customDuration } - fun addActionEvent(ievents: OnAnimatorListener) { - this.mZAnimatorListener = ievents + fun addActionEvent(iEvents: OnAnimatorListener) { + this.mZAnimatorListener = iEvents } diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt index 36397a201..7855856f4 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt @@ -1,16 +1,16 @@ package io.legado.app.ui.widget.dialog import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogPhotoViewBinding import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.page.provider.ImageProvider -import kotlinx.android.synthetic.main.dialog_photo_view.* +import io.legado.app.utils.viewbindingdelegate.viewBinding class PhotoDialog : BaseDialogFragment() { @@ -32,10 +32,10 @@ class PhotoDialog : BaseDialogFragment() { } + private val binding by viewBinding(DialogPhotoViewBinding::bind) + override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) dialog?.window?.setLayout( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT @@ -60,7 +60,7 @@ class PhotoDialog : BaseDialogFragment() { ImageProvider.getImage(book, chapterIndex, src) }.onSuccess { bitmap -> if (bitmap != null) { - photo_view.setImageBitmap(bitmap) + binding.photoView.setImageBitmap(bitmap) } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt index dc6ba868b..8c1df6110 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt @@ -1,17 +1,21 @@ package io.legado.app.ui.widget.dialog import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.FragmentManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment -import kotlinx.android.synthetic.main.dialog_text_view.* +import io.legado.app.databinding.DialogTextViewBinding +import io.legado.app.utils.getSize +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.noties.markwon.Markwon +import io.noties.markwon.ext.tables.TablePlugin +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.image.glide.GlideImagesPlugin import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import ru.noties.markwon.Markwon class TextDialog : BaseDialogFragment() { @@ -39,14 +43,13 @@ class TextDialog : BaseDialogFragment() { } + private val binding by viewBinding(DialogTextViewBinding::bind) private var time = 0L - private var autoClose: Boolean = false override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -62,24 +65,25 @@ class TextDialog : BaseDialogFragment() { arguments?.let { val content = it.getString("content") ?: "" when (it.getInt("mode")) { - MD -> text_view.post { - Markwon.create(requireContext()) - .setMarkdown( - text_view, - content - ) + MD -> binding.textView.post { + Markwon.builder(requireContext()) + .usePlugin(GlideImagesPlugin.create(requireContext())) + .usePlugin(HtmlPlugin.create()) + .usePlugin(TablePlugin.create(requireContext())) + .build() + .setMarkdown(binding.textView, content) } - else -> text_view.text = content + else -> binding.textView.text = content } time = it.getLong("time", 0L) } if (time > 0) { - badge_view.setBadgeCount((time / 1000).toInt()) + binding.badgeView.setBadgeCount((time / 1000).toInt()) launch { while (time > 0) { delay(1000) time -= 1000 - badge_view.setBadgeCount((time / 1000).toInt()) + binding.badgeView.setBadgeCount((time / 1000).toInt()) if (time <= 0) { view.post { dialog?.setCancelable(true) diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt index 323e622ea..cc79dc726 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.widget.dialog import android.content.Context import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,9 +10,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_log.view.* +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemLogBinding +import io.legado.app.utils.getSize +import io.legado.app.utils.viewbindingdelegate.viewBinding class TextListDialog : BaseDialogFragment() { @@ -28,13 +29,13 @@ class TextListDialog : BaseDialogFragment() { } } + private val binding by viewBinding(DialogRecyclerViewBinding::bind) lateinit var adapter: TextAdapter - var values: ArrayList? = null + private var values: ArrayList? = null override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -46,38 +47,48 @@ class TextListDialog : BaseDialogFragment() { return inflater.inflate(R.layout.dialog_recycler_view, container) } - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { arguments?.let { - tool_bar.title = it.getString("title") + toolBar.title = it.getString("title") values = it.getStringArrayList("values") } - recycler_view.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) adapter = TextAdapter(requireContext()) - recycler_view.adapter = adapter + recyclerView.adapter = adapter adapter.setItems(values) } class TextAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_log) { - override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { - holder.itemView.apply { - if (text_view.getTag(R.id.tag1) == null) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { val listener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { - text_view.isCursorVisible = false - text_view.isCursorVisible = true + textView.isCursorVisible = false + textView.isCursorVisible = true } override fun onViewDetachedFromWindow(v: View) {} } - text_view.addOnAttachStateChangeListener(listener) - text_view.setTag(R.id.tag1, listener) + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) } - text_view.text = item + textView.text = item } } - override fun registerListener(holder: ItemViewHolder) { + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { //nothing } } diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt new file mode 100644 index 000000000..9e2d9ed2f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt @@ -0,0 +1,28 @@ +package io.legado.app.ui.widget.dialog + +import android.app.Dialog +import android.content.Context +import io.legado.app.databinding.DialogWaitBinding + + +@Suppress("unused") +class WaitDialog(context: Context) : Dialog(context) { + + val binding = DialogWaitBinding.inflate(layoutInflater) + + init { + setCanceledOnTouchOutside(false) + setContentView(binding.root) + } + + fun setText(text: String): WaitDialog { + binding.tvMsg.text = text + return this + } + + fun setText(res: Int): WaitDialog { + binding.tvMsg.setText(res) + return this + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt index 99e0cd702..c2a45147f 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt +++ b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt @@ -4,15 +4,19 @@ import android.content.Context import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View +import android.view.ViewStub import android.widget.FrameLayout import android.widget.ProgressBar import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import io.legado.app.R -import kotlinx.android.synthetic.main.view_dynamic.view.* -class DynamicFrameLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs), ViewSwitcher { +@Suppress("unused") +class DynamicFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs), ViewSwitcher { private var errorView: View? = null private var errorImage: AppCompatImageView? = null @@ -146,7 +150,7 @@ class DynamicFrameLayout(context: Context, attrs: AttributeSet?) : FrameLayout(c private fun ensureErrorView() { if (errorView == null) { - errorView = errorViewStub.inflate() + errorView = findViewById(R.id.error_view_stub).inflate() errorImage = errorView?.findViewById(R.id.iv_error_image) errorTextView = errorView?.findViewById(R.id.tv_error_message) actionBtn = errorView?.findViewById(R.id.btn_error_retry) @@ -162,7 +166,7 @@ class DynamicFrameLayout(context: Context, attrs: AttributeSet?) : FrameLayout(c private fun ensureProgressView() { if (progressView == null) { - progressView = progressViewStub.inflate() + progressView = findViewById(R.id.progress_view_stub).inflate() progressBar = progressView?.findViewById(R.id.loading_progress) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt index a54ee0182..ddd689a12 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt @@ -3,24 +3,29 @@ package io.legado.app.ui.widget.font import android.content.Context import android.graphics.Typeface import android.os.Build -import io.legado.app.R +import android.view.ViewGroup import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.utils.DocItem -import io.legado.app.utils.RealPathUtil -import io.legado.app.utils.invisible -import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_font.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -import org.jetbrains.anko.toast +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemFontBinding +import io.legado.app.utils.* import java.io.File +import java.net.URLDecoder class FontAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_font) { + RecyclerAdapter(context) { - override fun convert(holder: ItemViewHolder, item: DocItem, payloads: MutableList) { - with(holder.itemView) { - try { + override fun getViewBinding(parent: ViewGroup): ItemFontBinding { + return ItemFontBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFontBinding, + item: DocItem, + payloads: MutableList + ) { + binding.run { + kotlin.runCatching { val typeface: Typeface? = if (item.isContentPath) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.contentResolver @@ -34,23 +39,25 @@ class FontAdapter(context: Context, val callBack: CallBack) : } else { Typeface.createFromFile(item.uri.toString()) } - tv_font.typeface = typeface - } catch (e: Exception) { - e.printStackTrace() - context.toast("Read ${item.name} Error: ${e.localizedMessage}") + tvFont.typeface = typeface + }.onFailure { + it.printStackTrace() + context.toastOnUi("Read ${item.name} Error: ${it.localizedMessage}") } - tv_font.text = item.name - this.onClick { callBack.onClick(item) } - if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) { - iv_checked.visible() + tvFont.text = item.name + root.setOnClickListener { callBack.onClick(item) } + if (item.name == URLDecoder.decode(callBack.curFilePath, "utf-8") + .substringAfterLast(File.separator) + ) { + ivChecked.visible() } else { - iv_checked.invisible() + ivChecked.invisible() } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener(holder: ItemViewHolder, binding: ItemFontBinding) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { callBack.onClick(it) } diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt index 0fa7bea7d..22f205101 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt @@ -1,10 +1,8 @@ package io.legado.app.ui.widget.font -import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.os.Bundle -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -12,39 +10,62 @@ import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.documentfile.provider.DocumentFile import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogFontSelectBinding import io.legado.app.help.AppConfig -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker +import io.legado.app.ui.document.FilePicker +import io.legado.app.ui.document.FilePickerParam import io.legado.app.utils.* -import kotlinx.android.synthetic.main.dialog_font_select.* +import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch +import splitties.init.appCtx import java.io.File import java.util.* +import kotlin.collections.ArrayList class FontSelectDialog : BaseDialogFragment(), - FileChooserDialog.CallBack, Toolbar.OnMenuItemClickListener, FontAdapter.CallBack { - private val fontFolderRequestCode = 35485 private val fontRegex = Regex(".*\\.[ot]tf") private val fontFolder by lazy { - FileUtils.createFolderIfNotExist(App.INSTANCE.filesDir, "Fonts") + FileUtils.createFolderIfNotExist(appCtx.filesDir, "Fonts") } private var adapter: FontAdapter? = null + private val binding by viewBinding(DialogFontSelectBinding::bind) + private val selectFontDir = registerForActivityResult(FilePicker()) { uri -> + uri ?: return@registerForActivityResult + if (uri.toString().isContentScheme()) { + putPrefString(PreferKey.fontFolder, uri.toString()) + val doc = DocumentFile.fromTreeUri(requireContext(), uri) + if (doc != null) { + context?.contentResolver?.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + loadFontFiles(doc) + } else { + RealPathUtil.getPath(requireContext(), uri)?.let { + loadFontFilesByPermission(it) + } + } + } else { + uri.path?.let { path -> + putPrefString(PreferKey.fontFolder, path) + loadFontFilesByPermission(path) + } + } + } override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) } @@ -57,20 +78,20 @@ class FontSelectDialog : BaseDialogFragment(), } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.setTitle(R.string.select_font) - tool_bar.inflateMenu(R.menu.font_select) - tool_bar.menu.applyTint(requireContext()) - tool_bar.setOnMenuItemClickListener(this) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.select_font) + binding.toolBar.inflateMenu(R.menu.font_select) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) adapter = FontAdapter(requireContext(), this) - recycler_view.layoutManager = LinearLayoutManager(context) - recycler_view.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.adapter = adapter val fontPath = getPrefString(PreferKey.fontFolder) if (fontPath.isNullOrEmpty()) { openFolder() } else { - if (fontPath.isContentPath()) { + if (fontPath.isContentScheme()) { val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath)) if (doc?.canRead() == true) { loadFontFiles(doc) @@ -87,13 +108,13 @@ class FontSelectDialog : BaseDialogFragment(), when (item?.itemId) { R.id.menu_default -> { val requireContext = requireContext() - requireContext.alert(titleResource = R.string.system_typeface) { + alert(titleResource = R.string.system_typeface) { items( requireContext.resources.getStringArray(R.array.system_typefaces).toList() ) { _, i -> AppConfig.systemTypefaces = i onDefaultFontChange() - dismiss() + dismissAllowingStateLoss() } }.show() } @@ -107,36 +128,48 @@ class FontSelectDialog : BaseDialogFragment(), private fun openFolder() { launch(Main) { val defaultPath = "SD${File.separator}Fonts" - FilePicker.selectFolder( - this@FontSelectDialog, - fontFolderRequestCode, - otherActions = arrayListOf(defaultPath) - ) { - when (it) { - defaultPath -> { - val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts" - putPrefString(PreferKey.fontFolder, path) - loadFontFilesByPermission(path) - } - } - } + selectFontDir.launch( + FilePickerParam( + otherActions = arrayOf(defaultPath) + ) + ) + } + } + + private fun getLocalFonts(): ArrayList { + val fontItems = arrayListOf() + val fontDir = + FileUtils.createFolderIfNotExist(requireContext().externalFiles, "font") + fontDir.listFiles { pathName -> + pathName.name.lowercase(Locale.getDefault()).matches(fontRegex) + }?.forEach { + fontItems.add( + DocItem( + it.name, + it.extension, + it.length(), + Date(it.lastModified()), + Uri.parse(it.absolutePath) + ) + ) } + return fontItems } private fun loadFontFiles(doc: DocumentFile) { execute { val fontItems = arrayListOf() - val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri) + val docItems = DocumentUtils.listFiles(appCtx, doc.uri) docItems.forEach { item -> - if (item.name.toLowerCase(Locale.getDefault()).matches(fontRegex)) { + if (item.name.lowercase(Locale.getDefault()).matches(fontRegex)) { fontItems.add(item) } } - fontItems.sortedBy { it.name } + mergeFontItems(fontItems, getLocalFonts()) }.onSuccess { adapter?.setItems(it) }.onError { - toast("getFontFiles:${it.localizedMessage}") + toastOnUi("getFontFiles:${it.localizedMessage}") } } @@ -155,7 +188,7 @@ class FontSelectDialog : BaseDialogFragment(), val fontItems = arrayListOf() val file = File(path) file.listFiles { pathName -> - pathName.name.toLowerCase(Locale.getDefault()).matches(fontRegex) + pathName.name.lowercase(Locale.getDefault()).matches(fontRegex) }?.forEach { fontItems.add( DocItem( @@ -167,11 +200,33 @@ class FontSelectDialog : BaseDialogFragment(), ) ) } - fontItems.sortedBy { it.name } + mergeFontItems(fontItems, getLocalFonts()) }.onSuccess { adapter?.setItems(it) }.onError { - toast("getFontFiles:${it.localizedMessage}") + toastOnUi("getFontFiles:${it.localizedMessage}") + } + } + + private fun mergeFontItems( + items1: ArrayList, + items2: ArrayList + ): List { + val items = ArrayList(items1) + items2.forEach { item2 -> + var isInFirst = false + items1.forEach for1@{ item1 -> + if (item2.name == item1.name) { + isInFirst = true + return@for1 + } + } + if (!isInFirst) { + items.add(item2) + } + } + return items.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) } } @@ -180,42 +235,7 @@ class FontSelectDialog : BaseDialogFragment(), FileUtils.deleteFile(fontFolder.absolutePath) callBack?.selectFont(docItem.uri.toString()) }.onSuccess { - dialog?.dismiss() - } - } - - /** - * 字体文件夹 - */ - override fun onFilePicked(requestCode: Int, currentPath: String) { - when (requestCode) { - fontFolderRequestCode -> { - putPrefString(PreferKey.fontFolder, currentPath) - loadFontFilesByPermission(currentPath) - } - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - fontFolderRequestCode -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - putPrefString(PreferKey.fontFolder, uri.toString()) - val doc = DocumentFile.fromTreeUri(requireContext(), uri) - if (doc != null) { - context?.contentResolver?.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - loadFontFiles(doc) - } else { - RealPathUtil.getPath(requireContext(), uri)?.let { - loadFontFilesByPermission(it) - } - } - } - } + dismissAllowingStateLoss() } } diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt index 661992931..c84097a80 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt @@ -24,11 +24,11 @@ import io.legado.app.utils.sp import kotlin.math.min import kotlin.math.pow -class CircleImageView(context: Context, attrs: AttributeSet) : - AppCompatImageView( - context, - attrs - ) { +@Suppress("unused", "MemberVisibilityCanBePrivate") +class CircleImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { private val mDrawableRect = RectF() private val mBorderRect = RectF() @@ -119,9 +119,9 @@ class CircleImageView(context: Context, attrs: AttributeSet) : private var textColor = context.getCompatColor(R.color.primaryText) private var textBold = false + var isInView = false init { - super.setScaleType(SCALE_TYPE) val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView) mBorderWidth = a.getDimensionPixelSize( @@ -138,6 +138,7 @@ class CircleImageView(context: Context, attrs: AttributeSet) : DEFAULT_CIRCLE_BACKGROUND_COLOR ) text = a.getString(R.styleable.CircleImageView_text) + contentDescription = text if (a.hasValue(R.styleable.CircleImageView_textColor)) { textColor = a.getColor( R.styleable.CircleImageView_textColor, @@ -158,16 +159,6 @@ class CircleImageView(context: Context, attrs: AttributeSet) : } } - override fun getScaleType(): ScaleType { - return SCALE_TYPE - } - - override fun setScaleType(scaleType: ScaleType) { - if (scaleType != SCALE_TYPE) { - throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)) - } - } - override fun setAdjustViewBounds(adjustViewBounds: Boolean) { if (adjustViewBounds) { throw IllegalArgumentException("adjustViewBounds not supported.") @@ -223,6 +214,12 @@ class CircleImageView(context: Context, attrs: AttributeSet) : } } + fun setText(text: String?) { + this.text = text + contentDescription = text + invalidate() + } + fun setTextColor(@ColorInt textColor: Int) { this.textColor = textColor invalidate() @@ -422,7 +419,12 @@ class CircleImageView(context: Context, attrs: AttributeSet) : @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { - return inTouchableArea(event.x, event.y) && super.onTouchEvent(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isInView = (inTouchableArea(event.x, event.y)) + } + } + return super.onTouchEvent(event) } private fun inTouchableArea(x: Float, y: Float): Boolean { @@ -443,7 +445,6 @@ class CircleImageView(context: Context, attrs: AttributeSet) : } companion object { - private val SCALE_TYPE = ScaleType.CENTER_CROP private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 private const val COLOR_DRAWABLE_DIMENSION = 2 private const val DEFAULT_BORDER_WIDTH = 0 diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt index f2b4373a4..3e823829c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt @@ -10,14 +10,24 @@ import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target -import io.legado.app.App import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig import io.legado.app.help.ImageLoader import io.legado.app.utils.getPrefString - - -class CoverImageView : androidx.appcompat.widget.AppCompatImageView { +import splitties.init.appCtx + +/** + * 封面 + */ +@Suppress("unused") +class CoverImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : androidx.appcompat.widget.AppCompatImageView( + context, + attrs +) { internal var width: Float = 0.toFloat() internal var height: Float = 0.toFloat() private var nameHeight = 0f @@ -42,16 +52,6 @@ class CoverImageView : androidx.appcompat.widget.AppCompatImageView { private var author: String? = null private var loadFailed = false - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) val measuredHeight = measuredWidth * 7 / 5 @@ -133,34 +133,40 @@ class CoverImageView : androidx.appcompat.widget.AppCompatImageView { fun load(path: String?, name: String?, author: String?) { setText(name, author) - ImageLoader.load(context, path)//Glide自动识别http://,content://和file:// - .placeholder(defaultDrawable) - .error(defaultDrawable) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - loadFailed = true - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - loadFailed = false - return false - } - - }) - .centerCrop() - .into(this) + if (AppConfig.useDefaultCover) { + ImageLoader.load(context, defaultDrawable) + .centerCrop() + .into(this) + } else { + ImageLoader.load(context, path)//Glide自动识别http://,content://和file:// + .placeholder(defaultDrawable) + .error(defaultDrawable) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + loadFailed = true + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + loadFailed = false + return false + } + + }) + .centerCrop() + .into(this) + } } companion object { @@ -171,12 +177,16 @@ class CoverImageView : androidx.appcompat.widget.AppCompatImageView { upDefaultCover() } + @SuppressLint("UseCompatLoadingForDrawables") fun upDefaultCover() { - val path = App.INSTANCE.getPrefString(PreferKey.defaultCover) + val preferKey = + if (AppConfig.isNightTheme) PreferKey.defaultCoverDark + else PreferKey.defaultCover + val path = appCtx.getPrefString(preferKey) var dw = Drawable.createFromPath(path) if (dw == null) { showBookName = true - dw = App.INSTANCE.resources.getDrawable(R.drawable.image_cover_default, null) + dw = appCtx.resources.getDrawable(R.drawable.image_cover_default, null) } else { showBookName = false } diff --git a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt index bcb449329..eefcdafc8 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt @@ -10,7 +10,10 @@ import io.legado.app.R import io.legado.app.utils.dp import kotlin.math.max -class FilletImageView : AppCompatImageView { +class FilletImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { internal var width: Float = 0.toFloat() internal var height: Float = 0.toFloat() private var leftTopRadius: Int = 0 @@ -18,26 +21,29 @@ class FilletImageView : AppCompatImageView { private var rightBottomRadius: Int = 0 private var leftBottomRadius: Int = 0 - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet) { + init { // 读取配置 val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView) val defaultRadius = 5.dp - val radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) - leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius) - rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius) + val radius = + array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) + leftTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_top_radius, + defaultRadius + ) + rightTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_top_radius, + defaultRadius + ) rightBottomRadius = - array.getDimensionPixelOffset(R.styleable.FilletImageView_right_bottom_radius, defaultRadius) - leftBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_bottom_radius, defaultRadius) + array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_bottom_radius, + defaultRadius + ) + leftBottomRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_bottom_radius, + defaultRadius + ) //如果四个角的值没有设置,那么就使用通用的radius的值。 if (defaultRadius == leftTopRadius) { @@ -53,7 +59,6 @@ class FilletImageView : AppCompatImageView { leftBottomRadius = radius } array.recycle() - } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { diff --git a/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt index 44f108e91..b028dd116 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt @@ -16,6 +16,7 @@ import android.view.animation.Interpolator import android.widget.ImageView import android.widget.OverScroller import android.widget.Scroller +import androidx.appcompat.widget.AppCompatImageView import io.legado.app.R import io.legado.app.ui.widget.image.photo.Info import io.legado.app.ui.widget.image.photo.OnRotateListener @@ -23,9 +24,11 @@ import io.legado.app.ui.widget.image.photo.RotateGestureDetector import kotlin.math.abs import kotlin.math.roundToInt - -@SuppressLint("AppCompatCustomView") -class PhotoView : ImageView { +@Suppress("UNUSED_PARAMETER", "unused", "MemberVisibilityCanBePrivate", "PropertyName") +class PhotoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { val MIN_ROTATE = 35 val ANIMA_DURING = 340 val MAX_SCALE = 2.5f @@ -44,9 +47,9 @@ class PhotoView : ImageView { private val mSynthesisMatrix: Matrix = Matrix() private val mTmpMatrix: Matrix = Matrix() - private var mRotateDetector: RotateGestureDetector? = null - private var mDetector: GestureDetector? = null - private var mScaleDetector: ScaleGestureDetector? = null + private val mRotateDetector: RotateGestureDetector + private val mDetector: GestureDetector + private val mScaleDetector: ScaleGestureDetector private var mClickListener: OnClickListener? = null private var mScaleType: ScaleType? = null @@ -99,23 +102,11 @@ class PhotoView : ImageView { private var mLongClick: OnLongClickListener? = null - constructor(context: Context) : super(context) { - init() - } + private val mRotateListener = RotateListener() + private val mGestureListener = GestureListener() + private val mScaleListener = ScaleGestureListener() - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - private fun init() { + init { super.setScaleType(ScaleType.MATRIX) if (mScaleType == null) mScaleType = ScaleType.CENTER_INSIDE mRotateDetector = RotateGestureDetector(mRotateListener) @@ -197,6 +188,7 @@ class PhotoView : ImageView { MAX_ANIM_FROM_WAITE = wait } + @SuppressLint("UseCompatLoadingForDrawables") override fun setImageResource(resId: Int) { var drawable: Drawable? = null try { @@ -482,11 +474,11 @@ class PhotoView : ImageView { return if (isEnable) { val action = event.actionMasked if (event.pointerCount >= 2) hasMultiTouch = true - mDetector!!.onTouchEvent(event) + mDetector.onTouchEvent(event) if (isRotateEnable) { - mRotateDetector!!.onTouchEvent(event) + mRotateDetector.onTouchEvent(event) } - mScaleDetector!!.onTouchEvent(event) + mScaleDetector.onTouchEvent(event) if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) onUp() true } else { @@ -564,49 +556,6 @@ class PhotoView : ImageView { return abs(rect.left.roundToInt() - (mWidgetRect.width() - rect.width()) / 2) < 1 } - private val mRotateListener: OnRotateListener = object : - OnRotateListener { - override fun onRotate( - degrees: Float, - focusX: Float, - focusY: Float - ) { - mRotateFlag += degrees - if (canRotate) { - mDegrees += degrees - mAnimMatrix.postRotate(degrees, focusX, focusY) - } else { - if (abs(mRotateFlag) >= mMinRotate) { - canRotate = true - mRotateFlag = 0f - } - } - } - } - - private val mScaleListener: OnScaleGestureListener = object : OnScaleGestureListener { - override fun onScale(detector: ScaleGestureDetector): Boolean { - val scaleFactor = detector.scaleFactor - if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false - mScale *= scaleFactor - //mScaleCenter.set(detector.getFocusX(), detector.getFocusY()); - mAnimMatrix.postScale( - scaleFactor, - scaleFactor, - detector.focusX, - detector.focusY - ) - executeTranslate() - return true - } - - override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { - return true - } - - override fun onScaleEnd(detector: ScaleGestureDetector) {} - } - private fun resistanceScrollByX( overScroll: Float, detalX: Float @@ -650,147 +599,6 @@ class PhotoView : ImageView { mClickListener?.onClick(this) } - private val mGestureListener: GestureDetector.OnGestureListener = - object : SimpleOnGestureListener() { - override fun onLongPress(e: MotionEvent) { - mLongClick?.onLongClick(this@PhotoView) - } - - override fun onDown(e: MotionEvent): Boolean { - hasOverTranslate = false - hasMultiTouch = false - canRotate = false - removeCallbacks(mClickRunnable) - return false - } - - override fun onFling( - e1: MotionEvent, - e2: MotionEvent, - velocityX: Float, - velocityY: Float - ): Boolean { - if (hasMultiTouch) return false - if (!imgLargeWidth && !imgLargeHeight) return false - if (mTranslate.isRunning) return false - var vx = velocityX - var vy = velocityY - if (mImgRect.left.roundToInt() >= mWidgetRect.left - || mImgRect.right.roundToInt() <= mWidgetRect.right - ) { - vx = 0f - } - if (mImgRect.top.roundToInt() >= mWidgetRect.top - || mImgRect.bottom.roundToInt() <= mWidgetRect.bottom - ) { - vy = 0f - } - if (canRotate || mDegrees % 90 != 0f) { - var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() - val remainder = mDegrees % 90 - if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f - mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) - mDegrees = toDegrees - } - doTranslateReset(mImgRect) - mTranslate.withFling(vx, vy) - mTranslate.start() - // onUp(e2); - return super.onFling(e1, e2, velocityX, velocityY) - } - - override fun onScroll( - e1: MotionEvent, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - var x = distanceX - var y = distanceY - if (mTranslate.isRunning) { - mTranslate.stop() - } - if (canScrollHorizontallySelf(x)) { - if (x < 0 && mImgRect.left - x > mWidgetRect.left) - x = mImgRect.left - if (x > 0 && mImgRect.right - x < mWidgetRect.right) - x = mImgRect.right - mWidgetRect.right - mAnimMatrix.postTranslate(-x, 0f) - mTranslateX -= x.toInt() - } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) { - checkRect() - if (!hasMultiTouch) { - if (x < 0 && mImgRect.left - x > mCommonRect.left) x = - resistanceScrollByX(mImgRect.left - mCommonRect.left, x) - if (x > 0 && mImgRect.right - x < mCommonRect.right) x = - resistanceScrollByX(mImgRect.right - mCommonRect.right, x) - } - mTranslateX -= x.toInt() - mAnimMatrix.postTranslate(-x, 0f) - hasOverTranslate = true - } - if (canScrollVerticallySelf(y)) { - if (y < 0 && mImgRect.top - y > mWidgetRect.top) y = - mImgRect.top - if (y > 0 && mImgRect.bottom - y < mWidgetRect.bottom) y = - mImgRect.bottom - mWidgetRect.bottom - mAnimMatrix.postTranslate(0f, -y) - mTranslateY -= y.toInt() - } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) { - checkRect() - if (!hasMultiTouch) { - if (y < 0 && mImgRect.top - y > mCommonRect.top) y = - resistanceScrollByY(mImgRect.top - mCommonRect.top, y) - if (y > 0 && mImgRect.bottom - y < mCommonRect.bottom) y = - resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, y) - } - mAnimMatrix.postTranslate(0f, -y) - mTranslateY -= y.toInt() - hasOverTranslate = true - } - executeTranslate() - return true - } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - postDelayed(mClickRunnable, 250) - return false - } - - override fun onDoubleTap(e: MotionEvent): Boolean { - mTranslate.stop() - val from: Float - val to: Float - val imgCx = mImgRect.left + mImgRect.width() / 2 - val imgCy = mImgRect.top + mImgRect.height() / 2 - mScaleCenter[imgCx] = imgCy - mRotateCenter[imgCx] = imgCy - mTranslateX = 0 - mTranslateY = 0 - if (isZoonUp) { - from = mScale - to = 1f - } else { - from = mScale - to = mMaxScale - mScaleCenter[e.x] = e.y - } - mTmpMatrix.reset() - mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) - mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) - mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) - mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) - mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y) - mTmpMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) - mTmpMatrix.mapRect(mTmpRect, mBaseRect) - doTranslateReset(mTmpRect) - isZoonUp = !isZoonUp - mTranslate.withScale(from, to) - mTranslate.start() - return false - } - } - fun canScrollHorizontallySelf(direction: Float): Boolean { if (mImgRect.width() <= mWidgetRect.width()) return false @@ -832,7 +640,7 @@ class PhotoView : ImageView { } - private inner class Transform internal constructor() : Runnable { + private inner class Transform : Runnable { var isRunning = false var mTranslateScroller: OverScroller var mFlingScroller: OverScroller @@ -1073,7 +881,7 @@ class PhotoView : ImageView { ) } - fun getImageViewInfo(imgView: ImageView): Info? { + fun getImageViewInfo(imgView: ImageView): Info { val p = IntArray(2) getLocation(imgView, p) val drawable: Drawable = imgView.drawable @@ -1230,7 +1038,7 @@ class PhotoView : ImageView { val scale = if (scaleX > scaleY) scaleX else scaleY mAnimMatrix.postRotate(mDegrees, mScaleCenter.x, mScaleCenter.y) mAnimMatrix.mapRect(mImgRect, mBaseRect) - mDegrees = mDegrees % 360 + mDegrees %= 360 mTranslate.withTranslate( 0, 0, @@ -1266,4 +1074,185 @@ class PhotoView : ImageView { executeTranslate() } + inner class RotateListener : OnRotateListener { + override fun onRotate( + degrees: Float, + focusX: Float, + focusY: Float + ) { + mRotateFlag += degrees + if (canRotate) { + mDegrees += degrees + mAnimMatrix.postRotate(degrees, focusX, focusY) + } else { + if (abs(mRotateFlag) >= mMinRotate) { + canRotate = true + mRotateFlag = 0f + } + } + } + } + + inner class GestureListener : SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + mLongClick?.onLongClick(this@PhotoView) + } + + override fun onDown(e: MotionEvent): Boolean { + hasOverTranslate = false + hasMultiTouch = false + canRotate = false + removeCallbacks(mClickRunnable) + return false + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (hasMultiTouch) return false + if (!imgLargeWidth && !imgLargeHeight) return false + if (mTranslate.isRunning) return false + var vx = velocityX + var vy = velocityY + if (mImgRect.left.roundToInt() >= mWidgetRect.left + || mImgRect.right.roundToInt() <= mWidgetRect.right + ) { + vx = 0f + } + if (mImgRect.top.roundToInt() >= mWidgetRect.top + || mImgRect.bottom.roundToInt() <= mWidgetRect.bottom + ) { + vy = 0f + } + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + doTranslateReset(mImgRect) + mTranslate.withFling(vx, vy) + mTranslate.start() + // onUp(e2); + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onScroll( + e1: MotionEvent, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var x = distanceX + var y = distanceY + if (mTranslate.isRunning) { + mTranslate.stop() + } + if (canScrollHorizontallySelf(x)) { + if (x < 0 && mImgRect.left - x > mWidgetRect.left) + x = mImgRect.left + if (x > 0 && mImgRect.right - x < mWidgetRect.right) + x = mImgRect.right - mWidgetRect.right + mAnimMatrix.postTranslate(-x, 0f) + mTranslateX -= x.toInt() + } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) { + checkRect() + if (!hasMultiTouch) { + if (x < 0 && mImgRect.left - x > mCommonRect.left) x = + resistanceScrollByX(mImgRect.left - mCommonRect.left, x) + if (x > 0 && mImgRect.right - x < mCommonRect.right) x = + resistanceScrollByX(mImgRect.right - mCommonRect.right, x) + } + mTranslateX -= x.toInt() + mAnimMatrix.postTranslate(-x, 0f) + hasOverTranslate = true + } + if (canScrollVerticallySelf(y)) { + if (y < 0 && mImgRect.top - y > mWidgetRect.top) y = + mImgRect.top + if (y > 0 && mImgRect.bottom - y < mWidgetRect.bottom) y = + mImgRect.bottom - mWidgetRect.bottom + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) { + checkRect() + if (!hasMultiTouch) { + if (y < 0 && mImgRect.top - y > mCommonRect.top) y = + resistanceScrollByY(mImgRect.top - mCommonRect.top, y) + if (y > 0 && mImgRect.bottom - y < mCommonRect.bottom) y = + resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, y) + } + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + hasOverTranslate = true + } + executeTranslate() + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + postDelayed(mClickRunnable, 250) + return false + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + mTranslate.stop() + val from: Float + val to: Float + val imgCx = mImgRect.left + mImgRect.width() / 2 + val imgCy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[imgCx] = imgCy + mRotateCenter[imgCx] = imgCy + mTranslateX = 0 + mTranslateY = 0 + if (isZoonUp) { + from = mScale + to = 1f + } else { + from = mScale + to = mMaxScale + mScaleCenter[e.x] = e.y + } + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y) + mTmpMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + isZoonUp = !isZoonUp + mTranslate.withScale(from, to) + mTranslate.start() + return false + } + } + + inner class ScaleGestureListener : OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + mScale *= scaleFactor + //mScaleCenter.set(detector.getFocusX(), detector.getFocusY()); + mAnimMatrix.postScale( + scaleFactor, + scaleFactor, + detector.focusX, + detector.focusY + ) + executeTranslate() + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) {} + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt index abd6f9080..6ce1adfc6 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt @@ -6,6 +6,7 @@ import android.graphics.RectF import android.widget.ImageView +@Suppress("MemberVisibilityCanBePrivate") class Info( rect: RectF, img: RectF, diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt index 4bc309b56..7dddbebbf 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt @@ -21,31 +21,31 @@ class RotateGestureDetector(private val mListener: OnRotateListener) { when (event.actionMasked) { MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> { - if (event.pointerCount == 2) mPrevSlope = caculateSlope(event) + if (event.pointerCount == 2) mPrevSlope = calculateSlope(event) } MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1) { - mCurrSlope = caculateSlope(event) + mCurrSlope = calculateSlope(event) - val currDegrees = Math.toDegrees(atan(mCurrSlope.toDouble())); - val prevDegrees = Math.toDegrees(atan(mPrevSlope.toDouble())); + val currDegrees = Math.toDegrees(atan(mCurrSlope.toDouble())) + val prevDegrees = Math.toDegrees(atan(mPrevSlope.toDouble())) - val deltaSlope = currDegrees - prevDegrees; + val deltaSlope = currDegrees - prevDegrees if (abs(deltaSlope) <= MAX_DEGREES_STEP) { - mListener?.onRotate(deltaSlope.toFloat(), (x2 + x1) / 2, (y2 + y1) / 2); + mListener.onRotate(deltaSlope.toFloat(), (x2 + x1) / 2, (y2 + y1) / 2) } - mPrevSlope = mCurrSlope; + mPrevSlope = mCurrSlope } } } - private fun caculateSlope(event: MotionEvent): Float { - val x1 = event.getX(0); - val y1 = event.getY(0); - val x2 = event.getX(1); - val y2 = event.getY(1); - return (y2 - y1) / (x2 - x1); + private fun calculateSlope(event: MotionEvent): Float { + val x1 = event.getX(0) + val y1 = event.getY(0) + val x2 = event.getX(1) + val y2 = event.getY(1) + return (y2 - y1) / (x2 - x1) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt index 3b4c8f5db..01e11efad 100644 --- a/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt @@ -6,7 +6,6 @@ import androidx.appcompat.app.AlertDialog import io.legado.app.R import io.legado.app.utils.applyTint import io.legado.app.utils.hideSoftInput -import kotlinx.android.synthetic.main.dialog_number_picker.* class NumberPickerDialog(context: Context) { @@ -48,7 +47,7 @@ class NumberPickerDialog(context: Context) { listener?.invoke() } } - return this; + return this } fun show(callBack: ((value: Int) -> Unit)?) { @@ -61,7 +60,7 @@ class NumberPickerDialog(context: Context) { } builder.setNegativeButton(R.string.cancel, null) val dialog = builder.show().applyTint() - numberPicker = dialog.number_picker + numberPicker = dialog.findViewById(R.id.number_picker) numberPicker?.let { np -> minValue?.let { np.minValue = it diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt index 8f293eebb..468f4ce71 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt @@ -16,6 +16,7 @@ import com.jaredrummler.android.colorpicker.* import io.legado.app.lib.theme.ATH import io.legado.app.utils.ColorUtils +@Suppress("MemberVisibilityCanBePrivate", "unused") class ColorPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), ColorPickerDialogListener { diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt index 8c0e39da3..444f47506 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/EditTextPreference.kt @@ -6,7 +6,8 @@ import android.widget.TextView import androidx.preference.PreferenceViewHolder import io.legado.app.R -class EditTextPreference(context: Context, attrs: AttributeSet) : androidx.preference.EditTextPreference(context, attrs) { +class EditTextPreference(context: Context, attrs: AttributeSet) : + androidx.preference.EditTextPreference(context, attrs) { init { // isPersistent = true diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt index 7747c6bf2..12b33a2c7 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt @@ -5,7 +5,6 @@ import android.content.ContextWrapper import android.graphics.drawable.Drawable import android.os.Bundle import android.util.AttributeSet -import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -17,12 +16,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemIconPreferenceBinding import io.legado.app.lib.theme.primaryColor import io.legado.app.utils.getCompatDrawable -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_icon_preference.view.* -import org.jetbrains.anko.sdk27.listeners.onClick +import io.legado.app.utils.getSize +import io.legado.app.utils.viewbindingdelegate.viewBinding class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { @@ -54,7 +54,17 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference override fun onBindViewHolder(holder: PreferenceViewHolder?) { super.onBindViewHolder(holder) - val v = Preference.bindView(context, holder, icon, title, summary, widgetLayoutResource, R.id.preview, 50, 50) + val v = Preference.bindView( + context, + holder, + icon, + title, + summary, + widgetLayoutResource, + R.id.preview, + 50, + 50 + ) if (v is ImageView) { val selectedIndex = findIndexOfValue(value) if (selectedIndex >= 0) { @@ -117,11 +127,11 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference var dialogEntries: Array? = null var dialogEntryValues: Array? = null var dialogIconNames: Array? = null + private val binding by viewBinding(DialogRecyclerViewBinding::bind) override fun onStart() { super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) + val dm = requireActivity().getSize() dialog?.window?.setLayout( (dm.widthPixels * 0.8).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT @@ -137,11 +147,11 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference } override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - tool_bar.setBackgroundColor(primaryColor) - tool_bar.setTitle(R.string.change_icon) - recycler_view.layoutManager = LinearLayoutManager(requireContext()) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.change_icon) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) val adapter = Adapter(requireContext()) - recycler_view.adapter = adapter + binding.recyclerView.adapter = adapter arguments?.let { dialogValue = it.getString("value") dialogEntries = it.getCharSequenceArray("entries") @@ -155,14 +165,19 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference inner class Adapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_icon_preference) { + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemIconPreferenceBinding { + return ItemIconPreferenceBinding.inflate(inflater, parent, false) + } override fun convert( holder: ItemViewHolder, + binding: ItemIconPreferenceBinding, item: CharSequence, payloads: MutableList ) { - with(holder.itemView) { + binding.run { val index = findIndexOfValue(item.toString()) dialogEntries?.let { label.text = it[index] @@ -180,15 +195,18 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference } } label.isChecked = item.toString() == dialogValue - onClick { + root.setOnClickListener { onChanged?.invoke(item.toString()) - this@IconDialog.dismiss() + this@IconDialog.dismissAllowingStateLoss() } } } - override fun registerListener(holder: ItemViewHolder) { - holder.itemView.onClick { + override fun registerListener( + holder: ItemViewHolder, + binding: ItemIconPreferenceBinding + ) { + holder.itemView.setOnClickListener { getItem(holder.layoutPosition)?.let { onChanged?.invoke(it.toString()) } diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt index c83ea7433..bb1e5d791 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt @@ -36,9 +36,11 @@ class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference ) if (v is TextView) { v.text = entry - val bgColor = context.bottomBackground - val pTextColor = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) - v.setTextColor(pTextColor) + if (isBottomBackground) { + val bgColor = context.bottomBackground + val pTextColor = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + v.setTextColor(pTextColor) + } } super.onBindViewHolder(holder) } diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt index 042b03868..baea502b3 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt @@ -17,8 +17,7 @@ import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getSecondaryTextColor import io.legado.app.utils.ColorUtils -import org.jetbrains.anko.layoutInflater -import org.jetbrains.anko.sdk27.listeners.onLongClick +import splitties.views.onLongClick import kotlin.math.roundToInt class Preference(context: Context, attrs: AttributeSet) : @@ -77,7 +76,7 @@ class Preference(context: Context, attrs: AttributeSet) : var needRequestLayout = false var v = it.itemView.findViewById(viewId) if (v == null) { - val inflater: LayoutInflater = context.layoutInflater + val inflater: LayoutInflater = LayoutInflater.from(context) val childView = inflater.inflate(weightLayoutRes, null) lay.removeAllViews() lay.addView(childView) @@ -119,7 +118,6 @@ class Preference(context: Context, attrs: AttributeSet) : super.onBindViewHolder(holder) holder?.itemView?.onLongClick { onLongClick?.invoke() - true } } diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt index a21d0e1ce..69e4fd249 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt @@ -14,7 +14,8 @@ import io.legado.app.lib.theme.backgroundColor import io.legado.app.utils.ColorUtils -class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) { +class PreferenceCategory(context: Context, attrs: AttributeSet) : + PreferenceCategory(context, attrs) { init { isPersistent = true @@ -28,15 +29,20 @@ class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCate if (view is TextView) { // && !view.isInEditMode view.text = title if (view.isInEditMode) return - view.setBackgroundColor(context.backgroundColor) view.setTextColor(context.accentColor) view.isVisible = title != null && title.isNotEmpty() val da = it.findViewById(R.id.preference_divider_above) val dividerColor = if (AppConfig.isNightTheme) { - ColorUtils.shiftColor(context.backgroundColor, 1.05f) + ColorUtils.withAlpha( + ColorUtils.shiftColor(context.backgroundColor, 1.05f), + 0.5f + ) } else { - ColorUtils.shiftColor(context.backgroundColor, 0.95f) + ColorUtils.withAlpha( + ColorUtils.shiftColor(context.backgroundColor, 0.95f), + 0.5f + ) } if (da is View) { da.setBackgroundColor(dividerColor) diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt index 5c9e38e4d..92ac1a672 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt @@ -14,6 +14,7 @@ import kotlin.math.roundToInt /** * 不画最后一条分隔线 */ +@Suppress("MemberVisibilityCanBePrivate", "RedundantRequireNotNullCall", "unused") class DividerNoLast(context: Context, orientation: Int) : RecyclerView.ItemDecoration() { diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt index 015595ec1..65705a306 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt @@ -51,7 +51,7 @@ import kotlin.math.min * | | ----------------------------------------------> | | * +-------------------+ +-----------------------+ */ -@Suppress("unused") +@Suppress("unused", "MemberVisibilityCanBePrivate") class DragSelectTouchHelper( /** * Developer callback which controls the behavior of DragSelectTouchHelper. @@ -136,10 +136,14 @@ class DragSelectTouchHelper( View.OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> if (oldLeft != left || oldRight != right || oldTop != top || oldBottom != bottom) { if (v === mRecyclerView) { - Logger.i("onLayoutChange:new: " - + left + " " + top + " " + right + " " + bottom) - Logger.i("onLayoutChange:old: " - + oldLeft + " " + oldTop + " " + oldRight + " " + oldBottom) + Logger.i( + "onLayoutChange:new: " + + left + " " + top + " " + right + " " + bottom + ) + Logger.i( + "onLayoutChange:old: " + + oldLeft + " " + oldTop + " " + oldRight + " " + oldBottom + ) init(bottom - top) } } @@ -203,8 +207,10 @@ class DragSelectTouchHelper( private val mOnItemTouchListener: OnItemTouchListener by lazy { object : OnItemTouchListener { override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { - Logger.d("onInterceptTouchEvent: x:" + e.x + ",y:" + e.y - + ", " + MotionEvent.actionToString(e.action)) + Logger.d( + "onInterceptTouchEvent: x:" + e.x + ",y:" + e.y + + ", " + MotionEvent.actionToString(e.action) + ) val adapter = rv.adapter if (adapter == null || adapter.itemCount == 0) { return false @@ -267,8 +273,10 @@ class DragSelectTouchHelper( if (!isActivated) { return } - Logger.d("onTouchEvent: x:" + e.x + ",y:" + e.y - + ", " + MotionEvent.actionToString(e.action)) + Logger.d( + "onTouchEvent: x:" + e.x + ",y:" + e.y + + ", " + MotionEvent.actionToString(e.action) + ) val action = e.action when (action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_MOVE -> { @@ -506,8 +514,10 @@ class DragSelectTouchHelper( mBottomRegionFrom = (rvHeight shr 1.toFloat().toInt()).toFloat() mTopRegionTo = mBottomRegionFrom } - Logger.d("Hotspot: [" + mTopRegionFrom + ", " + mTopRegionTo + "], [" - + mBottomRegionFrom + ", " + mBottomRegionTo + "]") + Logger.d( + "Hotspot: [" + mTopRegionFrom + ", " + mTopRegionTo + "], [" + + mBottomRegionFrom + ", " + mBottomRegionTo + "]" + ) } private fun activeSelectInternal(position: Int) { @@ -613,12 +623,14 @@ class DragSelectTouchHelper( SELECT_STATE_DRAG_FROM_NORMAL -> mSelectState = if (mShouldAutoChangeState) { Logger.logSelectStateChange( mSelectState, - SELECT_STATE_SLIDE) + SELECT_STATE_SLIDE + ) SELECT_STATE_SLIDE } else { Logger.logSelectStateChange( mSelectState, - SELECT_STATE_NORMAL) + SELECT_STATE_NORMAL + ) SELECT_STATE_NORMAL } SELECT_STATE_DRAG_FROM_SLIDE -> { @@ -705,8 +717,10 @@ class DragSelectTouchHelper( } private fun dp2px(dpVal: Float): Int { - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - dpVal, mDisplayMetrics).toInt() + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dpVal, mDisplayMetrics + ).toInt() } private val isRtl: Boolean @@ -848,8 +862,10 @@ class DragSelectTouchHelper( if (isSelected) { updateSelectState(position, true) } else { - updateSelectState(position, - mOriginalSelection.contains(getItemId(position))) + updateSelectState( + position, + mOriginalSelection.contains(getItemId(position)) + ) } } Mode.ToggleAndKeep -> { @@ -866,8 +882,10 @@ class DragSelectTouchHelper( if (isSelected) { updateSelectState(position, !mFirstWasSelected) } else { - updateSelectState(position, - mOriginalSelection.contains(getItemId(position))) + updateSelectState( + position, + mOriginalSelection.contains(getItemId(position)) + ) } } else -> // SelectAndReverse Mode diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt index b015a0086..8c8a0d907 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt @@ -6,26 +6,20 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.viewpager.widget.ViewPager /** * Created by GKF on 2018/3/16. */ - -class ItemTouchCallback : ItemTouchHelper.Callback() { +@Suppress("MemberVisibilityCanBePrivate") +class ItemTouchCallback(private val callback: Callback) : ItemTouchHelper.Callback() { private var swipeRefreshLayout: SwipeRefreshLayout? = null - private var viewPager: ViewPager? = null - - /** - * Item操作的回调 - */ - var onItemTouchCallbackListener: OnItemTouchCallbackListener? = null /** * 是否可以拖拽 */ var isCanDrag = false + /** * 是否可以被滑动 */ @@ -48,11 +42,15 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { /** * 当用户拖拽或者滑动Item的时候需要我们告诉系统滑动或者拖拽的方向 */ - override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { val layoutManager = recyclerView.layoutManager if (layoutManager is GridLayoutManager) {// GridLayoutManager // flag如果值是0,相当于这个功能被关闭 - val dragFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN + val dragFlag = + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN val swipeFlag = 0 // create make return makeMovementFlags(dragFlag, swipeFlag) @@ -88,28 +86,36 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { srcViewHolder: RecyclerView.ViewHolder, targetViewHolder: RecyclerView.ViewHolder ): Boolean { - return onItemTouchCallbackListener - ?.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition) - ?: false + val fromPosition: Int = srcViewHolder.adapterPosition + val toPosition: Int = targetViewHolder.adapterPosition + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + callback.swap(i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + callback.swap(i, i - 1) + } + } + return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - onItemTouchCallbackListener?.onSwiped(viewHolder.adapterPosition) + callback.onSwiped(viewHolder.adapterPosition) } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { super.onSelectedChanged(viewHolder, actionState) val swiping = actionState == ItemTouchHelper.ACTION_STATE_DRAG swipeRefreshLayout?.isEnabled = !swiping - viewPager?.requestDisallowInterceptTouchEvent(swiping) } override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) - onItemTouchCallbackListener?.onClearView(recyclerView, viewHolder) + callback.onClearView(recyclerView, viewHolder) } - interface OnItemTouchCallbackListener { + interface Callback { /** * 当某个Item被滑动删除的时候 @@ -127,7 +133,7 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { * @param targetPosition 目的地的Item的position * @return 开发者处理了操作应该返回true,开发者没有处理就返回false */ - fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + fun swap(srcPosition: Int, targetPosition: Int): Boolean { return true } diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt index a2437c0b4..c623938ae 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt @@ -1,46 +1,57 @@ package io.legado.app.ui.widget.recycler import android.content.Context -import android.view.View +import android.util.AttributeSet +import android.view.LayoutInflater import android.view.ViewGroup import android.widget.FrameLayout import io.legado.app.R +import io.legado.app.databinding.ViewLoadMoreBinding import io.legado.app.utils.invisible import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.view_load_more.view.* - -class LoadMoreView(context: Context) : FrameLayout(context) { +@Suppress("unused") +class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + private val binding = ViewLoadMoreBinding.inflate(LayoutInflater.from(context), this) var hasMore = true private set - init { - View.inflate(context, R.layout.view_load_more, this) - } - override fun onAttachedToWindow() { super.onAttachedToWindow() layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT } fun startLoad() { - tv_text.invisible() - rotate_loading.show() + binding.tvText.invisible() + binding.rotateLoading.show() } fun stopLoad() { - rotate_loading.hide() + binding.rotateLoading.hide() + } + + fun hasMore() { + hasMore = true + binding.tvText.invisible() + binding.rotateLoading.show() } fun noMore(msg: String? = null) { hasMore = false - rotate_loading.hide() + binding.rotateLoading.hide() if (msg != null) { - tv_text.text = msg + binding.tvText.text = msg } else { - tv_text.setText(R.string.bottom_line) + binding.tvText.setText(R.string.bottom_line) } - tv_text.visible() + binding.tvText.visible() + } + + fun error(msg: String) { + hasMore = false + binding.rotateLoading.hide() + binding.tvText.text = msg + binding.tvText.visible() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtViewPager2.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt similarity index 55% rename from app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtViewPager2.kt rename to app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt index 79132f627..1dc4d7b5f 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtViewPager2.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt @@ -6,11 +6,18 @@ import android.view.MotionEvent import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class RecyclerViewAtViewPager2(context: Context, attrs: AttributeSet?) : - RecyclerView(context, attrs) { +class RecyclerViewAtPager2 : RecyclerView { - private var startX: Int = 0 - private var startY: Int = 0 + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + private var startX = 0 + private var startY = 0 override fun dispatchTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { @@ -25,17 +32,17 @@ class RecyclerViewAtViewPager2(context: Context, attrs: AttributeSet?) : val disX = abs(endX - startX) val disY = abs(endY - startY) if (disX > disY) { - parent.requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX)) + if (disX > 50) { + parent.requestDisallowInterceptTouchEvent(false) + } } else { - parent.requestDisallowInterceptTouchEvent(canScrollVertically(startY - endY)) + parent.requestDisallowInterceptTouchEvent(true) } } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent( - false - ) + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false) } return super.dispatchTouchEvent(ev) } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/RefreshRecyclerView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/RefreshRecyclerView.kt deleted file mode 100644 index 931a7d138..000000000 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/RefreshRecyclerView.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.legado.app.ui.widget.recycler - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.widget.LinearLayout -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.R -import kotlinx.android.synthetic.main.view_refresh_recycler.view.* - - -class RefreshRecyclerView(context: Context?, attrs: AttributeSet?) : LinearLayout(context, attrs) { - - private var durTouchX = -1000000f - private var durTouchY = -1000000f - - var onRefreshStart: (() -> Unit)? = null - - init { - orientation = VERTICAL - LayoutInflater.from(context).inflate(R.layout.view_refresh_recycler, this, true) - recycler_view.setOnTouchListener(object : OnTouchListener { - @SuppressLint("ClickableViewAccessibility") - override fun onTouch(v: View?, event: MotionEvent?): Boolean { - when (event?.action) { - MotionEvent.ACTION_DOWN -> { - durTouchX = event.x - durTouchY = event.y - } - MotionEvent.ACTION_MOVE -> { - if (durTouchX == -1000000f) { - durTouchX = event.x - } - if (durTouchY == -1000000f) - durTouchY = event.y - - val dY = event.y - durTouchY //>0下拉 - durTouchY = event.y - if (!refresh_progress_bar.isAutoLoading && refresh_progress_bar.getSecondDurProgress() == refresh_progress_bar.secondFinalProgress) { - recycler_view.adapter?.let { - if (it.itemCount > 0) { - if (0 == (recycler_view.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()) { - refresh_progress_bar.setSecondDurProgress((refresh_progress_bar.getSecondDurProgress() + dY / 2).toInt()) - } - } else { - refresh_progress_bar.setSecondDurProgress((refresh_progress_bar.getSecondDurProgress() + dY / 2).toInt()) - } - } - return refresh_progress_bar.getSecondDurProgress() > 0 - } - } - MotionEvent.ACTION_UP -> { - if (!refresh_progress_bar.isAutoLoading && refresh_progress_bar.secondMaxProgress > 0 && refresh_progress_bar.getSecondDurProgress() > 0) { - if (refresh_progress_bar.getSecondDurProgress() >= refresh_progress_bar.secondMaxProgress) { - refresh_progress_bar.isAutoLoading = true - onRefreshStart?.invoke() - } else { - refresh_progress_bar.setSecondDurProgressWithAnim(0) - } - } - durTouchX = -1000000f - durTouchY = -1000000f - } - } - return false - } - }) - } - - fun startLoading() { - refresh_progress_bar.isAutoLoading = true - onRefreshStart?.invoke() - } - - fun stopLoading() { - refresh_progress_bar.isAutoLoading = false - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt index 0af559c45..f0236bbea 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller +@Suppress("MemberVisibilityCanBePrivate", "unused") class UpLinearLayoutManager(val context: Context) : LinearLayoutManager(context) { fun smoothScrollToPosition(position: Int) { @@ -16,23 +17,30 @@ class UpLinearLayoutManager(val context: Context) : LinearLayoutManager(context) scroller.offset = offset startSmoothScroll(scroller) } -} -class UpLinearSmoothScroller(context: Context?): LinearSmoothScroller(context) { - var offset = 0 + class UpLinearSmoothScroller(context: Context?) : LinearSmoothScroller(context) { + var offset = 0 - override fun getVerticalSnapPreference(): Int { - return SNAP_TO_START - } + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } - override fun getHorizontalSnapPreference(): Int { - return SNAP_TO_START - } + override fun getHorizontalSnapPreference(): Int { + return SNAP_TO_START + } - override fun calculateDtToFit(viewStart: Int, viewEnd: Int, boxStart: Int, boxEnd: Int, snapPreference: Int): Int { - if (snapPreference == SNAP_TO_START) { - return boxStart - viewStart + offset + override fun calculateDtToFit( + viewStart: Int, + viewEnd: Int, + boxStart: Int, + boxEnd: Int, + snapPreference: Int + ): Int { + if (snapPreference == SNAP_TO_START) { + return boxStart - viewStart + offset + } + throw IllegalArgumentException("snap preference should be SNAP_TO_START") } - throw IllegalArgumentException("snap preference should be SNAP_TO_START") } -} \ No newline at end of file + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt index c2ebb8352..a7299f5e6 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt @@ -7,46 +7,44 @@ import androidx.annotation.ColorInt import androidx.recyclerview.widget.RecyclerView import io.legado.app.R +@Suppress("MemberVisibilityCanBePrivate", "unused") class FastScrollRecyclerView : RecyclerView { - private var mFastScroller: FastScroller? = null + private lateinit var mFastScroller: FastScroller constructor(context: Context) : super(context) { - layout(context, null) - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) - } - @JvmOverloads - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) { - + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0 + ) : super(context, attrs, defStyleAttr) { layout(context, attrs) - } + private fun layout(context: Context, attrs: AttributeSet?) { + mFastScroller = FastScroller(context, attrs) + mFastScroller.id = R.id.fast_scroller + } override fun setAdapter(adapter: Adapter<*>?) { - super.setAdapter(adapter) - if (adapter is FastScroller.SectionIndexer) { setSectionIndexer(adapter as FastScroller.SectionIndexer?) } else if (adapter == null) { setSectionIndexer(null) } - } override fun setVisibility(visibility: Int) { - super.setVisibility(visibility) - mFastScroller?.visibility = visibility - + mFastScroller.visibility = visibility } @@ -56,9 +54,7 @@ class FastScrollRecyclerView : RecyclerView { * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller */ fun setSectionIndexer(sectionIndexer: FastScroller.SectionIndexer?) { - - mFastScroller?.setSectionIndexer(sectionIndexer) - + mFastScroller.setSectionIndexer(sectionIndexer) } @@ -68,9 +64,7 @@ class FastScrollRecyclerView : RecyclerView { * @param enabled True to enable fast scrolling, false otherwise */ fun setFastScrollEnabled(enabled: Boolean) { - - mFastScroller!!.isEnabled = enabled - + mFastScroller.isEnabled = enabled } @@ -80,9 +74,7 @@ class FastScrollRecyclerView : RecyclerView { * @param hideScrollbar True to hide the scrollbar, false to show */ fun setHideScrollbar(hideScrollbar: Boolean) { - - mFastScroller?.setFadeScrollbar(hideScrollbar) - + mFastScroller.setFadeScrollbar(hideScrollbar) } /** @@ -91,9 +83,7 @@ class FastScrollRecyclerView : RecyclerView { * @param visible True to show scroll track, false to hide */ fun setTrackVisible(visible: Boolean) { - - mFastScroller?.setTrackVisible(visible) - + mFastScroller.setTrackVisible(visible) } /** @@ -102,9 +92,7 @@ class FastScrollRecyclerView : RecyclerView { * @param color The color for the scroll track */ fun setTrackColor(@ColorInt color: Int) { - - mFastScroller?.setTrackColor(color) - + mFastScroller.setTrackColor(color) } @@ -114,9 +102,7 @@ class FastScrollRecyclerView : RecyclerView { * @param color The color for the scroll handle */ fun setHandleColor(@ColorInt color: Int) { - - mFastScroller?.setHandleColor(color) - + mFastScroller.setHandleColor(color) } @@ -126,9 +112,7 @@ class FastScrollRecyclerView : RecyclerView { * @param visible True to show the bubble, false to hide */ fun setBubbleVisible(visible: Boolean) { - - mFastScroller?.setBubbleVisible(visible) - + mFastScroller.setBubbleVisible(visible) } @@ -138,9 +122,7 @@ class FastScrollRecyclerView : RecyclerView { * @param color The background color for the index bubble */ fun setBubbleColor(@ColorInt color: Int) { - - mFastScroller?.setBubbleColor(color) - + mFastScroller.setBubbleColor(color) } @@ -150,7 +132,7 @@ class FastScrollRecyclerView : RecyclerView { * @param color The text color for the index bubble */ fun setBubbleTextColor(@ColorInt color: Int) { - mFastScroller?.setBubbleTextColor(color) + mFastScroller.setBubbleTextColor(color) } @@ -160,35 +142,24 @@ class FastScrollRecyclerView : RecyclerView { * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events */ fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) { - - mFastScroller?.setFastScrollStateChangeListener(fastScrollStateChangeListener) - + mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener) } override fun onAttachedToWindow() { - super.onAttachedToWindow() - - mFastScroller?.attachRecyclerView(this) - + mFastScroller.attachRecyclerView(this) val parent = parent - if (parent is ViewGroup) { + if (parent is ViewGroup && parent.indexOfChild(mFastScroller) == -1) { parent.addView(mFastScroller) - mFastScroller?.setLayoutParams(parent) + mFastScroller.setLayoutParams(parent) } } override fun onDetachedFromWindow() { - mFastScroller?.detachRecyclerView() + mFastScroller.detachRecyclerView() super.onDetachedFromWindow() } - - private fun layout(context: Context, attrs: AttributeSet?) { - mFastScroller = FastScroller(context, attrs) - mFastScroller?.id = R.id.fast_scroller - } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt index 20f638d61..5aafdade1 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt @@ -33,9 +33,11 @@ import kotlin.math.min import kotlin.math.roundToInt +@Suppress("SameParameterValue") class FastScroller : LinearLayout { @ColorInt private var mBubbleColor: Int = 0 + @ColorInt private var mHandleColor: Int = 0 private var mBubbleHeight: Int = 0 @@ -89,7 +91,11 @@ class FastScroller : LinearLayout { } @JvmOverloads - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super( + context, + attrs, + defStyleAttr + ) { layout(context, attrs) layoutParams = generateLayoutParams(attrs) } @@ -102,16 +108,32 @@ class FastScroller : LinearLayout { fun setLayoutParams(viewGroup: ViewGroup) { @IdRes val recyclerViewId = mRecyclerView?.id ?: View.NO_ID val marginTop = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top) - val marginBottom = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom) + val marginBottom = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom) require(recyclerViewId != View.NO_ID) { "RecyclerView must have a view ID" } when (viewGroup) { is ConstraintLayout -> { val constraintSet = ConstraintSet() @IdRes val layoutId = id constraintSet.clone(viewGroup) - constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP) - constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM) - constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END) + constraintSet.connect( + layoutId, + ConstraintSet.TOP, + recyclerViewId, + ConstraintSet.TOP + ) + constraintSet.connect( + layoutId, + ConstraintSet.BOTTOM, + recyclerViewId, + ConstraintSet.BOTTOM + ) + constraintSet.connect( + layoutId, + ConstraintSet.END, + recyclerViewId, + ConstraintSet.END + ) constraintSet.applyTo(viewGroup) val layoutParams = layoutParams as ConstraintLayout.LayoutParams layoutParams.setMargins(0, marginTop, 0, marginBottom) @@ -150,12 +172,10 @@ class FastScroller : LinearLayout { fun attachRecyclerView(recyclerView: RecyclerView) { mRecyclerView = recyclerView - if (mRecyclerView != null) { - mRecyclerView!!.addOnScrollListener(mScrollListener) - post { - // set initial positions for bubble and handle - setViewPositions(getScrollProportion(mRecyclerView)) - } + mRecyclerView!!.addOnScrollListener(mScrollListener) + post { + // set initial positions for bubble and handle + setViewPositions(getScrollProportion(mRecyclerView)) } } @@ -173,7 +193,7 @@ class FastScroller : LinearLayout { */ fun setFadeScrollbar(fadeScrollbar: Boolean) { mFadeScrollbar = fadeScrollbar - mScrollbar.visibility = if (fadeScrollbar) View.GONE else View.VISIBLE + mScrollbar.visibility = if (fadeScrollbar) View.INVISIBLE else View.VISIBLE } /** @@ -191,7 +211,7 @@ class FastScroller : LinearLayout { * @param visible True to show scroll track, false to hide */ fun setTrackVisible(visible: Boolean) { - mTrackView.visibility = if (visible) View.VISIBLE else View.GONE + mTrackView.visibility = if (visible) View.VISIBLE else View.INVISIBLE } /** @@ -264,7 +284,7 @@ class FastScroller : LinearLayout { override fun setEnabled(enabled: Boolean) { super.setEnabled(enabled) - visibility = if (enabled) View.VISIBLE else View.GONE + visibility = if (enabled) View.VISIBLE else View.INVISIBLE } @SuppressLint("ClickableViewAccessibility") @@ -359,8 +379,13 @@ class FastScroller : LinearLayout { private fun setViewPositions(y: Float) { mBubbleHeight = mBubbleView.height mHandleHeight = mHandleView.height - val bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (y - mBubbleHeight).toInt()) - val handleY = getValueInRange(0, mViewHeight - mHandleHeight, (y - mHandleHeight / 2).toInt()) + val bubbleY = getValueInRange( + 0, + mViewHeight - mBubbleHeight - mHandleHeight / 2, + (y - mBubbleHeight).toInt() + ) + val handleY = + getValueInRange(0, mViewHeight - mHandleHeight, (y - mHandleHeight / 2).toInt()) if (mShowBubble) { mBubbleView.y = bubbleY.toFloat() } @@ -368,7 +393,8 @@ class FastScroller : LinearLayout { } private fun updateViewHeights() { - val measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val measureSpec = + MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) mBubbleView.measure(measureSpec, measureSpec) mBubbleHeight = mBubbleView.measuredHeight mHandleView.measure(measureSpec, measureSpec) @@ -411,13 +437,13 @@ class FastScroller : LinearLayout { .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) - mBubbleView.visibility = View.GONE + mBubbleView.visibility = View.INVISIBLE mBubbleAnimator = null } override fun onAnimationCancel(animation: Animator) { super.onAnimationCancel(animation) - mBubbleView.visibility = View.GONE + mBubbleView.visibility = View.INVISIBLE mBubbleAnimator = null } }) @@ -427,7 +453,9 @@ class FastScroller : LinearLayout { private fun showScrollbar() { mRecyclerView?.let { mRecyclerView -> if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) { - val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() + val transX = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end) + .toFloat() mScrollbar.translationX = transX mScrollbar.visibility = View.VISIBLE mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f) @@ -441,19 +469,20 @@ class FastScroller : LinearLayout { } private fun hideScrollbar() { - val transX = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() + val transX = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f) .setDuration(sScrollbarAnimDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) - mScrollbar.visibility = View.GONE + mScrollbar.visibility = View.INVISIBLE mScrollbarAnimator = null } override fun onAnimationCancel(animation: Animator) { super.onAnimationCancel(animation) - mScrollbar.visibility = View.GONE + mScrollbar.visibility = View.INVISIBLE mScrollbarAnimator = null } }) diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt new file mode 100644 index 000000000..eaa159029 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt @@ -0,0 +1,19 @@ +package io.legado.app.ui.widget.seekbar + +import android.widget.SeekBar + +interface SeekBarChangeListener : SeekBar.OnSeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt index f08e2b3c6..736b9eb3a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt @@ -16,7 +16,9 @@ import io.legado.app.lib.theme.ThemeStore import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method -class VerticalSeekBar : AppCompatSeekBar { +@Suppress("SameParameterValue") +class VerticalSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + AppCompatSeekBar(context, attrs) { private var mIsDragging: Boolean = false private var mThumb: Drawable? = null @@ -53,38 +55,12 @@ class VerticalSeekBar : AppCompatSeekBar { } } - constructor(context: Context) : super(context) { - initialize(context, null, 0, 0) - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - initialize(context, attrs, 0, 0) - } - - constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( - context, - attrs, - defStyle - ) { - initialize(context, attrs, defStyle, 0) - } - - private fun initialize( - context: Context, - attrs: AttributeSet?, - defStyleAttr: Int, - defStyleRes: Int - ) { + init { ATH.setTint(this, ThemeStore.accentColor(context)) ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR) if (attrs != null) { - val a = context.obtainStyledAttributes( - attrs, - R.styleable.VerticalSeekBar, - defStyleAttr, - defStyleRes - ) + val a = context.obtainStyledAttributes(attrs, R.styleable.VerticalSeekBar) val rotationAngle = a.getInteger(R.styleable.VerticalSeekBar_seekBarRotation, 0) if (isValidRotationAngle(rotationAngle)) { mRotationAngle = rotationAngle diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt index da69c33ab..683953f1a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt @@ -13,9 +13,8 @@ import kotlin.math.max class VerticalSeekBarWrapper @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { private val childSeekBar: VerticalSeekBar? get() { diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt index f6fc05cd4..ccaffaf79 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt @@ -11,8 +11,10 @@ import io.legado.app.utils.ColorUtils import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor -class AccentBgTextView(context: Context, attrs: AttributeSet?) : - AppCompatTextView(context, attrs) { +class AccentBgTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { private var radius = 0 diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt index 37b18051a..abd14615a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt @@ -6,25 +6,50 @@ import androidx.appcompat.widget.AppCompatTextView import io.legado.app.R import io.legado.app.lib.theme.Selector import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.utils.ColorUtils import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor class AccentStrokeTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { + private var radius = 3.dp + private val isBottomBackground: Boolean + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentStrokeTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.StrokeTextView_radius, radius) + isBottomBackground = + typedArray.getBoolean(R.styleable.StrokeTextView_isBottomBackground, false) + typedArray.recycle() + upStyle() + } + + private fun upStyle() { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val disableColor = if (isBottomBackground) { + if (isLight) { + context.getCompatColor(R.color.md_light_disabled) + } else { + context.getCompatColor(R.color.md_dark_disabled) + } + } else { + context.getCompatColor(R.color.disabled) + } background = Selector.shapeBuild() - .setCornerRadius(3.dp) + .setCornerRadius(radius) .setStrokeWidth(1.dp) - .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDisabledStrokeColor(disableColor) .setDefaultStrokeColor(ThemeStore.accentColor(context)) .setPressedBgColor(context.getCompatColor(R.color.transparent30)) .create() setTextColor( Selector.colorBuild() .setDefaultColor(ThemeStore.accentColor(context)) - .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .setDisabledColor(disableColor) .create() ) } + } diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt index be3074766..8dee26350 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt @@ -5,17 +5,16 @@ import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import io.legado.app.R import io.legado.app.lib.theme.accentColor -import org.jetbrains.anko.textColor -import org.jetbrains.anko.textColorResource +import io.legado.app.utils.getCompatColor class AccentTextView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { init { if (!isInEditMode) { - textColor = context.accentColor + setTextColor(context.accentColor) } else { - textColorResource = R.color.accent + setTextColor(context.getCompatColor(R.color.accent)) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt index 1319aac5e..fbf4ac26c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt @@ -8,20 +8,20 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView import androidx.appcompat.widget.AppCompatAutoCompleteTextView import io.legado.app.R import io.legado.app.lib.theme.ATH import io.legado.app.utils.gone import io.legado.app.utils.visible -import kotlinx.android.synthetic.main.item_1line_text_and_del.view.* -import org.jetbrains.anko.sdk27.listeners.onClick -class AutoCompleteTextView : AppCompatAutoCompleteTextView { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) +@Suppress("unused") +class AutoCompleteTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatAutoCompleteTextView(context, attrs) { var delCallBack: ((value: String) -> Unit)? = null @@ -33,7 +33,6 @@ class AutoCompleteTextView : AppCompatAutoCompleteTextView { return true } - @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent?): Boolean { if (event?.action == MotionEvent.ACTION_DOWN) { @@ -58,9 +57,11 @@ class AutoCompleteTextView : AppCompatAutoCompleteTextView { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = convertView ?: LayoutInflater.from(context) .inflate(R.layout.item_1line_text_and_del, parent, false) - view.text_view.text = getItem(position) - if (delCallBack != null) view.iv_delete.visible() else view.iv_delete.gone() - view.iv_delete.onClick { + val textView = view.findViewById(R.id.text_view) + textView.text = getItem(position) + val ivDelete = view.findViewById(R.id.iv_delete) + if (delCallBack != null) ivDelete.visible() else ivDelete.gone() + ivDelete.setOnClickListener { getItem(position)?.let { remove(it) delCallBack?.invoke(it) diff --git a/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt index c24e13e87..771448b7d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt @@ -12,7 +12,6 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams -import android.widget.TabWidget import androidx.appcompat.widget.AppCompatTextView import io.legado.app.R import io.legado.app.lib.theme.accentColor @@ -24,11 +23,11 @@ import io.legado.app.utils.visible /** * Created by milad heydari on 5/6/2016. */ +@Suppress("MemberVisibilityCanBePrivate", "unused") class BadgeView @JvmOverloads constructor( context: Context, - attrs: AttributeSet? = null, - defStyle: Int = android.R.attr.textViewStyle -) : AppCompatTextView(context, attrs, defStyle) { + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { var isHideOnNull = true set(hideOnNull) { @@ -44,12 +43,9 @@ class BadgeView @JvmOverloads constructor( return null } val text = text.toString() - return try { + return kotlin.runCatching { Integer.parseInt(text) - } catch (e: NumberFormatException) { - null - } - + }.getOrNull() } var badgeGravity: Int @@ -114,7 +110,9 @@ class BadgeView @JvmOverloads constructor( val radius = dip2Px(dipRadius).toFloat() val radiusArray = floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius) - if (flatangle) { radiusArray.fill(0f, 0, 3) } + if (flatangle) { + radiusArray.fill(0f, 0, 3) + } val roundRect = RoundRectShape(radiusArray, null, null) val bgDrawable = ShapeDrawable(roundRect) @@ -186,16 +184,6 @@ class BadgeView @JvmOverloads constructor( incrementBadgeCount(-decrement) } - /** - * Attach the BadgeView to the TabWidget - * @param target the TabWidget to attach the BadgeView - * @param tabIndex index of the tab - */ - fun setTargetView(target: TabWidget, tabIndex: Int) { - val tabView = target.getChildTabViewAt(tabIndex) - setTargetView(tabView) - } - /** * Attach the BadgeView to the target view * @param target the view to attach the BadgeView diff --git a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt index 051d5af4e..41afecaff 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt @@ -2,6 +2,7 @@ package io.legado.app.ui.widget.text import android.annotation.SuppressLint import android.content.Context +import android.text.method.LinkMovementMethod import android.util.AttributeSet import android.view.MotionEvent import android.view.VelocityTracker @@ -15,14 +16,11 @@ import kotlin.math.max import kotlin.math.min -open class InertiaScrollTextView : AppCompatTextView { - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) - : super(context, attrs, defStyleAttr) - +@Suppress("unused") +open class InertiaScrollTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { private val scrollStateIdle = 0 private val scrollStateDragging = 1 @@ -51,6 +49,7 @@ open class InertiaScrollTextView : AppCompatTextView { mTouchSlop = vc.scaledTouchSlop mMinFlingVelocity = vc.scaledMinimumFlingVelocity mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + movementMethod = LinkMovementMethod.getInstance() } fun atTop(): Boolean { @@ -218,7 +217,7 @@ open class InertiaScrollTextView : AppCompatTextView { } } - internal fun postOnAnimation() { + fun postOnAnimation() { if (mEatRunOnAnimationRequest) { mReSchedulePostAnimationCallback = true } else { diff --git a/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt index 62f8f18f2..b992f772f 100644 --- a/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt @@ -9,6 +9,7 @@ import io.legado.app.utils.ColorUtils import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor +@Suppress("unused") open class StrokeTextView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { @@ -40,7 +41,7 @@ open class StrokeTextView(context: Context, attrs: AttributeSet?) : .setSelectedStrokeColor(context.getCompatColor(R.color.accent)) .setPressedBgColor(context.getCompatColor(R.color.transparent30)) .create() - this.setTextColor( + setTextColor( Selector.colorBuild() .setDefaultColor(context.getCompatColor(R.color.secondaryText)) .setSelectedColor(context.getCompatColor(R.color.accent)) @@ -58,7 +59,7 @@ open class StrokeTextView(context: Context, attrs: AttributeSet?) : .setSelectedStrokeColor(context.accentColor) .setPressedBgColor(context.getCompatColor(R.color.transparent30)) .create() - this.setTextColor( + setTextColor( Selector.colorBuild() .setDefaultColor(context.getPrimaryTextColor(isLight)) .setSelectedColor(context.accentColor) @@ -75,7 +76,7 @@ open class StrokeTextView(context: Context, attrs: AttributeSet?) : .setSelectedStrokeColor(ThemeStore.accentColor(context)) .setPressedBgColor(context.getCompatColor(R.color.transparent30)) .create() - this.setTextColor( + setTextColor( Selector.colorBuild() .setDefaultColor(ThemeStore.textColorSecondary(context)) .setSelectedColor(ThemeStore.accentColor(context)) diff --git a/app/src/main/java/io/legado/app/utils/ACache.kt b/app/src/main/java/io/legado/app/utils/ACache.kt index 03a330d6a..934e0b1f1 100644 --- a/app/src/main/java/io/legado/app/utils/ACache.kt +++ b/app/src/main/java/io/legado/app/utils/ACache.kt @@ -9,9 +9,9 @@ import android.graphics.PixelFormat import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.Log -import io.legado.app.App import org.json.JSONArray import org.json.JSONObject +import splitties.init.appCtx import java.io.* import java.util.* import java.util.concurrent.atomic.AtomicInteger @@ -22,7 +22,7 @@ import kotlin.math.min /** * 本地缓存 */ -@Suppress("unused") +@Suppress("unused", "MemberVisibilityCanBePrivate") class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) { companion object { @@ -525,7 +525,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) } fun hasDateInfo(data: ByteArray?): Boolean { - return (data != null && data.size > 15 && data[13] == '-'.toByte() + return (data != null && data.size > 15 && data[13] == '-'.code.toByte() && indexOf(data, mSeparator) > 14) } @@ -543,9 +543,10 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) return null } + @Suppress("SameParameterValue") private fun indexOf(data: ByteArray, c: Char): Int { for (i in data.indices) { - if (data[i] == c.toByte()) { + if (data[i] == c.code.toByte()) { return i } } @@ -618,7 +619,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) fun bitmap2Drawable(bm: Bitmap?): Drawable? { return if (bm == null) { null - } else BitmapDrawable(App.INSTANCE.resources, bm) + } else BitmapDrawable(appCtx.resources, bm) } } diff --git a/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt b/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt new file mode 100644 index 000000000..72840c097 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt @@ -0,0 +1,35 @@ +package io.legado.app.utils + +import android.app.Activity +import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowInsets +import android.view.WindowMetrics + +fun Activity.getSize(): DisplayMetrics { + val displayMetrics = DisplayMetrics() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics + val insets = windowMetrics.windowInsets + .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) + displayMetrics.widthPixels = windowMetrics.bounds.width() - insets.left - insets.right + displayMetrics.heightPixels = windowMetrics.bounds.height() - insets.top - insets.bottom + } else { + @Suppress("DEPRECATION") + windowManager.defaultDisplay.getMetrics(displayMetrics) + } + return displayMetrics +} + +/** + * 该方法需要在View完全被绘制出来之后调用,否则判断不了 + * 在比如 onWindowFocusChanged()方法中可以得到正确的结果 + */ +val Activity.navigationBarHeight: Int + get() { + if (SystemUtils.isNavigationBarExist(this)) { + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + return 0 + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ActivityResultContractUtils.kt b/app/src/main/java/io/legado/app/utils/ActivityResultContractUtils.kt new file mode 100644 index 000000000..3f0198023 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ActivityResultContractUtils.kt @@ -0,0 +1,32 @@ +package io.legado.app.utils + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +object ActivityResultContractUtils { + + class SelectImage : ActivityResultContract?>() { + + var requestCode: Int? = null + + override fun createIntent(context: Context, input: Int?): Intent { + requestCode = input + return Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("image/*") + } + + override fun parseResult(resultCode: Int, intent: Intent?): Pair? { + if (resultCode == RESULT_OK) { + return Pair(requestCode, intent?.data) + } + return null + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/AlertDialogExtensions.kt b/app/src/main/java/io/legado/app/utils/AlertDialogExtensions.kt index 2f261a2e7..44bacecbd 100644 --- a/app/src/main/java/io/legado/app/utils/AlertDialogExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/AlertDialogExtensions.kt @@ -8,6 +8,6 @@ fun AlertDialog.applyTint(): AlertDialog { return ATH.setAlertDialogTint(this) } -fun AlertDialog.requestInputMethod(){ +fun AlertDialog.requestInputMethod() { window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) } diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt index 7786bfed4..2e7ae06ba 100644 --- a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt +++ b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt @@ -1,22 +1,20 @@ package io.legado.app.utils import android.content.Context -import android.graphics.Bitmap +import android.graphics.* import android.graphics.Bitmap.Config -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Color import android.renderscript.Allocation import android.renderscript.Element import android.renderscript.RenderScript import android.renderscript.ScriptIntrinsicBlur import android.view.View -import io.legado.app.App +import splitties.init.appCtx +import java.io.FileInputStream import java.io.IOException import kotlin.math.* -@Suppress("unused", "WeakerAccess") +@Suppress("unused", "WeakerAccess", "MemberVisibilityCanBePrivate") object BitmapUtils { /** @@ -28,11 +26,12 @@ object BitmapUtils { * @param height 想要显示的图片的高度 * @return */ - fun decodeBitmap(path: String, width: Int, height: Int): Bitmap { + fun decodeBitmap(path: String, width: Int, height: Int): Bitmap? { val op = BitmapFactory.Options() + val ips = FileInputStream(path) // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; op.inJustDecodeBounds = true - BitmapFactory.decodeFile(path, op) //获取尺寸信息 + BitmapFactory.decodeFileDescriptor(ips.fd, null, op) //获取比例大小 val wRatio = ceil((op.outWidth / width).toDouble()).toInt() val hRatio = ceil((op.outHeight / height).toDouble()).toInt() @@ -45,21 +44,23 @@ object BitmapUtils { } } op.inJustDecodeBounds = false - return BitmapFactory.decodeFile(path, op) + return BitmapFactory.decodeFileDescriptor(ips.fd, null, op) + } /** 从path中获取Bitmap图片 * @param path 图片路径 * @return */ - fun decodeBitmap(path: String): Bitmap { + fun decodeBitmap(path: String): Bitmap? { + val opts = BitmapFactory.Options() + val ips = FileInputStream(path) opts.inJustDecodeBounds = true - BitmapFactory.decodeFile(path, opts) + BitmapFactory.decodeFileDescriptor(ips.fd, null, opts) opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128) opts.inJustDecodeBounds = false - - return BitmapFactory.decodeFile(path, opts) + return BitmapFactory.decodeFileDescriptor(ips.fd, null, opts) } /** @@ -221,12 +222,36 @@ object BitmapUtils { } } + fun changeBitmapSize(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap { + + val width = bitmap.width + val height = bitmap.height + + //计算压缩的比率 + var scaleWidth = newWidth.toFloat() / width + var scaleHeight = newHeight.toFloat() / height + + if (scaleWidth > scaleHeight) { + scaleWidth = scaleHeight + } else { + scaleHeight = scaleWidth + } + + //获取想要缩放的matrix + val matrix = Matrix() + matrix.postScale(scaleWidth, scaleHeight) + + //获取新的bitmap + return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true) + + } + /** * 高斯模糊 */ fun stackBlur(srcBitmap: Bitmap?): Bitmap? { if (srcBitmap == null) return null - val rs = RenderScript.create(App.INSTANCE) + val rs = RenderScript.create(appCtx) val blurredBitmap = srcBitmap.copy(Config.ARGB_8888, true) //分配用于渲染脚本的内存 diff --git a/app/src/main/java/io/legado/app/utils/ColorUtils.kt b/app/src/main/java/io/legado/app/utils/ColorUtils.kt index 8a9a35052..800cd3c8b 100644 --- a/app/src/main/java/io/legado/app/utils/ColorUtils.kt +++ b/app/src/main/java/io/legado/app/utils/ColorUtils.kt @@ -7,7 +7,7 @@ import androidx.annotation.FloatRange import java.util.* import kotlin.math.* -@Suppress("unused") +@Suppress("unused", "MemberVisibilityCanBePrivate") object ColorUtils { fun intToString(intColor: Int): String { diff --git a/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt b/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt index b7ef740cc..bb2cc7bad 100644 --- a/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt +++ b/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt @@ -6,6 +6,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.transition.TransitionManager +@Suppress("MemberVisibilityCanBePrivate", "unused") class ConstraintUtil(private val constraintLayout: ConstraintLayout) { private var begin: ConstraintBegin? = null @@ -55,6 +56,7 @@ class ConstraintUtil(private val constraintLayout: ConstraintLayout) { } +@Suppress("unused", "MemberVisibilityCanBePrivate") class ConstraintBegin( private val constraintLayout: ConstraintLayout, private val applyConstraintSet: ConstraintSet diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index 489f5d508..c0a5b054a 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -3,6 +3,8 @@ package io.legado.app.utils import android.annotation.SuppressLint +import android.app.Activity +import android.app.Service import android.content.* import android.content.pm.PackageManager import android.content.res.ColorStateList @@ -12,29 +14,64 @@ import android.graphics.drawable.Drawable import android.net.Uri import android.os.BatteryManager import android.provider.Settings +import android.widget.Toast import androidx.annotation.ColorRes import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.edit -import cn.bingoogolapple.qrcode.zxing.QRCodeEncoder -import com.google.zxing.EncodeHintType +import androidx.preference.PreferenceManager import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import io.legado.app.BuildConfig import io.legado.app.R -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.longToast -import org.jetbrains.anko.toast +import io.legado.app.constant.AppConst import java.io.File import java.io.FileOutputStream +inline fun Context.startActivity(configIntent: Intent.() -> Unit = {}) { + val intent = Intent(this, A::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.apply(configIntent) + startActivity(intent) +} + +inline fun Context.startService(configIntent: Intent.() -> Unit = {}) { + startService(Intent(this, T::class.java).apply(configIntent)) +} + +inline fun Context.stopService() { + stopService(Intent(this, T::class.java)) +} + +fun Context.toastOnUi(message: Int) { + runOnUI { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } +} + +fun Context.toastOnUi(message: CharSequence?) { + runOnUI { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } +} + +fun Context.longToastOnUi(message: Int) { + runOnUI { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } +} + +fun Context.longToastOnUi(message: CharSequence?) { + runOnUI { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } +} + +val Context.defaultSharedPreferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(this) + fun Context.getPrefBoolean(key: String, defValue: Boolean = false) = defaultSharedPreferences.getBoolean(key, defValue) -fun Context.getPrefBoolean(@StringRes keyId: Int, defValue: Boolean = false) = - defaultSharedPreferences.getBoolean(getString(keyId), defValue) - fun Context.putPrefBoolean(key: String, value: Boolean = false) = defaultSharedPreferences.edit { putBoolean(key, value) } @@ -53,10 +90,7 @@ fun Context.putPrefLong(key: String, value: Long) = fun Context.getPrefString(key: String, defValue: String? = null) = defaultSharedPreferences.getString(key, defValue) -fun Context.getPrefString(@StringRes keyId: Int, defValue: String? = null) = - defaultSharedPreferences.getString(getString(keyId), defValue) - -fun Context.putPrefString(key: String, value: String) = +fun Context.putPrefString(key: String, value: String?) = defaultSharedPreferences.edit { putString(key, value) } fun Context.getPrefStringSet( @@ -105,13 +139,26 @@ val Context.navigationBarHeight: Int return resources.getDimensionPixelSize(resourceId) } +fun Context.share(text: String, title: String = getString(R.string.share)) { + kotlin.runCatching { + val intent = Intent(Intent.ACTION_SEND) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(Intent.EXTRA_SUBJECT, title) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.type = "text/plain" + startActivity(Intent.createChooser(intent, title)) + } +} + @SuppressLint("SetWorldReadable") -fun Context.shareWithQr(title: String, text: String) { - QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L - val bitmap = QRCodeEncoder.syncEncodeQRCode(text, 600) - QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H +fun Context.shareWithQr( + text: String, + title: String = getString(R.string.share), + errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H +) { + val bitmap = QRCodeUtils.createQRCode(text, errorCorrectionLevel = errorCorrectionLevel) if (bitmap == null) { - toast(R.string.text_too_long_qr_error) + toastOnUi(R.string.text_too_long_qr_error) } else { try { val file = File(externalCacheDir, "qr.png") @@ -120,18 +167,14 @@ fun Context.shareWithQr(title: String, text: String) { fOut.flush() fOut.close() file.setReadable(true, false) - val contentUri = FileProvider.getUriForFile( - this, - "${BuildConfig.APPLICATION_ID}.fileProvider", - file - ) + val contentUri = FileProvider.getUriForFile(this, AppConst.authority, file) val intent = Intent(Intent.ACTION_SEND) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.putExtra(Intent.EXTRA_STREAM, contentUri) intent.type = "image/png" startActivity(Intent.createChooser(intent, title)) } catch (e: Exception) { - toast(e.localizedMessage ?: "ERROR") + toastOnUi(e.localizedMessage ?: "ERROR") } } } @@ -142,7 +185,7 @@ fun Context.sendToClip(text: String) { val clipData = ClipData.newPlainText(null, text) clipboard?.let { clipboard.setPrimaryClip(clipData) - longToast(R.string.copy_complete) + longToastOnUi(R.string.copy_complete) } } @@ -156,6 +199,17 @@ fun Context.getClipText(): String? { return null } +fun Context.sendMail(mail: String) { + try { + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:$mail") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } catch (e: Exception) { + toastOnUi(e.localizedMessage ?: "Error") + } +} + /** * 系统是否暗色主题 */ @@ -174,10 +228,10 @@ val Context.sysBattery: Int return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 } -val Context.externalFilesDir: File +val Context.externalFiles: File get() = this.getExternalFilesDir(null) ?: this.filesDir -val Context.eCacheDir: File +val Context.externalCache: File get() = this.externalCacheDir ?: this.cacheDir fun Context.openUrl(url: String) { @@ -187,17 +241,18 @@ fun Context.openUrl(url: String) { fun Context.openUrl(uri: Uri) { val intent = Intent(Intent.ACTION_VIEW) intent.data = uri + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (intent.resolveActivity(packageManager) != null) { try { startActivity(intent) } catch (e: Exception) { - toast(e.localizedMessage ?: "open url error") + toastOnUi(e.localizedMessage ?: "open url error") } } else { try { startActivity(Intent.createChooser(intent, "请选择浏览器")) } catch (e: Exception) { - toast(e.localizedMessage ?: "open url error") + toastOnUi(e.localizedMessage ?: "open url error") } } } diff --git a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt index 041189c7f..4a4341a0d 100644 --- a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt +++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt @@ -5,9 +5,11 @@ import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile +import java.nio.charset.Charset import java.util.* +@Suppress("MemberVisibilityCanBePrivate") object DocumentUtils { fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean { @@ -15,6 +17,11 @@ object DocumentUtils { return parent.findFile(fileName)?.exists() ?: false } + fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) { + val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) + parent?.findFile(fileName)?.delete() + } + fun createFileIfNotExist( root: DocumentFile, fileName: String, @@ -46,8 +53,13 @@ object DocumentUtils { @JvmStatic @Throws(Exception::class) - fun writeText(context: Context, data: String, fileUri: Uri): Boolean { - return writeBytes(context, data.toByteArray(), fileUri) + fun writeText( + context: Context, + data: String, + fileUri: Uri, + charset: Charset = Charsets.UTF_8 + ): Boolean { + return writeBytes(context, data.toByteArray(charset), fileUri) } @JvmStatic @@ -87,10 +99,8 @@ object DocumentUtils { val docList = arrayListOf() var c: Cursor? = null try { - val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( - uri, - DocumentsContract.getDocumentId(uri) - ) + val childrenUri = DocumentsContract + .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri)) c = context.contentResolver.query( childrenUri, arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, @@ -106,17 +116,18 @@ object DocumentUtils { val sci = c.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) val mci = c.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) val dci = c.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - c.moveToFirst() - do { - val item = DocItem( - name = c.getString(nci), - attr = c.getString(mci), - size = c.getLong(sci), - date = Date(c.getLong(dci)), - uri = DocumentsContract.buildDocumentUriUsingTree(uri, c.getString(ici)) - ) - docList.add(item) - } while (c.moveToNext()) + if (c.moveToFirst()) { + do { + val item = DocItem( + name = c.getString(nci), + attr = c.getString(mci), + size = c.getLong(sci), + date = Date(c.getLong(dci)), + uri = DocumentsContract.buildDocumentUriUsingTree(uri, c.getString(ici)) + ) + docList.add(item) + } while (c.moveToNext()) + } } } catch (e: Exception) { e.printStackTrace() @@ -139,12 +150,12 @@ data class DocItem( DocumentsContract.Document.MIME_TYPE_DIR == attr } - val isContentPath get() = uri.toString().isContentPath() + val isContentPath get() = uri.isContentScheme() } @Throws(Exception::class) -fun DocumentFile.writeText(context: Context, data: String) { - DocumentUtils.writeText(context, data, this.uri) +fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) { + DocumentUtils.writeText(context, data, this.uri, charset) } @Throws(Exception::class) diff --git a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt index 406aa3815..aafdb0d98 100644 --- a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt +++ b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt @@ -1,7 +1,10 @@ package io.legado.app.utils import android.util.Base64 -import java.nio.charset.StandardCharsets +import java.security.spec.AlgorithmParameterSpec +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec @Suppress("unused") object EncoderUtils { @@ -9,7 +12,7 @@ object EncoderUtils { fun escape(src: String): String { val tmp = StringBuilder() for (char in src) { - val charCode = char.toInt() + val charCode = char.code if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) { tmp.append(char) continue @@ -25,16 +28,134 @@ object EncoderUtils { return tmp.toString() } - fun base64Decode(str: String): String { - val bytes = Base64.decode(str, Base64.DEFAULT) - return try { - String(bytes, StandardCharsets.UTF_8) - } catch (e: Exception) { - String(bytes) - } + @JvmOverloads + fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String { + val bytes = Base64.decode(str, flags) + return String(bytes) } + @JvmOverloads fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { return Base64.encodeToString(str.toByteArray(), flags) } + //////////AES Start + + /** + * Return the Base64-encode bytes of AES encryption. + * + * @param data The data. + * @param key The key. + * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. + * @param iv The buffer with the IV. The contents of the + * buffer are copied to protect against subsequent modification. + * @return the Base64-encode bytes of AES encryption + */ + fun encryptAES2Base64( + data: ByteArray?, + key: ByteArray?, + transformation: String?, + iv: ByteArray? + ): ByteArray? { + return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP) + } + + /** + * Return the bytes of AES encryption. + * + * @param data The data. + * @param key The key. + * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. + * @param iv The buffer with the IV. The contents of the + * buffer are copied to protect against subsequent modification. + * @return the bytes of AES encryption + */ + fun encryptAES( + data: ByteArray?, + key: ByteArray?, + transformation: String?, + iv: ByteArray? + ): ByteArray? { + return symmetricTemplate(data, key, "AES", transformation!!, iv, true) + } + + + /** + * Return the bytes of AES decryption for Base64-encode bytes. + * + * @param data The data. + * @param key The key. + * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. + * @param iv The buffer with the IV. The contents of the + * buffer are copied to protect against subsequent modification. + * @return the bytes of AES decryption for Base64-encode bytes + */ + fun decryptBase64AES( + data: ByteArray?, + key: ByteArray?, + transformation: String?, + iv: ByteArray? + ): ByteArray? { + return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) + } + + /** + * Return the bytes of AES decryption. + * + * @param data The data. + * @param key The key. + * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*. + * @param iv The buffer with the IV. The contents of the + * buffer are copied to protect against subsequent modification. + * @return the bytes of AES decryption + */ + fun decryptAES( + data: ByteArray?, + key: ByteArray?, + transformation: String?, + iv: ByteArray? + ): ByteArray? { + return symmetricTemplate(data, key, "AES", transformation!!, iv, false) + } + + + /** + * Return the bytes of symmetric encryption or decryption. + * + * @param data The data. + * @param key The key. + * @param algorithm The name of algorithm. + * @param transformation The name of the transformation, e.g., DES/CBC/PKCS5Padding. + * @param isEncrypt True to encrypt, false otherwise. + * @return the bytes of symmetric encryption or decryption + */ + + private fun symmetricTemplate( + data: ByteArray?, + key: ByteArray?, + algorithm: String, + transformation: String, + iv: ByteArray?, + isEncrypt: Boolean + ): ByteArray? { + return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null else try { + val keySpec = SecretKeySpec(key, algorithm) + val cipher = Cipher.getInstance(transformation) + if (iv == null || iv.isEmpty()) { + cipher.init(if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, keySpec) + } else { + val params: AlgorithmParameterSpec = IvParameterSpec(iv) + cipher.init( + if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, + keySpec, + params + ) + } + cipher.doFinal(data) + } catch (e: Throwable) { + e.printStackTrace() + null + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EncodingDetect.java b/app/src/main/java/io/legado/app/utils/EncodingDetect.java deleted file mode 100644 index b5bd4cf66..000000000 --- a/app/src/main/java/io/legado/app/utils/EncodingDetect.java +++ /dev/null @@ -1,4509 +0,0 @@ -package io.legado.app.utils; - -import androidx.annotation.NonNull; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -import static android.text.TextUtils.isEmpty; - -/** - * Copyright (C) <2009> - *

    - * This program is free software: you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later - * version. - *

    - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - * details. - *

    - * EncodingDetect.java
    - * 自动获取文件的编码 - * - * @author Billows.Van - * @version 1.0 - * @since Create on 2010-01-27 11:19:00 - */ -public class EncodingDetect { - - public static String getHtmlEncode(@NonNull byte[] bytes) { - try { - Document doc = Jsoup.parse(new String(bytes, StandardCharsets.UTF_8)); - Elements metaTags = doc.getElementsByTag("meta"); - String charsetStr; - for (Element metaTag : metaTags) { - charsetStr = metaTag.attr("charset"); - if (!isEmpty(charsetStr)) { - return charsetStr; - } - String content = metaTag.attr("content"); - String http_equiv = metaTag.attr("http-equiv"); - if (http_equiv.toLowerCase().equals("content-type")) { - if (content.toLowerCase().contains("charset")) { - charsetStr = content.substring(content.toLowerCase().indexOf("charset") + "charset=".length()); - } else { - charsetStr = content.substring(content.toLowerCase().indexOf(";") + 1); - } - if (!isEmpty(charsetStr)) { - return charsetStr; - } - } - } - } catch (Exception ignored) { - } - return getEncode(bytes); - } - - public static String getEncode(@NonNull byte[] bytes) { - int len = Math.min(bytes.length, 2000); - byte[] cBytes = new byte[len]; - System.arraycopy(bytes, 0, cBytes, 0, len); - BytesEncodingDetect bytesEncodingDetect = new BytesEncodingDetect(); - String code = BytesEncodingDetect.javaname[bytesEncodingDetect.detectEncoding(cBytes)]; - // UTF-16LE 特殊处理 - if ("Unicode".equals(code)) { - if (cBytes[0] == -1) { - code = "UTF-16LE"; - } - } - return code; - } - - /** - * 得到文件的编码 - */ - public static String getEncode(@NonNull String filePath) { - BytesEncodingDetect s = new BytesEncodingDetect(); - String fileCode = BytesEncodingDetect.javaname[s - .detectEncoding(new File(filePath))]; - - // UTF-16LE 特殊处理 - if ("Unicode".equals(fileCode)) { - byte[] tempByte = BytesEncodingDetect.getFileBytes(new File( - filePath)); - if (tempByte[0] == -1) { - fileCode = "UTF-16LE"; - } - } - return fileCode; - } - - /** - * 得到文件的编码 - */ - public static String getEncode(@NonNull File file) { - BytesEncodingDetect s = new BytesEncodingDetect(); - String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)]; - // UTF-16LE 特殊处理 - if ("Unicode".equals(fileCode)) { - byte[] tempByte = BytesEncodingDetect.getFileBytes(file); - if (tempByte[0] == -1) { - fileCode = "UTF-16LE"; - } - } - return fileCode; - } - -} - -class BytesEncodingDetect extends Encoding { - // Frequency tables to hold the GB, Big5, and EUC-TW character - // frequencies - private int[][] GBFreq; - - private int[][] GBKFreq; - - private int[][] Big5Freq; - - private int[][] Big5PFreq; - - private int[][] EUC_TWFreq; - - private int[][] KRFreq; - - private int[][] JPFreq; - - public boolean debug; - - BytesEncodingDetect() { - super(); - debug = false; - GBFreq = new int[94][94]; - GBKFreq = new int[126][191]; - Big5Freq = new int[94][158]; - Big5PFreq = new int[126][191]; - EUC_TWFreq = new int[94][94]; - KRFreq = new int[94][94]; - JPFreq = new int[94][94]; - // Initialize the Frequency Table for GB, GBK, Big5, EUC-TW, KR, JP - initialize_frequencies(); - } - - /** - * Function : detectEncoding Aruguments: URL Returns : One of the encodings - * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) - * Description: This function looks at the URL contents and assigns it a - * probability score for each encoding type. The encoding type with the - * highest probability is returned. - */ - public int detectEncoding(URL testurl) { - byte[] rawtext = new byte[10000]; - int bytesread = 0, byteoffset = 0; - int guess = OTHER; - InputStream chinesestream; - try { - chinesestream = testurl.openStream(); - while ((bytesread = chinesestream.read(rawtext, byteoffset, - rawtext.length - byteoffset)) > 0) { - byteoffset += bytesread; - } - chinesestream.close(); - guess = detectEncoding(rawtext); - } catch (Exception e) { - System.err.println("Error loading or using URL " + e.toString()); - guess = -1; - } - return guess; - } - - /** - * Function : detectEncoding Aruguments: File Returns : One of the encodings - * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER) - * Description: This function looks at the file and assigns it a probability - * score for each encoding type. The encoding type with the highest - * probability is returned. - */ - int detectEncoding(File testfile) { - byte[] rawtext = getFileBytes(testfile); - return detectEncoding(rawtext); - } - - static byte[] getFileBytes(File testfile) { - FileInputStream chinesefile; - byte[] rawtext; - rawtext = new byte[2000]; - try { - chinesefile = new FileInputStream(testfile); - chinesefile.read(rawtext); - chinesefile.close(); - } catch (Exception e) { - System.err.println("Error: " + e); - } - return rawtext; - } - - - /** - * Function : detectEncoding Aruguments: byte array Returns : One of the - * encodings from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, - * or OTHER) Description: This function looks at the byte array and assigns - * it a probability score for each encoding type. The encoding type with the - * highest probability is returned. - */ - int detectEncoding(byte[] rawtext) { - int[] scores; - int index, maxscore = 0; - int encoding_guess = OTHER; - scores = new int[TOTALTYPES]; - // Assign Scores - scores[GB2312] = gb2312_probability(rawtext); - scores[GBK] = gbk_probability(rawtext); - scores[GB18030] = gb18030_probability(rawtext); - scores[HZ] = hz_probability(rawtext); - scores[BIG5] = big5_probability(rawtext); - scores[CNS11643] = euc_tw_probability(rawtext); - scores[ISO2022CN] = iso_2022_cn_probability(rawtext); - scores[UTF8] = utf8_probability(rawtext); - scores[UNICODE] = utf16_probability(rawtext); - scores[EUC_KR] = euc_kr_probability(rawtext); - scores[CP949] = cp949_probability(rawtext); - scores[JOHAB] = 0; - scores[ISO2022KR] = iso_2022_kr_probability(rawtext); - scores[ASCII] = ascii_probability(rawtext); - scores[SJIS] = sjis_probability(rawtext); - scores[EUC_JP] = euc_jp_probability(rawtext); - scores[ISO2022JP] = iso_2022_jp_probability(rawtext); - scores[UNICODET] = 0; - scores[UNICODES] = 0; - scores[ISO2022CN_GB] = 0; - scores[ISO2022CN_CNS] = 0; - scores[OTHER] = 0; - // Tabulate Scores - for (index = 0; index < TOTALTYPES; index++) { - if (debug) - System.err.println("Encoding " + nicename[index] + " score " - + scores[index]); - if (scores[index] > maxscore) { - encoding_guess = index; - maxscore = scores[index]; - } - } - // Return OTHER if nothing scored above 50 - if (maxscore <= 50) { - encoding_guess = OTHER; - } - return encoding_guess; - } - - /* - * Function: gb2312_probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses GB-2312 - * encoding - */ - int gb2312_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, gbchars = 1; - long gbfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 - && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - gbchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - if (GBFreq[row][column] != 0) { - gbfreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - // In GB high-freq character range - gbfreq += 200; - } - } - i++; - } - } - rangeval = 50 * ((float) gbchars / (float) dbchars); - freqval = 50 * ((float) gbfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - /* - * Function: gbk_probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses GBK - * encoding - */ - int gbk_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, gbchars = 1; - long gbfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 - && // Original GB range - (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - gbchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - // System.out.println("original row " + row + " column " + - // column); - if (GBFreq[row][column] != 0) { - gbfreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - gbfreq += 200; - } - } else if ((byte) 0x81 <= rawtext[i] - && rawtext[i] <= (byte) 0xFE && // Extended GB range - (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) { - gbchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0x81; - if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { - column = rawtext[i + 1] - 0x40; - } else { - column = rawtext[i + 1] + 256 - 0x40; - } - // System.out.println("extended row " + row + " column " + - // column + " rawtext[i] " + rawtext[i]); - if (GBKFreq[row][column] != 0) { - gbfreq += GBKFreq[row][column]; - } - } - i++; - } - } - rangeval = 50 * ((float) gbchars / (float) dbchars); - freqval = 50 * ((float) gbfreq / (float) totalfreq); - // For regular GB files, this would give the same score, so I handicap - // it slightly - return (int) (rangeval + freqval) - 1; - } - - /* - * Function: gb18030_probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses GBK - * encoding - */ - int gb18030_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, gbchars = 1; - long gbfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7 - && // Original GB range - i + 1 < rawtextlen && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - gbchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - // System.out.println("original row " + row + " column " + - // column); - if (GBFreq[row][column] != 0) { - gbfreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - gbfreq += 200; - } - } else if ((byte) 0x81 <= rawtext[i] - && rawtext[i] <= (byte) 0xFE - && // Extended GB range - i + 1 < rawtextlen - && (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) { - gbchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0x81; - if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { - column = rawtext[i + 1] - 0x40; - } else { - column = rawtext[i + 1] + 256 - 0x40; - } - // System.out.println("extended row " + row + " column " + - // column + " rawtext[i] " + rawtext[i]); - if (GBKFreq[row][column] != 0) { - gbfreq += GBKFreq[row][column]; - } - } else if ((byte) 0x81 <= rawtext[i] - && rawtext[i] <= (byte) 0xFE - && // Extended GB range - i + 3 < rawtextlen && (byte) 0x30 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0x39 - && (byte) 0x81 <= rawtext[i + 2] - && rawtext[i + 2] <= (byte) 0xFE - && (byte) 0x30 <= rawtext[i + 3] - && rawtext[i + 3] <= (byte) 0x39) { - gbchars++; - } - i++; - } - } - rangeval = 50 * ((float) gbchars / (float) dbchars); - freqval = 50 * ((float) gbfreq / (float) totalfreq); - // For regular GB files, this would give the same score, so I handicap - // it slightly - return (int) (rangeval + freqval) - 1; - } - - /* - * Function: hz_probability Argument: byte array Returns : number from 0 to - * 100 representing probability text in array uses HZ encoding - */ - int hz_probability(byte[] rawtext) { - int i, rawtextlen; - int hzchars = 0, dbchars = 1; - long hzfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int hzstart = 0, hzend = 0; - int row, column; - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen; i++) { - if (rawtext[i] == '~') { - if (rawtext[i + 1] == '{') { - hzstart++; - i += 2; - while (i < rawtextlen - 1) { - if (rawtext[i] == 0x0A || rawtext[i] == 0x0D) { - break; - } else if (rawtext[i] == '~' && rawtext[i + 1] == '}') { - hzend++; - i++; - break; - } else if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) - && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { - hzchars += 2; - row = rawtext[i] - 0x21; - column = rawtext[i + 1] - 0x21; - totalfreq += 500; - if (GBFreq[row][column] != 0) { - hzfreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - hzfreq += 200; - } - } else if ((0xA1 <= rawtext[i] && rawtext[i] <= 0xF7) - && (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xF7)) { - hzchars += 2; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - totalfreq += 500; - if (GBFreq[row][column] != 0) { - hzfreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - hzfreq += 200; - } - } - dbchars += 2; - i += 2; - } - } else if (rawtext[i + 1] == '}') { - hzend++; - i++; - } else if (rawtext[i + 1] == '~') { - i++; - } - } - } - if (hzstart > 4) { - rangeval = 50; - } else if (hzstart > 1) { - rangeval = 41; - } else if (hzstart > 0) { // Only 39 in case the sequence happened to - // occur - rangeval = 39; // in otherwise non-Hz text - } else { - rangeval = 0; - } - freqval = 50 * ((float) hzfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - /** - * Function: big5_probability Argument: byte array Returns : number from 0 - * to 100 representing probability text in array uses Big5 encoding - */ - int big5_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, bfchars = 1; - float rangeval = 0, freqval = 0; - long bffreq = 0, totalfreq = 1; - int row, column; - // Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] - && rawtext[i] <= (byte) 0xF9 - && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE))) { - bfchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { - column = rawtext[i + 1] - 0x40; - } else { - column = rawtext[i + 1] + 256 - 0x61; - } - if (Big5Freq[row][column] != 0) { - bffreq += Big5Freq[row][column]; - } else if (3 <= row && row <= 37) { - bffreq += 200; - } - } - i++; - } - } - rangeval = 50 * ((float) bfchars / (float) dbchars); - freqval = 50 * ((float) bffreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - /* - * Function: big5plus_probability Argument: pointer to unsigned char array - * Returns : number from 0 to 100 representing probability text in array - * uses Big5+ encoding - */ - int big5plus_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, bfchars = 1; - long bffreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 128) { - // asciichars++; - } else { - dbchars++; - if (0xA1 <= rawtext[i] - && rawtext[i] <= 0xF9 - && // Original Big5 range - ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) { - bfchars++; - totalfreq += 500; - row = rawtext[i] - 0xA1; - if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { - column = rawtext[i + 1] - 0x40; - } else { - column = rawtext[i + 1] - 0x61; - } - // System.out.println("original row " + row + " column " + - // column); - if (Big5Freq[row][column] != 0) { - bffreq += Big5Freq[row][column]; - } else if (3 <= row && row < 37) { - bffreq += 200; - } - } else if (0x81 <= rawtext[i] - && rawtext[i] <= 0xFE - && // Extended Big5 range - ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0x80 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) { - bfchars++; - totalfreq += 500; - row = rawtext[i] - 0x81; - if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) { - column = rawtext[i + 1] - 0x40; - } else { - column = rawtext[i + 1] - 0x40; - } - // System.out.println("extended row " + row + " column " + - // column + " rawtext[i] " + rawtext[i]); - if (Big5PFreq[row][column] != 0) { - bffreq += Big5PFreq[row][column]; - } - } - i++; - } - } - rangeval = 50 * ((float) bfchars / (float) dbchars); - freqval = 50 * ((float) bffreq / (float) totalfreq); - // For regular Big5 files, this would give the same score, so I handicap - // it slightly - return (int) (rangeval + freqval) - 1; - } - - /* - * Function: euc_tw_probability Argument: byte array Returns : number from 0 - * to 100 representing probability text in array uses EUC-TW (CNS 11643) - * encoding - */ - int euc_tw_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, cnschars = 1; - long cnsfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Check to see if characters fit into acceptable ranges - // and have expected frequency of use - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - if (rawtext[i] >= 0) { // in ASCII range - // asciichars++; - } else { // high bit set - dbchars++; - if (i + 3 < rawtextlen && (byte) 0x8E == rawtext[i] - && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xB0 - && (byte) 0xA1 <= rawtext[i + 2] - && rawtext[i + 2] <= (byte) 0xFE - && (byte) 0xA1 <= rawtext[i + 3] - && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16 - cnschars++; - // System.out.println("plane 2 or above CNS char"); - // These are all less frequent chars so just ignore freq - i += 3; - } else if ((byte) 0xA1 <= rawtext[i] - && rawtext[i] <= (byte) 0xFE - && // Plane 1 - (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - cnschars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - if (EUC_TWFreq[row][column] != 0) { - cnsfreq += EUC_TWFreq[row][column]; - } else if (35 <= row && row <= 92) { - cnsfreq += 150; - } - i++; - } - } - } - rangeval = 50 * ((float) cnschars / (float) dbchars); - freqval = 50 * ((float) cnsfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - /* - * Function: iso_2022_cn_probability Argument: byte array Returns : number - * from 0 to 100 representing probability text in array uses ISO 2022-CN - * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK - */ - int iso_2022_cn_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, isochars = 1; - long isofreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Check to see if characters fit into acceptable ranges - // and have expected frequency of use - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape - // char ESC - if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29 - && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A - i += 4; - while (rawtext[i] != (byte) 0x1B) { - dbchars++; - if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77) - && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) { - isochars++; - row = rawtext[i] - 0x21; - column = rawtext[i + 1] - 0x21; - totalfreq += 500; - if (GBFreq[row][column] != 0) { - isofreq += GBFreq[row][column]; - } else if (15 <= row && row < 55) { - isofreq += 200; - } - i++; - } - i++; - } - } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24 - && rawtext[i + 2] == (byte) 0x29 - && rawtext[i + 3] == (byte) 0x47) { - // CNS Escape $ ) G - i += 4; - while (rawtext[i] != (byte) 0x1B) { - dbchars++; - if ((byte) 0x21 <= rawtext[i] - && rawtext[i] <= (byte) 0x7E - && (byte) 0x21 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0x7E) { - isochars++; - totalfreq += 500; - row = rawtext[i] - 0x21; - column = rawtext[i + 1] - 0x21; - if (EUC_TWFreq[row][column] != 0) { - isofreq += EUC_TWFreq[row][column]; - } else if (35 <= row && row <= 92) { - isofreq += 150; - } - i++; - } - i++; - } - } - if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen - && rawtext[i + 1] == (byte) 0x28 - && rawtext[i + 2] == (byte) 0x42) { // ASCII: - // ESC - // ( B - i += 2; - } - } - } - rangeval = 50 * ((float) isochars / (float) dbchars); - freqval = 50 * ((float) isofreq / (float) totalfreq); - // System.out.println("isochars dbchars isofreq totalfreq " + isochars + - // " " + dbchars + " " + isofreq + " " + totalfreq + " - // " + rangeval + " " + freqval); - return (int) (rangeval + freqval); - // return 0; - } - - /* - * Function: utf8_probability Argument: byte array Returns : number from 0 - * to 100 representing probability text in array uses UTF-8 encoding of - * Unicode - */ - int utf8_probability(byte[] rawtext) { - int score = 0; - int i, rawtextlen = 0; - int goodbytes = 0, asciibytes = 0; - // Maybe also use UTF8 Byte Order Mark: EF BB BF - // Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen; i++) { - if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) { // One byte - asciibytes++; - // Ignore ASCII, can throw off count - } else if (-64 <= rawtext[i] && rawtext[i] <= -33 - && // Two bytes - i + 1 < rawtextlen && -128 <= rawtext[i + 1] - && rawtext[i + 1] <= -65) { - goodbytes += 2; - i++; - } else if (-32 <= rawtext[i] - && rawtext[i] <= -17 - && // Three bytes - i + 2 < rawtextlen && -128 <= rawtext[i + 1] - && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2] - && rawtext[i + 2] <= -65) { - goodbytes += 3; - i += 2; - } - } - if (asciibytes == rawtextlen) { - return 0; - } - score = (int) (100 * ((float) goodbytes / (float) (rawtextlen - asciibytes))); - // System.out.println("rawtextlen " + rawtextlen + " goodbytes " + - // goodbytes + " asciibytes " + asciibytes + " score " + - // score); - // If not above 98, reduce to zero to prevent coincidental matches - // Allows for some (few) bad formed sequences - if (score > 98) { - return score; - } else if (score > 95 && goodbytes > 30) { - return score; - } else { - return 0; - } - } - - /* - * Function: utf16_probability Argument: byte array Returns : number from 0 - * to 100 representing probability text in array uses UTF-16 encoding of - * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK - */ - int utf16_probability(byte[] rawtext) { - if (rawtext.length > 1 - && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian - ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian - return 100; - } - return 0; - } - - /* - * Function: ascii_probability Argument: byte array Returns : number from 0 - * to 100 representing probability text in array uses all ASCII Description: - * Sees if array has any characters not in ASCII range, if so, score is - * reduced - */ - int ascii_probability(byte[] rawtext) { - int score = 75; - int i, rawtextlen; - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen; i++) { - if (rawtext[i] < 0) { - score = score - 5; - } else if (rawtext[i] == (byte) 0x1B) { // ESC (used by ISO 2022) - score = score - 5; - } - if (score <= 0) { - return 0; - } - } - return score; - } - - /* - * Function: euc_kr__probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses EUC-KR - * encoding - */ - int euc_kr_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, krchars = 1; - long krfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE - && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - krchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - if (KRFreq[row][column] != 0) { - krfreq += KRFreq[row][column]; - } else if (15 <= row && row < 55) { - krfreq += 0; - } - } - i++; - } - } - rangeval = 50 * ((float) krchars / (float) dbchars); - freqval = 50 * ((float) krfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - /* - * Function: cp949__probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses Cp949 - * encoding - */ - int cp949_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, krchars = 1; - long krfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0x81 <= rawtext[i] - && rawtext[i] <= (byte) 0xFE - && ((byte) 0x41 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0x5A - || (byte) 0x61 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0x7A || (byte) 0x81 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE)) { - krchars++; - totalfreq += 500; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE - && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - if (KRFreq[row][column] != 0) { - krfreq += KRFreq[row][column]; - } - } - } - i++; - } - } - rangeval = 50 * ((float) krchars / (float) dbchars); - freqval = 50 * ((float) krfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - int iso_2022_kr_probability(byte[] rawtext) { - int i; - for (i = 0; i < rawtext.length; i++) { - if (i + 3 < rawtext.length && rawtext[i] == 0x1b - && (char) rawtext[i + 1] == '$' - && (char) rawtext[i + 2] == ')' - && (char) rawtext[i + 3] == 'C') { - return 100; - } - } - return 0; - } - - /* - * Function: euc_jp_probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses EUC-JP - * encoding - */ - int euc_jp_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, jpchars = 1; - long jpfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE - && (byte) 0xA1 <= rawtext[i + 1] - && rawtext[i + 1] <= (byte) 0xFE) { - jpchars++; - totalfreq += 500; - row = rawtext[i] + 256 - 0xA1; - column = rawtext[i + 1] + 256 - 0xA1; - if (JPFreq[row][column] != 0) { - jpfreq += JPFreq[row][column]; - } else if (15 <= row && row < 55) { - jpfreq += 0; - } - } - i++; - } - } - rangeval = 50 * ((float) jpchars / (float) dbchars); - freqval = 50 * ((float) jpfreq / (float) totalfreq); - return (int) (rangeval + freqval); - } - - int iso_2022_jp_probability(byte[] rawtext) { - int i; - for (i = 0; i < rawtext.length; i++) { - if (i + 2 < rawtext.length && rawtext[i] == 0x1b - && (char) rawtext[i + 1] == '$' - && (char) rawtext[i + 2] == 'B') { - return 100; - } - } - return 0; - } - - /* - * Function: sjis_probability Argument: pointer to byte array Returns : - * number from 0 to 100 representing probability text in array uses - * Shift-JIS encoding - */ - int sjis_probability(byte[] rawtext) { - int i, rawtextlen = 0; - int dbchars = 1, jpchars = 1; - long jpfreq = 0, totalfreq = 1; - float rangeval = 0, freqval = 0; - int row, column, adjust; - // Stage 1: Check to see if characters fit into acceptable ranges - rawtextlen = rawtext.length; - for (i = 0; i < rawtextlen - 1; i++) { - // System.err.println(rawtext[i]); - if (rawtext[i] >= 0) { - // asciichars++; - } else { - dbchars++; - if (i + 1 < rawtext.length - && (((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0x9F) || ((byte) 0xE0 <= rawtext[i] && rawtext[i] <= (byte) 0xEF)) - && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFC))) { - jpchars++; - totalfreq += 500; - row = rawtext[i] + 256; - column = rawtext[i + 1] + 256; - if (column < 0x9f) { - adjust = 1; - } else { - adjust = 0; - } - if (row < 0xa0) { - row = ((row - 0x70) << 1) - adjust; - } else { - row = ((row - 0xb0) << 1) - adjust; - } - row -= 0x20; - column = 0x20; - // System.out.println("original row " + row + " column " + - // column); - if (row < JPFreq.length && column < JPFreq[row].length - && JPFreq[row][column] != 0) { - jpfreq += JPFreq[row][column]; - } - i++; - } else if ((byte) 0xA1 <= rawtext[i] - && rawtext[i] <= (byte) 0xDF) { - // half-width katakana, convert to full-width - } - } - } - rangeval = 50 * ((float) jpchars / (float) dbchars); - freqval = 50 * ((float) jpfreq / (float) totalfreq); - // For regular GB files, this would give the same score, so I handicap - // it slightly - return (int) (rangeval + freqval) - 1; - } - - void initialize_frequencies() { - int i, j; - for (i = 93; i >= 0; i--) { - for (j = 93; j >= 0; j--) { - GBFreq[i][j] = 0; - } - } - for (i = 125; i >= 0; i--) { - for (j = 190; j >= 0; j--) { - GBKFreq[i][j] = 0; - } - } - for (i = 93; i >= 0; i--) { - for (j = 157; j >= 0; j--) { - Big5Freq[i][j] = 0; - } - } - for (i = 125; i >= 0; i--) { - for (j = 190; j >= 0; j--) { - Big5PFreq[i][j] = 0; - } - } - for (i = 93; i >= 0; i--) { - for (j = 93; j >= 0; j--) { - EUC_TWFreq[i][j] = 0; - } - } - for (i = 93; i >= 0; i--) { - for (j = 93; j >= 0; j--) { - JPFreq[i][j] = 0; - } - } - GBFreq[20][35] = 599; - GBFreq[49][26] = 598; - GBFreq[41][38] = 597; - GBFreq[17][26] = 596; - GBFreq[32][42] = 595; - GBFreq[39][42] = 594; - GBFreq[45][49] = 593; - GBFreq[51][57] = 592; - GBFreq[50][47] = 591; - GBFreq[42][90] = 590; - GBFreq[52][65] = 589; - GBFreq[53][47] = 588; - GBFreq[19][82] = 587; - GBFreq[31][19] = 586; - GBFreq[40][46] = 585; - GBFreq[24][89] = 584; - GBFreq[23][85] = 583; - GBFreq[20][28] = 582; - GBFreq[42][20] = 581; - GBFreq[34][38] = 580; - GBFreq[45][9] = 579; - GBFreq[54][50] = 578; - GBFreq[25][44] = 577; - GBFreq[35][66] = 576; - GBFreq[20][55] = 575; - GBFreq[18][85] = 574; - GBFreq[20][31] = 573; - GBFreq[49][17] = 572; - GBFreq[41][16] = 571; - GBFreq[35][73] = 570; - GBFreq[20][34] = 569; - GBFreq[29][44] = 568; - GBFreq[35][38] = 567; - GBFreq[49][9] = 566; - GBFreq[46][33] = 565; - GBFreq[49][51] = 564; - GBFreq[40][89] = 563; - GBFreq[26][64] = 562; - GBFreq[54][51] = 561; - GBFreq[54][36] = 560; - GBFreq[39][4] = 559; - GBFreq[53][13] = 558; - GBFreq[24][92] = 557; - GBFreq[27][49] = 556; - GBFreq[48][6] = 555; - GBFreq[21][51] = 554; - GBFreq[30][40] = 553; - GBFreq[42][92] = 552; - GBFreq[31][78] = 551; - GBFreq[25][82] = 550; - GBFreq[47][0] = 549; - GBFreq[34][19] = 548; - GBFreq[47][35] = 547; - GBFreq[21][63] = 546; - GBFreq[43][75] = 545; - GBFreq[21][87] = 544; - GBFreq[35][59] = 543; - GBFreq[25][34] = 542; - GBFreq[21][27] = 541; - GBFreq[39][26] = 540; - GBFreq[34][26] = 539; - GBFreq[39][52] = 538; - GBFreq[50][57] = 537; - GBFreq[37][79] = 536; - GBFreq[26][24] = 535; - GBFreq[22][1] = 534; - GBFreq[18][40] = 533; - GBFreq[41][33] = 532; - GBFreq[53][26] = 531; - GBFreq[54][86] = 530; - GBFreq[20][16] = 529; - GBFreq[46][74] = 528; - GBFreq[30][19] = 527; - GBFreq[45][35] = 526; - GBFreq[45][61] = 525; - GBFreq[30][9] = 524; - GBFreq[41][53] = 523; - GBFreq[41][13] = 522; - GBFreq[50][34] = 521; - GBFreq[53][86] = 520; - GBFreq[47][47] = 519; - GBFreq[22][28] = 518; - GBFreq[50][53] = 517; - GBFreq[39][70] = 516; - GBFreq[38][15] = 515; - GBFreq[42][88] = 514; - GBFreq[16][29] = 513; - GBFreq[27][90] = 512; - GBFreq[29][12] = 511; - GBFreq[44][22] = 510; - GBFreq[34][69] = 509; - GBFreq[24][10] = 508; - GBFreq[44][11] = 507; - GBFreq[39][92] = 506; - GBFreq[49][48] = 505; - GBFreq[31][46] = 504; - GBFreq[19][50] = 503; - GBFreq[21][14] = 502; - GBFreq[32][28] = 501; - GBFreq[18][3] = 500; - GBFreq[53][9] = 499; - GBFreq[34][80] = 498; - GBFreq[48][88] = 497; - GBFreq[46][53] = 496; - GBFreq[22][53] = 495; - GBFreq[28][10] = 494; - GBFreq[44][65] = 493; - GBFreq[20][10] = 492; - GBFreq[40][76] = 491; - GBFreq[47][8] = 490; - GBFreq[50][74] = 489; - GBFreq[23][62] = 488; - GBFreq[49][65] = 487; - GBFreq[28][87] = 486; - GBFreq[15][48] = 485; - GBFreq[22][7] = 484; - GBFreq[19][42] = 483; - GBFreq[41][20] = 482; - GBFreq[26][55] = 481; - GBFreq[21][93] = 480; - GBFreq[31][76] = 479; - GBFreq[34][31] = 478; - GBFreq[20][66] = 477; - GBFreq[51][33] = 476; - GBFreq[34][86] = 475; - GBFreq[37][67] = 474; - GBFreq[53][53] = 473; - GBFreq[40][88] = 472; - GBFreq[39][10] = 471; - GBFreq[24][3] = 470; - GBFreq[27][25] = 469; - GBFreq[26][15] = 468; - GBFreq[21][88] = 467; - GBFreq[52][62] = 466; - GBFreq[46][81] = 465; - GBFreq[38][72] = 464; - GBFreq[17][30] = 463; - GBFreq[52][92] = 462; - GBFreq[34][90] = 461; - GBFreq[21][7] = 460; - GBFreq[36][13] = 459; - GBFreq[45][41] = 458; - GBFreq[32][5] = 457; - GBFreq[26][89] = 456; - GBFreq[23][87] = 455; - GBFreq[20][39] = 454; - GBFreq[27][23] = 453; - GBFreq[25][59] = 452; - GBFreq[49][20] = 451; - GBFreq[54][77] = 450; - GBFreq[27][67] = 449; - GBFreq[47][33] = 448; - GBFreq[41][17] = 447; - GBFreq[19][81] = 446; - GBFreq[16][66] = 445; - GBFreq[45][26] = 444; - GBFreq[49][81] = 443; - GBFreq[53][55] = 442; - GBFreq[16][26] = 441; - GBFreq[54][62] = 440; - GBFreq[20][70] = 439; - GBFreq[42][35] = 438; - GBFreq[20][57] = 437; - GBFreq[34][36] = 436; - GBFreq[46][63] = 435; - GBFreq[19][45] = 434; - GBFreq[21][10] = 433; - GBFreq[52][93] = 432; - GBFreq[25][2] = 431; - GBFreq[30][57] = 430; - GBFreq[41][24] = 429; - GBFreq[28][43] = 428; - GBFreq[45][86] = 427; - GBFreq[51][56] = 426; - GBFreq[37][28] = 425; - GBFreq[52][69] = 424; - GBFreq[43][92] = 423; - GBFreq[41][31] = 422; - GBFreq[37][87] = 421; - GBFreq[47][36] = 420; - GBFreq[16][16] = 419; - GBFreq[40][56] = 418; - GBFreq[24][55] = 417; - GBFreq[17][1] = 416; - GBFreq[35][57] = 415; - GBFreq[27][50] = 414; - GBFreq[26][14] = 413; - GBFreq[50][40] = 412; - GBFreq[39][19] = 411; - GBFreq[19][89] = 410; - GBFreq[29][91] = 409; - GBFreq[17][89] = 408; - GBFreq[39][74] = 407; - GBFreq[46][39] = 406; - GBFreq[40][28] = 405; - GBFreq[45][68] = 404; - GBFreq[43][10] = 403; - GBFreq[42][13] = 402; - GBFreq[44][81] = 401; - GBFreq[41][47] = 400; - GBFreq[48][58] = 399; - GBFreq[43][68] = 398; - GBFreq[16][79] = 397; - GBFreq[19][5] = 396; - GBFreq[54][59] = 395; - GBFreq[17][36] = 394; - GBFreq[18][0] = 393; - GBFreq[41][5] = 392; - GBFreq[41][72] = 391; - GBFreq[16][39] = 390; - GBFreq[54][0] = 389; - GBFreq[51][16] = 388; - GBFreq[29][36] = 387; - GBFreq[47][5] = 386; - GBFreq[47][51] = 385; - GBFreq[44][7] = 384; - GBFreq[35][30] = 383; - GBFreq[26][9] = 382; - GBFreq[16][7] = 381; - GBFreq[32][1] = 380; - GBFreq[33][76] = 379; - GBFreq[34][91] = 378; - GBFreq[52][36] = 377; - GBFreq[26][77] = 376; - GBFreq[35][48] = 375; - GBFreq[40][80] = 374; - GBFreq[41][92] = 373; - GBFreq[27][93] = 372; - GBFreq[15][17] = 371; - GBFreq[16][76] = 370; - GBFreq[51][12] = 369; - GBFreq[18][20] = 368; - GBFreq[15][54] = 367; - GBFreq[50][5] = 366; - GBFreq[33][22] = 365; - GBFreq[37][57] = 364; - GBFreq[28][47] = 363; - GBFreq[42][31] = 362; - GBFreq[18][2] = 361; - GBFreq[43][64] = 360; - GBFreq[23][47] = 359; - GBFreq[28][79] = 358; - GBFreq[25][45] = 357; - GBFreq[23][91] = 356; - GBFreq[22][19] = 355; - GBFreq[25][46] = 354; - GBFreq[22][36] = 353; - GBFreq[54][85] = 352; - GBFreq[46][20] = 351; - GBFreq[27][37] = 350; - GBFreq[26][81] = 349; - GBFreq[42][29] = 348; - GBFreq[31][90] = 347; - GBFreq[41][59] = 346; - GBFreq[24][65] = 345; - GBFreq[44][84] = 344; - GBFreq[24][90] = 343; - GBFreq[38][54] = 342; - GBFreq[28][70] = 341; - GBFreq[27][15] = 340; - GBFreq[28][80] = 339; - GBFreq[29][8] = 338; - GBFreq[45][80] = 337; - GBFreq[53][37] = 336; - GBFreq[28][65] = 335; - GBFreq[23][86] = 334; - GBFreq[39][45] = 333; - GBFreq[53][32] = 332; - GBFreq[38][68] = 331; - GBFreq[45][78] = 330; - GBFreq[43][7] = 329; - GBFreq[46][82] = 328; - GBFreq[27][38] = 327; - GBFreq[16][62] = 326; - GBFreq[24][17] = 325; - GBFreq[22][70] = 324; - GBFreq[52][28] = 323; - GBFreq[23][40] = 322; - GBFreq[28][50] = 321; - GBFreq[42][91] = 320; - GBFreq[47][76] = 319; - GBFreq[15][42] = 318; - GBFreq[43][55] = 317; - GBFreq[29][84] = 316; - GBFreq[44][90] = 315; - GBFreq[53][16] = 314; - GBFreq[22][93] = 313; - GBFreq[34][10] = 312; - GBFreq[32][53] = 311; - GBFreq[43][65] = 310; - GBFreq[28][7] = 309; - GBFreq[35][46] = 308; - GBFreq[21][39] = 307; - GBFreq[44][18] = 306; - GBFreq[40][10] = 305; - GBFreq[54][53] = 304; - GBFreq[38][74] = 303; - GBFreq[28][26] = 302; - GBFreq[15][13] = 301; - GBFreq[39][34] = 300; - GBFreq[39][46] = 299; - GBFreq[42][66] = 298; - GBFreq[33][58] = 297; - GBFreq[15][56] = 296; - GBFreq[18][51] = 295; - GBFreq[49][68] = 294; - GBFreq[30][37] = 293; - GBFreq[51][84] = 292; - GBFreq[51][9] = 291; - GBFreq[40][70] = 290; - GBFreq[41][84] = 289; - GBFreq[28][64] = 288; - GBFreq[32][88] = 287; - GBFreq[24][5] = 286; - GBFreq[53][23] = 285; - GBFreq[42][27] = 284; - GBFreq[22][38] = 283; - GBFreq[32][86] = 282; - GBFreq[34][30] = 281; - GBFreq[38][63] = 280; - GBFreq[24][59] = 279; - GBFreq[22][81] = 278; - GBFreq[32][11] = 277; - GBFreq[51][21] = 276; - GBFreq[54][41] = 275; - GBFreq[21][50] = 274; - GBFreq[23][89] = 273; - GBFreq[19][87] = 272; - GBFreq[26][7] = 271; - GBFreq[30][75] = 270; - GBFreq[43][84] = 269; - GBFreq[51][25] = 268; - GBFreq[16][67] = 267; - GBFreq[32][9] = 266; - GBFreq[48][51] = 265; - GBFreq[39][7] = 264; - GBFreq[44][88] = 263; - GBFreq[52][24] = 262; - GBFreq[23][34] = 261; - GBFreq[32][75] = 260; - GBFreq[19][10] = 259; - GBFreq[28][91] = 258; - GBFreq[32][83] = 257; - GBFreq[25][75] = 256; - GBFreq[53][45] = 255; - GBFreq[29][85] = 254; - GBFreq[53][59] = 253; - GBFreq[16][2] = 252; - GBFreq[19][78] = 251; - GBFreq[15][75] = 250; - GBFreq[51][42] = 249; - GBFreq[45][67] = 248; - GBFreq[15][74] = 247; - GBFreq[25][81] = 246; - GBFreq[37][62] = 245; - GBFreq[16][55] = 244; - GBFreq[18][38] = 243; - GBFreq[23][23] = 242; - GBFreq[38][30] = 241; - GBFreq[17][28] = 240; - GBFreq[44][73] = 239; - GBFreq[23][78] = 238; - GBFreq[40][77] = 237; - GBFreq[38][87] = 236; - GBFreq[27][19] = 235; - GBFreq[38][82] = 234; - GBFreq[37][22] = 233; - GBFreq[41][30] = 232; - GBFreq[54][9] = 231; - GBFreq[32][30] = 230; - GBFreq[30][52] = 229; - GBFreq[40][84] = 228; - GBFreq[53][57] = 227; - GBFreq[27][27] = 226; - GBFreq[38][64] = 225; - GBFreq[18][43] = 224; - GBFreq[23][69] = 223; - GBFreq[28][12] = 222; - GBFreq[50][78] = 221; - GBFreq[50][1] = 220; - GBFreq[26][88] = 219; - GBFreq[36][40] = 218; - GBFreq[33][89] = 217; - GBFreq[41][28] = 216; - GBFreq[31][77] = 215; - GBFreq[46][1] = 214; - GBFreq[47][19] = 213; - GBFreq[35][55] = 212; - GBFreq[41][21] = 211; - GBFreq[27][10] = 210; - GBFreq[32][77] = 209; - GBFreq[26][37] = 208; - GBFreq[20][33] = 207; - GBFreq[41][52] = 206; - GBFreq[32][18] = 205; - GBFreq[38][13] = 204; - GBFreq[20][18] = 203; - GBFreq[20][24] = 202; - GBFreq[45][19] = 201; - GBFreq[18][53] = 200; - - Big5Freq[9][89] = 600; - Big5Freq[11][15] = 599; - Big5Freq[3][66] = 598; - Big5Freq[6][121] = 597; - Big5Freq[3][0] = 596; - Big5Freq[5][82] = 595; - Big5Freq[3][42] = 594; - Big5Freq[5][34] = 593; - Big5Freq[3][8] = 592; - Big5Freq[3][6] = 591; - Big5Freq[3][67] = 590; - Big5Freq[7][139] = 589; - Big5Freq[23][137] = 588; - Big5Freq[12][46] = 587; - Big5Freq[4][8] = 586; - Big5Freq[4][41] = 585; - Big5Freq[18][47] = 584; - Big5Freq[12][114] = 583; - Big5Freq[6][1] = 582; - Big5Freq[22][60] = 581; - Big5Freq[5][46] = 580; - Big5Freq[11][79] = 579; - Big5Freq[3][23] = 578; - Big5Freq[7][114] = 577; - Big5Freq[29][102] = 576; - Big5Freq[19][14] = 575; - Big5Freq[4][133] = 574; - Big5Freq[3][29] = 573; - Big5Freq[4][109] = 572; - Big5Freq[14][127] = 571; - Big5Freq[5][48] = 570; - Big5Freq[13][104] = 569; - Big5Freq[3][132] = 568; - Big5Freq[26][64] = 567; - Big5Freq[7][19] = 566; - Big5Freq[4][12] = 565; - Big5Freq[11][124] = 564; - Big5Freq[7][89] = 563; - Big5Freq[15][124] = 562; - Big5Freq[4][108] = 561; - Big5Freq[19][66] = 560; - Big5Freq[3][21] = 559; - Big5Freq[24][12] = 558; - Big5Freq[28][111] = 557; - Big5Freq[12][107] = 556; - Big5Freq[3][112] = 555; - Big5Freq[8][113] = 554; - Big5Freq[5][40] = 553; - Big5Freq[26][145] = 552; - Big5Freq[3][48] = 551; - Big5Freq[3][70] = 550; - Big5Freq[22][17] = 549; - Big5Freq[16][47] = 548; - Big5Freq[3][53] = 547; - Big5Freq[4][24] = 546; - Big5Freq[32][120] = 545; - Big5Freq[24][49] = 544; - Big5Freq[24][142] = 543; - Big5Freq[18][66] = 542; - Big5Freq[29][150] = 541; - Big5Freq[5][122] = 540; - Big5Freq[5][114] = 539; - Big5Freq[3][44] = 538; - Big5Freq[10][128] = 537; - Big5Freq[15][20] = 536; - Big5Freq[13][33] = 535; - Big5Freq[14][87] = 534; - Big5Freq[3][126] = 533; - Big5Freq[4][53] = 532; - Big5Freq[4][40] = 531; - Big5Freq[9][93] = 530; - Big5Freq[15][137] = 529; - Big5Freq[10][123] = 528; - Big5Freq[4][56] = 527; - Big5Freq[5][71] = 526; - Big5Freq[10][8] = 525; - Big5Freq[5][16] = 524; - Big5Freq[5][146] = 523; - Big5Freq[18][88] = 522; - Big5Freq[24][4] = 521; - Big5Freq[20][47] = 520; - Big5Freq[5][33] = 519; - Big5Freq[9][43] = 518; - Big5Freq[20][12] = 517; - Big5Freq[20][13] = 516; - Big5Freq[5][156] = 515; - Big5Freq[22][140] = 514; - Big5Freq[8][146] = 513; - Big5Freq[21][123] = 512; - Big5Freq[4][90] = 511; - Big5Freq[5][62] = 510; - Big5Freq[17][59] = 509; - Big5Freq[10][37] = 508; - Big5Freq[18][107] = 507; - Big5Freq[14][53] = 506; - Big5Freq[22][51] = 505; - Big5Freq[8][13] = 504; - Big5Freq[5][29] = 503; - Big5Freq[9][7] = 502; - Big5Freq[22][14] = 501; - Big5Freq[8][55] = 500; - Big5Freq[33][9] = 499; - Big5Freq[16][64] = 498; - Big5Freq[7][131] = 497; - Big5Freq[34][4] = 496; - Big5Freq[7][101] = 495; - Big5Freq[11][139] = 494; - Big5Freq[3][135] = 493; - Big5Freq[7][102] = 492; - Big5Freq[17][13] = 491; - Big5Freq[3][20] = 490; - Big5Freq[27][106] = 489; - Big5Freq[5][88] = 488; - Big5Freq[6][33] = 487; - Big5Freq[5][139] = 486; - Big5Freq[6][0] = 485; - Big5Freq[17][58] = 484; - Big5Freq[5][133] = 483; - Big5Freq[9][107] = 482; - Big5Freq[23][39] = 481; - Big5Freq[5][23] = 480; - Big5Freq[3][79] = 479; - Big5Freq[32][97] = 478; - Big5Freq[3][136] = 477; - Big5Freq[4][94] = 476; - Big5Freq[21][61] = 475; - Big5Freq[23][123] = 474; - Big5Freq[26][16] = 473; - Big5Freq[24][137] = 472; - Big5Freq[22][18] = 471; - Big5Freq[5][1] = 470; - Big5Freq[20][119] = 469; - Big5Freq[3][7] = 468; - Big5Freq[10][79] = 467; - Big5Freq[15][105] = 466; - Big5Freq[3][144] = 465; - Big5Freq[12][80] = 464; - Big5Freq[15][73] = 463; - Big5Freq[3][19] = 462; - Big5Freq[8][109] = 461; - Big5Freq[3][15] = 460; - Big5Freq[31][82] = 459; - Big5Freq[3][43] = 458; - Big5Freq[25][119] = 457; - Big5Freq[16][111] = 456; - Big5Freq[7][77] = 455; - Big5Freq[3][95] = 454; - Big5Freq[24][82] = 453; - Big5Freq[7][52] = 452; - Big5Freq[9][151] = 451; - Big5Freq[3][129] = 450; - Big5Freq[5][87] = 449; - Big5Freq[3][55] = 448; - Big5Freq[8][153] = 447; - Big5Freq[4][83] = 446; - Big5Freq[3][114] = 445; - Big5Freq[23][147] = 444; - Big5Freq[15][31] = 443; - Big5Freq[3][54] = 442; - Big5Freq[11][122] = 441; - Big5Freq[4][4] = 440; - Big5Freq[34][149] = 439; - Big5Freq[3][17] = 438; - Big5Freq[21][64] = 437; - Big5Freq[26][144] = 436; - Big5Freq[4][62] = 435; - Big5Freq[8][15] = 434; - Big5Freq[35][80] = 433; - Big5Freq[7][110] = 432; - Big5Freq[23][114] = 431; - Big5Freq[3][108] = 430; - Big5Freq[3][62] = 429; - Big5Freq[21][41] = 428; - Big5Freq[15][99] = 427; - Big5Freq[5][47] = 426; - Big5Freq[4][96] = 425; - Big5Freq[20][122] = 424; - Big5Freq[5][21] = 423; - Big5Freq[4][157] = 422; - Big5Freq[16][14] = 421; - Big5Freq[3][117] = 420; - Big5Freq[7][129] = 419; - Big5Freq[4][27] = 418; - Big5Freq[5][30] = 417; - Big5Freq[22][16] = 416; - Big5Freq[5][64] = 415; - Big5Freq[17][99] = 414; - Big5Freq[17][57] = 413; - Big5Freq[8][105] = 412; - Big5Freq[5][112] = 411; - Big5Freq[20][59] = 410; - Big5Freq[6][129] = 409; - Big5Freq[18][17] = 408; - Big5Freq[3][92] = 407; - Big5Freq[28][118] = 406; - Big5Freq[3][109] = 405; - Big5Freq[31][51] = 404; - Big5Freq[13][116] = 403; - Big5Freq[6][15] = 402; - Big5Freq[36][136] = 401; - Big5Freq[12][74] = 400; - Big5Freq[20][88] = 399; - Big5Freq[36][68] = 398; - Big5Freq[3][147] = 397; - Big5Freq[15][84] = 396; - Big5Freq[16][32] = 395; - Big5Freq[16][58] = 394; - Big5Freq[7][66] = 393; - Big5Freq[23][107] = 392; - Big5Freq[9][6] = 391; - Big5Freq[12][86] = 390; - Big5Freq[23][112] = 389; - Big5Freq[37][23] = 388; - Big5Freq[3][138] = 387; - Big5Freq[20][68] = 386; - Big5Freq[15][116] = 385; - Big5Freq[18][64] = 384; - Big5Freq[12][139] = 383; - Big5Freq[11][155] = 382; - Big5Freq[4][156] = 381; - Big5Freq[12][84] = 380; - Big5Freq[18][49] = 379; - Big5Freq[25][125] = 378; - Big5Freq[25][147] = 377; - Big5Freq[15][110] = 376; - Big5Freq[19][96] = 375; - Big5Freq[30][152] = 374; - Big5Freq[6][31] = 373; - Big5Freq[27][117] = 372; - Big5Freq[3][10] = 371; - Big5Freq[6][131] = 370; - Big5Freq[13][112] = 369; - Big5Freq[36][156] = 368; - Big5Freq[4][60] = 367; - Big5Freq[15][121] = 366; - Big5Freq[4][112] = 365; - Big5Freq[30][142] = 364; - Big5Freq[23][154] = 363; - Big5Freq[27][101] = 362; - Big5Freq[9][140] = 361; - Big5Freq[3][89] = 360; - Big5Freq[18][148] = 359; - Big5Freq[4][69] = 358; - Big5Freq[16][49] = 357; - Big5Freq[6][117] = 356; - Big5Freq[36][55] = 355; - Big5Freq[5][123] = 354; - Big5Freq[4][126] = 353; - Big5Freq[4][119] = 352; - Big5Freq[9][95] = 351; - Big5Freq[5][24] = 350; - Big5Freq[16][133] = 349; - Big5Freq[10][134] = 348; - Big5Freq[26][59] = 347; - Big5Freq[6][41] = 346; - Big5Freq[6][146] = 345; - Big5Freq[19][24] = 344; - Big5Freq[5][113] = 343; - Big5Freq[10][118] = 342; - Big5Freq[34][151] = 341; - Big5Freq[9][72] = 340; - Big5Freq[31][25] = 339; - Big5Freq[18][126] = 338; - Big5Freq[18][28] = 337; - Big5Freq[4][153] = 336; - Big5Freq[3][84] = 335; - Big5Freq[21][18] = 334; - Big5Freq[25][129] = 333; - Big5Freq[6][107] = 332; - Big5Freq[12][25] = 331; - Big5Freq[17][109] = 330; - Big5Freq[7][76] = 329; - Big5Freq[15][15] = 328; - Big5Freq[4][14] = 327; - Big5Freq[23][88] = 326; - Big5Freq[18][2] = 325; - Big5Freq[6][88] = 324; - Big5Freq[16][84] = 323; - Big5Freq[12][48] = 322; - Big5Freq[7][68] = 321; - Big5Freq[5][50] = 320; - Big5Freq[13][54] = 319; - Big5Freq[7][98] = 318; - Big5Freq[11][6] = 317; - Big5Freq[9][80] = 316; - Big5Freq[16][41] = 315; - Big5Freq[7][43] = 314; - Big5Freq[28][117] = 313; - Big5Freq[3][51] = 312; - Big5Freq[7][3] = 311; - Big5Freq[20][81] = 310; - Big5Freq[4][2] = 309; - Big5Freq[11][16] = 308; - Big5Freq[10][4] = 307; - Big5Freq[10][119] = 306; - Big5Freq[6][142] = 305; - Big5Freq[18][51] = 304; - Big5Freq[8][144] = 303; - Big5Freq[10][65] = 302; - Big5Freq[11][64] = 301; - Big5Freq[11][130] = 300; - Big5Freq[9][92] = 299; - Big5Freq[18][29] = 298; - Big5Freq[18][78] = 297; - Big5Freq[18][151] = 296; - Big5Freq[33][127] = 295; - Big5Freq[35][113] = 294; - Big5Freq[10][155] = 293; - Big5Freq[3][76] = 292; - Big5Freq[36][123] = 291; - Big5Freq[13][143] = 290; - Big5Freq[5][135] = 289; - Big5Freq[23][116] = 288; - Big5Freq[6][101] = 287; - Big5Freq[14][74] = 286; - Big5Freq[7][153] = 285; - Big5Freq[3][101] = 284; - Big5Freq[9][74] = 283; - Big5Freq[3][156] = 282; - Big5Freq[4][147] = 281; - Big5Freq[9][12] = 280; - Big5Freq[18][133] = 279; - Big5Freq[4][0] = 278; - Big5Freq[7][155] = 277; - Big5Freq[9][144] = 276; - Big5Freq[23][49] = 275; - Big5Freq[5][89] = 274; - Big5Freq[10][11] = 273; - Big5Freq[3][110] = 272; - Big5Freq[3][40] = 271; - Big5Freq[29][115] = 270; - Big5Freq[9][100] = 269; - Big5Freq[21][67] = 268; - Big5Freq[23][145] = 267; - Big5Freq[10][47] = 266; - Big5Freq[4][31] = 265; - Big5Freq[4][81] = 264; - Big5Freq[22][62] = 263; - Big5Freq[4][28] = 262; - Big5Freq[27][39] = 261; - Big5Freq[27][54] = 260; - Big5Freq[32][46] = 259; - Big5Freq[4][76] = 258; - Big5Freq[26][15] = 257; - Big5Freq[12][154] = 256; - Big5Freq[9][150] = 255; - Big5Freq[15][17] = 254; - Big5Freq[5][129] = 253; - Big5Freq[10][40] = 252; - Big5Freq[13][37] = 251; - Big5Freq[31][104] = 250; - Big5Freq[3][152] = 249; - Big5Freq[5][22] = 248; - Big5Freq[8][48] = 247; - Big5Freq[4][74] = 246; - Big5Freq[6][17] = 245; - Big5Freq[30][82] = 244; - Big5Freq[4][116] = 243; - Big5Freq[16][42] = 242; - Big5Freq[5][55] = 241; - Big5Freq[4][64] = 240; - Big5Freq[14][19] = 239; - Big5Freq[35][82] = 238; - Big5Freq[30][139] = 237; - Big5Freq[26][152] = 236; - Big5Freq[32][32] = 235; - Big5Freq[21][102] = 234; - Big5Freq[10][131] = 233; - Big5Freq[9][128] = 232; - Big5Freq[3][87] = 231; - Big5Freq[4][51] = 230; - Big5Freq[10][15] = 229; - Big5Freq[4][150] = 228; - Big5Freq[7][4] = 227; - Big5Freq[7][51] = 226; - Big5Freq[7][157] = 225; - Big5Freq[4][146] = 224; - Big5Freq[4][91] = 223; - Big5Freq[7][13] = 222; - Big5Freq[17][116] = 221; - Big5Freq[23][21] = 220; - Big5Freq[5][106] = 219; - Big5Freq[14][100] = 218; - Big5Freq[10][152] = 217; - Big5Freq[14][89] = 216; - Big5Freq[6][138] = 215; - Big5Freq[12][157] = 214; - Big5Freq[10][102] = 213; - Big5Freq[19][94] = 212; - Big5Freq[7][74] = 211; - Big5Freq[18][128] = 210; - Big5Freq[27][111] = 209; - Big5Freq[11][57] = 208; - Big5Freq[3][131] = 207; - Big5Freq[30][23] = 206; - Big5Freq[30][126] = 205; - Big5Freq[4][36] = 204; - Big5Freq[26][124] = 203; - Big5Freq[4][19] = 202; - Big5Freq[9][152] = 201; - - Big5PFreq[41][122] = 600; - Big5PFreq[35][0] = 599; - Big5PFreq[43][15] = 598; - Big5PFreq[35][99] = 597; - Big5PFreq[35][6] = 596; - Big5PFreq[35][8] = 595; - Big5PFreq[38][154] = 594; - Big5PFreq[37][34] = 593; - Big5PFreq[37][115] = 592; - Big5PFreq[36][12] = 591; - Big5PFreq[18][77] = 590; - Big5PFreq[35][100] = 589; - Big5PFreq[35][42] = 588; - Big5PFreq[120][75] = 587; - Big5PFreq[35][23] = 586; - Big5PFreq[13][72] = 585; - Big5PFreq[0][67] = 584; - Big5PFreq[39][172] = 583; - Big5PFreq[22][182] = 582; - Big5PFreq[15][186] = 581; - Big5PFreq[15][165] = 580; - Big5PFreq[35][44] = 579; - Big5PFreq[40][13] = 578; - Big5PFreq[38][1] = 577; - Big5PFreq[37][33] = 576; - Big5PFreq[36][24] = 575; - Big5PFreq[56][4] = 574; - Big5PFreq[35][29] = 573; - Big5PFreq[9][96] = 572; - Big5PFreq[37][62] = 571; - Big5PFreq[48][47] = 570; - Big5PFreq[51][14] = 569; - Big5PFreq[39][122] = 568; - Big5PFreq[44][46] = 567; - Big5PFreq[35][21] = 566; - Big5PFreq[36][8] = 565; - Big5PFreq[36][141] = 564; - Big5PFreq[3][81] = 563; - Big5PFreq[37][155] = 562; - Big5PFreq[42][84] = 561; - Big5PFreq[36][40] = 560; - Big5PFreq[35][103] = 559; - Big5PFreq[11][84] = 558; - Big5PFreq[45][33] = 557; - Big5PFreq[121][79] = 556; - Big5PFreq[2][77] = 555; - Big5PFreq[36][41] = 554; - Big5PFreq[37][47] = 553; - Big5PFreq[39][125] = 552; - Big5PFreq[37][26] = 551; - Big5PFreq[35][48] = 550; - Big5PFreq[35][28] = 549; - Big5PFreq[35][159] = 548; - Big5PFreq[37][40] = 547; - Big5PFreq[35][145] = 546; - Big5PFreq[37][147] = 545; - Big5PFreq[46][160] = 544; - Big5PFreq[37][46] = 543; - Big5PFreq[50][99] = 542; - Big5PFreq[52][13] = 541; - Big5PFreq[10][82] = 540; - Big5PFreq[35][169] = 539; - Big5PFreq[35][31] = 538; - Big5PFreq[47][31] = 537; - Big5PFreq[18][79] = 536; - Big5PFreq[16][113] = 535; - Big5PFreq[37][104] = 534; - Big5PFreq[39][134] = 533; - Big5PFreq[36][53] = 532; - Big5PFreq[38][0] = 531; - Big5PFreq[4][86] = 530; - Big5PFreq[54][17] = 529; - Big5PFreq[43][157] = 528; - Big5PFreq[35][165] = 527; - Big5PFreq[69][147] = 526; - Big5PFreq[117][95] = 525; - Big5PFreq[35][162] = 524; - Big5PFreq[35][17] = 523; - Big5PFreq[36][142] = 522; - Big5PFreq[36][4] = 521; - Big5PFreq[37][166] = 520; - Big5PFreq[35][168] = 519; - Big5PFreq[35][19] = 518; - Big5PFreq[37][48] = 517; - Big5PFreq[42][37] = 516; - Big5PFreq[40][146] = 515; - Big5PFreq[36][123] = 514; - Big5PFreq[22][41] = 513; - Big5PFreq[20][119] = 512; - Big5PFreq[2][74] = 511; - Big5PFreq[44][113] = 510; - Big5PFreq[35][125] = 509; - Big5PFreq[37][16] = 508; - Big5PFreq[35][20] = 507; - Big5PFreq[35][55] = 506; - Big5PFreq[37][145] = 505; - Big5PFreq[0][88] = 504; - Big5PFreq[3][94] = 503; - Big5PFreq[6][65] = 502; - Big5PFreq[26][15] = 501; - Big5PFreq[41][126] = 500; - Big5PFreq[36][129] = 499; - Big5PFreq[31][75] = 498; - Big5PFreq[19][61] = 497; - Big5PFreq[35][128] = 496; - Big5PFreq[29][79] = 495; - Big5PFreq[36][62] = 494; - Big5PFreq[37][189] = 493; - Big5PFreq[39][109] = 492; - Big5PFreq[39][135] = 491; - Big5PFreq[72][15] = 490; - Big5PFreq[47][106] = 489; - Big5PFreq[54][14] = 488; - Big5PFreq[24][52] = 487; - Big5PFreq[38][162] = 486; - Big5PFreq[41][43] = 485; - Big5PFreq[37][121] = 484; - Big5PFreq[14][66] = 483; - Big5PFreq[37][30] = 482; - Big5PFreq[35][7] = 481; - Big5PFreq[49][58] = 480; - Big5PFreq[43][188] = 479; - Big5PFreq[24][66] = 478; - Big5PFreq[35][171] = 477; - Big5PFreq[40][186] = 476; - Big5PFreq[39][164] = 475; - Big5PFreq[78][186] = 474; - Big5PFreq[8][72] = 473; - Big5PFreq[36][190] = 472; - Big5PFreq[35][53] = 471; - Big5PFreq[35][54] = 470; - Big5PFreq[22][159] = 469; - Big5PFreq[35][9] = 468; - Big5PFreq[41][140] = 467; - Big5PFreq[37][22] = 466; - Big5PFreq[48][97] = 465; - Big5PFreq[50][97] = 464; - Big5PFreq[36][127] = 463; - Big5PFreq[37][23] = 462; - Big5PFreq[40][55] = 461; - Big5PFreq[35][43] = 460; - Big5PFreq[26][22] = 459; - Big5PFreq[35][15] = 458; - Big5PFreq[72][179] = 457; - Big5PFreq[20][129] = 456; - Big5PFreq[52][101] = 455; - Big5PFreq[35][12] = 454; - Big5PFreq[42][156] = 453; - Big5PFreq[15][157] = 452; - Big5PFreq[50][140] = 451; - Big5PFreq[26][28] = 450; - Big5PFreq[54][51] = 449; - Big5PFreq[35][112] = 448; - Big5PFreq[36][116] = 447; - Big5PFreq[42][11] = 446; - Big5PFreq[37][172] = 445; - Big5PFreq[37][29] = 444; - Big5PFreq[44][107] = 443; - Big5PFreq[50][17] = 442; - Big5PFreq[39][107] = 441; - Big5PFreq[19][109] = 440; - Big5PFreq[36][60] = 439; - Big5PFreq[49][132] = 438; - Big5PFreq[26][16] = 437; - Big5PFreq[43][155] = 436; - Big5PFreq[37][120] = 435; - Big5PFreq[15][159] = 434; - Big5PFreq[43][6] = 433; - Big5PFreq[45][188] = 432; - Big5PFreq[35][38] = 431; - Big5PFreq[39][143] = 430; - Big5PFreq[48][144] = 429; - Big5PFreq[37][168] = 428; - Big5PFreq[37][1] = 427; - Big5PFreq[36][109] = 426; - Big5PFreq[46][53] = 425; - Big5PFreq[38][54] = 424; - Big5PFreq[36][0] = 423; - Big5PFreq[72][33] = 422; - Big5PFreq[42][8] = 421; - Big5PFreq[36][31] = 420; - Big5PFreq[35][150] = 419; - Big5PFreq[118][93] = 418; - Big5PFreq[37][61] = 417; - Big5PFreq[0][85] = 416; - Big5PFreq[36][27] = 415; - Big5PFreq[35][134] = 414; - Big5PFreq[36][145] = 413; - Big5PFreq[6][96] = 412; - Big5PFreq[36][14] = 411; - Big5PFreq[16][36] = 410; - Big5PFreq[15][175] = 409; - Big5PFreq[35][10] = 408; - Big5PFreq[36][189] = 407; - Big5PFreq[35][51] = 406; - Big5PFreq[35][109] = 405; - Big5PFreq[35][147] = 404; - Big5PFreq[35][180] = 403; - Big5PFreq[72][5] = 402; - Big5PFreq[36][107] = 401; - Big5PFreq[49][116] = 400; - Big5PFreq[73][30] = 399; - Big5PFreq[6][90] = 398; - Big5PFreq[2][70] = 397; - Big5PFreq[17][141] = 396; - Big5PFreq[35][62] = 395; - Big5PFreq[16][180] = 394; - Big5PFreq[4][91] = 393; - Big5PFreq[15][171] = 392; - Big5PFreq[35][177] = 391; - Big5PFreq[37][173] = 390; - Big5PFreq[16][121] = 389; - Big5PFreq[35][5] = 388; - Big5PFreq[46][122] = 387; - Big5PFreq[40][138] = 386; - Big5PFreq[50][49] = 385; - Big5PFreq[36][152] = 384; - Big5PFreq[13][43] = 383; - Big5PFreq[9][88] = 382; - Big5PFreq[36][159] = 381; - Big5PFreq[27][62] = 380; - Big5PFreq[40][18] = 379; - Big5PFreq[17][129] = 378; - Big5PFreq[43][97] = 377; - Big5PFreq[13][131] = 376; - Big5PFreq[46][107] = 375; - Big5PFreq[60][64] = 374; - Big5PFreq[36][179] = 373; - Big5PFreq[37][55] = 372; - Big5PFreq[41][173] = 371; - Big5PFreq[44][172] = 370; - Big5PFreq[23][187] = 369; - Big5PFreq[36][149] = 368; - Big5PFreq[17][125] = 367; - Big5PFreq[55][180] = 366; - Big5PFreq[51][129] = 365; - Big5PFreq[36][51] = 364; - Big5PFreq[37][122] = 363; - Big5PFreq[48][32] = 362; - Big5PFreq[51][99] = 361; - Big5PFreq[54][16] = 360; - Big5PFreq[41][183] = 359; - Big5PFreq[37][179] = 358; - Big5PFreq[38][179] = 357; - Big5PFreq[35][143] = 356; - Big5PFreq[37][24] = 355; - Big5PFreq[40][177] = 354; - Big5PFreq[47][117] = 353; - Big5PFreq[39][52] = 352; - Big5PFreq[22][99] = 351; - Big5PFreq[40][142] = 350; - Big5PFreq[36][49] = 349; - Big5PFreq[38][17] = 348; - Big5PFreq[39][188] = 347; - Big5PFreq[36][186] = 346; - Big5PFreq[35][189] = 345; - Big5PFreq[41][7] = 344; - Big5PFreq[18][91] = 343; - Big5PFreq[43][137] = 342; - Big5PFreq[35][142] = 341; - Big5PFreq[35][117] = 340; - Big5PFreq[39][138] = 339; - Big5PFreq[16][59] = 338; - Big5PFreq[39][174] = 337; - Big5PFreq[55][145] = 336; - Big5PFreq[37][21] = 335; - Big5PFreq[36][180] = 334; - Big5PFreq[37][156] = 333; - Big5PFreq[49][13] = 332; - Big5PFreq[41][107] = 331; - Big5PFreq[36][56] = 330; - Big5PFreq[53][8] = 329; - Big5PFreq[22][114] = 328; - Big5PFreq[5][95] = 327; - Big5PFreq[37][0] = 326; - Big5PFreq[26][183] = 325; - Big5PFreq[22][66] = 324; - Big5PFreq[35][58] = 323; - Big5PFreq[48][117] = 322; - Big5PFreq[36][102] = 321; - Big5PFreq[22][122] = 320; - Big5PFreq[35][11] = 319; - Big5PFreq[46][19] = 318; - Big5PFreq[22][49] = 317; - Big5PFreq[48][166] = 316; - Big5PFreq[41][125] = 315; - Big5PFreq[41][1] = 314; - Big5PFreq[35][178] = 313; - Big5PFreq[41][12] = 312; - Big5PFreq[26][167] = 311; - Big5PFreq[42][152] = 310; - Big5PFreq[42][46] = 309; - Big5PFreq[42][151] = 308; - Big5PFreq[20][135] = 307; - Big5PFreq[37][162] = 306; - Big5PFreq[37][50] = 305; - Big5PFreq[22][185] = 304; - Big5PFreq[36][166] = 303; - Big5PFreq[19][40] = 302; - Big5PFreq[22][107] = 301; - Big5PFreq[22][102] = 300; - Big5PFreq[57][162] = 299; - Big5PFreq[22][124] = 298; - Big5PFreq[37][138] = 297; - Big5PFreq[37][25] = 296; - Big5PFreq[0][69] = 295; - Big5PFreq[43][172] = 294; - Big5PFreq[42][167] = 293; - Big5PFreq[35][120] = 292; - Big5PFreq[41][128] = 291; - Big5PFreq[2][88] = 290; - Big5PFreq[20][123] = 289; - Big5PFreq[35][123] = 288; - Big5PFreq[36][28] = 287; - Big5PFreq[42][188] = 286; - Big5PFreq[42][164] = 285; - Big5PFreq[42][4] = 284; - Big5PFreq[43][57] = 283; - Big5PFreq[39][3] = 282; - Big5PFreq[42][3] = 281; - Big5PFreq[57][158] = 280; - Big5PFreq[35][146] = 279; - Big5PFreq[24][54] = 278; - Big5PFreq[13][110] = 277; - Big5PFreq[23][132] = 276; - Big5PFreq[26][102] = 275; - Big5PFreq[55][178] = 274; - Big5PFreq[17][117] = 273; - Big5PFreq[41][161] = 272; - Big5PFreq[38][150] = 271; - Big5PFreq[10][71] = 270; - Big5PFreq[47][60] = 269; - Big5PFreq[16][114] = 268; - Big5PFreq[21][47] = 267; - Big5PFreq[39][101] = 266; - Big5PFreq[18][45] = 265; - Big5PFreq[40][121] = 264; - Big5PFreq[45][41] = 263; - Big5PFreq[22][167] = 262; - Big5PFreq[26][149] = 261; - Big5PFreq[15][189] = 260; - Big5PFreq[41][177] = 259; - Big5PFreq[46][36] = 258; - Big5PFreq[20][40] = 257; - Big5PFreq[41][54] = 256; - Big5PFreq[3][87] = 255; - Big5PFreq[40][16] = 254; - Big5PFreq[42][15] = 253; - Big5PFreq[11][83] = 252; - Big5PFreq[0][94] = 251; - Big5PFreq[122][81] = 250; - Big5PFreq[41][26] = 249; - Big5PFreq[36][34] = 248; - Big5PFreq[44][148] = 247; - Big5PFreq[35][3] = 246; - Big5PFreq[36][114] = 245; - Big5PFreq[42][112] = 244; - Big5PFreq[35][183] = 243; - Big5PFreq[49][73] = 242; - Big5PFreq[39][2] = 241; - Big5PFreq[38][121] = 240; - Big5PFreq[44][114] = 239; - Big5PFreq[49][32] = 238; - Big5PFreq[1][65] = 237; - Big5PFreq[38][25] = 236; - Big5PFreq[39][4] = 235; - Big5PFreq[42][62] = 234; - Big5PFreq[35][40] = 233; - Big5PFreq[24][2] = 232; - Big5PFreq[53][49] = 231; - Big5PFreq[41][133] = 230; - Big5PFreq[43][134] = 229; - Big5PFreq[3][83] = 228; - Big5PFreq[38][158] = 227; - Big5PFreq[24][17] = 226; - Big5PFreq[52][59] = 225; - Big5PFreq[38][41] = 224; - Big5PFreq[37][127] = 223; - Big5PFreq[22][175] = 222; - Big5PFreq[44][30] = 221; - Big5PFreq[47][178] = 220; - Big5PFreq[43][99] = 219; - Big5PFreq[19][4] = 218; - Big5PFreq[37][97] = 217; - Big5PFreq[38][181] = 216; - Big5PFreq[45][103] = 215; - Big5PFreq[1][86] = 214; - Big5PFreq[40][15] = 213; - Big5PFreq[22][136] = 212; - Big5PFreq[75][165] = 211; - Big5PFreq[36][15] = 210; - Big5PFreq[46][80] = 209; - Big5PFreq[59][55] = 208; - Big5PFreq[37][108] = 207; - Big5PFreq[21][109] = 206; - Big5PFreq[24][165] = 205; - Big5PFreq[79][158] = 204; - Big5PFreq[44][139] = 203; - Big5PFreq[36][124] = 202; - Big5PFreq[42][185] = 201; - Big5PFreq[39][186] = 200; - Big5PFreq[22][128] = 199; - Big5PFreq[40][44] = 198; - Big5PFreq[41][105] = 197; - Big5PFreq[1][70] = 196; - Big5PFreq[1][68] = 195; - Big5PFreq[53][22] = 194; - Big5PFreq[36][54] = 193; - Big5PFreq[47][147] = 192; - Big5PFreq[35][36] = 191; - Big5PFreq[35][185] = 190; - Big5PFreq[45][37] = 189; - Big5PFreq[43][163] = 188; - Big5PFreq[56][115] = 187; - Big5PFreq[38][164] = 186; - Big5PFreq[35][141] = 185; - Big5PFreq[42][132] = 184; - Big5PFreq[46][120] = 183; - Big5PFreq[69][142] = 182; - Big5PFreq[38][175] = 181; - Big5PFreq[22][112] = 180; - Big5PFreq[38][142] = 179; - Big5PFreq[40][37] = 178; - Big5PFreq[37][109] = 177; - Big5PFreq[40][144] = 176; - Big5PFreq[44][117] = 175; - Big5PFreq[35][181] = 174; - Big5PFreq[26][105] = 173; - Big5PFreq[16][48] = 172; - Big5PFreq[44][122] = 171; - Big5PFreq[12][86] = 170; - Big5PFreq[84][53] = 169; - Big5PFreq[17][44] = 168; - Big5PFreq[59][54] = 167; - Big5PFreq[36][98] = 166; - Big5PFreq[45][115] = 165; - Big5PFreq[73][9] = 164; - Big5PFreq[44][123] = 163; - Big5PFreq[37][188] = 162; - Big5PFreq[51][117] = 161; - Big5PFreq[15][156] = 160; - Big5PFreq[36][155] = 159; - Big5PFreq[44][25] = 158; - Big5PFreq[38][12] = 157; - Big5PFreq[38][140] = 156; - Big5PFreq[23][4] = 155; - Big5PFreq[45][149] = 154; - Big5PFreq[22][189] = 153; - Big5PFreq[38][147] = 152; - Big5PFreq[27][5] = 151; - Big5PFreq[22][42] = 150; - Big5PFreq[3][68] = 149; - Big5PFreq[39][51] = 148; - Big5PFreq[36][29] = 147; - Big5PFreq[20][108] = 146; - Big5PFreq[50][57] = 145; - Big5PFreq[55][104] = 144; - Big5PFreq[22][46] = 143; - Big5PFreq[18][164] = 142; - Big5PFreq[50][159] = 141; - Big5PFreq[85][131] = 140; - Big5PFreq[26][79] = 139; - Big5PFreq[38][100] = 138; - Big5PFreq[53][112] = 137; - Big5PFreq[20][190] = 136; - Big5PFreq[14][69] = 135; - Big5PFreq[23][11] = 134; - Big5PFreq[40][114] = 133; - Big5PFreq[40][148] = 132; - Big5PFreq[53][130] = 131; - Big5PFreq[36][2] = 130; - Big5PFreq[66][82] = 129; - Big5PFreq[45][166] = 128; - Big5PFreq[4][88] = 127; - Big5PFreq[16][57] = 126; - Big5PFreq[22][116] = 125; - Big5PFreq[36][108] = 124; - Big5PFreq[13][48] = 123; - Big5PFreq[54][12] = 122; - Big5PFreq[40][136] = 121; - Big5PFreq[36][128] = 120; - Big5PFreq[23][6] = 119; - Big5PFreq[38][125] = 118; - Big5PFreq[45][154] = 117; - Big5PFreq[51][127] = 116; - Big5PFreq[44][163] = 115; - Big5PFreq[16][173] = 114; - Big5PFreq[43][49] = 113; - Big5PFreq[20][112] = 112; - Big5PFreq[15][168] = 111; - Big5PFreq[35][129] = 110; - Big5PFreq[20][45] = 109; - Big5PFreq[38][10] = 108; - Big5PFreq[57][171] = 107; - Big5PFreq[44][190] = 106; - Big5PFreq[40][56] = 105; - Big5PFreq[36][156] = 104; - Big5PFreq[3][88] = 103; - Big5PFreq[50][122] = 102; - Big5PFreq[36][7] = 101; - Big5PFreq[39][43] = 100; - Big5PFreq[15][166] = 99; - Big5PFreq[42][136] = 98; - Big5PFreq[22][131] = 97; - Big5PFreq[44][23] = 96; - Big5PFreq[54][147] = 95; - Big5PFreq[41][32] = 94; - Big5PFreq[23][121] = 93; - Big5PFreq[39][108] = 92; - Big5PFreq[2][78] = 91; - Big5PFreq[40][155] = 90; - Big5PFreq[55][51] = 89; - Big5PFreq[19][34] = 88; - Big5PFreq[48][128] = 87; - Big5PFreq[48][159] = 86; - Big5PFreq[20][70] = 85; - Big5PFreq[34][71] = 84; - Big5PFreq[16][31] = 83; - Big5PFreq[42][157] = 82; - Big5PFreq[20][44] = 81; - Big5PFreq[11][92] = 80; - Big5PFreq[44][180] = 79; - Big5PFreq[84][33] = 78; - Big5PFreq[16][116] = 77; - Big5PFreq[61][163] = 76; - Big5PFreq[35][164] = 75; - Big5PFreq[36][42] = 74; - Big5PFreq[13][40] = 73; - Big5PFreq[43][176] = 72; - Big5PFreq[2][66] = 71; - Big5PFreq[20][133] = 70; - Big5PFreq[36][65] = 69; - Big5PFreq[38][33] = 68; - Big5PFreq[12][91] = 67; - Big5PFreq[36][26] = 66; - Big5PFreq[15][174] = 65; - Big5PFreq[77][32] = 64; - Big5PFreq[16][1] = 63; - Big5PFreq[25][86] = 62; - Big5PFreq[17][13] = 61; - Big5PFreq[5][75] = 60; - Big5PFreq[36][52] = 59; - Big5PFreq[51][164] = 58; - Big5PFreq[12][85] = 57; - Big5PFreq[39][168] = 56; - Big5PFreq[43][16] = 55; - Big5PFreq[40][69] = 54; - Big5PFreq[26][108] = 53; - Big5PFreq[51][56] = 52; - Big5PFreq[16][37] = 51; - Big5PFreq[40][29] = 50; - Big5PFreq[46][171] = 49; - Big5PFreq[40][128] = 48; - Big5PFreq[72][114] = 47; - Big5PFreq[21][103] = 46; - Big5PFreq[22][44] = 45; - Big5PFreq[40][115] = 44; - Big5PFreq[43][7] = 43; - Big5PFreq[43][153] = 42; - Big5PFreq[17][20] = 41; - Big5PFreq[16][49] = 40; - Big5PFreq[36][57] = 39; - Big5PFreq[18][38] = 38; - Big5PFreq[45][184] = 37; - Big5PFreq[37][167] = 36; - Big5PFreq[26][106] = 35; - Big5PFreq[61][121] = 34; - Big5PFreq[89][140] = 33; - Big5PFreq[46][61] = 32; - Big5PFreq[39][163] = 31; - Big5PFreq[40][62] = 30; - Big5PFreq[38][165] = 29; - Big5PFreq[47][37] = 28; - Big5PFreq[18][155] = 27; - Big5PFreq[20][33] = 26; - Big5PFreq[29][90] = 25; - Big5PFreq[20][103] = 24; - Big5PFreq[37][51] = 23; - Big5PFreq[57][0] = 22; - Big5PFreq[40][31] = 21; - Big5PFreq[45][32] = 20; - Big5PFreq[59][23] = 19; - Big5PFreq[18][47] = 18; - Big5PFreq[45][134] = 17; - Big5PFreq[37][59] = 16; - Big5PFreq[21][128] = 15; - Big5PFreq[36][106] = 14; - Big5PFreq[31][39] = 13; - Big5PFreq[40][182] = 12; - Big5PFreq[52][155] = 11; - Big5PFreq[42][166] = 10; - Big5PFreq[35][27] = 9; - Big5PFreq[38][3] = 8; - Big5PFreq[13][44] = 7; - Big5PFreq[58][157] = 6; - Big5PFreq[47][51] = 5; - Big5PFreq[41][37] = 4; - Big5PFreq[41][172] = 3; - Big5PFreq[51][165] = 2; - Big5PFreq[15][161] = 1; - Big5PFreq[24][181] = 0; - EUC_TWFreq[48][49] = 599; - EUC_TWFreq[35][65] = 598; - EUC_TWFreq[41][27] = 597; - EUC_TWFreq[35][0] = 596; - EUC_TWFreq[39][19] = 595; - EUC_TWFreq[35][42] = 594; - EUC_TWFreq[38][66] = 593; - EUC_TWFreq[35][8] = 592; - EUC_TWFreq[35][6] = 591; - EUC_TWFreq[35][66] = 590; - EUC_TWFreq[43][14] = 589; - EUC_TWFreq[69][80] = 588; - EUC_TWFreq[50][48] = 587; - EUC_TWFreq[36][71] = 586; - EUC_TWFreq[37][10] = 585; - EUC_TWFreq[60][52] = 584; - EUC_TWFreq[51][21] = 583; - EUC_TWFreq[40][2] = 582; - EUC_TWFreq[67][35] = 581; - EUC_TWFreq[38][78] = 580; - EUC_TWFreq[49][18] = 579; - EUC_TWFreq[35][23] = 578; - EUC_TWFreq[42][83] = 577; - EUC_TWFreq[79][47] = 576; - EUC_TWFreq[61][82] = 575; - EUC_TWFreq[38][7] = 574; - EUC_TWFreq[35][29] = 573; - EUC_TWFreq[37][77] = 572; - EUC_TWFreq[54][67] = 571; - EUC_TWFreq[38][80] = 570; - EUC_TWFreq[52][74] = 569; - EUC_TWFreq[36][37] = 568; - EUC_TWFreq[74][8] = 567; - EUC_TWFreq[41][83] = 566; - EUC_TWFreq[36][75] = 565; - EUC_TWFreq[49][63] = 564; - EUC_TWFreq[42][58] = 563; - EUC_TWFreq[56][33] = 562; - EUC_TWFreq[37][76] = 561; - EUC_TWFreq[62][39] = 560; - EUC_TWFreq[35][21] = 559; - EUC_TWFreq[70][19] = 558; - EUC_TWFreq[77][88] = 557; - EUC_TWFreq[51][14] = 556; - EUC_TWFreq[36][17] = 555; - EUC_TWFreq[44][51] = 554; - EUC_TWFreq[38][72] = 553; - EUC_TWFreq[74][90] = 552; - EUC_TWFreq[35][48] = 551; - EUC_TWFreq[35][69] = 550; - EUC_TWFreq[66][86] = 549; - EUC_TWFreq[57][20] = 548; - EUC_TWFreq[35][53] = 547; - EUC_TWFreq[36][87] = 546; - EUC_TWFreq[84][67] = 545; - EUC_TWFreq[70][56] = 544; - EUC_TWFreq[71][54] = 543; - EUC_TWFreq[60][70] = 542; - EUC_TWFreq[80][1] = 541; - EUC_TWFreq[39][59] = 540; - EUC_TWFreq[39][51] = 539; - EUC_TWFreq[35][44] = 538; - EUC_TWFreq[48][4] = 537; - EUC_TWFreq[55][24] = 536; - EUC_TWFreq[52][4] = 535; - EUC_TWFreq[54][26] = 534; - EUC_TWFreq[36][31] = 533; - EUC_TWFreq[37][22] = 532; - EUC_TWFreq[37][9] = 531; - EUC_TWFreq[46][0] = 530; - EUC_TWFreq[56][46] = 529; - EUC_TWFreq[47][93] = 528; - EUC_TWFreq[37][25] = 527; - EUC_TWFreq[39][8] = 526; - EUC_TWFreq[46][73] = 525; - EUC_TWFreq[38][48] = 524; - EUC_TWFreq[39][83] = 523; - EUC_TWFreq[60][92] = 522; - EUC_TWFreq[70][11] = 521; - EUC_TWFreq[63][84] = 520; - EUC_TWFreq[38][65] = 519; - EUC_TWFreq[45][45] = 518; - EUC_TWFreq[63][49] = 517; - EUC_TWFreq[63][50] = 516; - EUC_TWFreq[39][93] = 515; - EUC_TWFreq[68][20] = 514; - EUC_TWFreq[44][84] = 513; - EUC_TWFreq[66][34] = 512; - EUC_TWFreq[37][58] = 511; - EUC_TWFreq[39][0] = 510; - EUC_TWFreq[59][1] = 509; - EUC_TWFreq[47][8] = 508; - EUC_TWFreq[61][17] = 507; - EUC_TWFreq[53][87] = 506; - EUC_TWFreq[67][26] = 505; - EUC_TWFreq[43][46] = 504; - EUC_TWFreq[38][61] = 503; - EUC_TWFreq[45][9] = 502; - EUC_TWFreq[66][83] = 501; - EUC_TWFreq[43][88] = 500; - EUC_TWFreq[85][20] = 499; - EUC_TWFreq[57][36] = 498; - EUC_TWFreq[43][6] = 497; - EUC_TWFreq[86][77] = 496; - EUC_TWFreq[42][70] = 495; - EUC_TWFreq[49][78] = 494; - EUC_TWFreq[36][40] = 493; - EUC_TWFreq[42][71] = 492; - EUC_TWFreq[58][49] = 491; - EUC_TWFreq[35][20] = 490; - EUC_TWFreq[76][20] = 489; - EUC_TWFreq[39][25] = 488; - EUC_TWFreq[40][34] = 487; - EUC_TWFreq[39][76] = 486; - EUC_TWFreq[40][1] = 485; - EUC_TWFreq[59][0] = 484; - EUC_TWFreq[39][70] = 483; - EUC_TWFreq[46][14] = 482; - EUC_TWFreq[68][77] = 481; - EUC_TWFreq[38][55] = 480; - EUC_TWFreq[35][78] = 479; - EUC_TWFreq[84][44] = 478; - EUC_TWFreq[36][41] = 477; - EUC_TWFreq[37][62] = 476; - EUC_TWFreq[65][67] = 475; - EUC_TWFreq[69][66] = 474; - EUC_TWFreq[73][55] = 473; - EUC_TWFreq[71][49] = 472; - EUC_TWFreq[66][87] = 471; - EUC_TWFreq[38][33] = 470; - EUC_TWFreq[64][61] = 469; - EUC_TWFreq[35][7] = 468; - EUC_TWFreq[47][49] = 467; - EUC_TWFreq[56][14] = 466; - EUC_TWFreq[36][49] = 465; - EUC_TWFreq[50][81] = 464; - EUC_TWFreq[55][76] = 463; - EUC_TWFreq[35][19] = 462; - EUC_TWFreq[44][47] = 461; - EUC_TWFreq[35][15] = 460; - EUC_TWFreq[82][59] = 459; - EUC_TWFreq[35][43] = 458; - EUC_TWFreq[73][0] = 457; - EUC_TWFreq[57][83] = 456; - EUC_TWFreq[42][46] = 455; - EUC_TWFreq[36][0] = 454; - EUC_TWFreq[70][88] = 453; - EUC_TWFreq[42][22] = 452; - EUC_TWFreq[46][58] = 451; - EUC_TWFreq[36][34] = 450; - EUC_TWFreq[39][24] = 449; - EUC_TWFreq[35][55] = 448; - EUC_TWFreq[44][91] = 447; - EUC_TWFreq[37][51] = 446; - EUC_TWFreq[36][19] = 445; - EUC_TWFreq[69][90] = 444; - EUC_TWFreq[55][35] = 443; - EUC_TWFreq[35][54] = 442; - EUC_TWFreq[49][61] = 441; - EUC_TWFreq[36][67] = 440; - EUC_TWFreq[88][34] = 439; - EUC_TWFreq[35][17] = 438; - EUC_TWFreq[65][69] = 437; - EUC_TWFreq[74][89] = 436; - EUC_TWFreq[37][31] = 435; - EUC_TWFreq[43][48] = 434; - EUC_TWFreq[89][27] = 433; - EUC_TWFreq[42][79] = 432; - EUC_TWFreq[69][57] = 431; - EUC_TWFreq[36][13] = 430; - EUC_TWFreq[35][62] = 429; - EUC_TWFreq[65][47] = 428; - EUC_TWFreq[56][8] = 427; - EUC_TWFreq[38][79] = 426; - EUC_TWFreq[37][64] = 425; - EUC_TWFreq[64][64] = 424; - EUC_TWFreq[38][53] = 423; - EUC_TWFreq[38][31] = 422; - EUC_TWFreq[56][81] = 421; - EUC_TWFreq[36][22] = 420; - EUC_TWFreq[43][4] = 419; - EUC_TWFreq[36][90] = 418; - EUC_TWFreq[38][62] = 417; - EUC_TWFreq[66][85] = 416; - EUC_TWFreq[39][1] = 415; - EUC_TWFreq[59][40] = 414; - EUC_TWFreq[58][93] = 413; - EUC_TWFreq[44][43] = 412; - EUC_TWFreq[39][49] = 411; - EUC_TWFreq[64][2] = 410; - EUC_TWFreq[41][35] = 409; - EUC_TWFreq[60][22] = 408; - EUC_TWFreq[35][91] = 407; - EUC_TWFreq[78][1] = 406; - EUC_TWFreq[36][14] = 405; - EUC_TWFreq[82][29] = 404; - EUC_TWFreq[52][86] = 403; - EUC_TWFreq[40][16] = 402; - EUC_TWFreq[91][52] = 401; - EUC_TWFreq[50][75] = 400; - EUC_TWFreq[64][30] = 399; - EUC_TWFreq[90][78] = 398; - EUC_TWFreq[36][52] = 397; - EUC_TWFreq[55][87] = 396; - EUC_TWFreq[57][5] = 395; - EUC_TWFreq[57][31] = 394; - EUC_TWFreq[42][35] = 393; - EUC_TWFreq[69][50] = 392; - EUC_TWFreq[45][8] = 391; - EUC_TWFreq[50][87] = 390; - EUC_TWFreq[69][55] = 389; - EUC_TWFreq[92][3] = 388; - EUC_TWFreq[36][43] = 387; - EUC_TWFreq[64][10] = 386; - EUC_TWFreq[56][25] = 385; - EUC_TWFreq[60][68] = 384; - EUC_TWFreq[51][46] = 383; - EUC_TWFreq[50][0] = 382; - EUC_TWFreq[38][30] = 381; - EUC_TWFreq[50][85] = 380; - EUC_TWFreq[60][54] = 379; - EUC_TWFreq[73][6] = 378; - EUC_TWFreq[73][28] = 377; - EUC_TWFreq[56][19] = 376; - EUC_TWFreq[62][69] = 375; - EUC_TWFreq[81][66] = 374; - EUC_TWFreq[40][32] = 373; - EUC_TWFreq[76][31] = 372; - EUC_TWFreq[35][10] = 371; - EUC_TWFreq[41][37] = 370; - EUC_TWFreq[52][82] = 369; - EUC_TWFreq[91][72] = 368; - EUC_TWFreq[37][29] = 367; - EUC_TWFreq[56][30] = 366; - EUC_TWFreq[37][80] = 365; - EUC_TWFreq[81][56] = 364; - EUC_TWFreq[70][3] = 363; - EUC_TWFreq[76][15] = 362; - EUC_TWFreq[46][47] = 361; - EUC_TWFreq[35][88] = 360; - EUC_TWFreq[61][58] = 359; - EUC_TWFreq[37][37] = 358; - EUC_TWFreq[57][22] = 357; - EUC_TWFreq[41][23] = 356; - EUC_TWFreq[90][66] = 355; - EUC_TWFreq[39][60] = 354; - EUC_TWFreq[38][0] = 353; - EUC_TWFreq[37][87] = 352; - EUC_TWFreq[46][2] = 351; - EUC_TWFreq[38][56] = 350; - EUC_TWFreq[58][11] = 349; - EUC_TWFreq[48][10] = 348; - EUC_TWFreq[74][4] = 347; - EUC_TWFreq[40][42] = 346; - EUC_TWFreq[41][52] = 345; - EUC_TWFreq[61][92] = 344; - EUC_TWFreq[39][50] = 343; - EUC_TWFreq[47][88] = 342; - EUC_TWFreq[88][36] = 341; - EUC_TWFreq[45][73] = 340; - EUC_TWFreq[82][3] = 339; - EUC_TWFreq[61][36] = 338; - EUC_TWFreq[60][33] = 337; - EUC_TWFreq[38][27] = 336; - EUC_TWFreq[35][83] = 335; - EUC_TWFreq[65][24] = 334; - EUC_TWFreq[73][10] = 333; - EUC_TWFreq[41][13] = 332; - EUC_TWFreq[50][27] = 331; - EUC_TWFreq[59][50] = 330; - EUC_TWFreq[42][45] = 329; - EUC_TWFreq[55][19] = 328; - EUC_TWFreq[36][77] = 327; - EUC_TWFreq[69][31] = 326; - EUC_TWFreq[60][7] = 325; - EUC_TWFreq[40][88] = 324; - EUC_TWFreq[57][56] = 323; - EUC_TWFreq[50][50] = 322; - EUC_TWFreq[42][37] = 321; - EUC_TWFreq[38][82] = 320; - EUC_TWFreq[52][25] = 319; - EUC_TWFreq[42][67] = 318; - EUC_TWFreq[48][40] = 317; - EUC_TWFreq[45][81] = 316; - EUC_TWFreq[57][14] = 315; - EUC_TWFreq[42][13] = 314; - EUC_TWFreq[78][0] = 313; - EUC_TWFreq[35][51] = 312; - EUC_TWFreq[41][67] = 311; - EUC_TWFreq[64][23] = 310; - EUC_TWFreq[36][65] = 309; - EUC_TWFreq[48][50] = 308; - EUC_TWFreq[46][69] = 307; - EUC_TWFreq[47][89] = 306; - EUC_TWFreq[41][48] = 305; - EUC_TWFreq[60][56] = 304; - EUC_TWFreq[44][82] = 303; - EUC_TWFreq[47][35] = 302; - EUC_TWFreq[49][3] = 301; - EUC_TWFreq[49][69] = 300; - EUC_TWFreq[45][93] = 299; - EUC_TWFreq[60][34] = 298; - EUC_TWFreq[60][82] = 297; - EUC_TWFreq[61][61] = 296; - EUC_TWFreq[86][42] = 295; - EUC_TWFreq[89][60] = 294; - EUC_TWFreq[48][31] = 293; - EUC_TWFreq[35][75] = 292; - EUC_TWFreq[91][39] = 291; - EUC_TWFreq[53][19] = 290; - EUC_TWFreq[39][72] = 289; - EUC_TWFreq[69][59] = 288; - EUC_TWFreq[41][7] = 287; - EUC_TWFreq[54][13] = 286; - EUC_TWFreq[43][28] = 285; - EUC_TWFreq[36][6] = 284; - EUC_TWFreq[45][75] = 283; - EUC_TWFreq[36][61] = 282; - EUC_TWFreq[38][21] = 281; - EUC_TWFreq[45][14] = 280; - EUC_TWFreq[61][43] = 279; - EUC_TWFreq[36][63] = 278; - EUC_TWFreq[43][30] = 277; - EUC_TWFreq[46][51] = 276; - EUC_TWFreq[68][87] = 275; - EUC_TWFreq[39][26] = 274; - EUC_TWFreq[46][76] = 273; - EUC_TWFreq[36][15] = 272; - EUC_TWFreq[35][40] = 271; - EUC_TWFreq[79][60] = 270; - EUC_TWFreq[46][7] = 269; - EUC_TWFreq[65][72] = 268; - EUC_TWFreq[69][88] = 267; - EUC_TWFreq[47][18] = 266; - EUC_TWFreq[37][0] = 265; - EUC_TWFreq[37][49] = 264; - EUC_TWFreq[67][37] = 263; - EUC_TWFreq[36][91] = 262; - EUC_TWFreq[75][48] = 261; - EUC_TWFreq[75][63] = 260; - EUC_TWFreq[83][87] = 259; - EUC_TWFreq[37][44] = 258; - EUC_TWFreq[73][54] = 257; - EUC_TWFreq[51][61] = 256; - EUC_TWFreq[46][57] = 255; - EUC_TWFreq[55][21] = 254; - EUC_TWFreq[39][66] = 253; - EUC_TWFreq[47][11] = 252; - EUC_TWFreq[52][8] = 251; - EUC_TWFreq[82][81] = 250; - EUC_TWFreq[36][57] = 249; - EUC_TWFreq[38][54] = 248; - EUC_TWFreq[43][81] = 247; - EUC_TWFreq[37][42] = 246; - EUC_TWFreq[40][18] = 245; - EUC_TWFreq[80][90] = 244; - EUC_TWFreq[37][84] = 243; - EUC_TWFreq[57][15] = 242; - EUC_TWFreq[38][87] = 241; - EUC_TWFreq[37][32] = 240; - EUC_TWFreq[53][53] = 239; - EUC_TWFreq[89][29] = 238; - EUC_TWFreq[81][53] = 237; - EUC_TWFreq[75][3] = 236; - EUC_TWFreq[83][73] = 235; - EUC_TWFreq[66][13] = 234; - EUC_TWFreq[48][7] = 233; - EUC_TWFreq[46][35] = 232; - EUC_TWFreq[35][86] = 231; - EUC_TWFreq[37][20] = 230; - EUC_TWFreq[46][80] = 229; - EUC_TWFreq[38][24] = 228; - EUC_TWFreq[41][68] = 227; - EUC_TWFreq[42][21] = 226; - EUC_TWFreq[43][32] = 225; - EUC_TWFreq[38][20] = 224; - EUC_TWFreq[37][59] = 223; - EUC_TWFreq[41][77] = 222; - EUC_TWFreq[59][57] = 221; - EUC_TWFreq[68][59] = 220; - EUC_TWFreq[39][43] = 219; - EUC_TWFreq[54][39] = 218; - EUC_TWFreq[48][28] = 217; - EUC_TWFreq[54][28] = 216; - EUC_TWFreq[41][44] = 215; - EUC_TWFreq[51][64] = 214; - EUC_TWFreq[47][72] = 213; - EUC_TWFreq[62][67] = 212; - EUC_TWFreq[42][43] = 211; - EUC_TWFreq[61][38] = 210; - EUC_TWFreq[76][25] = 209; - EUC_TWFreq[48][91] = 208; - EUC_TWFreq[36][36] = 207; - EUC_TWFreq[80][32] = 206; - EUC_TWFreq[81][40] = 205; - EUC_TWFreq[37][5] = 204; - EUC_TWFreq[74][69] = 203; - EUC_TWFreq[36][82] = 202; - EUC_TWFreq[46][59] = 201; - - GBKFreq[52][132] = 600; - GBKFreq[73][135] = 599; - GBKFreq[49][123] = 598; - GBKFreq[77][146] = 597; - GBKFreq[81][123] = 596; - GBKFreq[82][144] = 595; - GBKFreq[51][179] = 594; - GBKFreq[83][154] = 593; - GBKFreq[71][139] = 592; - GBKFreq[64][139] = 591; - GBKFreq[85][144] = 590; - GBKFreq[52][125] = 589; - GBKFreq[88][25] = 588; - GBKFreq[81][106] = 587; - GBKFreq[81][148] = 586; - GBKFreq[62][137] = 585; - GBKFreq[94][0] = 584; - GBKFreq[1][64] = 583; - GBKFreq[67][163] = 582; - GBKFreq[20][190] = 581; - GBKFreq[57][131] = 580; - GBKFreq[29][169] = 579; - GBKFreq[72][143] = 578; - GBKFreq[0][173] = 577; - GBKFreq[11][23] = 576; - GBKFreq[61][141] = 575; - GBKFreq[60][123] = 574; - GBKFreq[81][114] = 573; - GBKFreq[82][131] = 572; - GBKFreq[67][156] = 571; - GBKFreq[71][167] = 570; - GBKFreq[20][50] = 569; - GBKFreq[77][132] = 568; - GBKFreq[84][38] = 567; - GBKFreq[26][29] = 566; - GBKFreq[74][187] = 565; - GBKFreq[62][116] = 564; - GBKFreq[67][135] = 563; - GBKFreq[5][86] = 562; - GBKFreq[72][186] = 561; - GBKFreq[75][161] = 560; - GBKFreq[78][130] = 559; - GBKFreq[94][30] = 558; - GBKFreq[84][72] = 557; - GBKFreq[1][67] = 556; - GBKFreq[75][172] = 555; - GBKFreq[74][185] = 554; - GBKFreq[53][160] = 553; - GBKFreq[123][14] = 552; - GBKFreq[79][97] = 551; - GBKFreq[85][110] = 550; - GBKFreq[78][171] = 549; - GBKFreq[52][131] = 548; - GBKFreq[56][100] = 547; - GBKFreq[50][182] = 546; - GBKFreq[94][64] = 545; - GBKFreq[106][74] = 544; - GBKFreq[11][102] = 543; - GBKFreq[53][124] = 542; - GBKFreq[24][3] = 541; - GBKFreq[86][148] = 540; - GBKFreq[53][184] = 539; - GBKFreq[86][147] = 538; - GBKFreq[96][161] = 537; - GBKFreq[82][77] = 536; - GBKFreq[59][146] = 535; - GBKFreq[84][126] = 534; - GBKFreq[79][132] = 533; - GBKFreq[85][123] = 532; - GBKFreq[71][101] = 531; - GBKFreq[85][106] = 530; - GBKFreq[6][184] = 529; - GBKFreq[57][156] = 528; - GBKFreq[75][104] = 527; - GBKFreq[50][137] = 526; - GBKFreq[79][133] = 525; - GBKFreq[76][108] = 524; - GBKFreq[57][142] = 523; - GBKFreq[84][130] = 522; - GBKFreq[52][128] = 521; - GBKFreq[47][44] = 520; - GBKFreq[52][152] = 519; - GBKFreq[54][104] = 518; - GBKFreq[30][47] = 517; - GBKFreq[71][123] = 516; - GBKFreq[52][107] = 515; - GBKFreq[45][84] = 514; - GBKFreq[107][118] = 513; - GBKFreq[5][161] = 512; - GBKFreq[48][126] = 511; - GBKFreq[67][170] = 510; - GBKFreq[43][6] = 509; - GBKFreq[70][112] = 508; - GBKFreq[86][174] = 507; - GBKFreq[84][166] = 506; - GBKFreq[79][130] = 505; - GBKFreq[57][141] = 504; - GBKFreq[81][178] = 503; - GBKFreq[56][187] = 502; - GBKFreq[81][162] = 501; - GBKFreq[53][104] = 500; - GBKFreq[123][35] = 499; - GBKFreq[70][169] = 498; - GBKFreq[69][164] = 497; - GBKFreq[109][61] = 496; - GBKFreq[73][130] = 495; - GBKFreq[62][134] = 494; - GBKFreq[54][125] = 493; - GBKFreq[79][105] = 492; - GBKFreq[70][165] = 491; - GBKFreq[71][189] = 490; - GBKFreq[23][147] = 489; - GBKFreq[51][139] = 488; - GBKFreq[47][137] = 487; - GBKFreq[77][123] = 486; - GBKFreq[86][183] = 485; - GBKFreq[63][173] = 484; - GBKFreq[79][144] = 483; - GBKFreq[84][159] = 482; - GBKFreq[60][91] = 481; - GBKFreq[66][187] = 480; - GBKFreq[73][114] = 479; - GBKFreq[85][56] = 478; - GBKFreq[71][149] = 477; - GBKFreq[84][189] = 476; - GBKFreq[104][31] = 475; - GBKFreq[83][82] = 474; - GBKFreq[68][35] = 473; - GBKFreq[11][77] = 472; - GBKFreq[15][155] = 471; - GBKFreq[83][153] = 470; - GBKFreq[71][1] = 469; - GBKFreq[53][190] = 468; - GBKFreq[50][135] = 467; - GBKFreq[3][147] = 466; - GBKFreq[48][136] = 465; - GBKFreq[66][166] = 464; - GBKFreq[55][159] = 463; - GBKFreq[82][150] = 462; - GBKFreq[58][178] = 461; - GBKFreq[64][102] = 460; - GBKFreq[16][106] = 459; - GBKFreq[68][110] = 458; - GBKFreq[54][14] = 457; - GBKFreq[60][140] = 456; - GBKFreq[91][71] = 455; - GBKFreq[54][150] = 454; - GBKFreq[78][177] = 453; - GBKFreq[78][117] = 452; - GBKFreq[104][12] = 451; - GBKFreq[73][150] = 450; - GBKFreq[51][142] = 449; - GBKFreq[81][145] = 448; - GBKFreq[66][183] = 447; - GBKFreq[51][178] = 446; - GBKFreq[75][107] = 445; - GBKFreq[65][119] = 444; - GBKFreq[69][176] = 443; - GBKFreq[59][122] = 442; - GBKFreq[78][160] = 441; - GBKFreq[85][183] = 440; - GBKFreq[105][16] = 439; - GBKFreq[73][110] = 438; - GBKFreq[104][39] = 437; - GBKFreq[119][16] = 436; - GBKFreq[76][162] = 435; - GBKFreq[67][152] = 434; - GBKFreq[82][24] = 433; - GBKFreq[73][121] = 432; - GBKFreq[83][83] = 431; - GBKFreq[82][145] = 430; - GBKFreq[49][133] = 429; - GBKFreq[94][13] = 428; - GBKFreq[58][139] = 427; - GBKFreq[74][189] = 426; - GBKFreq[66][177] = 425; - GBKFreq[85][184] = 424; - GBKFreq[55][183] = 423; - GBKFreq[71][107] = 422; - GBKFreq[11][98] = 421; - GBKFreq[72][153] = 420; - GBKFreq[2][137] = 419; - GBKFreq[59][147] = 418; - GBKFreq[58][152] = 417; - GBKFreq[55][144] = 416; - GBKFreq[73][125] = 415; - GBKFreq[52][154] = 414; - GBKFreq[70][178] = 413; - GBKFreq[79][148] = 412; - GBKFreq[63][143] = 411; - GBKFreq[50][140] = 410; - GBKFreq[47][145] = 409; - GBKFreq[48][123] = 408; - GBKFreq[56][107] = 407; - GBKFreq[84][83] = 406; - GBKFreq[59][112] = 405; - GBKFreq[124][72] = 404; - GBKFreq[79][99] = 403; - GBKFreq[3][37] = 402; - GBKFreq[114][55] = 401; - GBKFreq[85][152] = 400; - GBKFreq[60][47] = 399; - GBKFreq[65][96] = 398; - GBKFreq[74][110] = 397; - GBKFreq[86][182] = 396; - GBKFreq[50][99] = 395; - GBKFreq[67][186] = 394; - GBKFreq[81][74] = 393; - GBKFreq[80][37] = 392; - GBKFreq[21][60] = 391; - GBKFreq[110][12] = 390; - GBKFreq[60][162] = 389; - GBKFreq[29][115] = 388; - GBKFreq[83][130] = 387; - GBKFreq[52][136] = 386; - GBKFreq[63][114] = 385; - GBKFreq[49][127] = 384; - GBKFreq[83][109] = 383; - GBKFreq[66][128] = 382; - GBKFreq[78][136] = 381; - GBKFreq[81][180] = 380; - GBKFreq[76][104] = 379; - GBKFreq[56][156] = 378; - GBKFreq[61][23] = 377; - GBKFreq[4][30] = 376; - GBKFreq[69][154] = 375; - GBKFreq[100][37] = 374; - GBKFreq[54][177] = 373; - GBKFreq[23][119] = 372; - GBKFreq[71][171] = 371; - GBKFreq[84][146] = 370; - GBKFreq[20][184] = 369; - GBKFreq[86][76] = 368; - GBKFreq[74][132] = 367; - GBKFreq[47][97] = 366; - GBKFreq[82][137] = 365; - GBKFreq[94][56] = 364; - GBKFreq[92][30] = 363; - GBKFreq[19][117] = 362; - GBKFreq[48][173] = 361; - GBKFreq[2][136] = 360; - GBKFreq[7][182] = 359; - GBKFreq[74][188] = 358; - GBKFreq[14][132] = 357; - GBKFreq[62][172] = 356; - GBKFreq[25][39] = 355; - GBKFreq[85][129] = 354; - GBKFreq[64][98] = 353; - GBKFreq[67][127] = 352; - GBKFreq[72][167] = 351; - GBKFreq[57][143] = 350; - GBKFreq[76][187] = 349; - GBKFreq[83][181] = 348; - GBKFreq[84][10] = 347; - GBKFreq[55][166] = 346; - GBKFreq[55][188] = 345; - GBKFreq[13][151] = 344; - GBKFreq[62][124] = 343; - GBKFreq[53][136] = 342; - GBKFreq[106][57] = 341; - GBKFreq[47][166] = 340; - GBKFreq[109][30] = 339; - GBKFreq[78][114] = 338; - GBKFreq[83][19] = 337; - GBKFreq[56][162] = 336; - GBKFreq[60][177] = 335; - GBKFreq[88][9] = 334; - GBKFreq[74][163] = 333; - GBKFreq[52][156] = 332; - GBKFreq[71][180] = 331; - GBKFreq[60][57] = 330; - GBKFreq[72][173] = 329; - GBKFreq[82][91] = 328; - GBKFreq[51][186] = 327; - GBKFreq[75][86] = 326; - GBKFreq[75][78] = 325; - GBKFreq[76][170] = 324; - GBKFreq[60][147] = 323; - GBKFreq[82][75] = 322; - GBKFreq[80][148] = 321; - GBKFreq[86][150] = 320; - GBKFreq[13][95] = 319; - GBKFreq[0][11] = 318; - GBKFreq[84][190] = 317; - GBKFreq[76][166] = 316; - GBKFreq[14][72] = 315; - GBKFreq[67][144] = 314; - GBKFreq[84][44] = 313; - GBKFreq[72][125] = 312; - GBKFreq[66][127] = 311; - GBKFreq[60][25] = 310; - GBKFreq[70][146] = 309; - GBKFreq[79][135] = 308; - GBKFreq[54][135] = 307; - GBKFreq[60][104] = 306; - GBKFreq[55][132] = 305; - GBKFreq[94][2] = 304; - GBKFreq[54][133] = 303; - GBKFreq[56][190] = 302; - GBKFreq[58][174] = 301; - GBKFreq[80][144] = 300; - GBKFreq[85][113] = 299; - - KRFreq[31][43] = 600; - KRFreq[19][56] = 599; - KRFreq[38][46] = 598; - KRFreq[3][3] = 597; - KRFreq[29][77] = 596; - KRFreq[19][33] = 595; - KRFreq[30][0] = 594; - KRFreq[29][89] = 593; - KRFreq[31][26] = 592; - KRFreq[31][38] = 591; - KRFreq[32][85] = 590; - KRFreq[15][0] = 589; - KRFreq[16][54] = 588; - KRFreq[15][76] = 587; - KRFreq[31][25] = 586; - KRFreq[23][13] = 585; - KRFreq[28][34] = 584; - KRFreq[18][9] = 583; - KRFreq[29][37] = 582; - KRFreq[22][45] = 581; - KRFreq[19][46] = 580; - KRFreq[16][65] = 579; - KRFreq[23][5] = 578; - KRFreq[26][70] = 577; - KRFreq[31][53] = 576; - KRFreq[27][12] = 575; - KRFreq[30][67] = 574; - KRFreq[31][57] = 573; - KRFreq[20][20] = 572; - KRFreq[30][31] = 571; - KRFreq[20][72] = 570; - KRFreq[15][51] = 569; - KRFreq[3][8] = 568; - KRFreq[32][53] = 567; - KRFreq[27][85] = 566; - KRFreq[25][23] = 565; - KRFreq[15][44] = 564; - KRFreq[32][3] = 563; - KRFreq[31][68] = 562; - KRFreq[30][24] = 561; - KRFreq[29][49] = 560; - KRFreq[27][49] = 559; - KRFreq[23][23] = 558; - KRFreq[31][91] = 557; - KRFreq[31][46] = 556; - KRFreq[19][74] = 555; - KRFreq[27][27] = 554; - KRFreq[3][17] = 553; - KRFreq[20][38] = 552; - KRFreq[21][82] = 551; - KRFreq[28][25] = 550; - KRFreq[32][5] = 549; - KRFreq[31][23] = 548; - KRFreq[25][45] = 547; - KRFreq[32][87] = 546; - KRFreq[18][26] = 545; - KRFreq[24][10] = 544; - KRFreq[26][82] = 543; - KRFreq[15][89] = 542; - KRFreq[28][36] = 541; - KRFreq[28][31] = 540; - KRFreq[16][23] = 539; - KRFreq[16][77] = 538; - KRFreq[19][84] = 537; - KRFreq[23][72] = 536; - KRFreq[38][48] = 535; - KRFreq[23][2] = 534; - KRFreq[30][20] = 533; - KRFreq[38][47] = 532; - KRFreq[39][12] = 531; - KRFreq[23][21] = 530; - KRFreq[18][17] = 529; - KRFreq[30][87] = 528; - KRFreq[29][62] = 527; - KRFreq[29][87] = 526; - KRFreq[34][53] = 525; - KRFreq[32][29] = 524; - KRFreq[35][0] = 523; - KRFreq[24][43] = 522; - KRFreq[36][44] = 521; - KRFreq[20][30] = 520; - KRFreq[39][86] = 519; - KRFreq[22][14] = 518; - KRFreq[29][39] = 517; - KRFreq[28][38] = 516; - KRFreq[23][79] = 515; - KRFreq[24][56] = 514; - KRFreq[29][63] = 513; - KRFreq[31][45] = 512; - KRFreq[23][26] = 511; - KRFreq[15][87] = 510; - KRFreq[30][74] = 509; - KRFreq[24][69] = 508; - KRFreq[20][4] = 507; - KRFreq[27][50] = 506; - KRFreq[30][75] = 505; - KRFreq[24][13] = 504; - KRFreq[30][8] = 503; - KRFreq[31][6] = 502; - KRFreq[25][80] = 501; - KRFreq[36][8] = 500; - KRFreq[15][18] = 499; - KRFreq[39][23] = 498; - KRFreq[16][24] = 497; - KRFreq[31][89] = 496; - KRFreq[15][71] = 495; - KRFreq[15][57] = 494; - KRFreq[30][11] = 493; - KRFreq[15][36] = 492; - KRFreq[16][60] = 491; - KRFreq[24][45] = 490; - KRFreq[37][35] = 489; - KRFreq[24][87] = 488; - KRFreq[20][45] = 487; - KRFreq[31][90] = 486; - KRFreq[32][21] = 485; - KRFreq[19][70] = 484; - KRFreq[24][15] = 483; - KRFreq[26][92] = 482; - KRFreq[37][13] = 481; - KRFreq[39][2] = 480; - KRFreq[23][70] = 479; - KRFreq[27][25] = 478; - KRFreq[15][69] = 477; - KRFreq[19][61] = 476; - KRFreq[31][58] = 475; - KRFreq[24][57] = 474; - KRFreq[36][74] = 473; - KRFreq[21][6] = 472; - KRFreq[30][44] = 471; - KRFreq[15][91] = 470; - KRFreq[27][16] = 469; - KRFreq[29][42] = 468; - KRFreq[33][86] = 467; - KRFreq[29][41] = 466; - KRFreq[20][68] = 465; - KRFreq[25][47] = 464; - KRFreq[22][0] = 463; - KRFreq[18][14] = 462; - KRFreq[31][28] = 461; - KRFreq[15][2] = 460; - KRFreq[23][76] = 459; - KRFreq[38][32] = 458; - KRFreq[29][82] = 457; - KRFreq[21][86] = 456; - KRFreq[24][62] = 455; - KRFreq[31][64] = 454; - KRFreq[38][26] = 453; - KRFreq[32][86] = 452; - KRFreq[22][32] = 451; - KRFreq[19][59] = 450; - KRFreq[34][18] = 449; - KRFreq[18][54] = 448; - KRFreq[38][63] = 447; - KRFreq[36][23] = 446; - KRFreq[35][35] = 445; - KRFreq[32][62] = 444; - KRFreq[28][35] = 443; - KRFreq[27][13] = 442; - KRFreq[31][59] = 441; - KRFreq[29][29] = 440; - KRFreq[15][64] = 439; - KRFreq[26][84] = 438; - KRFreq[21][90] = 437; - KRFreq[20][24] = 436; - KRFreq[16][18] = 435; - KRFreq[22][23] = 434; - KRFreq[31][14] = 433; - KRFreq[15][1] = 432; - KRFreq[18][63] = 431; - KRFreq[19][10] = 430; - KRFreq[25][49] = 429; - KRFreq[36][57] = 428; - KRFreq[20][22] = 427; - KRFreq[15][15] = 426; - KRFreq[31][51] = 425; - KRFreq[24][60] = 424; - KRFreq[31][70] = 423; - KRFreq[15][7] = 422; - KRFreq[28][40] = 421; - KRFreq[18][41] = 420; - KRFreq[15][38] = 419; - KRFreq[32][0] = 418; - KRFreq[19][51] = 417; - KRFreq[34][62] = 416; - KRFreq[16][27] = 415; - KRFreq[20][70] = 414; - KRFreq[22][33] = 413; - KRFreq[26][73] = 412; - KRFreq[20][79] = 411; - KRFreq[23][6] = 410; - KRFreq[24][85] = 409; - KRFreq[38][51] = 408; - KRFreq[29][88] = 407; - KRFreq[38][55] = 406; - KRFreq[32][32] = 405; - KRFreq[27][18] = 404; - KRFreq[23][87] = 403; - KRFreq[35][6] = 402; - KRFreq[34][27] = 401; - KRFreq[39][35] = 400; - KRFreq[30][88] = 399; - KRFreq[32][92] = 398; - KRFreq[32][49] = 397; - KRFreq[24][61] = 396; - KRFreq[18][74] = 395; - KRFreq[23][77] = 394; - KRFreq[23][50] = 393; - KRFreq[23][32] = 392; - KRFreq[23][36] = 391; - KRFreq[38][38] = 390; - KRFreq[29][86] = 389; - KRFreq[36][15] = 388; - KRFreq[31][50] = 387; - KRFreq[15][86] = 386; - KRFreq[39][13] = 385; - KRFreq[34][26] = 384; - KRFreq[19][34] = 383; - KRFreq[16][3] = 382; - KRFreq[26][93] = 381; - KRFreq[19][67] = 380; - KRFreq[24][72] = 379; - KRFreq[29][17] = 378; - KRFreq[23][24] = 377; - KRFreq[25][19] = 376; - KRFreq[18][65] = 375; - KRFreq[30][78] = 374; - KRFreq[27][52] = 373; - KRFreq[22][18] = 372; - KRFreq[16][38] = 371; - KRFreq[21][26] = 370; - KRFreq[34][20] = 369; - KRFreq[15][42] = 368; - KRFreq[16][71] = 367; - KRFreq[17][17] = 366; - KRFreq[24][71] = 365; - KRFreq[18][84] = 364; - KRFreq[15][40] = 363; - KRFreq[31][62] = 362; - KRFreq[15][8] = 361; - KRFreq[16][69] = 360; - KRFreq[29][79] = 359; - KRFreq[38][91] = 358; - KRFreq[31][92] = 357; - KRFreq[20][77] = 356; - KRFreq[3][16] = 355; - KRFreq[27][87] = 354; - KRFreq[16][25] = 353; - KRFreq[36][33] = 352; - KRFreq[37][76] = 351; - KRFreq[30][12] = 350; - KRFreq[26][75] = 349; - KRFreq[25][14] = 348; - KRFreq[32][26] = 347; - KRFreq[23][22] = 346; - KRFreq[20][90] = 345; - KRFreq[19][8] = 344; - KRFreq[38][41] = 343; - KRFreq[34][2] = 342; - KRFreq[39][4] = 341; - KRFreq[27][89] = 340; - KRFreq[28][41] = 339; - KRFreq[28][44] = 338; - KRFreq[24][92] = 337; - KRFreq[34][65] = 336; - KRFreq[39][14] = 335; - KRFreq[21][38] = 334; - KRFreq[19][31] = 333; - KRFreq[37][39] = 332; - KRFreq[33][41] = 331; - KRFreq[38][4] = 330; - KRFreq[23][80] = 329; - KRFreq[25][24] = 328; - KRFreq[37][17] = 327; - KRFreq[22][16] = 326; - KRFreq[22][46] = 325; - KRFreq[33][91] = 324; - KRFreq[24][89] = 323; - KRFreq[30][52] = 322; - KRFreq[29][38] = 321; - KRFreq[38][85] = 320; - KRFreq[15][12] = 319; - KRFreq[27][58] = 318; - KRFreq[29][52] = 317; - KRFreq[37][38] = 316; - KRFreq[34][41] = 315; - KRFreq[31][65] = 314; - KRFreq[29][53] = 313; - KRFreq[22][47] = 312; - KRFreq[22][19] = 311; - KRFreq[26][0] = 310; - KRFreq[37][86] = 309; - KRFreq[35][4] = 308; - KRFreq[36][54] = 307; - KRFreq[20][76] = 306; - KRFreq[30][9] = 305; - KRFreq[30][33] = 304; - KRFreq[23][17] = 303; - KRFreq[23][33] = 302; - KRFreq[38][52] = 301; - KRFreq[15][19] = 300; - KRFreq[28][45] = 299; - KRFreq[29][78] = 298; - KRFreq[23][15] = 297; - KRFreq[33][5] = 296; - KRFreq[17][40] = 295; - KRFreq[30][83] = 294; - KRFreq[18][1] = 293; - KRFreq[30][81] = 292; - KRFreq[19][40] = 291; - KRFreq[24][47] = 290; - KRFreq[17][56] = 289; - KRFreq[39][80] = 288; - KRFreq[30][46] = 287; - KRFreq[16][61] = 286; - KRFreq[26][78] = 285; - KRFreq[26][57] = 284; - KRFreq[20][46] = 283; - KRFreq[25][15] = 282; - KRFreq[25][91] = 281; - KRFreq[21][83] = 280; - KRFreq[30][77] = 279; - KRFreq[35][30] = 278; - KRFreq[30][34] = 277; - KRFreq[20][69] = 276; - KRFreq[35][10] = 275; - KRFreq[29][70] = 274; - KRFreq[22][50] = 273; - KRFreq[18][0] = 272; - KRFreq[22][64] = 271; - KRFreq[38][65] = 270; - KRFreq[22][70] = 269; - KRFreq[24][58] = 268; - KRFreq[19][66] = 267; - KRFreq[30][59] = 266; - KRFreq[37][14] = 265; - KRFreq[16][56] = 264; - KRFreq[29][85] = 263; - KRFreq[31][15] = 262; - KRFreq[36][84] = 261; - KRFreq[39][15] = 260; - KRFreq[39][90] = 259; - KRFreq[18][12] = 258; - KRFreq[21][93] = 257; - KRFreq[24][66] = 256; - KRFreq[27][90] = 255; - KRFreq[25][90] = 254; - KRFreq[22][24] = 253; - KRFreq[36][67] = 252; - KRFreq[33][90] = 251; - KRFreq[15][60] = 250; - KRFreq[23][85] = 249; - KRFreq[34][1] = 248; - KRFreq[39][37] = 247; - KRFreq[21][18] = 246; - KRFreq[34][4] = 245; - KRFreq[28][33] = 244; - KRFreq[15][13] = 243; - KRFreq[32][22] = 242; - KRFreq[30][76] = 241; - KRFreq[20][21] = 240; - KRFreq[38][66] = 239; - KRFreq[32][55] = 238; - KRFreq[32][89] = 237; - KRFreq[25][26] = 236; - KRFreq[16][80] = 235; - KRFreq[15][43] = 234; - KRFreq[38][54] = 233; - KRFreq[39][68] = 232; - KRFreq[22][88] = 231; - KRFreq[21][84] = 230; - KRFreq[21][17] = 229; - KRFreq[20][28] = 228; - KRFreq[32][1] = 227; - KRFreq[33][87] = 226; - KRFreq[38][71] = 225; - KRFreq[37][47] = 224; - KRFreq[18][77] = 223; - KRFreq[37][58] = 222; - KRFreq[34][74] = 221; - KRFreq[32][54] = 220; - KRFreq[27][33] = 219; - KRFreq[32][93] = 218; - KRFreq[23][51] = 217; - KRFreq[20][57] = 216; - KRFreq[22][37] = 215; - KRFreq[39][10] = 214; - KRFreq[39][17] = 213; - KRFreq[33][4] = 212; - KRFreq[32][84] = 211; - KRFreq[34][3] = 210; - KRFreq[28][27] = 209; - KRFreq[15][79] = 208; - KRFreq[34][21] = 207; - KRFreq[34][69] = 206; - KRFreq[21][62] = 205; - KRFreq[36][24] = 204; - KRFreq[16][89] = 203; - KRFreq[18][48] = 202; - KRFreq[38][15] = 201; - KRFreq[36][58] = 200; - KRFreq[21][56] = 199; - KRFreq[34][48] = 198; - KRFreq[21][15] = 197; - KRFreq[39][3] = 196; - KRFreq[16][44] = 195; - KRFreq[18][79] = 194; - KRFreq[25][13] = 193; - KRFreq[29][47] = 192; - KRFreq[38][88] = 191; - KRFreq[20][71] = 190; - KRFreq[16][58] = 189; - KRFreq[35][57] = 188; - KRFreq[29][30] = 187; - KRFreq[29][23] = 186; - KRFreq[34][93] = 185; - KRFreq[30][85] = 184; - KRFreq[15][80] = 183; - KRFreq[32][78] = 182; - KRFreq[37][82] = 181; - KRFreq[22][40] = 180; - KRFreq[21][69] = 179; - KRFreq[26][85] = 178; - KRFreq[31][31] = 177; - KRFreq[28][64] = 176; - KRFreq[38][13] = 175; - KRFreq[25][2] = 174; - KRFreq[22][34] = 173; - KRFreq[28][28] = 172; - KRFreq[24][91] = 171; - KRFreq[33][74] = 170; - KRFreq[29][40] = 169; - KRFreq[15][77] = 168; - KRFreq[32][80] = 167; - KRFreq[30][41] = 166; - KRFreq[23][30] = 165; - KRFreq[24][63] = 164; - KRFreq[30][53] = 163; - KRFreq[39][70] = 162; - KRFreq[23][61] = 161; - KRFreq[37][27] = 160; - KRFreq[16][55] = 159; - KRFreq[22][74] = 158; - KRFreq[26][50] = 157; - KRFreq[16][10] = 156; - KRFreq[34][63] = 155; - KRFreq[35][14] = 154; - KRFreq[17][7] = 153; - KRFreq[15][59] = 152; - KRFreq[27][23] = 151; - KRFreq[18][70] = 150; - KRFreq[32][56] = 149; - KRFreq[37][87] = 148; - KRFreq[17][61] = 147; - KRFreq[18][83] = 146; - KRFreq[23][86] = 145; - KRFreq[17][31] = 144; - KRFreq[23][83] = 143; - KRFreq[35][2] = 142; - KRFreq[18][64] = 141; - KRFreq[27][43] = 140; - KRFreq[32][42] = 139; - KRFreq[25][76] = 138; - KRFreq[19][85] = 137; - KRFreq[37][81] = 136; - KRFreq[38][83] = 135; - KRFreq[35][7] = 134; - KRFreq[16][51] = 133; - KRFreq[27][22] = 132; - KRFreq[16][76] = 131; - KRFreq[22][4] = 130; - KRFreq[38][84] = 129; - KRFreq[17][83] = 128; - KRFreq[24][46] = 127; - KRFreq[33][15] = 126; - KRFreq[20][48] = 125; - KRFreq[17][30] = 124; - KRFreq[30][93] = 123; - KRFreq[28][11] = 122; - KRFreq[28][30] = 121; - KRFreq[15][62] = 120; - KRFreq[17][87] = 119; - KRFreq[32][81] = 118; - KRFreq[23][37] = 117; - KRFreq[30][22] = 116; - KRFreq[32][66] = 115; - KRFreq[33][78] = 114; - KRFreq[21][4] = 113; - KRFreq[31][17] = 112; - KRFreq[39][61] = 111; - KRFreq[18][76] = 110; - KRFreq[15][85] = 109; - KRFreq[31][47] = 108; - KRFreq[19][57] = 107; - KRFreq[23][55] = 106; - KRFreq[27][29] = 105; - KRFreq[29][46] = 104; - KRFreq[33][0] = 103; - KRFreq[16][83] = 102; - KRFreq[39][78] = 101; - KRFreq[32][77] = 100; - KRFreq[36][25] = 99; - KRFreq[34][19] = 98; - KRFreq[38][49] = 97; - KRFreq[19][25] = 96; - KRFreq[23][53] = 95; - KRFreq[28][43] = 94; - KRFreq[31][44] = 93; - KRFreq[36][34] = 92; - KRFreq[16][34] = 91; - KRFreq[35][1] = 90; - KRFreq[19][87] = 89; - KRFreq[18][53] = 88; - KRFreq[29][54] = 87; - KRFreq[22][41] = 86; - KRFreq[38][18] = 85; - KRFreq[22][2] = 84; - KRFreq[20][3] = 83; - KRFreq[39][69] = 82; - KRFreq[30][29] = 81; - KRFreq[28][19] = 80; - KRFreq[29][90] = 79; - KRFreq[17][86] = 78; - KRFreq[15][9] = 77; - KRFreq[39][73] = 76; - KRFreq[15][37] = 75; - KRFreq[35][40] = 74; - KRFreq[33][77] = 73; - KRFreq[27][86] = 72; - KRFreq[36][79] = 71; - KRFreq[23][18] = 70; - KRFreq[34][87] = 69; - KRFreq[39][24] = 68; - KRFreq[26][8] = 67; - KRFreq[33][48] = 66; - KRFreq[39][30] = 65; - KRFreq[33][28] = 64; - KRFreq[16][67] = 63; - KRFreq[31][78] = 62; - KRFreq[32][23] = 61; - KRFreq[24][55] = 60; - KRFreq[30][68] = 59; - KRFreq[18][60] = 58; - KRFreq[15][17] = 57; - KRFreq[23][34] = 56; - KRFreq[20][49] = 55; - KRFreq[15][78] = 54; - KRFreq[24][14] = 53; - KRFreq[19][41] = 52; - KRFreq[31][55] = 51; - KRFreq[21][39] = 50; - KRFreq[35][9] = 49; - KRFreq[30][15] = 48; - KRFreq[20][52] = 47; - KRFreq[35][71] = 46; - KRFreq[20][7] = 45; - KRFreq[29][72] = 44; - KRFreq[37][77] = 43; - KRFreq[22][35] = 42; - KRFreq[20][61] = 41; - KRFreq[31][60] = 40; - KRFreq[20][93] = 39; - KRFreq[27][92] = 38; - KRFreq[28][16] = 37; - KRFreq[36][26] = 36; - KRFreq[18][89] = 35; - KRFreq[21][63] = 34; - KRFreq[22][52] = 33; - KRFreq[24][65] = 32; - KRFreq[31][8] = 31; - KRFreq[31][49] = 30; - KRFreq[33][30] = 29; - KRFreq[37][15] = 28; - KRFreq[18][18] = 27; - KRFreq[25][50] = 26; - KRFreq[29][20] = 25; - KRFreq[35][48] = 24; - KRFreq[38][75] = 23; - KRFreq[26][83] = 22; - KRFreq[21][87] = 21; - KRFreq[27][71] = 20; - KRFreq[32][91] = 19; - KRFreq[25][73] = 18; - KRFreq[16][84] = 17; - KRFreq[25][31] = 16; - KRFreq[17][90] = 15; - KRFreq[18][40] = 14; - KRFreq[17][77] = 13; - KRFreq[17][35] = 12; - KRFreq[23][52] = 11; - KRFreq[23][35] = 10; - KRFreq[16][5] = 9; - KRFreq[23][58] = 8; - KRFreq[19][60] = 7; - KRFreq[30][32] = 6; - KRFreq[38][34] = 5; - KRFreq[23][4] = 4; - KRFreq[23][1] = 3; - KRFreq[27][57] = 2; - KRFreq[39][38] = 1; - KRFreq[32][33] = 0; - JPFreq[3][74] = 600; - JPFreq[3][45] = 599; - JPFreq[3][3] = 598; - JPFreq[3][24] = 597; - JPFreq[3][30] = 596; - JPFreq[3][42] = 595; - JPFreq[3][46] = 594; - JPFreq[3][39] = 593; - JPFreq[3][11] = 592; - JPFreq[3][37] = 591; - JPFreq[3][38] = 590; - JPFreq[3][31] = 589; - JPFreq[3][41] = 588; - JPFreq[3][5] = 587; - JPFreq[3][10] = 586; - JPFreq[3][75] = 585; - JPFreq[3][65] = 584; - JPFreq[3][72] = 583; - JPFreq[37][91] = 582; - JPFreq[0][27] = 581; - JPFreq[3][18] = 580; - JPFreq[3][22] = 579; - JPFreq[3][61] = 578; - JPFreq[3][14] = 577; - JPFreq[24][80] = 576; - JPFreq[4][82] = 575; - JPFreq[17][80] = 574; - JPFreq[30][44] = 573; - JPFreq[3][73] = 572; - JPFreq[3][64] = 571; - JPFreq[38][14] = 570; - JPFreq[33][70] = 569; - JPFreq[3][1] = 568; - JPFreq[3][16] = 567; - JPFreq[3][35] = 566; - JPFreq[3][40] = 565; - JPFreq[4][74] = 564; - JPFreq[4][24] = 563; - JPFreq[42][59] = 562; - JPFreq[3][7] = 561; - JPFreq[3][71] = 560; - JPFreq[3][12] = 559; - JPFreq[15][75] = 558; - JPFreq[3][20] = 557; - JPFreq[4][39] = 556; - JPFreq[34][69] = 555; - JPFreq[3][28] = 554; - JPFreq[35][24] = 553; - JPFreq[3][82] = 552; - JPFreq[28][47] = 551; - JPFreq[3][67] = 550; - JPFreq[37][16] = 549; - JPFreq[26][93] = 548; - JPFreq[4][1] = 547; - JPFreq[26][85] = 546; - JPFreq[31][14] = 545; - JPFreq[4][3] = 544; - JPFreq[4][72] = 543; - JPFreq[24][51] = 542; - JPFreq[27][51] = 541; - JPFreq[27][49] = 540; - JPFreq[22][77] = 539; - JPFreq[27][10] = 538; - JPFreq[29][68] = 537; - JPFreq[20][35] = 536; - JPFreq[41][11] = 535; - JPFreq[24][70] = 534; - JPFreq[36][61] = 533; - JPFreq[31][23] = 532; - JPFreq[43][16] = 531; - JPFreq[23][68] = 530; - JPFreq[32][15] = 529; - JPFreq[3][32] = 528; - JPFreq[19][53] = 527; - JPFreq[40][83] = 526; - JPFreq[4][14] = 525; - JPFreq[36][9] = 524; - JPFreq[4][73] = 523; - JPFreq[23][10] = 522; - JPFreq[3][63] = 521; - JPFreq[39][14] = 520; - JPFreq[3][78] = 519; - JPFreq[33][47] = 518; - JPFreq[21][39] = 517; - JPFreq[34][46] = 516; - JPFreq[36][75] = 515; - JPFreq[41][92] = 514; - JPFreq[37][93] = 513; - JPFreq[4][34] = 512; - JPFreq[15][86] = 511; - JPFreq[46][1] = 510; - JPFreq[37][65] = 509; - JPFreq[3][62] = 508; - JPFreq[32][73] = 507; - JPFreq[21][65] = 506; - JPFreq[29][75] = 505; - JPFreq[26][51] = 504; - JPFreq[3][34] = 503; - JPFreq[4][10] = 502; - JPFreq[30][22] = 501; - JPFreq[35][73] = 500; - JPFreq[17][82] = 499; - JPFreq[45][8] = 498; - JPFreq[27][73] = 497; - JPFreq[18][55] = 496; - JPFreq[25][2] = 495; - JPFreq[3][26] = 494; - JPFreq[45][46] = 493; - JPFreq[4][22] = 492; - JPFreq[4][40] = 491; - JPFreq[18][10] = 490; - JPFreq[32][9] = 489; - JPFreq[26][49] = 488; - JPFreq[3][47] = 487; - JPFreq[24][65] = 486; - JPFreq[4][76] = 485; - JPFreq[43][67] = 484; - JPFreq[3][9] = 483; - JPFreq[41][37] = 482; - JPFreq[33][68] = 481; - JPFreq[43][31] = 480; - JPFreq[19][55] = 479; - JPFreq[4][30] = 478; - JPFreq[27][33] = 477; - JPFreq[16][62] = 476; - JPFreq[36][35] = 475; - JPFreq[37][15] = 474; - JPFreq[27][70] = 473; - JPFreq[22][71] = 472; - JPFreq[33][45] = 471; - JPFreq[31][78] = 470; - JPFreq[43][59] = 469; - JPFreq[32][19] = 468; - JPFreq[17][28] = 467; - JPFreq[40][28] = 466; - JPFreq[20][93] = 465; - JPFreq[18][15] = 464; - JPFreq[4][23] = 463; - JPFreq[3][23] = 462; - JPFreq[26][64] = 461; - JPFreq[44][92] = 460; - JPFreq[17][27] = 459; - JPFreq[3][56] = 458; - JPFreq[25][38] = 457; - JPFreq[23][31] = 456; - JPFreq[35][43] = 455; - JPFreq[4][54] = 454; - JPFreq[35][19] = 453; - JPFreq[22][47] = 452; - JPFreq[42][0] = 451; - JPFreq[23][28] = 450; - JPFreq[46][33] = 449; - JPFreq[36][85] = 448; - JPFreq[31][12] = 447; - JPFreq[3][76] = 446; - JPFreq[4][75] = 445; - JPFreq[36][56] = 444; - JPFreq[4][64] = 443; - JPFreq[25][77] = 442; - JPFreq[15][52] = 441; - JPFreq[33][73] = 440; - JPFreq[3][55] = 439; - JPFreq[43][82] = 438; - JPFreq[27][82] = 437; - JPFreq[20][3] = 436; - JPFreq[40][51] = 435; - JPFreq[3][17] = 434; - JPFreq[27][71] = 433; - JPFreq[4][52] = 432; - JPFreq[44][48] = 431; - JPFreq[27][2] = 430; - JPFreq[17][39] = 429; - JPFreq[31][8] = 428; - JPFreq[44][54] = 427; - JPFreq[43][18] = 426; - JPFreq[43][77] = 425; - JPFreq[4][61] = 424; - JPFreq[19][91] = 423; - JPFreq[31][13] = 422; - JPFreq[44][71] = 421; - JPFreq[20][0] = 420; - JPFreq[23][87] = 419; - JPFreq[21][14] = 418; - JPFreq[29][13] = 417; - JPFreq[3][58] = 416; - JPFreq[26][18] = 415; - JPFreq[4][47] = 414; - JPFreq[4][18] = 413; - JPFreq[3][53] = 412; - JPFreq[26][92] = 411; - JPFreq[21][7] = 410; - JPFreq[4][37] = 409; - JPFreq[4][63] = 408; - JPFreq[36][51] = 407; - JPFreq[4][32] = 406; - JPFreq[28][73] = 405; - JPFreq[4][50] = 404; - JPFreq[41][60] = 403; - JPFreq[23][1] = 402; - JPFreq[36][92] = 401; - JPFreq[15][41] = 400; - JPFreq[21][71] = 399; - JPFreq[41][30] = 398; - JPFreq[32][76] = 397; - JPFreq[17][34] = 396; - JPFreq[26][15] = 395; - JPFreq[26][25] = 394; - JPFreq[31][77] = 393; - JPFreq[31][3] = 392; - JPFreq[46][34] = 391; - JPFreq[27][84] = 390; - JPFreq[23][8] = 389; - JPFreq[16][0] = 388; - JPFreq[28][80] = 387; - JPFreq[26][54] = 386; - JPFreq[33][18] = 385; - JPFreq[31][20] = 384; - JPFreq[31][62] = 383; - JPFreq[30][41] = 382; - JPFreq[33][30] = 381; - JPFreq[45][45] = 380; - JPFreq[37][82] = 379; - JPFreq[15][33] = 378; - JPFreq[20][12] = 377; - JPFreq[18][5] = 376; - JPFreq[28][86] = 375; - JPFreq[30][19] = 374; - JPFreq[42][43] = 373; - JPFreq[36][31] = 372; - JPFreq[17][93] = 371; - JPFreq[4][15] = 370; - JPFreq[21][20] = 369; - JPFreq[23][21] = 368; - JPFreq[28][72] = 367; - JPFreq[4][20] = 366; - JPFreq[26][55] = 365; - JPFreq[21][5] = 364; - JPFreq[19][16] = 363; - JPFreq[23][64] = 362; - JPFreq[40][59] = 361; - JPFreq[37][26] = 360; - JPFreq[26][56] = 359; - JPFreq[4][12] = 358; - JPFreq[33][71] = 357; - JPFreq[32][39] = 356; - JPFreq[38][40] = 355; - JPFreq[22][74] = 354; - JPFreq[3][25] = 353; - JPFreq[15][48] = 352; - JPFreq[41][82] = 351; - JPFreq[41][9] = 350; - JPFreq[25][48] = 349; - JPFreq[31][71] = 348; - JPFreq[43][29] = 347; - JPFreq[26][80] = 346; - JPFreq[4][5] = 345; - JPFreq[18][71] = 344; - JPFreq[29][0] = 343; - JPFreq[43][43] = 342; - JPFreq[23][81] = 341; - JPFreq[4][42] = 340; - JPFreq[44][28] = 339; - JPFreq[23][93] = 338; - JPFreq[17][81] = 337; - JPFreq[25][25] = 336; - JPFreq[41][23] = 335; - JPFreq[34][35] = 334; - JPFreq[4][53] = 333; - JPFreq[28][36] = 332; - JPFreq[4][41] = 331; - JPFreq[25][60] = 330; - JPFreq[23][20] = 329; - JPFreq[3][43] = 328; - JPFreq[24][79] = 327; - JPFreq[29][41] = 326; - JPFreq[30][83] = 325; - JPFreq[3][50] = 324; - JPFreq[22][18] = 323; - JPFreq[18][3] = 322; - JPFreq[39][30] = 321; - JPFreq[4][28] = 320; - JPFreq[21][64] = 319; - JPFreq[4][68] = 318; - JPFreq[17][71] = 317; - JPFreq[27][0] = 316; - JPFreq[39][28] = 315; - JPFreq[30][13] = 314; - JPFreq[36][70] = 313; - JPFreq[20][82] = 312; - JPFreq[33][38] = 311; - JPFreq[44][87] = 310; - JPFreq[34][45] = 309; - JPFreq[4][26] = 308; - JPFreq[24][44] = 307; - JPFreq[38][67] = 306; - JPFreq[38][6] = 305; - JPFreq[30][68] = 304; - JPFreq[15][89] = 303; - JPFreq[24][93] = 302; - JPFreq[40][41] = 301; - JPFreq[38][3] = 300; - JPFreq[28][23] = 299; - JPFreq[26][17] = 298; - JPFreq[4][38] = 297; - JPFreq[22][78] = 296; - JPFreq[15][37] = 295; - JPFreq[25][85] = 294; - JPFreq[4][9] = 293; - JPFreq[4][7] = 292; - JPFreq[27][53] = 291; - JPFreq[39][29] = 290; - JPFreq[41][43] = 289; - JPFreq[25][62] = 288; - JPFreq[4][48] = 287; - JPFreq[28][28] = 286; - JPFreq[21][40] = 285; - JPFreq[36][73] = 284; - JPFreq[26][39] = 283; - JPFreq[22][54] = 282; - JPFreq[33][5] = 281; - JPFreq[19][21] = 280; - JPFreq[46][31] = 279; - JPFreq[20][64] = 278; - JPFreq[26][63] = 277; - JPFreq[22][23] = 276; - JPFreq[25][81] = 275; - JPFreq[4][62] = 274; - JPFreq[37][31] = 273; - JPFreq[40][52] = 272; - JPFreq[29][79] = 271; - JPFreq[41][48] = 270; - JPFreq[31][57] = 269; - JPFreq[32][92] = 268; - JPFreq[36][36] = 267; - JPFreq[27][7] = 266; - JPFreq[35][29] = 265; - JPFreq[37][34] = 264; - JPFreq[34][42] = 263; - JPFreq[27][15] = 262; - JPFreq[33][27] = 261; - JPFreq[31][38] = 260; - JPFreq[19][79] = 259; - JPFreq[4][31] = 258; - JPFreq[4][66] = 257; - JPFreq[17][32] = 256; - JPFreq[26][67] = 255; - JPFreq[16][30] = 254; - JPFreq[26][46] = 253; - JPFreq[24][26] = 252; - JPFreq[35][10] = 251; - JPFreq[18][37] = 250; - JPFreq[3][19] = 249; - JPFreq[33][69] = 248; - JPFreq[31][9] = 247; - JPFreq[45][29] = 246; - JPFreq[3][15] = 245; - JPFreq[18][54] = 244; - JPFreq[3][44] = 243; - JPFreq[31][29] = 242; - JPFreq[18][45] = 241; - JPFreq[38][28] = 240; - JPFreq[24][12] = 239; - JPFreq[35][82] = 238; - JPFreq[17][43] = 237; - JPFreq[28][9] = 236; - JPFreq[23][25] = 235; - JPFreq[44][37] = 234; - JPFreq[23][75] = 233; - JPFreq[23][92] = 232; - JPFreq[0][24] = 231; - JPFreq[19][74] = 230; - JPFreq[45][32] = 229; - JPFreq[16][72] = 228; - JPFreq[16][93] = 227; - JPFreq[45][13] = 226; - JPFreq[24][8] = 225; - JPFreq[25][47] = 224; - JPFreq[28][26] = 223; - JPFreq[43][81] = 222; - JPFreq[32][71] = 221; - JPFreq[18][41] = 220; - JPFreq[26][62] = 219; - JPFreq[41][24] = 218; - JPFreq[40][11] = 217; - JPFreq[43][57] = 216; - JPFreq[34][53] = 215; - JPFreq[20][32] = 214; - JPFreq[34][43] = 213; - JPFreq[41][91] = 212; - JPFreq[29][57] = 211; - JPFreq[15][43] = 210; - JPFreq[22][89] = 209; - JPFreq[33][83] = 208; - JPFreq[43][20] = 207; - JPFreq[25][58] = 206; - JPFreq[30][30] = 205; - JPFreq[4][56] = 204; - JPFreq[17][64] = 203; - JPFreq[23][0] = 202; - JPFreq[44][12] = 201; - JPFreq[25][37] = 200; - JPFreq[35][13] = 199; - JPFreq[20][30] = 198; - JPFreq[21][84] = 197; - JPFreq[29][14] = 196; - JPFreq[30][5] = 195; - JPFreq[37][2] = 194; - JPFreq[4][78] = 193; - JPFreq[29][78] = 192; - JPFreq[29][84] = 191; - JPFreq[32][86] = 190; - JPFreq[20][68] = 189; - JPFreq[30][39] = 188; - JPFreq[15][69] = 187; - JPFreq[4][60] = 186; - JPFreq[20][61] = 185; - JPFreq[41][67] = 184; - JPFreq[16][35] = 183; - JPFreq[36][57] = 182; - JPFreq[39][80] = 181; - JPFreq[4][59] = 180; - JPFreq[4][44] = 179; - JPFreq[40][54] = 178; - JPFreq[30][8] = 177; - JPFreq[44][30] = 176; - JPFreq[31][93] = 175; - JPFreq[31][47] = 174; - JPFreq[16][70] = 173; - JPFreq[21][0] = 172; - JPFreq[17][35] = 171; - JPFreq[21][67] = 170; - JPFreq[44][18] = 169; - JPFreq[36][29] = 168; - JPFreq[18][67] = 167; - JPFreq[24][28] = 166; - JPFreq[36][24] = 165; - JPFreq[23][5] = 164; - JPFreq[31][65] = 163; - JPFreq[26][59] = 162; - JPFreq[28][2] = 161; - JPFreq[39][69] = 160; - JPFreq[42][40] = 159; - JPFreq[37][80] = 158; - JPFreq[15][66] = 157; - JPFreq[34][38] = 156; - JPFreq[28][48] = 155; - JPFreq[37][77] = 154; - JPFreq[29][34] = 153; - JPFreq[33][12] = 152; - JPFreq[4][65] = 151; - JPFreq[30][31] = 150; - JPFreq[27][92] = 149; - JPFreq[4][2] = 148; - JPFreq[4][51] = 147; - JPFreq[23][77] = 146; - JPFreq[4][35] = 145; - JPFreq[3][13] = 144; - JPFreq[26][26] = 143; - JPFreq[44][4] = 142; - JPFreq[39][53] = 141; - JPFreq[20][11] = 140; - JPFreq[40][33] = 139; - JPFreq[45][7] = 138; - JPFreq[4][70] = 137; - JPFreq[3][49] = 136; - JPFreq[20][59] = 135; - JPFreq[21][12] = 134; - JPFreq[33][53] = 133; - JPFreq[20][14] = 132; - JPFreq[37][18] = 131; - JPFreq[18][17] = 130; - JPFreq[36][23] = 129; - JPFreq[18][57] = 128; - JPFreq[26][74] = 127; - JPFreq[35][2] = 126; - JPFreq[38][58] = 125; - JPFreq[34][68] = 124; - JPFreq[29][81] = 123; - JPFreq[20][69] = 122; - JPFreq[39][86] = 121; - JPFreq[4][16] = 120; - JPFreq[16][49] = 119; - JPFreq[15][72] = 118; - JPFreq[26][35] = 117; - JPFreq[32][14] = 116; - JPFreq[40][90] = 115; - JPFreq[33][79] = 114; - JPFreq[35][4] = 113; - JPFreq[23][33] = 112; - JPFreq[19][19] = 111; - JPFreq[31][41] = 110; - JPFreq[44][1] = 109; - JPFreq[22][56] = 108; - JPFreq[31][27] = 107; - JPFreq[32][18] = 106; - JPFreq[27][32] = 105; - JPFreq[37][39] = 104; - JPFreq[42][11] = 103; - JPFreq[29][71] = 102; - JPFreq[32][58] = 101; - JPFreq[46][10] = 100; - JPFreq[17][30] = 99; - JPFreq[38][15] = 98; - JPFreq[29][60] = 97; - JPFreq[4][11] = 96; - JPFreq[38][31] = 95; - JPFreq[40][79] = 94; - JPFreq[28][49] = 93; - JPFreq[28][84] = 92; - JPFreq[26][77] = 91; - JPFreq[22][32] = 90; - JPFreq[33][17] = 89; - JPFreq[23][18] = 88; - JPFreq[32][64] = 87; - JPFreq[4][6] = 86; - JPFreq[33][51] = 85; - JPFreq[44][77] = 84; - JPFreq[29][5] = 83; - JPFreq[46][25] = 82; - JPFreq[19][58] = 81; - JPFreq[4][46] = 80; - JPFreq[15][71] = 79; - JPFreq[18][58] = 78; - JPFreq[26][45] = 77; - JPFreq[45][66] = 76; - JPFreq[34][10] = 75; - JPFreq[19][37] = 74; - JPFreq[33][65] = 73; - JPFreq[44][52] = 72; - JPFreq[16][38] = 71; - JPFreq[36][46] = 70; - JPFreq[20][26] = 69; - JPFreq[30][37] = 68; - JPFreq[4][58] = 67; - JPFreq[43][2] = 66; - JPFreq[30][18] = 65; - JPFreq[19][35] = 64; - JPFreq[15][68] = 63; - JPFreq[3][36] = 62; - JPFreq[35][40] = 61; - JPFreq[36][32] = 60; - JPFreq[37][14] = 59; - JPFreq[17][11] = 58; - JPFreq[19][78] = 57; - JPFreq[37][11] = 56; - JPFreq[28][63] = 55; - JPFreq[29][61] = 54; - JPFreq[33][3] = 53; - JPFreq[41][52] = 52; - JPFreq[33][63] = 51; - JPFreq[22][41] = 50; - JPFreq[4][19] = 49; - JPFreq[32][41] = 48; - JPFreq[24][4] = 47; - JPFreq[31][28] = 46; - JPFreq[43][30] = 45; - JPFreq[17][3] = 44; - JPFreq[43][70] = 43; - JPFreq[34][19] = 42; - JPFreq[20][77] = 41; - JPFreq[18][83] = 40; - JPFreq[17][15] = 39; - JPFreq[23][61] = 38; - JPFreq[40][27] = 37; - JPFreq[16][48] = 36; - JPFreq[39][78] = 35; - JPFreq[41][53] = 34; - JPFreq[40][91] = 33; - JPFreq[40][72] = 32; - JPFreq[18][52] = 31; - JPFreq[35][66] = 30; - JPFreq[39][93] = 29; - JPFreq[19][48] = 28; - JPFreq[26][36] = 27; - JPFreq[27][25] = 26; - JPFreq[42][71] = 25; - JPFreq[42][85] = 24; - JPFreq[26][48] = 23; - JPFreq[28][15] = 22; - JPFreq[3][66] = 21; - JPFreq[25][24] = 20; - JPFreq[27][43] = 19; - JPFreq[27][78] = 18; - JPFreq[45][43] = 17; - JPFreq[27][72] = 16; - JPFreq[40][29] = 15; - JPFreq[41][0] = 14; - JPFreq[19][57] = 13; - JPFreq[15][59] = 12; - JPFreq[29][29] = 11; - JPFreq[4][25] = 10; - JPFreq[21][42] = 9; - JPFreq[23][35] = 8; - JPFreq[33][1] = 7; - JPFreq[4][57] = 6; - JPFreq[17][60] = 5; - JPFreq[25][19] = 4; - JPFreq[22][65] = 3; - JPFreq[42][29] = 2; - JPFreq[27][66] = 1; - JPFreq[26][89] = 0; - } -} - -class Encoding { - // Supported Encoding Types - static int GB2312 = 0; - static int GBK = 1; - static int GB18030 = 2; - static int HZ = 3; - static int BIG5 = 4; - static int CNS11643 = 5; - static int UTF8 = 6; - static int UTF8T = 7; - static int UTF8S = 8; - static int UNICODE = 9; - static int UNICODET = 10; - static int UNICODES = 11; - static int ISO2022CN = 12; - static int ISO2022CN_CNS = 13; - static int ISO2022CN_GB = 14; - static int EUC_KR = 15; - static int CP949 = 16; - static int ISO2022KR = 17; - static int JOHAB = 18; - static int SJIS = 19; - static int EUC_JP = 20; - static int ISO2022JP = 21; - static int ASCII = 22; - static int OTHER = 23; - static int TOTALTYPES = 24; - - public final static int SIMP = 0; - - public final static int TRAD = 1; - - // Names of the encodings as understood by Java - static String[] javaname; - - // Names of the encodings for human viewing - static String[] nicename; - - // Names of charsets as used in charset parameter of HTML Meta tag - static String[] htmlname; - - // Constructor - Encoding() { - javaname = new String[TOTALTYPES]; - nicename = new String[TOTALTYPES]; - htmlname = new String[TOTALTYPES]; - // Assign encoding names - javaname[GB2312] = "GB2312"; - javaname[GBK] = "GBK"; - javaname[GB18030] = "GB18030"; - javaname[HZ] = "ASCII"; // What to put here? Sun doesn't support HZ - javaname[ISO2022CN_GB] = "ISO2022CN_GB"; - javaname[BIG5] = "BIG5"; - javaname[CNS11643] = "EUC-TW"; - javaname[ISO2022CN_CNS] = "ISO2022CN_CNS"; - javaname[ISO2022CN] = "ISO2022CN"; - javaname[UTF8] = "UTF-8"; - javaname[UTF8T] = "UTF-8"; - javaname[UTF8S] = "UTF-8"; - javaname[UNICODE] = "Unicode"; - javaname[UNICODET] = "Unicode"; - javaname[UNICODES] = "Unicode"; - javaname[EUC_KR] = "EUC_KR"; - javaname[CP949] = "MS949"; - javaname[ISO2022KR] = "ISO2022KR"; - javaname[JOHAB] = "Johab"; - javaname[SJIS] = "SJIS"; - javaname[EUC_JP] = "EUC_JP"; - javaname[ISO2022JP] = "ISO2022JP"; - javaname[ASCII] = "ASCII"; - javaname[OTHER] = "ISO8859_1"; - // Assign encoding names - htmlname[GB2312] = "GB2312"; - htmlname[GBK] = "GBK"; - htmlname[GB18030] = "GB18030"; - htmlname[HZ] = "HZ-GB-2312"; - htmlname[ISO2022CN_GB] = "ISO-2022-CN-EXT"; - htmlname[BIG5] = "BIG5"; - htmlname[CNS11643] = "EUC-TW"; - htmlname[ISO2022CN_CNS] = "ISO-2022-CN-EXT"; - htmlname[ISO2022CN] = "ISO-2022-CN"; - htmlname[UTF8] = "UTF-8"; - htmlname[UTF8T] = "UTF-8"; - htmlname[UTF8S] = "UTF-8"; - htmlname[UNICODE] = "UTF-16"; - htmlname[UNICODET] = "UTF-16"; - htmlname[UNICODES] = "UTF-16"; - htmlname[EUC_KR] = "EUC-KR"; - htmlname[CP949] = "x-windows-949"; - htmlname[ISO2022KR] = "ISO-2022-KR"; - htmlname[JOHAB] = "x-Johab"; - htmlname[SJIS] = "Shift_JIS"; - htmlname[EUC_JP] = "EUC-JP"; - htmlname[ISO2022JP] = "ISO-2022-JP"; - htmlname[ASCII] = "ASCII"; - htmlname[OTHER] = "ISO8859-1"; - // Assign Human readable names - nicename[GB2312] = "GB-2312"; - nicename[GBK] = "GBK"; - nicename[GB18030] = "GB18030"; - nicename[HZ] = "HZ"; - nicename[ISO2022CN_GB] = "ISO2022CN-GB"; - nicename[BIG5] = "Big5"; - nicename[CNS11643] = "CNS11643"; - nicename[ISO2022CN_CNS] = "ISO2022CN-CNS"; - nicename[ISO2022CN] = "ISO2022 CN"; - nicename[UTF8] = "UTF-8"; - nicename[UTF8T] = "UTF-8 (Trad)"; - nicename[UTF8S] = "UTF-8 (Simp)"; - nicename[UNICODE] = "Unicode"; - nicename[UNICODET] = "Unicode (Trad)"; - nicename[UNICODES] = "Unicode (Simp)"; - nicename[EUC_KR] = "EUC-KR"; - nicename[CP949] = "CP949"; - nicename[ISO2022KR] = "ISO 2022 KR"; - nicename[JOHAB] = "Johab"; - nicename[SJIS] = "Shift-JIS"; - nicename[EUC_JP] = "EUC-JP"; - nicename[ISO2022JP] = "ISO 2022 JP"; - nicename[ASCII] = "ASCII"; - nicename[OTHER] = "OTHER"; - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EncodingDetect.kt b/app/src/main/java/io/legado/app/utils/EncodingDetect.kt new file mode 100644 index 000000000..de698c8e0 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/EncodingDetect.kt @@ -0,0 +1,82 @@ +package io.legado.app.utils + +import android.text.TextUtils +import io.legado.app.lib.icu4j.CharsetDetector +import org.jsoup.Jsoup +import java.io.File +import java.io.FileInputStream +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * 自动获取文件的编码 + * */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +object EncodingDetect { + + fun getHtmlEncode(bytes: ByteArray): String? { + try { + val doc = Jsoup.parse(String(bytes, StandardCharsets.UTF_8)) + val metaTags = doc.getElementsByTag("meta") + var charsetStr: String + for (metaTag in metaTags) { + charsetStr = metaTag.attr("charset") + if (!TextUtils.isEmpty(charsetStr)) { + return charsetStr + } + val content = metaTag.attr("content") + val httpEquiv = metaTag.attr("http-equiv") + if (httpEquiv.lowercase(Locale.getDefault()) == "content-type") { + charsetStr = if (content.lowercase(Locale.getDefault()).contains("charset")) { + content.substring( + content.lowercase(Locale.getDefault()) + .indexOf("charset") + "charset=".length + ) + } else { + content.substring(content.lowercase(Locale.getDefault()).indexOf(";") + 1) + } + if (!TextUtils.isEmpty(charsetStr)) { + return charsetStr + } + } + } + } catch (ignored: Exception) { + } + return getEncode(bytes) + } + + fun getEncode(bytes: ByteArray): String { + val detector = CharsetDetector() + detector.setText(bytes) + val match = detector.detect() + return match.name + } + + /** + * 得到文件的编码 + */ + fun getEncode(filePath: String): String { + return getEncode(File(filePath)) + } + + /** + * 得到文件的编码 + */ + fun getEncode(file: File): String { + val tempByte = getFileBytes(file) + return getEncode(tempByte) + } + + private fun getFileBytes(testFile: File?): ByteArray { + val fis: FileInputStream + val byteArray = ByteArray(2000) + try { + fis = FileInputStream(testFile) + fis.read(byteArray) + fis.close() + } catch (e: Exception) { + System.err.println("Error: $e") + } + return byteArray + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EventBusKt.kt b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt similarity index 86% rename from app/src/main/java/io/legado/app/utils/EventBusKt.kt rename to app/src/main/java/io/legado/app/utils/EventBusExtensions.kt index 0d9473a3c..0293e4893 100644 --- a/app/src/main/java/io/legado/app/utils/EventBusKt.kt +++ b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package io.legado.app.utils import androidx.appcompat.app.AppCompatActivity @@ -11,11 +13,15 @@ inline fun eventObservable(tag: String): Observable { } inline fun postEvent(tag: String, event: EVENT) { - LiveEventBus.get(tag).post(event) + LiveEventBus.get(tag).post(event) } inline fun postEventDelay(tag: String, event: EVENT, delay: Long) { - LiveEventBus.get(tag).postDelay(event, delay) + LiveEventBus.get(tag).postDelay(event, delay) +} + +inline fun postEventOrderly(tag: String, event: EVENT) { + LiveEventBus.get(tag).postOrderly(event) } inline fun AppCompatActivity.observeEvent( @@ -30,7 +36,6 @@ inline fun AppCompatActivity.observeEvent( } } - inline fun AppCompatActivity.observeEventSticky( vararg tags: String, noinline observer: (EVENT) -> Unit @@ -65,5 +70,4 @@ inline fun Fragment.observeEventSticky( tags.forEach { eventObservable(it).observeSticky(this, o) } -} - +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt index eef940a86..feac3c98a 100644 --- a/app/src/main/java/io/legado/app/utils/FileUtils.kt +++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt @@ -3,15 +3,15 @@ package io.legado.app.utils import android.os.Environment import android.webkit.MimeTypeMap import androidx.annotation.IntDef -import io.legado.app.App -import io.legado.app.ui.filechooser.utils.ConvertUtils +import io.legado.app.ui.document.utils.ConvertUtils +import splitties.init.appCtx import java.io.* import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.* import java.util.regex.Pattern -@Suppress("unused") +@Suppress("unused", "MemberVisibilityCanBePrivate") object FileUtils { fun exists(root: File, vararg subDirFiles: String): Boolean { @@ -55,6 +55,22 @@ object FileUtils { return file } + fun createFileWithReplace(filePath: String): File { + val file = File(filePath) + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) + } + //创建文件 + file.createNewFile() + } else { + file.delete() + file.createNewFile() + } + return file + } + fun getFile(root: File, vararg subDirFiles: String): File { val filePath = getPath(root, *subDirFiles) return File(filePath) @@ -88,8 +104,7 @@ object FileUtils { } fun getCachePath(): String { - return App.INSTANCE.externalCacheDir?.absolutePath - ?: App.INSTANCE.cacheDir.absolutePath + return appCtx.externalCache.absolutePath } fun getSdCardPath(): String { @@ -472,11 +487,10 @@ object FileUtils { */ @JvmOverloads fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean { - try { + return try { writeBytes(filepath, content.toByteArray(charset(charset))) - return true } catch (e: UnsupportedEncodingException) { - return false + false } } @@ -502,6 +516,45 @@ object FileUtils { } } + /** + * 保存文件内容 + */ + fun writeInputStream(filepath: String, data: InputStream): Boolean { + val file = File(filepath) + return writeInputStream(file, data) + } + + /** + * 保存文件内容 + */ + fun writeInputStream(file: File, data: InputStream): Boolean { + var fos: FileOutputStream? = null + return try { + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + val buffer = ByteArray(1024 * 4) + fos = FileOutputStream(file) + while (true) { + val len = data.read(buffer, 0, buffer.size) + if (len == -1) { + break + } else { + fos.write(buffer, 0, len) + } + } + data.close() + fos.flush() + + true + } catch (e: IOException) { + false + } finally { + closeSilently(fos) + } + } + /** * 追加文本内容 */ @@ -552,15 +605,15 @@ object FileUtils { * 获取文件名(不包括扩展名) */ fun getNameExcludeExtension(path: String): String { - try { + return try { var fileName = File(path).name val lastIndexOf = fileName.lastIndexOf(".") if (lastIndexOf != -1) { fileName = fileName.substring(0, lastIndexOf) } - return fileName + fileName } catch (e: Exception) { - return "" + "" } } @@ -694,7 +747,7 @@ object FileUtils { val s1 = f1.name val s2 = f2.name if (caseSensitive) { - s1.compareTo(s2) + s1.cnCompare(s2) } else { s1.compareTo(s2, ignoreCase = true) } diff --git a/app/src/main/java/io/legado/app/utils/FloatExtensions.kt b/app/src/main/java/io/legado/app/utils/FloatExtensions.kt index 4e3c7f9ed..ab5178cd6 100644 --- a/app/src/main/java/io/legado/app/utils/FloatExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/FloatExtensions.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package io.legado.app.utils import android.content.res.Resources @@ -7,7 +9,6 @@ val Float.dp: Float android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics ) - val Float.sp: Float get() = android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics diff --git a/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt index de7f10b96..1883dd7e0 100644 --- a/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt @@ -1,7 +1,8 @@ +@file:Suppress("unused") + package io.legado.app.utils import android.app.Activity -import android.app.Service import android.content.Intent import android.content.res.ColorStateList import android.graphics.drawable.Drawable @@ -9,12 +10,6 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.edit import androidx.fragment.app.Fragment -import org.jetbrains.anko.connectivityManager -import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.internals.AnkoInternals - -@Suppress("DEPRECATION") -fun Fragment.isOnline() = requireContext().connectivityManager.activeNetworkInfo?.isConnected == true fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) = requireContext().defaultSharedPreferences.getBoolean(key, defValue) @@ -40,7 +35,10 @@ fun Fragment.getPrefString(key: String, defValue: String? = null) = fun Fragment.putPrefString(key: String, value: String) = requireContext().defaultSharedPreferences.edit { putString(key, value) } -fun Fragment.getPrefStringSet(key: String, defValue: MutableSet? = null) = +fun Fragment.getPrefStringSet( + key: String, + defValue: MutableSet? = null +): MutableSet? = requireContext().defaultSharedPreferences.getStringSet(key, defValue) fun Fragment.putPrefStringSet(key: String, value: MutableSet) = @@ -51,21 +49,14 @@ fun Fragment.removePref(key: String) = fun Fragment.getCompatColor(@ColorRes id: Int): Int = requireContext().getCompatColor(id) -fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = requireContext().getCompatDrawable(id) - -fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = requireContext().getCompatColorStateList(id) - -inline fun Fragment.startActivity(vararg params: Pair) = - AnkoInternals.internalStartActivity(requireActivity(), T::class.java, params) - -inline fun Fragment.startActivityForResult(requestCode: Int, vararg params: Pair) = - startActivityForResult(AnkoInternals.createIntent(requireActivity(), T::class.java, params), requestCode) - -inline fun Fragment.startService(vararg params: Pair) = - AnkoInternals.internalStartService(requireActivity(), T::class.java, params) +fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = + requireContext().getCompatDrawable(id) -inline fun Fragment.stopService(vararg params: Pair) = - AnkoInternals.internalStopService(requireActivity(), T::class.java, params) +fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = + requireContext().getCompatColorStateList(id) -inline fun Fragment.intentFor(vararg params: Pair): Intent = - AnkoInternals.createIntent(requireActivity(), T::class.java, params) \ No newline at end of file +inline fun Fragment.startActivity( + configIntent: Intent.() -> Unit = {} +) { + startActivity(Intent(requireContext(), T::class.java).apply(configIntent)) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/GsonExtensions.kt b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt index b29b4e2b4..db32ca60a 100644 --- a/app/src/main/java/io/legado/app/utils/GsonExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt @@ -3,7 +3,6 @@ package io.legado.app.utils import com.google.gson.* import com.google.gson.internal.LinkedTreeMap import com.google.gson.reflect.TypeToken -import org.jetbrains.anko.attempt import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import kotlin.math.ceil @@ -24,17 +23,15 @@ val GSON: Gson by lazy { inline fun genericType(): Type = object : TypeToken() {}.type inline fun Gson.fromJsonObject(json: String?): T? {//可转成任意类型 - return attempt { - val result: T? = fromJson(json, genericType()) - result - }.value + return kotlin.runCatching { + fromJson(json, genericType()) as? T + }.getOrNull() } inline fun Gson.fromJsonArray(json: String?): List? { - return attempt { - val result: List? = fromJson(json, ParameterizedTypeImpl(T::class.java)) - result - }.value + return kotlin.runCatching { + fromJson(json, ParameterizedTypeImpl(T::class.java)) as? List + }.getOrNull() } class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { diff --git a/app/src/main/java/io/legado/app/utils/HandlerUtils.kt b/app/src/main/java/io/legado/app/utils/HandlerUtils.kt new file mode 100644 index 000000000..0eacea43b --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/HandlerUtils.kt @@ -0,0 +1,52 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch + +/** This main looper cache avoids synchronization overhead when accessed repeatedly. */ +@JvmField +val mainLooper: Looper = Looper.getMainLooper()!! + +@JvmField +val mainThread: Thread = mainLooper.thread + +val isMainThread: Boolean inline get() = mainThread === Thread.currentThread() + +@PublishedApi +internal val currentThread: Any? + inline get() = Thread.currentThread() + +@JvmField +val mainHandler: Handler = if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try { + Handler::class.java.getDeclaredConstructor( + Looper::class.java, + Handler.Callback::class.java, + Boolean::class.javaPrimitiveType // async + ).newInstance(mainLooper, null, true) +} catch (ignored: NoSuchMethodException) { + Handler(mainLooper) // Hidden constructor absent. Fall back to non-async constructor. +} + +fun runOnUI(function: () -> Unit) { + if (isMainThread) { + function() + } else { + mainHandler.post(function) + } +} + +fun CoroutineScope.runOnIO(function: () -> Unit) { + if (isMainThread) { + launch(IO) { + function() + } + } else { + function() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt new file mode 100644 index 000000000..ab779e288 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt @@ -0,0 +1,61 @@ +package io.legado.app.utils + +import io.legado.app.model.analyzeRule.AnalyzeUrl +import java.net.URL +import java.util.regex.Pattern + +object HtmlFormatter { + private val wrapHtmlRegex = "]*>".toRegex() + private val commentRegex = "".toRegex() //注释 + private val notImgHtmlRegex = "])[^<>]*>".toRegex() + private val otherHtmlRegex = "])[^<>]*>".toRegex() + private val formatImagePattern = Pattern.compile( + "]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|]*data-[^=]*= *\"([^\"]*)\"[^>]*>|]*src *= *\"([^\"]*)\"[^>]*>", + Pattern.CASE_INSENSITIVE + ) + + fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String { + html ?: return "" + return html.replace(wrapHtmlRegex, "\n") + .replace(commentRegex, "") + .replace(otherRegex, "") + .replace("\\s*\\n+\\s*".toRegex(), "\n  ") + .replace("^[\\n\\s]+".toRegex(), "  ") + .replace("[\\n\\s]+$".toRegex(), "") + } + + fun formatKeepImg(html: String?, redirectUrl: URL? = null): String { + html ?: return "" + val keepImgHtml = format(html, notImgHtmlRegex) + + //正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码 + val matcher = formatImagePattern.matcher(keepImgHtml) + var appendPos = 0 + val sb = StringBuffer() + while (matcher.find()) { + var param = "" + sb.append( + keepImgHtml.substring(appendPos, matcher.start()), "" + ) + appendPos = matcher.end() + } + if (appendPos < keepImgHtml.length) sb.append( + keepImgHtml.substring( + appendPos, + keepImgHtml.length + ) + ) + return sb.toString() + } +} diff --git a/app/src/main/java/io/legado/app/utils/IntExtensions.kt b/app/src/main/java/io/legado/app/utils/IntExtensions.kt index 2354a6a78..dc03d0258 100644 --- a/app/src/main/java/io/legado/app/utils/IntExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/IntExtensions.kt @@ -4,12 +4,16 @@ import android.content.res.Resources val Int.dp: Int get() = android.util.TypedValue.applyDimension( - android.util.TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics + android.util.TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + Resources.getSystem().displayMetrics ).toInt() val Int.sp: Int get() = android.util.TypedValue.applyDimension( - android.util.TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics + android.util.TypedValue.COMPLEX_UNIT_SP, + this.toFloat(), + Resources.getSystem().displayMetrics ).toInt() val Int.hexString: String diff --git a/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt b/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt index 3f347e4a1..2c9829ddd 100644 --- a/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt @@ -10,41 +10,38 @@ import org.jsoup.select.NodeVisitor fun Element.textArray(): Array { - val accum = StringUtil.borrowBuilder() + val sb = StringUtil.borrowBuilder() NodeTraversor.traverse(object : NodeVisitor { override fun head(node: Node, depth: Int) { if (node is TextNode) { - appendNormalisedText(accum, node) + appendNormalisedText(sb, node) } else if (node is Element) { - if (accum.isNotEmpty() && + if (sb.isNotEmpty() && (node.isBlock || node.tag().name == "br") && - !lastCharIsWhitespace(accum) - ) accum.append("\n") + !lastCharIsWhitespace(sb) + ) sb.append("\n") } } override fun tail(node: Node, depth: Int) { if (node is Element) { - if (node.isBlock && node.nextSibling() is TextNode && !lastCharIsWhitespace( - accum - ) - ) accum.append("\n") + if (node.isBlock && node.nextSibling() is TextNode + && !lastCharIsWhitespace(sb) + ) { + sb.append("\n") + } } } }, this) - val text = StringUtil.releaseBuilder(accum).trim { it <= ' ' } + val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' } return text.splitNotBlank("\n") } -private fun appendNormalisedText(accum: StringBuilder, textNode: TextNode) { +private fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) { val text = textNode.wholeText if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode) - accum.append(text) - else StringUtil.appendNormalisedWhitespace( - accum, - text, - lastCharIsWhitespace(accum) - ) + sb.append(text) + else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb)) } private fun preserveWhitespace(node: Node?): Boolean { diff --git a/app/src/main/java/io/legado/app/utils/LanguageUtils.kt b/app/src/main/java/io/legado/app/utils/LanguageUtils.kt index ae6c465a8..0eed85d6f 100644 --- a/app/src/main/java/io/legado/app/utils/LanguageUtils.kt +++ b/app/src/main/java/io/legado/app/utils/LanguageUtils.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.os.Build +import android.os.LocaleList import io.legado.app.constant.PreferKey import java.util.* @@ -16,14 +17,13 @@ object LanguageUtils { fun setConfiguration(context: Context): Context { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val resources: Resources = context.resources - val targetLocale: Locale = when (context.getPrefString(PreferKey.language)) { - "zh" -> Locale.CHINESE - "tw" -> Locale.TRADITIONAL_CHINESE - "en" -> Locale.ENGLISH - else -> getSystemLocale() - } + val metrics = resources.displayMetrics val configuration: Configuration = resources.configuration + val targetLocale = getSetLocale(context) configuration.setLocale(targetLocale) + configuration.setLocales(LocaleList(targetLocale)) + @Suppress("DEPRECATION") + resources.updateConfiguration(configuration, metrics) context.createConfigurationContext(configuration) } else { setConfigurationOld(context) @@ -34,15 +34,10 @@ object LanguageUtils { /** * 设置语言 */ - fun setConfigurationOld(context: Context) { + private fun setConfigurationOld(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { val resources: Resources = context.resources - val targetLocale: Locale = when (context.getPrefString(PreferKey.language)) { - "zh" -> Locale.CHINESE - "tw" -> Locale.TRADITIONAL_CHINESE - "en" -> Locale.ENGLISH - else -> getSystemLocale() - } + val targetLocale = getSetLocale(context) val configuration: Configuration = resources.configuration @Suppress("DEPRECATION") configuration.locale = targetLocale @@ -55,13 +50,54 @@ object LanguageUtils { * 当前系统语言 */ private fun getSystemLocale(): Locale { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言 - Resources.getSystem().configuration.locales.get(0) + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言 + locale = Resources.getSystem().configuration.locales.get(0) } else { @Suppress("DEPRECATION") - Resources.getSystem().configuration.locale + locale = Resources.getSystem().configuration.locale } + return locale + } + + /** + * 当前App语言 + */ + private fun getAppLocale(context: Context): Locale { + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + locale = context.resources.configuration.locales[0] + } else { + @Suppress("DEPRECATION") + locale = context.resources.configuration.locale + } + return locale + } + /** + * 当前设置语言 + */ + private fun getSetLocale(context: Context): Locale { + return when (context.getPrefString(PreferKey.language)) { + "zh" -> Locale.SIMPLIFIED_CHINESE + "tw" -> Locale.TRADITIONAL_CHINESE + "en" -> Locale.ENGLISH + else -> getSystemLocale() + } + } + + /** + * 判断App语言和设置语言是否相同 + */ + fun isSameWithSetting(context: Context): Boolean { + val locale = getAppLocale(context) + val language = locale.language + val country = locale.country + val pfLocale = getSetLocale(context) + val pfLanguage = pfLocale.language + val pfCountry = pfLocale.country + return language == pfLanguage && country == pfCountry + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/LogUtils.kt b/app/src/main/java/io/legado/app/utils/LogUtils.kt index a96bc7c75..b94274a3b 100644 --- a/app/src/main/java/io/legado/app/utils/LogUtils.kt +++ b/app/src/main/java/io/legado/app/utils/LogUtils.kt @@ -1,11 +1,10 @@ package io.legado.app.utils import android.annotation.SuppressLint -import io.legado.app.App +import splitties.init.appCtx import java.text.SimpleDateFormat import java.util.* import java.util.logging.* -import java.util.logging.Formatter @Suppress("unused") object LogUtils { @@ -30,17 +29,17 @@ object LogUtils { } private val fileHandler by lazy { - val root = App.INSTANCE.externalCacheDir ?: return@lazy null + val root = appCtx.externalCacheDir ?: return@lazy null val logFolder = FileUtils.createFolderIfNotExist(root, "logs") val logPath = FileUtils.getPath(root = logFolder, "appLog") FileHandler(logPath, 10240, 10).apply { - formatter = object : Formatter() { + formatter = object : java.util.logging.Formatter() { override fun format(record: LogRecord): String { // 设置文件输出格式 return (getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n") } } - level = if (App.INSTANCE.getPrefBoolean("recordLog")) { + level = if (appCtx.getPrefBoolean("recordLog")) { Level.INFO } else { Level.OFF @@ -49,7 +48,7 @@ object LogUtils { } fun upLevel() { - fileHandler?.level = if (App.INSTANCE.getPrefBoolean("recordLog")) { + fileHandler?.level = if (appCtx.getPrefBoolean("recordLog")) { Level.INFO } else { Level.OFF diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt index 4a3e236cb..74f573f38 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -1,6 +1,9 @@ package io.legado.app.utils -import retrofit2.Response +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import splitties.systemservices.connectivityManager import java.net.InetAddress import java.net.NetworkInterface import java.net.SocketException @@ -8,27 +11,53 @@ import java.net.URL import java.util.* import java.util.regex.Pattern -@Suppress("unused") + +@Suppress("unused", "MemberVisibilityCanBePrivate") object NetworkUtils { - fun getUrl(response: Response<*>): String { - val networkResponse = response.raw().networkResponse() - return networkResponse?.request()?.url()?.toString() - ?: response.raw().request().url().toString() + + /** + * 判断是否联网 + */ + @Suppress("DEPRECATION") + fun isAvailable(): Boolean { + if (Build.VERSION.SDK_INT < 23) { + val mWiFiNetworkInfo = connectivityManager.activeNetworkInfo + if (mWiFiNetworkInfo != null) { + //移动数据 + return if (mWiFiNetworkInfo.type == ConnectivityManager.TYPE_WIFI) { + //WIFI + true + } else mWiFiNetworkInfo.type == ConnectivityManager.TYPE_MOBILE + } + } else { + val network = connectivityManager.activeNetwork + if (network != null) { + val nc = connectivityManager.getNetworkCapabilities(network) + if (nc != null) { + //移动数据 + return if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + //WIFI + true + } else nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + } + } + } + return false } private val notNeedEncoding: BitSet by lazy { val bitSet = BitSet(256) - for (i in 'a'.toInt()..'z'.toInt()) { + for (i in 'a'.code..'z'.code) { bitSet.set(i) } - for (i in 'A'.toInt()..'Z'.toInt()) { + for (i in 'A'.code..'Z'.code) { bitSet.set(i) } - for (i in '0'.toInt()..'9'.toInt()) { + for (i in '0'.code..'9'.code) { bitSet.set(i) } for (char in "+-_.$:()!*@&#,[]") { - bitSet.set(char.toInt()) + bitSet.set(char.code) } return@lazy bitSet } @@ -44,7 +73,7 @@ object NetworkUtils { var i = 0 while (i < str.length) { val c = str[i] - if (notNeedEncoding.get(c.toInt())) { + if (notNeedEncoding.get(c.code)) { i++ continue } @@ -75,9 +104,9 @@ object NetworkUtils { /** * 获取绝对地址 */ - fun getAbsoluteURL(baseURL: String?, relativePath: String?): String? { + fun getAbsoluteURL(baseURL: String?, relativePath: String): String { if (baseURL.isNullOrEmpty()) return relativePath - if (relativePath.isNullOrEmpty()) return baseURL + if (relativePath.isAbsUrl()) return relativePath var relativeUrl = relativePath try { val absoluteUrl = URL(baseURL.substringBefore(",")) @@ -90,6 +119,22 @@ object NetworkUtils { return relativeUrl } + /** + * 获取绝对地址 + */ + fun getAbsoluteURL(baseURL: URL?, relativePath: String): String { + if (baseURL == null) return relativePath + var relativeUrl = relativePath + try { + val parseUrl = URL(baseURL, relativePath) + relativeUrl = parseUrl.toString() + return relativeUrl + } catch (e: Exception) { + e.printStackTrace() + } + return relativeUrl + } + fun getBaseUrl(url: String?): String? { if (url == null || !url.startsWith("http")) return null val index = url.indexOf("/", 9) @@ -98,14 +143,13 @@ object NetworkUtils { } else url.substring(0, index) } - fun getSubDomain(url: String?): String { - var baseUrl = getBaseUrl(url) - if (baseUrl == null) return "" + fun getSubDomain(url: String?): String { + val baseUrl = getBaseUrl(url) ?: return "" return if (baseUrl.indexOf(".") == baseUrl.lastIndexOf(".")) { - baseUrl.substring(baseUrl.lastIndexOf("/")+1) - } else baseUrl.substring(baseUrl.indexOf(".")+1) + baseUrl.substring(baseUrl.lastIndexOf("/") + 1) + } else baseUrl.substring(baseUrl.indexOf(".") + 1) } - + /** * Get local Ip address. */ diff --git a/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt b/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt new file mode 100644 index 000000000..2a42839d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt @@ -0,0 +1,467 @@ +package io.legado.app.utils + +import android.graphics.* +import android.text.TextPaint +import android.text.TextUtils +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import com.google.zxing.* +import com.google.zxing.common.GlobalHistogramBinarizer +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.king.zxing.DecodeFormatManager +import com.king.zxing.util.LogUtils +import java.util.* +import kotlin.math.max + + +@Suppress("MemberVisibilityCanBePrivate", "unused") +object QRCodeUtils { + + const val DEFAULT_REQ_WIDTH = 480 + const val DEFAULT_REQ_HEIGHT = 640 + + /** + * 生成二维码 + * @param content 二维码的内容 + * @param heightPix 二维码的高 + * @param logo 二维码中间的logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @param errorCorrectionLevel + */ + fun createQRCode( + content: String, + heightPix: Int = DEFAULT_REQ_HEIGHT, + logo: Bitmap? = null, + @FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f, + errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H + ): Bitmap? { + //配置参数 + val hints: MutableMap = EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.CHARACTER_SET] = "utf-8" + //容错级别 + hints[EncodeHintType.ERROR_CORRECTION] = errorCorrectionLevel + //设置空白边距的宽度 + hints[EncodeHintType.MARGIN] = 1 //default is 4 + return createQRCode(content, heightPix, logo, ratio, hints) + } + + /** + * 生成二维码 + * @param content 二维码的内容 + * @param heightPix 二维码的高 + * @param logo 二维码中间的logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @param hints + * @param codeColor 二维码的颜色 + * @return + */ + fun createQRCode( + content: String?, + heightPix: Int, + logo: Bitmap?, + @FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f, + hints: Map, + codeColor: Int = Color.BLACK + ): Bitmap? { + try { + // 图像数据转换,使用了矩阵转换 + val bitMatrix = + QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, heightPix, heightPix, hints) + val pixels = IntArray(heightPix * heightPix) + // 下面这里按照二维码的算法,逐个生成二维码的图片, + // 两个for循环是图片横列扫描的结果 + for (y in 0 until heightPix) { + for (x in 0 until heightPix) { + if (bitMatrix[x, y]) { + pixels[y * heightPix + x] = codeColor + } else { + pixels[y * heightPix + x] = Color.WHITE + } + } + } + + // 生成二维码图片的格式 + var bitmap = Bitmap.createBitmap(heightPix, heightPix, Bitmap.Config.ARGB_8888) + bitmap!!.setPixels(pixels, 0, heightPix, 0, 0, heightPix, heightPix) + if (logo != null) { + bitmap = addLogo(bitmap, logo, ratio) + } + return bitmap + } catch (e: WriterException) { + LogUtils.w(e.message) + } + return null + } + + /** + * 在二维码中间添加Logo图案 + * @param src + * @param logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @return + */ + private fun addLogo( + src: Bitmap?, + logo: Bitmap?, + @FloatRange(from = 0.0, to = 1.0) ratio: Float + ): Bitmap? { + if (src == null) { + return null + } + if (logo == null) { + return src + } + + //获取图片的宽高 + val srcWidth = src.width + val srcHeight = src.height + val logoWidth = logo.width + val logoHeight = logo.height + if (srcWidth == 0 || srcHeight == 0) { + return null + } + if (logoWidth == 0 || logoHeight == 0) { + return src + } + + //logo大小为二维码整体大小 + val scaleFactor = srcWidth * ratio / logoWidth + var bitmap = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888) + try { + val canvas = Canvas(bitmap!!) + canvas.drawBitmap(src, 0f, 0f, null) + canvas.scale( + scaleFactor, + scaleFactor, + (srcWidth / 2).toFloat(), + (srcHeight / 2).toFloat() + ) + canvas.drawBitmap( + logo, + ((srcWidth - logoWidth) / 2).toFloat(), + ((srcHeight - logoHeight) / 2).toFloat(), + null + ) + canvas.save() + canvas.restore() + } catch (e: Exception) { + bitmap = null + LogUtils.w(e.message) + } + return bitmap + } + + /** + * 解析一维码/二维码图片 + * @param bitmap 解析的图片 + * @param hints 解析编码类型 + * @return + */ + fun parseCode( + bitmap: Bitmap, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): String? { + val result = parseCodeResult(bitmap, reqWidth, reqHeight, hints) + return result?.text + } + + /** + * 解析一维码/二维码图片 + * @param bitmap 解析的图片 + * @param hints 解析编码类型 + * @return + */ + fun parseCodeResult( + bitmap: Bitmap, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): Result? { + if (bitmap.width > reqWidth || bitmap.height > reqHeight) { + val bm = BitmapUtils.changeBitmapSize(bitmap, reqWidth, reqHeight) + return parseCodeResult(getRGBLuminanceSource(bm), hints) + } + return parseCodeResult(getRGBLuminanceSource(bitmap), hints) + } + + /** + * 解析一维码/二维码图片 + * @param source + * @param hints + * @return + */ + fun parseCodeResult(source: LuminanceSource?, hints: Map?): Result? { + var result: Result? = null + val reader = MultiFormatReader() + try { + reader.setHints(hints) + if (source != null) { + result = decodeInternal(reader, source) + if (result == null) { + result = decodeInternal(reader, source.invert()) + } + if (result == null && source.isRotateSupported) { + result = decodeInternal(reader, source.rotateCounterClockwise()) + } + } + } catch (e: java.lang.Exception) { + LogUtils.w(e.message) + } finally { + reader.reset() + } + return result + } + + /** + * 解析二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @return + */ + fun parseQRCode(bitmapPath: String?): String? { + val result = parseQRCodeResult(bitmapPath) + return result?.text + } + + /** + * 解析二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @return + */ + fun parseQRCodeResult( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT + ): Result? { + return parseCodeResult(bitmapPath, reqWidth, reqHeight, DecodeFormatManager.QR_CODE_HINTS) + } + + /** + * 解析一维码/二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @return + */ + fun parseCode( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): String? { + return parseCodeResult(bitmapPath, reqWidth, reqHeight, hints)?.text + } + + /** + * 解析一维码/二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param hints 解析编码类型 + * @return + */ + fun parseCodeResult( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): Result? { + var result: Result? = null + val reader = MultiFormatReader() + try { + reader.setHints(hints) + val source = getRGBLuminanceSource(compressBitmap(bitmapPath, reqWidth, reqHeight)) + result = decodeInternal(reader, source) + if (result == null) { + result = decodeInternal(reader, source.invert()) + } + if (result == null && source.isRotateSupported) { + result = decodeInternal(reader, source.rotateCounterClockwise()) + } + } catch (e: Exception) { + LogUtils.w(e.message) + } finally { + reader.reset() + } + return result + } + + private fun decodeInternal(reader: MultiFormatReader, source: LuminanceSource): Result? { + var result: Result? = null + try { + try { + //采用HybridBinarizer解析 + result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + } catch (e: Exception) { + } + if (result == null) { + //如果没有解析成功,再采用GlobalHistogramBinarizer解析一次 + result = reader.decodeWithState(BinaryBitmap(GlobalHistogramBinarizer(source))) + } + } catch (e: Exception) { + } + return result + } + + + /** + * 压缩图片 + * @param path + * @return + */ + private fun compressBitmap(path: String?, reqWidth: Int, reqHeight: Int): Bitmap { + if (reqWidth > 0 && reqHeight > 0) { //都大于进行判断是否压缩 + val newOpts = BitmapFactory.Options() + // 开始读入图片,此时把options.inJustDecodeBounds 设回true了 + newOpts.inJustDecodeBounds = true //获取原始图片大小 + BitmapFactory.decodeFile(path, newOpts) // 此时返回bm为空 + val width = newOpts.outWidth.toFloat() + val height = newOpts.outHeight.toFloat() + // 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可 + var wSize = 1 // wSize=1表示不缩放 + if (width > reqWidth) { // 如果宽度大的话根据宽度固定大小缩放 + wSize = (width / reqWidth).toInt() + } + var hSize = 1 // wSize=1表示不缩放 + if (height > reqHeight) { // 如果高度高的话根据宽度固定大小缩放 + hSize = (height / reqHeight).toInt() + } + var size = max(wSize, hSize) + if (size <= 0) size = 1 + newOpts.inSampleSize = size // 设置缩放比例 + // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了 + newOpts.inJustDecodeBounds = false + return BitmapFactory.decodeFile(path, newOpts) + } + return BitmapFactory.decodeFile(path) + } + + + /** + * 获取RGBLuminanceSource + * @param bitmap + * @return + */ + private fun getRGBLuminanceSource(bitmap: Bitmap): RGBLuminanceSource { + val width = bitmap.width + val height = bitmap.height + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + return RGBLuminanceSource(width, height, pixels) + } + + /** + * 生成条形码 + * @param content + * @param format + * @param desiredWidth + * @param desiredHeight + * @param hints + * @param isShowText + * @param textSize + * @param codeColor + * @return + */ + fun createBarCode( + content: String?, + desiredWidth: Int, + desiredHeight: Int, + format: BarcodeFormat = BarcodeFormat.CODE_128, + hints: Map? = null, + isShowText: Boolean = true, + textSize: Int = 40, + @ColorInt codeColor: Int = Color.BLACK + ): Bitmap? { + if (TextUtils.isEmpty(content)) { + return null + } + val writer = MultiFormatWriter() + try { + val result = writer.encode( + content, format, desiredWidth, + desiredHeight, hints + ) + val width = result.width + val height = result.height + val pixels = IntArray(width * height) + // All are 0, or black, by default + for (y in 0 until height) { + val offset = y * width + for (x in 0 until width) { + pixels[offset + x] = if (result[x, y]) codeColor else Color.WHITE + } + } + val bitmap = Bitmap.createBitmap( + width, height, + Bitmap.Config.ARGB_8888 + ) + bitmap.setPixels(pixels, 0, width, 0, 0, width, height) + return if (isShowText) { + addCode(bitmap, content, textSize, codeColor, textSize / 2) + } else bitmap + } catch (e: WriterException) { + LogUtils.w(e.message) + } + return null + } + + /** + * 条形码下面添加文本信息 + * @param src + * @param code + * @param textSize + * @param textColor + * @return + */ + private fun addCode( + src: Bitmap?, + code: String?, + textSize: Int, + @ColorInt textColor: Int, + offset: Int + ): Bitmap? { + if (src == null) { + return null + } + if (TextUtils.isEmpty(code)) { + return src + } + + //获取图片的宽高 + val srcWidth = src.width + val srcHeight = src.height + if (srcWidth <= 0 || srcHeight <= 0) { + return null + } + var bitmap = Bitmap.createBitmap( + srcWidth, + srcHeight + textSize + offset * 2, + Bitmap.Config.ARGB_8888 + ) + try { + val canvas = Canvas(bitmap!!) + canvas.drawBitmap(src, 0f, 0f, null) + val paint = TextPaint() + paint.textSize = textSize.toFloat() + paint.color = textColor + paint.textAlign = Paint.Align.CENTER + canvas.drawText( + code!!, + (srcWidth / 2).toFloat(), + (srcHeight + textSize / 2 + offset).toFloat(), + paint + ) + canvas.save() + canvas.restore() + } catch (e: Exception) { + bitmap = null + LogUtils.w(e.message) + } + return bitmap + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/Snackbars.kt b/app/src/main/java/io/legado/app/utils/Snackbars.kt index 2706bd7ca..5e4e28951 100644 --- a/app/src/main/java/io/legado/app/utils/Snackbars.kt +++ b/app/src/main/java/io/legado/app/utils/Snackbars.kt @@ -10,7 +10,9 @@ import com.google.android.material.snackbar.Snackbar * @param message the message text resource. */ @JvmName("snackbar2") -fun View.snackbar(@StringRes message: Int) = Snackbar +fun View.snackbar( + @StringRes message: Int +) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .apply { show() } @@ -20,7 +22,9 @@ fun View.snackbar(@StringRes message: Int) = Snackbar * @param message the message text resource. */ @JvmName("longSnackbar2") -fun View.longSnackbar(@StringRes message: Int) = Snackbar +fun View.longSnackbar( + @StringRes message: Int +) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .apply { show() } @@ -30,7 +34,9 @@ fun View.longSnackbar(@StringRes message: Int) = Snackbar * @param message the message text resource. */ @JvmName("indefiniteSnackbar2") -fun View.indefiniteSnackbar(@StringRes message: Int) = Snackbar +fun View.indefiniteSnackbar( + @StringRes message: Int +) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .apply { show() } @@ -40,7 +46,9 @@ fun View.indefiniteSnackbar(@StringRes message: Int) = Snackbar * @param message the message text. */ @JvmName("snackbar2") -fun View.snackbar(message: CharSequence) = Snackbar +fun View.snackbar( + message: CharSequence +) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .apply { show() } @@ -50,7 +58,9 @@ fun View.snackbar(message: CharSequence) = Snackbar * @param message the message text. */ @JvmName("longSnackbar2") -fun View.longSnackbar(message: CharSequence) = Snackbar +fun View.longSnackbar( + message: CharSequence +) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .apply { show() } @@ -60,7 +70,9 @@ fun View.longSnackbar(message: CharSequence) = Snackbar * @param message the message text. */ @JvmName("indefiniteSnackbar2") -fun View.indefiniteSnackbar(message: CharSequence) = Snackbar +fun View.indefiniteSnackbar( + message: CharSequence +) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .apply { show() } @@ -70,7 +82,11 @@ fun View.indefiniteSnackbar(message: CharSequence) = Snackbar * @param message the message text resource. */ @JvmName("snackbar2") -fun View.snackbar(message: Int, @StringRes actionText: Int, action: (View) -> Unit) = Snackbar +fun View.snackbar( + message: Int, + @StringRes actionText: + Int, action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .setAction(actionText, action) .apply { show() } @@ -81,8 +97,11 @@ fun View.snackbar(message: Int, @StringRes actionText: Int, action: (View) -> Un * @param message the message text resource. */ @JvmName("longSnackbar2") -fun View.longSnackbar(@StringRes message: Int, @StringRes actionText: Int, action: (View) -> Unit) = - Snackbar +fun View.longSnackbar( + @StringRes message: Int, + @StringRes actionText: Int, + action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .setAction(actionText, action) .apply { show() } @@ -93,8 +112,11 @@ fun View.longSnackbar(@StringRes message: Int, @StringRes actionText: Int, actio * @param message the message text resource. */ @JvmName("indefiniteSnackbar2") -fun View.indefiniteSnackbar(@StringRes message: Int, @StringRes actionText: Int, action: (View) -> Unit) = - Snackbar +fun View.indefiniteSnackbar( + @StringRes message: Int, + @StringRes actionText: Int, + action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .setAction(actionText, action) .apply { show() } @@ -105,8 +127,11 @@ fun View.indefiniteSnackbar(@StringRes message: Int, @StringRes actionText: Int, * @param message the message text. */ @JvmName("snackbar2") -fun View.snackbar(message: CharSequence, actionText: CharSequence, action: (View) -> Unit) = - Snackbar +fun View.snackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .setAction(actionText, action) .apply { show() } @@ -117,8 +142,11 @@ fun View.snackbar(message: CharSequence, actionText: CharSequence, action: (View * @param message the message text. */ @JvmName("longSnackbar2") -fun View.longSnackbar(message: CharSequence, actionText: CharSequence, action: (View) -> Unit) = - Snackbar +fun View.longSnackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .setAction(actionText, action) .apply { show() } diff --git a/app/src/main/java/io/legado/app/utils/StringExtensions.kt b/app/src/main/java/io/legado/app/utils/StringExtensions.kt index dc53b7bc8..1e57f6416 100644 --- a/app/src/main/java/io/legado/app/utils/StringExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt @@ -1,18 +1,19 @@ +@file:Suppress("unused") + package io.legado.app.utils +import android.icu.text.Collator +import android.icu.util.ULocale import android.net.Uri import java.io.File - -val removeHtmlRegex = "]*>".toRegex() -val imgRegex = "]*>".toRegex() -val notImgHtmlRegex = "]*>".toRegex() +import java.util.* fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() -fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true +fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true fun String.parseToUri(): Uri { - return if (isContentPath()) { + return if (isContentScheme()) { Uri.parse(this) } else { Uri.fromFile(File(this)) @@ -21,8 +22,7 @@ fun String.parseToUri(): Uri { fun String?.isAbsUrl() = this?.let { - it.startsWith("http://", true) - || it.startsWith("https://", true) + it.startsWith("http://", true) || it.startsWith("https://", true) } ?: false fun String?.isJson(): Boolean = @@ -47,17 +47,6 @@ fun String?.isJsonArray(): Boolean = str.startsWith("[") && str.endsWith("]") } ?: false -fun String?.htmlFormat(): String { - this ?: return "" - return this - .replace(imgRegex, "\n$0\n") - .replace(removeHtmlRegex, "\n") - .replace(notImgHtmlRegex, "") - .replace("\\s*\\n+\\s*".toRegex(), "\n  ") - .replace("^[\\n\\s]+".toRegex(), "  ") - .replace("[\\n\\s]+$".toRegex(), "") -} - fun String.splitNotBlank(vararg delimiter: String): Array = run { this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } @@ -66,6 +55,14 @@ fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } +fun String.cnCompare(other: String): Int { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other) + } else { + java.text.Collator.getInstance(Locale.CHINA).compare(this, other) + } +} + /** * 将字符串拆分为单个字符,包含emoji */ diff --git a/app/src/main/java/io/legado/app/utils/StringUtils.kt b/app/src/main/java/io/legado/app/utils/StringUtils.kt index e5b658c21..3a2bda745 100644 --- a/app/src/main/java/io/legado/app/utils/StringUtils.kt +++ b/app/src/main/java/io/legado/app/utils/StringUtils.kt @@ -3,7 +3,6 @@ package io.legado.app.utils import android.annotation.SuppressLint import android.text.TextUtils.isEmpty import java.text.DecimalFormat -import java.text.ParseException import java.text.SimpleDateFormat import java.util.* import java.util.regex.Matcher @@ -12,7 +11,7 @@ import kotlin.math.abs import kotlin.math.log10 import kotlin.math.pow -@Suppress("unused") +@Suppress("unused", "MemberVisibilityCanBePrivate") object StringUtils { private const val HOUR_OF_DAY = 24 private const val DAY_OF_YESTERDAY = 2 @@ -45,6 +44,7 @@ object StringUtils { //将时间转换成日期 fun dateConvert(time: Long, pattern: String): String { val date = Date(time) + @SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) return format.format(date) @@ -55,7 +55,7 @@ object StringUtils { @SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) val calendar = Calendar.getInstance() - try { + kotlin.runCatching { val date = format.parse(source) ?: return "" val curTime = calendar.timeInMillis calendar.time = date @@ -94,8 +94,8 @@ object StringUtils { convertFormat.format(date) } } - } catch (e: ParseException) { - e.printStackTrace() + }.onFailure { + it.printStackTrace() } return "" @@ -105,18 +105,16 @@ object StringUtils { if (length <= 0) return "0" val units = arrayOf("b", "kb", "M", "G", "T") //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 - //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt() //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 - //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 return DecimalFormat("#,##0.##") .format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] } @SuppressLint("DefaultLocale") fun toFirstCapital(str: String): String { - return str.substring(0, 1).toUpperCase() + str.substring(1) + return str.substring(0, 1).uppercase(Locale.getDefault()) + str.substring(1) } /** @@ -125,7 +123,7 @@ object StringUtils { fun halfToFull(input: String): String { val c = input.toCharArray() for (i in c.indices) { - if (c[i].toInt() == 32) + if (c[i].code == 32) //半角空格 { c[i] = 12288.toChar() @@ -135,9 +133,9 @@ object StringUtils { //if (c[i] == 46) //半角点号,不转换 // continue; - if (c[i].toInt() in 33..126) + if (c[i].code in 33..126) //其他符号都转换为全角 - c[i] = (c[i].toInt() + 65248).toChar() + c[i] = (c[i].code + 65248).toChar() } return String(c) } @@ -146,15 +144,15 @@ object StringUtils { fun fullToHalf(input: String): String { val c = input.toCharArray() for (i in c.indices) { - if (c[i].toInt() == 12288) + if (c[i].code == 12288) //全角空格 { c[i] = 32.toChar() continue } - if (c[i].toInt() in 65281..65374) - c[i] = (c[i].toInt() - 65248).toChar() + if (c[i].code in 65281..65374) + c[i] = (c[i].code - 65248).toChar() } return String(c) } @@ -174,7 +172,7 @@ object StringUtils { } // "一千零二十五", "一千二" 形式 - try { + return kotlin.runCatching { for (i in cn.indices) { val tmpNum = ChnMap[cn[i]]!! when { @@ -205,22 +203,18 @@ object StringUtils { } } result += tmp + billion - return result - } catch (e: Exception) { - return -1 - } - + result + }.getOrDefault(-1) } fun stringToInt(str: String?): Int { if (str != null) { val num = fullToHalf(str).replace("\\s+".toRegex(), "") - return try { + return kotlin.runCatching { Integer.parseInt(num) - } catch (e: Exception) { + }.getOrElse { chineseNumToInt(num) } - } return -1 } @@ -261,10 +255,10 @@ object StringUtils { var start = 0 val len = s.length var end = len - 1 - while (start < end && (s[start].toInt() <= 0x20 || s[start] == ' ')) { + while (start < end && (s[start].code <= 0x20 || s[start] == ' ')) { ++start } - while (start < end && (s[end].toInt() <= 0x20 || s[end] == ' ')) { + while (start < end && (s[end].code <= 0x20 || s[end] == ' ')) { --end } if (end < len) ++end @@ -292,4 +286,30 @@ object StringUtils { return buf.toString() } + fun byteToHexString(bytes: ByteArray?): String { + if (bytes == null) return "" + val sb = StringBuilder(bytes.size * 2) + for (b in bytes) { + val hex = 0xff and b.toInt() + if (hex < 16) { + sb.append('0') + } + sb.append(Integer.toHexString(hex)) + } + return sb.toString() + } + + fun hexStringToByte(hexString: String): ByteArray { + val hexStr = hexString.replace(" ", "") + val len = hexStr.length + val bytes = ByteArray(len / 2) + var i = 0 + while (i < len) { + // 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节 + bytes[i / 2] = ((Character.digit(hexString[i], 16) shl 4) + + Character.digit(hexString[i + 1], 16)).toByte() + i += 2 + } + return bytes + } } diff --git a/app/src/main/java/io/legado/app/utils/SystemUtils.kt b/app/src/main/java/io/legado/app/utils/SystemUtils.kt index b8027528d..4625ebeb1 100644 --- a/app/src/main/java/io/legado/app/utils/SystemUtils.kt +++ b/app/src/main/java/io/legado/app/utils/SystemUtils.kt @@ -19,13 +19,13 @@ object SystemUtils { fun getScreenOffTime(context: Context): Int { var screenOffTime = 0 - try { + kotlin.runCatching { screenOffTime = Settings.System.getInt( context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT ) - } catch (e: Exception) { - e.printStackTrace() + }.onFailure { + it.printStackTrace() } return screenOffTime @@ -56,12 +56,16 @@ object SystemUtils { */ fun isNavigationBarExist(activity: Activity?): Boolean { activity?.let { - val vp = it.window.decorView as? ViewGroup - if (vp != null) { - for (i in 0 until vp.childCount) { - vp.getChildAt(i).context.packageName - if (vp.getChildAt(i).id != View.NO_ID - && NAVIGATION == activity.resources.getResourceEntryName(vp.getChildAt(i).id) + val viewGroup = it.window.decorView as? ViewGroup + if (viewGroup != null) { + for (i in 0 until viewGroup.childCount) { + viewGroup.getChildAt(i).context.packageName + if (viewGroup.getChildAt(i).id != View.NO_ID + && NAVIGATION == activity.resources.getResourceEntryName( + viewGroup.getChildAt( + i + ).id + ) ) { return true } diff --git a/app/src/main/java/io/legado/app/utils/Toasts.kt b/app/src/main/java/io/legado/app/utils/Toasts.kt index 4348b4daf..c98f3561c 100644 --- a/app/src/main/java/io/legado/app/utils/Toasts.kt +++ b/app/src/main/java/io/legado/app/utils/Toasts.kt @@ -1,9 +1,9 @@ +@file:Suppress("unused") + package io.legado.app.utils import android.widget.Toast import androidx.fragment.app.Fragment -import org.jetbrains.anko.longToast -import org.jetbrains.anko.toast /** @@ -11,25 +11,25 @@ import org.jetbrains.anko.toast * * @param message the message text resource. */ -fun Fragment.toast(message: Int) = requireActivity().toast(message) +fun Fragment.toastOnUi(message: Int) = requireActivity().toastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param message the message text. */ -fun Fragment.toast(message: CharSequence) = requireActivity().toast(message) +fun Fragment.toastOnUi(message: CharSequence) = requireActivity().toastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text resource. */ -fun Fragment.longToast(message: Int) = requireActivity().longToast(message) +fun Fragment.longToast(message: Int) = requireContext().longToastOnUi(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text. */ -fun Fragment.longToast(message: CharSequence) = requireActivity().longToast(message) +fun Fragment.longToast(message: CharSequence) = requireContext().longToastOnUi(message) diff --git a/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt b/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt new file mode 100644 index 000000000..325923f15 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt @@ -0,0 +1,19 @@ +package io.legado.app.utils + +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Build +import android.widget.Toolbar +import androidx.core.content.ContextCompat +import io.legado.app.R + +/** + * 设置toolBar更多图标颜色 + */ +fun Toolbar.setMoreIconColor(color: Int) { + val moreIcon = ContextCompat.getDrawable(context, R.drawable.ic_more) + if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + overflowIcon = moreIcon + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/UIUtils.kt b/app/src/main/java/io/legado/app/utils/UIUtils.kt index b50a4c493..059f94a07 100644 --- a/app/src/main/java/io/legado/app/utils/UIUtils.kt +++ b/app/src/main/java/io/legado/app/utils/UIUtils.kt @@ -1,11 +1,6 @@ package io.legado.app.utils import android.content.Context -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.os.Build -import androidx.appcompat.widget.Toolbar -import androidx.core.content.ContextCompat import io.legado.app.R import io.legado.app.constant.Theme import io.legado.app.lib.theme.primaryTextColor @@ -13,19 +8,6 @@ import io.legado.app.lib.theme.primaryTextColor @Suppress("unused") object UIUtils { - /** 设置更多工具条图标和颜色 */ - fun setToolbarMoreIconCustomColor(toolbar: Toolbar?, color: Int? = null) { - toolbar ?: return - val moreIcon = ContextCompat.getDrawable(toolbar.context, R.drawable.ic_more) - if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (color != null) { - moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } - toolbar.overflowIcon = moreIcon - } - } - - fun getMenuColor( context: Context, theme: Theme = Theme.Auto, diff --git a/app/src/main/java/io/legado/app/utils/UriExtensions.kt b/app/src/main/java/io/legado/app/utils/UriExtensions.kt index 77c0fcaee..3e99bd0a2 100644 --- a/app/src/main/java/io/legado/app/utils/UriExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/UriExtensions.kt @@ -2,13 +2,14 @@ package io.legado.app.utils import android.content.Context import android.net.Uri +import androidx.documentfile.provider.DocumentFile import java.io.File -fun Uri.isContentPath() = this.toString().isContentPath() +fun Uri.isContentScheme() = this.scheme == "content" @Throws(Exception::class) fun Uri.readBytes(context: Context): ByteArray? { - if (this.toString().isContentPath()) { + if (this.isContentScheme()) { return DocumentUtils.readBytes(context, this) } else { val path = RealPathUtil.getPath(context, this) @@ -32,7 +33,7 @@ fun Uri.writeBytes( context: Context, byteArray: ByteArray ): Boolean { - if (this.toString().isContentPath()) { + if (this.isContentScheme()) { return DocumentUtils.writeBytes(context, byteArray, this) } else { val path = RealPathUtil.getPath(context, this) @@ -48,3 +49,22 @@ fun Uri.writeBytes( fun Uri.writeText(context: Context, text: String): Boolean { return writeBytes(context, text.toByteArray()) } + +fun Uri.writeBytes( + context: Context, + fileName: String, + byteArray: ByteArray +): Boolean { + if (this.isContentScheme()) { + DocumentFile.fromTreeUri(context, this)?.let { pDoc -> + DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let { + return it.uri.writeBytes(context, byteArray) + } + } + } else { + FileUtils.createFileWithReplace(path + File.separatorChar + fileName) + .writeBytes(byteArray) + return true + } + return false +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt index 7ed6d8cd6..f2b08ef3c 100644 --- a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt @@ -7,7 +7,6 @@ import android.graphics.Canvas import android.os.Build import android.view.View import android.view.View.* -import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.RadioGroup import android.widget.SeekBar @@ -15,7 +14,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.widget.PopupMenu import androidx.core.view.get -import io.legado.app.App +import splitties.init.appCtx import java.lang.reflect.Field @@ -32,10 +31,8 @@ val View.activity: AppCompatActivity? get() = getCompatActivity(context) fun View.hideSoftInput() = run { - val imm = App.INSTANCE.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.let { - imm.hideSoftInputFromWindow(this.windowToken, 0) - } + val imm = appCtx.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(this.windowToken, 0) } fun View.disableAutoFill() = run { @@ -80,13 +77,6 @@ fun View.screenshot(): Bitmap? { }.getOrNull() } -fun View.setMargin(left: Int, top: Int, right: Int, bottom: Int) { - if (layoutParams is ViewGroup.MarginLayoutParams) { - (layoutParams as ViewGroup.MarginLayoutParams).setMargins(left, top, right, bottom) - requestLayout() - } -} - fun SeekBar.progressAdd(int: Int) { progress += int } @@ -115,13 +105,11 @@ fun RadioGroup.checkByIndex(index: Int) { @SuppressLint("RestrictedApi") fun PopupMenu.show(x: Int, y: Int) { - try { + kotlin.runCatching { val field: Field = this.javaClass.getDeclaredField("mPopup") field.isAccessible = true (field.get(this) as MenuPopupHelper).show(x, y) - } catch (e: NoSuchFieldException) { - e.printStackTrace() - } catch (e: IllegalAccessException) { - e.printStackTrace() + }.onFailure { + it.printStackTrace() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ViewModelKt.kt b/app/src/main/java/io/legado/app/utils/ViewModelKt.kt deleted file mode 100644 index 9e1f74f24..000000000 --- a/app/src/main/java/io/legado/app/utils/ViewModelKt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.legado.app.utils - -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -fun AppCompatActivity.getViewModel(clazz: Class) = - ViewModelProvider(this).get(clazz) - -fun Fragment.getViewModel(clazz: Class) = - ViewModelProvider(this).get(clazz) - -/** - * 与activity数据同步 - */ -fun Fragment.getViewModelOfActivity(clazz: Class) = - ViewModelProvider(requireActivity()).get(clazz) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/ZipUtils.kt index a8ec51d45..b839066af 100644 --- a/app/src/main/java/io/legado/app/utils/ZipUtils.kt +++ b/app/src/main/java/io/legado/app/utils/ZipUtils.kt @@ -1,13 +1,15 @@ package io.legado.app.utils import android.util.Log +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext import java.io.* import java.util.* import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream -@Suppress("unused") +@Suppress("unused", "BlockingMethodInNonBlockingContext") object ZipUtils { /** @@ -18,8 +20,7 @@ object ZipUtils { * @return `true`: success

    `false`: fail * @throws IOException if an I/O error has occurred */ - @Throws(IOException::class) - fun zipFiles( + suspend fun zipFiles( srcFiles: Collection, zipFilePath: String ): Boolean { @@ -35,25 +36,18 @@ object ZipUtils { * @return `true`: success

    `false`: fail * @throws IOException if an I/O error has occurred */ - @Throws(IOException::class) - fun zipFiles( + suspend fun zipFiles( srcFilePaths: Collection?, zipFilePath: String?, comment: String? - ): Boolean { - if (srcFilePaths == null || zipFilePath == null) return false - var zos: ZipOutputStream? = null - try { - zos = ZipOutputStream(FileOutputStream(zipFilePath)) + ): Boolean = withContext(IO) { + if (srcFilePaths == null || zipFilePath == null) return@withContext false + ZipOutputStream(FileOutputStream(zipFilePath)).use { for (srcFile in srcFilePaths) { - if (!zipFile(getFileByPath(srcFile)!!, "", zos, comment)) return false - } - return true - } finally { - zos?.let { - zos.finish() - zos.close() + if (!zipFile(getFileByPath(srcFile)!!, "", it, comment)) + return@withContext false } + return@withContext true } } @@ -74,18 +68,11 @@ object ZipUtils { comment: String? = null ): Boolean { if (srcFiles == null || zipFile == null) return false - var zos: ZipOutputStream? = null - try { - zos = ZipOutputStream(FileOutputStream(zipFile)) + ZipOutputStream(FileOutputStream(zipFile)).use { for (srcFile in srcFiles) { - if (!zipFile(srcFile, "", zos, comment)) return false + if (!zipFile(srcFile, "", it, comment)) return false } return true - } finally { - zos?.let { - zos.finish() - zos.close() - } } } @@ -141,12 +128,7 @@ object ZipUtils { ): Boolean { if (srcFile == null || zipFile == null) return false ZipOutputStream(FileOutputStream(zipFile)).use { zos -> - return zipFile( - srcFile, - "", - zos, - comment - ) + return zipFile(srcFile, "", zos, comment) } } diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt new file mode 100644 index 000000000..321181022 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt @@ -0,0 +1,22 @@ +@file:Suppress("RedundantVisibilityModifier", "unused") + +package io.legado.app.utils.viewbindingdelegate + +import android.view.LayoutInflater +import androidx.core.app.ComponentActivity +import androidx.viewbinding.ViewBinding + +/** + * Create new [ViewBinding] associated with the [ComponentActivity] + */ +@JvmName("viewBindingActivity") +inline fun ComponentActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T, + setContentView: Boolean = false +) = lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + val binding = bindingInflater.invoke(layoutInflater) + if (setContentView) { + setContentView(binding.root) + } + binding +} diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt new file mode 100644 index 000000000..cae870edb --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt @@ -0,0 +1,54 @@ +@file:Suppress("RedundantVisibilityModifier", "unused") +@file:JvmName("ReflectionFragmentViewBindings") + +package io.legado.app.utils.viewbindingdelegate + +import android.view.View +import androidx.annotation.IdRes +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding + +private class FragmentViewBindingProperty( + viewBinder: (F) -> T +) : ViewBindingProperty(viewBinder) { + + override fun getLifecycleOwner(thisRef: F) = thisRef.viewLifecycleOwner +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + */ +@JvmName("viewBindingFragment") +public fun Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty { + return FragmentViewBindingProperty(viewBinder) +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + * + * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used + * @param viewProvider Provide a [View] from the Fragment. By default call [Fragment.requireView] + */ +@JvmName("viewBindingFragment") +public inline fun Fragment.viewBinding( + crossinline vbFactory: (View) -> T, + crossinline viewProvider: (F) -> View = Fragment::requireView +): ViewBindingProperty { + return viewBinding { fragment: F -> vbFactory(viewProvider(fragment)) } +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + * + * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used + * @param viewBindingRootId Root view's id that will be used as root for the view binding + */ +@JvmName("viewBindingFragment") +public inline fun Fragment.viewBinding( + crossinline vbFactory: (View) -> T, + @IdRes viewBindingRootId: Int +): ViewBindingProperty { + return viewBinding(vbFactory) { fragment: Fragment -> + fragment.requireView().findViewById(viewBindingRootId) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt new file mode 100644 index 000000000..10a23ce3c --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt @@ -0,0 +1,57 @@ +@file:Suppress("RedundantVisibilityModifier") + +package io.legado.app.utils.viewbindingdelegate + +import android.os.Handler +import android.os.Looper +import androidx.annotation.MainThread +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +public abstract class ViewBindingProperty( + private val viewBinder: (R) -> T +) : ReadOnlyProperty { + + private var viewBinding: T? = null + private val lifecycleObserver = ClearOnDestroyLifecycleObserver() + private var thisRef: R? = null + + protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner + + @MainThread + public override fun getValue(thisRef: R, property: KProperty<*>): T { + viewBinding?.let { return it } + + this.thisRef = thisRef + val lifecycle = getLifecycleOwner(thisRef).lifecycle + if (lifecycle.currentState == Lifecycle.State.DESTROYED) { + mainHandler.post { viewBinding = null } + } else { + lifecycle.addObserver(lifecycleObserver) + } + return viewBinder(thisRef).also { viewBinding = it } + } + + @MainThread + public fun clear() { + val thisRef = thisRef ?: return + this.thisRef = null + getLifecycleOwner(thisRef).lifecycle.removeObserver(lifecycleObserver) + mainHandler.post { viewBinding = null } + } + + private inner class ClearOnDestroyLifecycleObserver : DefaultLifecycleObserver { + + @MainThread + override fun onDestroy(owner: LifecycleOwner): Unit = clear() + } + + private companion object { + + private val mainHandler = Handler(Looper.getMainLooper()) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/web/HttpServer.kt b/app/src/main/java/io/legado/app/web/HttpServer.kt index 066a921c3..84f8de3bb 100644 --- a/app/src/main/java/io/legado/app/web/HttpServer.kt +++ b/app/src/main/java/io/legado/app/web/HttpServer.kt @@ -1,13 +1,17 @@ package io.legado.app.web +import android.graphics.Bitmap import com.google.gson.Gson import fi.iki.elonen.NanoHTTPD -import io.legado.app.web.controller.BookshelfController -import io.legado.app.web.controller.SourceController +import io.legado.app.api.ReturnData +import io.legado.app.api.controller.BookController +import io.legado.app.api.controller.SourceController import io.legado.app.web.utils.AssetsWeb -import io.legado.app.web.utils.ReturnData +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.util.* + class HttpServer(port: Int) : NanoHTTPD(port) { private val assetsWeb = AssetsWeb("web") @@ -17,8 +21,8 @@ class HttpServer(port: Int) : NanoHTTPD(port) { var uri = session.uri try { - when (session.method.name) { - "OPTIONS" -> { + when (session.method) { + Method.OPTIONS -> { val response = newFixedLengthResponse("") response.addHeader("Access-Control-Allow-Methods", "POST") response.addHeader("Access-Control-Allow-Headers", "content-type") @@ -26,33 +30,35 @@ class HttpServer(port: Int) : NanoHTTPD(port) { //response.addHeader("Access-Control-Max-Age", "3600"); return response } - - "POST" -> { + Method.POST -> { val files = HashMap() session.parseBody(files) val postData = files["postData"] - when (uri) { - "/saveSource" -> returnData = SourceController.saveSource(postData) - "/saveSources" -> returnData = SourceController.saveSources(postData) - "/saveBook" -> returnData = BookshelfController.saveBook(postData) - "/deleteSources" -> returnData = SourceController.deleteSources(postData) + returnData = when (uri) { + "/saveSource" -> SourceController.saveSource(postData) + "/saveSources" -> SourceController.saveSources(postData) + "/saveBook" -> BookController.saveBook(postData) + "/deleteSources" -> SourceController.deleteSources(postData) + "/addLocalBook" -> BookController.addLocalBook(session, postData) + else -> null } } - - "GET" -> { + Method.GET -> { val parameters = session.parameters - when (uri) { - "/getSource" -> returnData = SourceController.getSource(parameters) - "/getSources" -> returnData = SourceController.sources - "/getBookshelf" -> returnData = BookshelfController.bookshelf - "/getChapterList" -> - returnData = BookshelfController.getChapterList(parameters) - "/getBookContent" -> - returnData = BookshelfController.getBookContent(parameters) + returnData = when (uri) { + "/getSource" -> SourceController.getSource(parameters) + "/getSources" -> SourceController.sources + "/getBookshelf" -> BookController.bookshelf + "/getChapterList" -> BookController.getChapterList(parameters) + "/refreshToc" -> BookController.refreshToc(parameters) + "/getBookContent" -> BookController.getBookContent(parameters) + "/cover" -> BookController.getCover(parameters) + else -> null } } + else -> Unit } if (returnData == null) { @@ -61,7 +67,20 @@ class HttpServer(port: Int) : NanoHTTPD(port) { return assetsWeb.getResponse(uri) } - val response = newFixedLengthResponse(Gson().toJson(returnData)) + val response = if (returnData.data is Bitmap) { + val outputStream = ByteArrayOutputStream() + (returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + val inputStream = ByteArrayInputStream(byteArray) + newFixedLengthResponse( + Response.Status.OK, + "image/png", + inputStream, + byteArray.size.toLong() + ) + } else { + newFixedLengthResponse(Gson().toJson(returnData)) + } response.addHeader("Access-Control-Allow-Methods", "GET, POST") response.addHeader("Access-Control-Allow-Origin", session.headers["origin"]) return response diff --git a/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt new file mode 100644 index 000000000..c2c2f893f --- /dev/null +++ b/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt @@ -0,0 +1,98 @@ +package io.legado.app.web + + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoWSD +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.model.Debug +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isJson +import io.legado.app.utils.runOnIO +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import splitties.init.appCtx +import java.io.IOException + + +class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : + NanoWSD.WebSocket(handshakeRequest), + CoroutineScope by MainScope(), + Debug.Callback { + + private val notPrintState = arrayOf(10, 20, 30, 40) + + override fun onOpen() { + launch(IO) { + kotlin.runCatching { + while (isOpen) { + ping("ping".toByteArray()) + delay(30000) + } + } + } + } + + override fun onClose( + code: NanoWSD.WebSocketFrame.CloseCode, + reason: String, + initiatedByRemote: Boolean + ) { + cancel() + Debug.cancelDebug(true) + } + + override fun onMessage(message: NanoWSD.WebSocketFrame) { + launch(IO) { + kotlin.runCatching { + if (!message.textPayload.isJson()) { + send("数据必须为Json格式") + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + val debugBean = GSON.fromJsonObject>(message.textPayload) + if (debugBean != null) { + val tag = debugBean["tag"] + val key = debugBean["key"] + if (tag.isNullOrBlank() || key.isNullOrBlank()) { + send(appCtx.getString(R.string.cannot_empty)) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + appDb.bookSourceDao.getBookSource(tag)?.let { + Debug.callback = this@SourceDebugWebSocket + Debug.startDebug(this, WebBook(it), key) + } + } + } + } + } + + override fun onPong(pong: NanoWSD.WebSocketFrame) { + + } + + override fun onException(exception: IOException) { + Debug.cancelDebug(true) + } + + override fun printLog(state: Int, msg: String) { + if (state in notPrintState) { + return + } + runOnIO { + runCatching { + send(msg) + if (state == -1 || state == 1000) { + Debug.cancelDebug(true) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + } + }.onFailure { + it.printStackTrace() + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/web/WebSocketServer.kt b/app/src/main/java/io/legado/app/web/WebSocketServer.kt index c885a1e0a..d6a259687 100644 --- a/app/src/main/java/io/legado/app/web/WebSocketServer.kt +++ b/app/src/main/java/io/legado/app/web/WebSocketServer.kt @@ -1,7 +1,6 @@ package io.legado.app.web import fi.iki.elonen.NanoWSD -import io.legado.app.web.controller.SourceDebugWebSocket class WebSocketServer(port: Int) : NanoWSD(port) { diff --git a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt b/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt deleted file mode 100644 index dba45d4c1..000000000 --- a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.legado.app.web.controller - -import io.legado.app.App -import io.legado.app.constant.PreferKey -import io.legado.app.data.entities.Book -import io.legado.app.help.BookHelp -import io.legado.app.model.webBook.WebBook -import io.legado.app.service.help.ReadBook -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonObject -import io.legado.app.utils.getPrefInt -import io.legado.app.web.utils.ReturnData -import kotlinx.coroutines.runBlocking - -object BookshelfController { - - val bookshelf: ReturnData - get() { - val books = App.db.bookDao().all - val returnData = ReturnData() - return if (books.isEmpty()) { - returnData.setErrorMsg("还没有添加小说") - } else { - val data = when (App.INSTANCE.getPrefInt(PreferKey.bookshelfSort)) { - 1 -> books.sortedByDescending { it.latestChapterTime } - 2 -> books.sortedBy { it.name } - 3 -> books.sortedBy { it.order } - else -> books.sortedByDescending { it.durChapterTime } - } - returnData.setData(data) - } - } - - fun getChapterList(parameters: Map>): ReturnData { - val bookUrl = parameters["url"]?.getOrNull(0) - val returnData = ReturnData() - if (bookUrl.isNullOrEmpty()) { - return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") - } - val chapterList = App.db.bookChapterDao().getChapterList(bookUrl) - return returnData.setData(chapterList) - } - - fun getBookContent(parameters: Map>): ReturnData { - val bookUrl = parameters["url"]?.getOrNull(0) - val index = parameters["index"]?.getOrNull(0)?.toInt() - val returnData = ReturnData() - if (bookUrl.isNullOrEmpty()) { - return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") - } - if (index == null) { - return returnData.setErrorMsg("参数index不能为空, 请指定目录序号") - } - val book = App.db.bookDao().getBook(bookUrl) - val chapter = App.db.bookChapterDao().getChapter(bookUrl, index) - if (book == null || chapter == null) { - returnData.setErrorMsg("未找到") - } else { - val content: String? = BookHelp.getContent(book, chapter) - if (content != null) { - saveBookReadIndex(book, index) - returnData.setData(content) - } else { - App.db.bookSourceDao().getBookSource(book.origin)?.let { source -> - runBlocking { - WebBook(source).getContentSuspend(book, chapter) - }.let { - saveBookReadIndex(book, index) - returnData.setData(it) - } - } ?: returnData.setErrorMsg("未找到书源") - } - } - return returnData - } - - fun saveBook(postData: String?): ReturnData { - val book = GSON.fromJsonObject(postData) - val returnData = ReturnData() - if (book != null) { - App.db.bookDao().insert(book) - return returnData.setData("") - } - return returnData.setErrorMsg("格式不对") - } - - private fun saveBookReadIndex(book: Book, index: Int) { - if (index > book.durChapterIndex) { - book.durChapterIndex = index - book.durChapterTime = System.currentTimeMillis() - App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { - book.durChapterTitle = it.title - } - App.db.bookDao().update(book) - if (ReadBook.book?.bookUrl == book.bookUrl) { - ReadBook.book = book - ReadBook.durChapterIndex = index - } - } - } - -} diff --git a/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt deleted file mode 100644 index ae3172d2f..000000000 --- a/app/src/main/java/io/legado/app/web/controller/SourceDebugWebSocket.kt +++ /dev/null @@ -1,83 +0,0 @@ -package io.legado.app.web.controller - - -import fi.iki.elonen.NanoHTTPD -import fi.iki.elonen.NanoWSD -import io.legado.app.App -import io.legado.app.R -import io.legado.app.model.Debug -import io.legado.app.model.webBook.WebBook -import io.legado.app.utils.GSON -import io.legado.app.utils.fromJsonObject -import io.legado.app.utils.isJson -import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers.IO -import java.io.IOException - - -class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : - NanoWSD.WebSocket(handshakeRequest), - CoroutineScope by MainScope(), - Debug.Callback { - - - override fun onOpen() { - launch(IO) { - do { - delay(30000) - runCatching { - ping(byteArrayOf("ping".toByte())) - } - } while (isOpen) - } - } - - override fun onClose( - code: NanoWSD.WebSocketFrame.CloseCode, - reason: String, - initiatedByRemote: Boolean - ) { - cancel() - Debug.cancelDebug(true) - } - - override fun onMessage(message: NanoWSD.WebSocketFrame) { - if (!message.textPayload.isJson()) return - kotlin.runCatching { - val debugBean = GSON.fromJsonObject>(message.textPayload) - if (debugBean != null) { - val tag = debugBean["tag"] - val key = debugBean["key"] - if (tag.isNullOrBlank() || key.isNullOrBlank()) { - kotlin.runCatching { - send(App.INSTANCE.getString(R.string.cannot_empty)) - close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) - } - return - } - App.db.bookSourceDao().getBookSource(tag)?.let { - Debug.callback = this - Debug.startDebug(WebBook(it), key) - } - } - } - } - - override fun onPong(pong: NanoWSD.WebSocketFrame) { - - } - - override fun onException(exception: IOException) { - Debug.cancelDebug(true) - } - - override fun printLog(state: Int, msg: String) { - kotlin.runCatching { - send(msg) - if (state == -1 || state == 1000) { - Debug.cancelDebug(true) - } - } - } - -} diff --git a/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt b/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt index 503f7aad5..22e375555 100644 --- a/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt +++ b/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt @@ -3,13 +3,13 @@ package io.legado.app.web.utils import android.content.res.AssetManager import android.text.TextUtils import fi.iki.elonen.NanoHTTPD -import io.legado.app.App +import splitties.init.appCtx import java.io.File import java.io.IOException class AssetsWeb(rootPath: String) { - private val assetManager: AssetManager = App.INSTANCE.assets + private val assetManager: AssetManager = appCtx.assets private var rootPath = "web" init { diff --git a/app/src/main/res/color/selector_image.xml b/app/src/main/res/color/selector_image.xml index 081b03cf0..ffc800f9f 100644 --- a/app/src/main/res/color/selector_image.xml +++ b/app/src/main/res/color/selector_image.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml deleted file mode 100644 index 3f2b1987a..000000000 --- a/app/src/main/res/drawable/ic_about.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back_last.xml b/app/src/main/res/drawable/ic_back_last.xml deleted file mode 100644 index f1355c878..000000000 --- a/app/src/main/res/drawable/ic_back_last.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_close.xml b/app/src/main/res/drawable/ic_baseline_close.xml new file mode 100644 index 000000000..b8af066c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_book_source_manage.xml b/app/src/main/res/drawable/ic_book_source_manage.xml deleted file mode 100644 index 11fb46c9e..000000000 --- a/app/src/main/res/drawable/ic_book_source_manage.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml deleted file mode 100644 index 21715879d..000000000 --- a/app/src/main/res/drawable/ic_cancel.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cfg_jz.xml b/app/src/main/res/drawable/ic_cfg_donate.xml similarity index 98% rename from app/src/main/res/drawable/ic_cfg_jz.xml rename to app/src/main/res/drawable/ic_cfg_donate.xml index 710bae7fd..9d5bb4788 100644 --- a/app/src/main/res/drawable/ic_cfg_jz.xml +++ b/app/src/main/res/drawable/ic_cfg_donate.xml @@ -1,12 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/ic_disclaimer.xml b/app/src/main/res/drawable/ic_disclaimer.xml deleted file mode 100644 index 6cc4ea597..000000000 --- a/app/src/main/res/drawable/ic_disclaimer.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_donate.xml b/app/src/main/res/drawable/ic_donate.xml deleted file mode 100644 index 1cf58b952..000000000 --- a/app/src/main/res/drawable/ic_donate.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exchange_order.xml b/app/src/main/res/drawable/ic_exchange_order.xml index 36c2bcefc..2beeb7441 100644 --- a/app/src/main/res/drawable/ic_exchange_order.xml +++ b/app/src/main/res/drawable/ic_exchange_order.xml @@ -5,7 +5,8 @@ android:viewportWidth="1112" android:viewportHeight="1024"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml new file mode 100644 index 000000000..78d6a76dd --- /dev/null +++ b/app/src/main/res/drawable/ic_export.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_format_line_spacing.xml b/app/src/main/res/drawable/ic_format_line_spacing.xml deleted file mode 100644 index 3308038da..000000000 --- a/app/src/main/res/drawable/ic_format_line_spacing.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml index b9929a808..dc493329e 100644 --- a/app/src/main/res/drawable/ic_import.xml +++ b/app/src/main/res/drawable/ic_import.xml @@ -1,32 +1,15 @@ - + + android:viewportWidth="1280" + android:viewportHeight="1024"> + + android:fillColor="#000000" + android:pathData="M394 376V230.8c0-19.2 15.6-34.8 34.8-34.8h477.4c19.2 0 34.8 15.6 34.8 34.8v477.4c0 19.2-15.6 34.8-34.8 34.8H761c-18.8 0-34 15.2-34 34s15.2 34 34 34h178.9c38.2 0 69.1-31 69.1-69.1V197.1c0-38.2-31-69.1-69.1-69.1H395.1c-38.2 0-69.1 31-69.1 69.1V376c0 18.8 15.2 34 34 34s34-15.2 34-34z" /> - - - - + android:fillColor="#000000" + android:pathData="M678.9 618.8l-0.1-0.1L545.1 485c-13.3-13.3-34.8-13.3-48.1 0-13.3 13.3-13.3 34.8 0 48.1l75.9 75.9H360c-18.8 0-34 15.2-34 34s15.2 34 34 34h212.7L497 752.7c-13.3 13.3-13.3 34.8 0 48.1 13.3 13.3 34.8 13.3 48.1 0l131.6-131.6c7.5-6.2 12.3-15.6 12.3-26.1 0-9.6-3.9-18.1-10.1-24.3z" /> + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_last_read.xml b/app/src/main/res/drawable/ic_last_read.xml deleted file mode 100644 index 9a2791b42..000000000 --- a/app/src/main/res/drawable/ic_last_read.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml deleted file mode 100644 index 4a1b0d561..000000000 --- a/app/src/main/res/drawable/ic_list.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mail.xml b/app/src/main/res/drawable/ic_mail.xml deleted file mode 100644 index cbe92189a..000000000 --- a/app/src/main/res/drawable/ic_mail.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_camera.xml b/app/src/main/res/drawable/ic_menu_camera.xml deleted file mode 100644 index 634fe9221..000000000 --- a/app/src/main/res/drawable/ic_menu_camera.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_menu_gallery.xml b/app/src/main/res/drawable/ic_menu_gallery.xml deleted file mode 100644 index 03c77099f..000000000 --- a/app/src/main/res/drawable/ic_menu_gallery.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu_manage.xml b/app/src/main/res/drawable/ic_menu_manage.xml deleted file mode 100644 index aeb047d02..000000000 --- a/app/src/main/res/drawable/ic_menu_manage.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_send.xml b/app/src/main/res/drawable/ic_menu_send.xml deleted file mode 100644 index fdf1c9009..000000000 --- a/app/src/main/res/drawable/ic_menu_send.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu_share.xml b/app/src/main/res/drawable/ic_menu_share.xml deleted file mode 100644 index 338d95ad5..000000000 --- a/app/src/main/res/drawable/ic_menu_share.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_menu_slideshow.xml b/app/src/main/res/drawable/ic_menu_slideshow.xml deleted file mode 100644 index 5e9e163a5..000000000 --- a/app/src/main/res/drawable/ic_menu_slideshow.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml index 3285a9327..2ca607005 100644 --- a/app/src/main/res/drawable/ic_scan.xml +++ b/app/src/main/res/drawable/ic_scan.xml @@ -1,5 +1,6 @@ + android:pathData="M725.333333 128h149.184C886.421333 128 896 137.578667 896 149.482667V298.666667a42.666667 42.666667 0 1 0 85.333333 0V149.482667A106.752 106.752 0 0 0 874.517333 42.666667H725.333333a42.666667 42.666667 0 1 0 0 85.333333z m170.666667 597.333333v149.184c0 11.904-9.578667 21.482667-21.482667 21.482667H725.333333a42.666667 42.666667 0 1 0 0 85.333333h149.184A106.752 106.752 0 0 0 981.333333 874.517333V725.333333a42.666667 42.666667 0 1 0-85.333333 0z m-597.333333 170.666667H149.482667A21.418667 21.418667 0 0 1 128 874.517333V725.333333a42.666667 42.666667 0 1 0-85.333333 0v149.184A106.752 106.752 0 0 0 149.482667 981.333333H298.666667a42.666667 42.666667 0 1 0 0-85.333333zM128 298.666667a42.666667 42.666667 0 1 1-85.333333 0V149.482667A106.752 106.752 0 0 1 149.482667 42.666667H298.666667a42.666667 42.666667 0 1 1 0 85.333333H149.482667C137.578667 128 128 137.578667 128 149.482667V298.666667zM85.333333 554.666667a42.666667 42.666667 0 1 1 0-85.333334h853.333334a42.666667 42.666667 0 1 1 0 85.333334H85.333333z" + tools:ignore="VectorPath" /> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 0424ce07a..1d4297767 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -1,5 +1,6 @@ + android:pathData="M13.735,20 L10.261,20 L10.12,18.065 C10.102,17.807,9.944,17.585,9.721,17.492 C9.472,17.387,9.214,17.429,9.024,17.592 L7.568,18.848 L5.113,16.391 L6.369,14.937 C6.531,14.749,6.573,14.49,6.477,14.258 C6.376,14.013,6.166,13.857,5.913,13.836 L4,13.697 L4,10.226 L5.916,10.085 C6.164,10.066,6.378,9.913,6.473,9.683 C6.574,9.438,6.536,9.185,6.371,8.992 L5.113,7.539 L7.573,5.094 L9.026,6.376 C9.222,6.544,9.495,6.605,9.703,6.522 L9.844,6.461 C10.026,6.273,10.107,6.141,10.119,5.974 L10.262,4 L13.734,4 L13.877,5.894 C13.895,6.139,14.041,6.335,14.277,6.43 C14.527,6.537,14.786,6.494,14.975,6.332 L16.427,5.073 L18.886,7.531 L17.629,8.984 C17.467,9.172,17.426,9.431,17.521,9.664 C17.622,9.909,17.833,10.066,18.085,10.085 L20,10.227 L20,13.698 L18.085,13.837 C17.837,13.858,17.623,14.011,17.528,14.242 C17.426,14.487,17.465,14.747,17.63,14.94 L18.886,16.391 L16.429,18.85 L14.975,17.594 C14.799,17.444,14.544,17.398,14.322,17.479 L14.295,17.493 C14.051,17.594,13.894,17.816,13.876,18.068 L13.735,20 Z M11.561,18.604 L12.437,18.604 L12.482,17.963 C12.538,17.216,13.011,16.538,13.691,16.232 L13.737,16.211 C14.526,15.883,15.308,16.04,15.886,16.54 L16.358,16.945 L16.98,16.321 L16.574,15.852 C16.064,15.26,15.933,14.446,16.231,13.725 C16.535,12.993,17.206,12.502,17.982,12.446 L18.604,12.399 L18.604,11.524 L17.984,11.477 C17.207,11.421,16.536,10.938,16.239,10.215 C15.935,9.485,16.065,8.664,16.574,8.07 L16.98,7.601 L16.358,6.979 L15.886,7.386 C15.308,7.885,14.472,8.022,13.761,7.729 C13.029,7.428,12.54,6.763,12.482,5.996 L12.438,5.397 L11.558,5.397 L11.514,6 C11.457,6.773,10.973,7.438,10.252,7.733 L10.091,7.789 C9.416,8.008,8.652,7.856,8.112,7.392 L7.639,6.982 L7.02,7.602 L7.426,8.075 C7.936,8.665,8.067,9.479,7.77,10.202 C7.467,10.931,6.796,11.422,6.02,11.477 L5.397,11.524 L5.397,12.399 L6.019,12.446 C6.796,12.503,7.465,12.987,7.762,13.71 C8.066,14.44,7.935,15.261,7.426,15.852 L7.02,16.32 L7.64,16.941 L8.112,16.535 C8.692,16.035,9.528,15.898,10.239,16.194 C10.958,16.488,11.458,17.181,11.515,17.961 L11.561,18.604 Z" + tools:ignore="VectorPath" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index 0022d3b38..df884e817 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,14 +1,11 @@ - - - - \ No newline at end of file + + + diff --git a/app/src/main/res/drawable/ic_swap_outline_24dp.xml b/app/src/main/res/drawable/ic_swap_outline_24dp.xml deleted file mode 100644 index f9026522d..000000000 --- a/app/src/main/res/drawable/ic_swap_outline_24dp.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_theme.xml b/app/src/main/res/drawable/ic_theme.xml deleted file mode 100644 index 90da16498..000000000 --- a/app/src/main/res/drawable/ic_theme.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_time_add_24dp.xml b/app/src/main/res/drawable/ic_time_add_24dp.xml index b3960a060..66afd8f8f 100644 --- a/app/src/main/res/drawable/ic_time_add_24dp.xml +++ b/app/src/main/res/drawable/ic_time_add_24dp.xml @@ -1,4 +1,5 @@ + android:pathData="M698.235505586 166.666668C698.235505586 166.666668 869.372215288 309.497277476 869.372215288 309.497277476C869.372215288 309.497277476 821.823923241 366.518082099 821.823923241 366.518082099C821.823923241 366.518082099 650.575796871 223.76176429 650.575796871 223.76176429C650.575796871 223.76176429 698.235505586 166.666668 698.235505586 166.666668M301.727348247 166.666668C301.727348247 166.666668 349.349936128 223.724618456 349.349936128 223.724618456C349.349936128 223.724618456 178.176084759 366.518082099 178.176084759 366.518082099C178.176084759 366.518082099 130.627792712 309.460131642 130.627792712 309.460131642C130.627792712 309.460131642 301.727348247 166.666668 301.727348247 166.666668M499.981420667 248.018822817C315.360335856 248.018822817 165.657509659 397.721628182 165.657509659 582.342754659C165.657509659 766.963839469 315.360335856 916.666674 499.981420667 916.666674C684.602547143 916.666674 834.305381674 766.963839469 834.305381674 582.342754659C834.305381674 397.721628182 684.602547143 248.018822817 499.981420667 248.018822817C499.981420667 248.018822817 499.981420667 248.018822817 499.981420667 248.018822817M499.981420667 842.372465072C356.593619519 842.372465072 239.95171442 725.730547473 239.95171442 582.342754659C239.95171442 438.954920178 356.593619519 322.313027579 499.981420667 322.313027579C643.369255147 322.313027579 760.011172747 438.954920178 760.011172747 582.342754659C760.011172747 725.730547473 643.369255147 842.372465072 499.981420667 842.372465072C499.981420667 842.372465072 499.981420667 842.372465072 499.981420667 842.372465072M537.128545964 433.754336803C537.128545964 433.754336803 462.834337036 433.754336803 462.834337036 433.754336803C462.834337036 433.754336803 462.834337036 545.195629362 462.834337036 545.195629362C462.834337036 545.195629362 351.393023644 545.195629362 351.393023644 545.195629362C351.393023644 545.195629362 351.393023644 619.489838289 351.393023644 619.489838289C351.393023644 619.489838289 462.834337036 619.489838289 462.834337036 619.489838289C462.834337036 619.489838289 462.834337036 730.931172514 462.834337036 730.931172514C462.834337036 730.931172514 537.128545964 730.931172514 537.128545964 730.931172514C537.128545964 730.931172514 537.128545964 619.489838289 537.128545964 619.489838289C537.128545964 619.489838289 648.569838522 619.489838289 648.569838522 619.489838289C648.569838522 619.489838289 648.569838522 545.195629362 648.569838522 545.195629362C648.569838522 545.195629362 537.128545964 545.195629362 537.128545964 545.195629362C537.128545964 545.195629362 537.128545964 433.754336803 537.128545964 433.754336803" + tools:ignore="VectorPath" /> diff --git a/app/src/main/res/drawable/ic_top_source.xml b/app/src/main/res/drawable/ic_top_source.xml deleted file mode 100644 index 0b19c9cb4..000000000 --- a/app/src/main/res/drawable/ic_top_source.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tune.xml b/app/src/main/res/drawable/ic_tune.xml deleted file mode 100644 index cdc9858e1..000000000 --- a/app/src/main/res/drawable/ic_tune.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_version.xml b/app/src/main/res/drawable/ic_version.xml deleted file mode 100644 index 551bee2c8..000000000 --- a/app/src/main/res/drawable/ic_version.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_web_service_phone.xml b/app/src/main/res/drawable/ic_web_service_phone.xml deleted file mode 100644 index f48dd2c8d..000000000 --- a/app/src/main/res/drawable/ic_web_service_phone.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/image_legado.png b/app/src/main/res/drawable/image_legado.png new file mode 100644 index 000000000..6d101d806 Binary files /dev/null and b/app/src/main/res/drawable/image_legado.png differ diff --git a/app/src/main/res/drawable/shape_translucent_card.xml b/app/src/main/res/drawable/shape_translucent_card.xml new file mode 100644 index 000000000..827bc335d --- /dev/null +++ b/app/src/main/res/drawable/shape_translucent_card.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_audio_play.xml b/app/src/main/res/layout/activity_audio_play.xml index 235b20cb4..20c64aba6 100644 --- a/app/src/main/res/layout/activity_audio_play.xml +++ b/app/src/main/res/layout/activity_audio_play.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/background" - tools:context=".ui.audio.AudioPlayActivity"> + tools:context=".ui.book.audio.AudioPlayActivity"> - diff --git a/app/src/main/res/layout/activity_book_search.xml b/app/src/main/res/layout/activity_book_search.xml index 6daf33cd4..4996fd3ff 100644 --- a/app/src/main/res/layout/activity_book_search.xml +++ b/app/src/main/res/layout/activity_book_search.xml @@ -1,20 +1,19 @@ - + android:id="@+id/title_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" + app:contentLayout="@layout/view_search" + app:layout_constraintTop_toTopOf="parent" + app:title="搜索" /> + android:orientation="vertical" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/title_bar"> diff --git a/app/src/main/res/layout/activity_book_source.xml b/app/src/main/res/layout/activity_book_source.xml index 090be7b00..21f662944 100644 --- a/app/src/main/res/layout/activity_book_source.xml +++ b/app/src/main/res/layout/activity_book_source.xml @@ -22,7 +22,8 @@ + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> diff --git a/app/src/main/res/layout/activity_book_source_edit.xml b/app/src/main/res/layout/activity_book_source_edit.xml index ab8104256..2e8ce28aa 100644 --- a/app/src/main/res/layout/activity_book_source_edit.xml +++ b/app/src/main/res/layout/activity_book_source_edit.xml @@ -2,7 +2,6 @@ @@ -12,7 +11,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:fitStatusBar="false" - app:layout_constraintTop_toTopOf="parent" app:title="@string/edit_book_source" /> + android:text="@string/discovery" /> diff --git a/app/src/main/res/layout/activity_download.xml b/app/src/main/res/layout/activity_cache_book.xml similarity index 90% rename from app/src/main/res/layout/activity_download.xml rename to app/src/main/res/layout/activity_cache_book.xml index 2f7ebd6f2..c499af731 100644 --- a/app/src/main/res/layout/activity_download.xml +++ b/app/src/main/res/layout/activity_cache_book.xml @@ -9,7 +9,7 @@ android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:title="@string/download_offline" /> + app:title="@string/offline_cache" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + android:id="@+id/title_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" + app:contentLayout="@layout/view_tab_layout" /> + android:id="@+id/view_pager" + android:layout_width="match_parent" + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_explore_show.xml b/app/src/main/res/layout/activity_explore_show.xml index 9bfab0f58..05b3d96df 100644 --- a/app/src/main/res/layout/activity_explore_show.xml +++ b/app/src/main/res/layout/activity_explore_show.xml @@ -12,7 +12,7 @@ android:layout_height="wrap_content" app:contentInsetStartWithNavigation="0dp" app:layout_constraintTop_toTopOf="parent" - app:title="@string/find" /> + app:title="@string/discovery" /> - + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_search" /> diff --git a/app/src/main/res/layout/activity_import_book.xml b/app/src/main/res/layout/activity_import_book.xml index a15b4fd87..73635c2f4 100644 --- a/app/src/main/res/layout/activity_import_book.xml +++ b/app/src/main/res/layout/activity_import_book.xml @@ -53,11 +53,17 @@ tools:ignore="UnusedAttribute" /> + + + + - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index abd62d1d7..a387ecb36 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,22 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> + + - - + app:menu="@menu/main_bnv" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_qrcode_capture.xml b/app/src/main/res/layout/activity_qrcode_capture.xml index 301d1e845..379109187 100644 --- a/app/src/main/res/layout/activity_qrcode_capture.xml +++ b/app/src/main/res/layout/activity_qrcode_capture.xml @@ -1,45 +1,9 @@ - - - - + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - - - + android:layout_height="match_parent" /> - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_read_record.xml b/app/src/main/res/layout/activity_read_record.xml index 382d390bf..9d94c5c8f 100644 --- a/app/src/main/res/layout/activity_read_record.xml +++ b/app/src/main/res/layout/activity_read_record.xml @@ -1,8 +1,8 @@ - + - + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_replace_edit.xml b/app/src/main/res/layout/activity_replace_edit.xml similarity index 74% rename from app/src/main/res/layout/dialog_replace_edit.xml rename to app/src/main/res/layout/activity_replace_edit.xml index c2b52ac39..af4253705 100644 --- a/app/src/main/res/layout/dialog_replace_edit.xml +++ b/app/src/main/res/layout/activity_replace_edit.xml @@ -1,23 +1,23 @@ + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_height="wrap_content"> - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + + + tools:context=".ui.replace.ReplaceRuleActivity"> - + android:layout_weight="1"> + + + + @@ -13,13 +12,19 @@ app:title="@string/favorites" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintVertical_bias="0.0"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_source.xml b/app/src/main/res/layout/activity_rss_source.xml index 527a9444f..703eea64f 100644 --- a/app/src/main/res/layout/activity_rss_source.xml +++ b/app/src/main/res/layout/activity_rss_source.xml @@ -22,7 +22,8 @@ + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> diff --git a/app/src/main/res/layout/activity_rss_source_edit.xml b/app/src/main/res/layout/activity_rss_source_edit.xml index ace4e6b64..f576312db 100644 --- a/app/src/main/res/layout/activity_rss_source_edit.xml +++ b/app/src/main/res/layout/activity_rss_source_edit.xml @@ -2,7 +2,6 @@ @@ -19,9 +18,17 @@ + + - + android:checked="false" + android:text="@string/single_url" /> + + + + + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_source_edit" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rule_sub.xml b/app/src/main/res/layout/activity_rule_sub.xml new file mode 100644 index 000000000..b4704f5eb --- /dev/null +++ b/app/src/main/res/layout/activity_rule_sub.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_content.xml b/app/src/main/res/layout/activity_search_content.xml new file mode 100644 index 000000000..e9bde053b --- /dev/null +++ b/app/src/main/res/layout/activity_search_content.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_source_debug.xml b/app/src/main/res/layout/activity_source_debug.xml index 1b79a6058..94620a861 100644 --- a/app/src/main/res/layout/activity_source_debug.xml +++ b/app/src/main/res/layout/activity_source_debug.xml @@ -10,6 +10,7 @@ android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" app:contentLayout="@layout/view_search" app:layout_constraintTop_toTopOf="parent" app:title="@string/debug_source" /> @@ -18,6 +19,7 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="0dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintTop_toBottomOf="@+id/title_bar" app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml index 26334eab6..707978c92 100644 --- a/app/src/main/res/layout/activity_welcome.xml +++ b/app/src/main/res/layout/activity_welcome.xml @@ -2,7 +2,6 @@ diff --git a/app/src/main/res/layout/dialog_bookmark.xml b/app/src/main/res/layout/dialog_bookmark.xml new file mode 100644 index 000000000..231b945ca --- /dev/null +++ b/app/src/main/res/layout/dialog_bookmark.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_bookshelf_config.xml b/app/src/main/res/layout/dialog_bookshelf_config.xml index 944913f4c..cbf550197 100644 --- a/app/src/main/res/layout/dialog_bookshelf_config.xml +++ b/app/src/main/res/layout/dialog_bookshelf_config.xml @@ -14,6 +14,27 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="parent"> + + + + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_click_action_config.xml b/app/src/main/res/layout/dialog_click_action_config.xml new file mode 100644 index 000000000..af1c00029 --- /dev/null +++ b/app/src/main/res/layout/dialog_click_action_config.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_dict.xml b/app/src/main/res/layout/dialog_dict.xml new file mode 100644 index 000000000..7eed5f326 --- /dev/null +++ b/app/src/main/res/layout/dialog_dict.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_page_key.xml b/app/src/main/res/layout/dialog_page_key.xml index f2e1c8a3a..bca82ea91 100644 --- a/app/src/main/res/layout/dialog_page_key.xml +++ b/app/src/main/res/layout/dialog_page_key.xml @@ -26,7 +26,6 @@ android:id="@+id/et_prev" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="number" android:singleLine="true" /> @@ -41,22 +40,55 @@ android:id="@+id/et_next" android:layout_width="match_parent" android:layout_height="wrap_content" - android:inputType="number" android:singleLine="true" /> + android:textColor="@color/secondaryText" + android:text="@string/page_key_set_help" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_progressbar_view.xml b/app/src/main/res/layout/dialog_progressbar_view.xml new file mode 100644 index 000000000..f310c9ced --- /dev/null +++ b/app/src/main/res/layout/dialog_progressbar_view.xml @@ -0,0 +1,36 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_aloud.xml b/app/src/main/res/layout/dialog_read_aloud.xml index 8d5b33f43..878cdbf8e 100644 --- a/app/src/main/res/layout/dialog_read_aloud.xml +++ b/app/src/main/res/layout/dialog_read_aloud.xml @@ -225,7 +225,8 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" android:text="@string/chapter_list" - android:maxLines="1" + android:singleLine="true" + android:ellipsize="middle" android:textColor="@color/primaryText" android:textSize="12sp" /> @@ -263,7 +264,8 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" android:text="@string/main_menu" - android:maxLines="1" + android:singleLine="true" + android:ellipsize="middle" android:textColor="@color/primaryText" android:textSize="12sp" /> @@ -301,7 +303,8 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" android:text="@string/to_backstage" - android:maxLines="1" + android:singleLine="true" + android:ellipsize="middle" android:textColor="@color/primaryText" android:textSize="12sp" /> @@ -339,7 +342,8 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" android:text="@string/setting" - android:maxLines="1" + android:singleLine="true" + android:ellipsize="middle" android:textColor="@color/primaryText" android:textSize="12sp" /> diff --git a/app/src/main/res/layout/dialog_read_bg_text.xml b/app/src/main/res/layout/dialog_read_bg_text.xml index e4b51710e..175bb68c0 100644 --- a/app/src/main/res/layout/dialog_read_bg_text.xml +++ b/app/src/main/res/layout/dialog_read_bg_text.xml @@ -9,6 +9,51 @@ android:orientation="vertical" android:padding="10dp"> + + + + + + + + + + + + + + - + android:contentDescription="@string/import_str" + android:src="@drawable/ic_import" + android:tooltipText="@string/import_str" + tools:ignore="UnusedAttribute" /> - + android:contentDescription="@string/import_str" + android:src="@drawable/ic_export" + android:tooltipText="@string/export_str" + tools:ignore="UnusedAttribute" /> - + android:contentDescription="@string/delete" + android:src="@drawable/ic_clear_all" + android:tooltipText="@string/delete" + tools:ignore="UnusedAttribute" /> + - + android:orientation="horizontal" + android:padding="6dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_bg_image" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_book_style.xml b/app/src/main/res/layout/dialog_read_book_style.xml index 2b0e7fd4a..32e1fbae0 100644 --- a/app/src/main/res/layout/dialog_read_book_style.xml +++ b/app/src/main/res/layout/dialog_read_book_style.xml @@ -1,6 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tool:listitem="@layout/item_read_style" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_padding.xml b/app/src/main/res/layout/dialog_read_padding.xml index c9be863dd..ab08bf930 100644 --- a/app/src/main/res/layout/dialog_read_padding.xml +++ b/app/src/main/res/layout/dialog_read_padding.xml @@ -1,31 +1,130 @@ - + android:layout_height="wrap_content"> + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="10dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:text="@string/footer" + android:textSize="18sp" /> @@ -41,126 +140,33 @@ + app:max="100" + app:title="@string/padding_top" /> + app:max="100" + app:title="@string/padding_bottom" /> + app:max="100" + app:title="@string/padding_left" /> + app:max="100" + app:title="@string/padding_right" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/dialog_recycler_view.xml b/app/src/main/res/layout/dialog_recycler_view.xml index e7ca72d9a..7f577dde3 100644 --- a/app/src/main/res/layout/dialog_recycler_view.xml +++ b/app/src/main/res/layout/dialog_recycler_view.xml @@ -1,63 +1,90 @@ + android:layout_height="match_parent" + android:orientation="vertical"> + app:popupTheme="@style/AppTheme.PopupOverlay" + app:titleTextAppearance="@style/ToolbarTitle" /> - + android:layout_weight="1"> - + - + android:layout_gravity="center" + app:loading_width="2dp" /> - + android:padding="16dp" + android:layout_gravity="center" + android:visibility="gone" + android:textColor="@color/secondaryText" /> + + + + - + android:orientation="horizontal"> + + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rule_sub_edit.xml b/app/src/main/res/layout/dialog_rule_sub_edit.xml new file mode 100644 index 000000000..3fd02524c --- /dev/null +++ b/app/src/main/res/layout/dialog_rule_sub_edit.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_tip_config.xml b/app/src/main/res/layout/dialog_tip_config.xml index 9ef889a05..2cd7353ab 100644 --- a/app/src/main/res/layout/dialog_tip_config.xml +++ b/app/src/main/res/layout/dialog_tip_config.xml @@ -1,125 +1,327 @@ - - - + android:layout_height="wrap_content"> + android:orientation="vertical" + android:padding="16dp"> - + android:text="@string/body_title" + android:textSize="18sp" /> - + android:orientation="horizontal"> - + + + + + + + + android:padding="3dp" + app:max="10" + app:title="@string/title_font_size" /> - + android:padding="3dp" + app:max="100" + app:title="@string/title_margin_top" /> - + + + android:text="@string/header" + android:textSize="18sp" /> - + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="RtlHardcoded,RtlSymmetry"> - + - + - + - + + + + + + + + + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="RtlHardcoded,RtlSymmetry"> - + + + + + + + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="RtlHardcoded,RtlSymmetry"> + + + + + + - + android:text="@string/footer" + android:textSize="18sp" /> - + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="RtlHardcoded,RtlSymmetry"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:text="@string/header_footer" + android:textSize="18sp" /> - + android:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="RtlHardcoded,RtlSymmetry"> - + - + - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_title_config.xml b/app/src/main/res/layout/dialog_title_config.xml deleted file mode 100644 index 9480c1602..000000000 --- a/app/src/main/res/layout/dialog_title_config.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_toc_regex.xml b/app/src/main/res/layout/dialog_toc_regex.xml index 95317f693..2712cabf4 100644 --- a/app/src/main/res/layout/dialog_toc_regex.xml +++ b/app/src/main/res/layout/dialog_toc_regex.xml @@ -18,7 +18,8 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1" /> + android:layout_weight="1" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_books.xml b/app/src/main/res/layout/fragment_books.xml index 30f8ce74c..8da85f913 100644 --- a/app/src/main/res/layout/fragment_books.xml +++ b/app/src/main/res/layout/fragment_books.xml @@ -1,12 +1,32 @@ - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="match_parent"> + + + + + + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookshelf.xml b/app/src/main/res/layout/fragment_bookshelf.xml index ea0a9ab40..91b638e02 100644 --- a/app/src/main/res/layout/fragment_bookshelf.xml +++ b/app/src/main/res/layout/fragment_bookshelf.xml @@ -1,5 +1,5 @@ - + app:title="@string/bookshelf" /> + android:layout_height="match_parent" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookshelf1.xml b/app/src/main/res/layout/fragment_bookshelf1.xml new file mode 100644 index 000000000..35ce510ec --- /dev/null +++ b/app/src/main/res/layout/fragment_bookshelf1.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml new file mode 100644 index 000000000..1b473573e --- /dev/null +++ b/app/src/main/res/layout/fragment_explore.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_find_book.xml b/app/src/main/res/layout/fragment_find_book.xml deleted file mode 100644 index 8609db403..000000000 --- a/app/src/main/res/layout/fragment_find_book.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rss.xml b/app/src/main/res/layout/fragment_rss.xml index f1469fbd7..f5a66f4c7 100644 --- a/app/src/main/res/layout/fragment_rss.xml +++ b/app/src/main/res/layout/fragment_rss.xml @@ -1,20 +1,42 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> - + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="@id/title_bar" + app:layout_constraintBottom_toBottomOf="parent" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + app:spanCount="4" + tools:listitem="@layout/item_rss" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rss_articles.xml b/app/src/main/res/layout/fragment_rss_articles.xml index 5ca0c9574..867cf055e 100644 --- a/app/src/main/res/layout/fragment_rss_articles.xml +++ b/app/src/main/res/layout/fragment_rss_articles.xml @@ -1,15 +1,12 @@ - + android:layout_height="match_parent"> - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_1line_text_and_del.xml b/app/src/main/res/layout/item_1line_text_and_del.xml index 9bb5e9c2a..85af06345 100644 --- a/app/src/main/res/layout/item_1line_text_and_del.xml +++ b/app/src/main/res/layout/item_1line_text_and_del.xml @@ -1,5 +1,6 @@ + android:singleLine="true" + android:textColor="@color/primaryText" /> + app:tint="@color/primaryText" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_arrange_book.xml b/app/src/main/res/layout/item_arrange_book.xml index 7323a53c7..bc397d43a 100644 --- a/app/src/main/res/layout/item_arrange_book.xml +++ b/app/src/main/res/layout/item_arrange_book.xml @@ -52,6 +52,7 @@ android:padding="3dp" android:hint="@string/no_group" android:textSize="12sp" + android:textColor="@color/secondaryText" android:singleLine="true" android:layout_marginTop="8dp" app:layout_constraintLeft_toRightOf="@+id/checkbox" @@ -66,6 +67,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:padding="10dp" android:text="@string/group" + android:textColor="@color/secondaryText" app:layout_constraintTop_toBottomOf="@id/tv_author" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toLeftOf="@+id/tv_delete" /> @@ -77,6 +79,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:padding="10dp" android:text="@string/delete" + android:textColor="@color/secondaryText" app:layout_constraintTop_toBottomOf="@id/tv_author" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toRightOf="parent" /> diff --git a/app/src/main/res/layout/item_bg_image.xml b/app/src/main/res/layout/item_bg_image.xml index 2530fb894..4bf9cf5fe 100644 --- a/app/src/main/res/layout/item_bg_image.xml +++ b/app/src/main/res/layout/item_bg_image.xml @@ -1,7 +1,8 @@ @@ -11,7 +12,8 @@ android:layout_height="0dp" android:layout_weight="1" android:scaleType="fitCenter" - android:contentDescription="@string/bg_image" /> + android:contentDescription="@string/bg_image" + tool:src="@drawable/ic_image" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_book_source.xml b/app/src/main/res/layout/item_book_source.xml index a9089f1af..4f3584999 100644 --- a/app/src/main/res/layout/item_book_source.xml +++ b/app/src/main/res/layout/item_book_source.xml @@ -62,6 +62,7 @@ android:id="@+id/iv_explore" android:layout_width="8dp" android:layout_height="8dp" + android:scaleType="centerCrop" android:contentDescription="@string/more_menu" android:src="@color/md_green_600" app:layout_constraintRight_toRightOf="parent" diff --git a/app/src/main/res/layout/item_bookmark.xml b/app/src/main/res/layout/item_bookmark.xml index 971d56416..362acff10 100644 --- a/app/src/main/res/layout/item_bookmark.xml +++ b/app/src/main/res/layout/item_bookmark.xml @@ -13,6 +13,14 @@ android:padding="4dp" android:singleLine="true" /> + + + android:clickable="true" + android:focusable="true" + tools:ignore="UnusedAttribute"> - + android:orientation="vertical" + android:paddingTop="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + android:layout_marginTop="6dp" + android:ellipsize="end" + android:gravity="top|center_horizontal" + android:includeFontPadding="false" + android:lines="2" + android:text="@string/book_name" + android:textColor="@color/primaryText" + android:textSize="12sp" + tools:ignore="RtlHardcoded,RtlSymmetry" /> - + - + + + - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookshelf_grid_group.xml b/app/src/main/res/layout/item_bookshelf_grid_group.xml new file mode 100644 index 000000000..648d3b85f --- /dev/null +++ b/app/src/main/res/layout/item_bookshelf_grid_group.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_bookshelf_list.xml b/app/src/main/res/layout/item_bookshelf_list.xml index f6e0a694d..00eb4966f 100644 --- a/app/src/main/res/layout/item_bookshelf_list.xml +++ b/app/src/main/res/layout/item_bookshelf_list.xml @@ -11,8 +11,8 @@ @@ -126,7 +126,7 @@ android:textSize="13sp" app:layout_constraintBottom_toTopOf="@id/tv_last" app:layout_constraintLeft_toRightOf="@+id/iv_read" - app:layout_constraintRight_toLeftOf="@id/fl_has_new" + app:layout_constraintRight_toRightOf="@+id/fl_has_new" app:layout_constraintTop_toBottomOf="@+id/tv_author" /> - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookshelf_list_add.xml b/app/src/main/res/layout/item_bookshelf_list_add.xml deleted file mode 100644 index 87c630d65..000000000 --- a/app/src/main/res/layout/item_bookshelf_list_add.xml +++ /dev/null @@ -1,8 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookshelf_list_group.xml b/app/src/main/res/layout/item_bookshelf_list_group.xml new file mode 100644 index 000000000..00eb4966f --- /dev/null +++ b/app/src/main/res/layout/item_bookshelf_list_group.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_change_source.xml b/app/src/main/res/layout/item_change_source.xml index db773e7fc..fa04abeda 100644 --- a/app/src/main/res/layout/item_change_source.xml +++ b/app/src/main/res/layout/item_change_source.xml @@ -1,10 +1,14 @@ + android:paddingLeft="10dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + tools:ignore="RtlHardcoded,RtlSymmetry"> + + + app:layout_constraintRight_toLeftOf="@+id/tv_export" + app:tint="@color/primaryText" /> + android:textColor="@color/primaryText" + tools:text="起点中文" /> + android:orientation="horizontal" + android:padding="8dp"> + android:layout_weight="1" + android:textColor="@color/primaryText" /> + android:text="@string/edit" + android:textColor="@color/primaryText" /> + android:text="@string/delete" + android:textColor="@color/primaryText" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_http_tts.xml b/app/src/main/res/layout/item_http_tts.xml index fae2a3bcc..6a65d5827 100644 --- a/app/src/main/res/layout/item_http_tts.xml +++ b/app/src/main/res/layout/item_http_tts.xml @@ -34,4 +34,9 @@ android:src="@drawable/ic_clear_all" android:tint="@color/primaryText" tools:ignore="RtlHardcoded" /> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_read_record.xml b/app/src/main/res/layout/item_read_record.xml index 2587d759a..e3cd01e07 100644 --- a/app/src/main/res/layout/item_read_record.xml +++ b/app/src/main/res/layout/item_read_record.xml @@ -1,36 +1,47 @@ - + android:paddingRight="10dp"> + android:padding="6dp" + android:singleLine="true" + android:textColor="@color/secondaryText" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toBottomOf="@id/tv_book_name" + app:layout_constraintRight_toLeftOf="@+id/tv_remove" + tools:text="time" /> - + android:padding="6dp" + android:text="@string/clear" + android:textColor="@color/primaryText" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="@id/tv_book_name" + app:layout_constraintBottom_toBottomOf="@+id/tv_read_time" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_read_style.xml b/app/src/main/res/layout/item_read_style.xml new file mode 100644 index 000000000..28a861f81 --- /dev/null +++ b/app/src/main/res/layout/item_read_style.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_rss.xml b/app/src/main/res/layout/item_rss.xml index f3765bf58..448710b37 100644 --- a/app/src/main/res/layout/item_rss.xml +++ b/app/src/main/res/layout/item_rss.xml @@ -1,6 +1,7 @@ @@ -24,6 +25,8 @@ android:gravity="top|center_horizontal" android:lines="2" android:ellipsize="end" + android:textColor="@color/secondaryText" + tools:text="RSS" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/iv_icon" /> diff --git a/app/src/main/res/layout/item_rule_sub.xml b/app/src/main/res/layout/item_rule_sub.xml new file mode 100644 index 000000000..1c8cf4852 --- /dev/null +++ b/app/src/main/res/layout/item_rule_sub.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_list.xml b/app/src/main/res/layout/item_search_list.xml new file mode 100644 index 000000000..2dcaf1436 --- /dev/null +++ b/app/src/main/res/layout/item_search_list.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_theme_config.xml b/app/src/main/res/layout/item_theme_config.xml index 008f70d15..bd7410a34 100644 --- a/app/src/main/res/layout/item_theme_config.xml +++ b/app/src/main/res/layout/item_theme_config.xml @@ -12,7 +12,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:maxLines="1" /> + android:maxLines="1" + android:textColor="@color/primaryText" + tools:text="theme" /> + android:orientation="vertical" + android:padding="5dp"> + android:gravity="center_vertical" + android:orientation="horizontal"> + app:flexDirection="row" + app:flexWrap="wrap" + app:layoutManager="com.google.android.flexbox.FlexboxLayoutManager" /> + android:visibility="gone" /> @@ -38,8 +40,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:visibility="gone" android:orientation="vertical" + android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> diff --git a/app/src/main/res/layout/view_book_page.xml b/app/src/main/res/layout/view_book_page.xml index 95fd4f1f3..69d35e794 100644 --- a/app/src/main/res/layout/view_book_page.xml +++ b/app/src/main/res/layout/view_book_page.xml @@ -148,4 +148,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_detail_seek_bar.xml b/app/src/main/res/layout/view_detail_seek_bar.xml index a63c1b8dd..675268252 100644 --- a/app/src/main/res/layout/view_detail_seek_bar.xml +++ b/app/src/main/res/layout/view_detail_seek_bar.xml @@ -8,10 +8,11 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/view_dynamic.xml b/app/src/main/res/layout/view_dynamic.xml index e94e2eaca..3602c6392 100644 --- a/app/src/main/res/layout/view_dynamic.xml +++ b/app/src/main/res/layout/view_dynamic.xml @@ -4,14 +4,14 @@ android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/view_preference.xml b/app/src/main/res/layout/view_preference.xml index 0fb402876..602619d51 100644 --- a/app/src/main/res/layout/view_preference.xml +++ b/app/src/main/res/layout/view_preference.xml @@ -8,7 +8,7 @@ android:paddingTop="10dp" android:paddingRight="16dp" android:paddingBottom="10dp" - android:minHeight="42dp" + android:minHeight="60dp" android:clickable="true" android:orientation="horizontal" android:gravity="center_vertical" diff --git a/app/src/main/res/layout/view_read_menu.xml b/app/src/main/res/layout/view_read_menu.xml index 1b9bc2a98..29421a357 100644 --- a/app/src/main/res/layout/view_read_menu.xml +++ b/app/src/main/res/layout/view_read_menu.xml @@ -10,6 +10,7 @@ android:id="@+id/vw_menu_bg" android:layout_width="match_parent" android:layout_height="match_parent" + android:contentDescription="@string/content" tools:layout_editor_absoluteX="0dp" tools:layout_editor_absoluteY="0dp" /> @@ -20,25 +21,49 @@ android:theme="?attr/actionBarStyle" app:layout_constraintTop_toTopOf="parent"> - - - + android:layout_height="wrap_content"> + + + + + + + + @@ -46,20 +71,22 @@ android:id="@+id/ll_brightness" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_margin="8dp" - android:orientation="vertical" + android:layout_marginLeft="16dp" + android:layout_marginTop="16dp" android:gravity="center_horizontal" - app:layout_constraintTop_toBottomOf="@+id/title_bar" + android:orientation="vertical" + app:layout_constraintBottom_toTopOf="@+id/bottom_menu" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintBottom_toTopOf="@+id/bottom_menu"> + app:layout_constraintTop_toBottomOf="@+id/title_bar" + tools:ignore="RtlHardcoded"> + android:contentDescription="@string/brightness_auto" + android:src="@drawable/ic_brightness_auto" /> + + + + + android:layout_height="100dp" + android:importantForAccessibility="no"> + android:layout_height="match_parent" + android:importantForAccessibility="no" /> + android:layout_weight="1" + android:importantForAccessibility="no" /> @@ -263,7 +317,9 @@ + android:layout_weight="2" + android:importantForAccessibility="no" /> + @@ -301,7 +358,9 @@ + android:layout_weight="2" + android:importantForAccessibility="no" /> + @@ -339,7 +399,9 @@ + android:layout_weight="2" + android:importantForAccessibility="no" /> + @@ -377,7 +440,9 @@ + android:layout_weight="1" + android:importantForAccessibility="no" /> + + tools:ignore="RtlHardcoded"> + android:text="@string/select_all" /> + android:minWidth="72dp" + android:padding="5dp" + android:text="@string/revert_selection" + app:isBottomBackground="true" /> + app:isBottomBackground="true" /> diff --git a/app/src/main/res/menu/arrange_book.xml b/app/src/main/res/menu/arrange_book.xml index 4eb85fb8e..625bbbfe5 100644 --- a/app/src/main/res/menu/arrange_book.xml +++ b/app/src/main/res/menu/arrange_book.xml @@ -14,22 +14,6 @@ android:id="@+id/menu_group_manage" android:title="@string/group_manage" /> - - - - - - - - diff --git a/app/src/main/res/menu/backup_restore.xml b/app/src/main/res/menu/backup_restore.xml new file mode 100644 index 000000000..02b6acbd0 --- /dev/null +++ b/app/src/main/res/menu/backup_restore.xml @@ -0,0 +1,13 @@ + +

    + + + + diff --git a/app/src/main/res/menu/book_cache.xml b/app/src/main/res/menu/book_cache.xml new file mode 100644 index 000000000..0f58daf7a --- /dev/null +++ b/app/src/main/res/menu/book_cache.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/book_group_manage.xml b/app/src/main/res/menu/book_group_manage.xml index 8c65f9f8d..6c558b1e9 100644 --- a/app/src/main/res/menu/book_group_manage.xml +++ b/app/src/main/res/menu/book_group_manage.xml @@ -10,36 +10,4 @@ app:showAsAction="always" tools:ignore="AlwaysShowAction" /> - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/book_info.xml b/app/src/main/res/menu/book_info.xml index ff060d6bd..0d8c97281 100644 --- a/app/src/main/res/menu/book_info.xml +++ b/app/src/main/res/menu/book_info.xml @@ -8,11 +8,32 @@ android:title="@string/edit" app:showAsAction="ifRoom" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/book_read_record.xml b/app/src/main/res/menu/book_read_record.xml new file mode 100644 index 000000000..6ba105d38 --- /dev/null +++ b/app/src/main/res/menu/book_read_record.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/book_source.xml b/app/src/main/res/menu/book_source.xml index 4ae595bb7..137b1b8ce 100644 --- a/app/src/main/res/menu/book_source.xml +++ b/app/src/main/res/menu/book_source.xml @@ -37,15 +37,25 @@ android:title="@string/sort_auto" /> + android:title="@string/sort_name" /> + + + + @@ -75,21 +85,33 @@ app:showAsAction="never" /> + + + + diff --git a/app/src/main/res/menu/book_source_debug.xml b/app/src/main/res/menu/book_source_debug.xml new file mode 100644 index 000000000..ec7878377 --- /dev/null +++ b/app/src/main/res/menu/book_source_debug.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/book_source_item.xml b/app/src/main/res/menu/book_source_item.xml index c61d69dab..bd9a0de7c 100644 --- a/app/src/main/res/menu/book_source_item.xml +++ b/app/src/main/res/menu/book_source_item.xml @@ -9,6 +9,10 @@ android:id="@+id/menu_bottom" android:title="@string/to_bottom" /> + + diff --git a/app/src/main/res/menu/book_source_sel.xml b/app/src/main/res/menu/book_source_sel.xml index 02a6178ba..7d947d35e 100644 --- a/app/src/main/res/menu/book_source_sel.xml +++ b/app/src/main/res/menu/book_source_sel.xml @@ -34,12 +34,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/change_source.xml b/app/src/main/res/menu/change_source.xml index ab2bfee4c..347877bde 100644 --- a/app/src/main/res/menu/change_source.xml +++ b/app/src/main/res/menu/change_source.xml @@ -18,6 +18,23 @@ app:showAsAction="always" tools:ignore="AlwaysShowAction" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/content_select_action.xml b/app/src/main/res/menu/content_select_action.xml index ff83efd96..dda5324dd 100644 --- a/app/src/main/res/menu/content_select_action.xml +++ b/app/src/main/res/menu/content_select_action.xml @@ -1,5 +1,6 @@ - + + + + + + + + android:title="@string/browser" + app:showAsAction="never" /> + android:title="@string/share" + app:showAsAction="never" /> \ No newline at end of file diff --git a/app/src/main/res/menu/download.xml b/app/src/main/res/menu/download.xml deleted file mode 100644 index 6303b5b9d..000000000 --- a/app/src/main/res/menu/download.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/import_book.xml b/app/src/main/res/menu/import_book.xml index 8ace963eb..7c6816f00 100644 --- a/app/src/main/res/menu/import_book.xml +++ b/app/src/main/res/menu/import_book.xml @@ -8,4 +8,14 @@ android:icon="@drawable/ic_folder_open" app:showAsAction="ifRoom" /> - \ No newline at end of file + + + + + diff --git a/app/src/main/res/menu/import_source.xml b/app/src/main/res/menu/import_source.xml index 37c34f4de..ead4b715e 100644 --- a/app/src/main/res/menu/import_source.xml +++ b/app/src/main/res/menu/import_source.xml @@ -4,14 +4,15 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/menu/main_bnv.xml b/app/src/main/res/menu/main_bnv.xml index 9197bec7b..9648c00ff 100644 --- a/app/src/main/res/menu/main_bnv.xml +++ b/app/src/main/res/menu/main_bnv.xml @@ -8,9 +8,9 @@ android:icon="@drawable/ic_bottom_books" android:title="@string/bookshelf" /> + android:title="@string/discovery" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="AlwaysShowAction"> + app:showAsAction="always" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_rss.xml b/app/src/main/res/menu/main_rss.xml index 97343e877..983d102b8 100644 --- a/app/src/main/res/menu/main_rss.xml +++ b/app/src/main/res/menu/main_rss.xml @@ -9,6 +9,16 @@ android:icon="@drawable/ic_star" app:showAsAction="always" /> + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/replace_rule.xml b/app/src/main/res/menu/replace_rule.xml index 00cf348fe..9b49c60db 100644 --- a/app/src/main/res/menu/replace_rule.xml +++ b/app/src/main/res/menu/replace_rule.xml @@ -31,21 +31,27 @@ app:showAsAction="never" /> + + diff --git a/app/src/main/res/menu/replace_rule_item.xml b/app/src/main/res/menu/replace_rule_item.xml index 17483eeec..77c3a18cd 100644 --- a/app/src/main/res/menu/replace_rule_item.xml +++ b/app/src/main/res/menu/replace_rule_item.xml @@ -5,6 +5,10 @@ android:id="@+id/menu_top" android:title="@string/to_top" /> + + diff --git a/app/src/main/res/menu/replace_rule_sel.xml b/app/src/main/res/menu/replace_rule_sel.xml index 000686775..24ea4d598 100644 --- a/app/src/main/res/menu/replace_rule_sel.xml +++ b/app/src/main/res/menu/replace_rule_sel.xml @@ -12,6 +12,16 @@ android:title="@string/disable_selection" app:showAsAction="never" /> + + + + + + + + + diff --git a/app/src/main/res/menu/rss_source_debug.xml b/app/src/main/res/menu/rss_source_debug.xml new file mode 100644 index 000000000..a23296632 --- /dev/null +++ b/app/src/main/res/menu/rss_source_debug.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/rss_source_sel.xml b/app/src/main/res/menu/rss_source_sel.xml index 2c0b61e80..24ea4d598 100644 --- a/app/src/main/res/menu/rss_source_sel.xml +++ b/app/src/main/res/menu/rss_source_sel.xml @@ -14,12 +14,12 @@ - - \ No newline at end of file diff --git a/app/src/main/res/menu/source_sub_item.xml b/app/src/main/res/menu/source_sub_item.xml new file mode 100644 index 000000000..f852b2bcf --- /dev/null +++ b/app/src/main/res/menu/source_sub_item.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/source_debug.xml b/app/src/main/res/menu/source_subscription.xml similarity index 51% rename from app/src/main/res/menu/source_debug.xml rename to app/src/main/res/menu/source_subscription.xml index a55aecf0f..52bf86d0d 100644 --- a/app/src/main/res/menu/source_debug.xml +++ b/app/src/main/res/menu/source_subscription.xml @@ -3,9 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:id="@+id/menu_add" + android:icon="@drawable/ic_add" + android:title="@string/add" + app:showAsAction="always" /> \ No newline at end of file diff --git a/app/src/main/res/menu/speak_engine.xml b/app/src/main/res/menu/speak_engine.xml index 9ca269d05..6956a1ebc 100644 --- a/app/src/main/res/menu/speak_engine.xml +++ b/app/src/main/res/menu/speak_engine.xml @@ -15,4 +15,19 @@ android:title="@string/import_default_rule" app:showAsAction="never" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher3.xml b/app/src/main/res/mipmap-anydpi-v26/launcher3.xml index 9e7ef6711..8234460dd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/launcher3.xml +++ b/app/src/main/res/mipmap-anydpi-v26/launcher3.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher4.xml b/app/src/main/res/mipmap-anydpi-v26/launcher4.xml index 63e51b918..6eb41f1a7 100644 --- a/app/src/main/res/mipmap-anydpi-v26/launcher4.xml +++ b/app/src/main/res/mipmap-anydpi-v26/launcher4.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher5.xml b/app/src/main/res/mipmap-anydpi-v26/launcher5.xml index 7a23c7c66..f26ce23e3 100644 --- a/app/src/main/res/mipmap-anydpi-v26/launcher5.xml +++ b/app/src/main/res/mipmap-anydpi-v26/launcher5.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values-es-rES/arrays.xml b/app/src/main/res/values-es-rES/arrays.xml new file mode 100644 index 000000000..991dc30c9 --- /dev/null +++ b/app/src/main/res/values-es-rES/arrays.xml @@ -0,0 +1,115 @@ + + + + Texto + Audio + + + + Pestaña + Carpeta + + + + @string/indent_0 + @string/indent_1 + @string/indent_2 + @string/indent_3 + @string/indent_4 + + + + .txt + .json + .xml + + + + @string/jf_convert_o + @string/jf_convert_j + @string/jf_convert_f + + + + Adaptar do sistema + Tema Claro + Tema Oscuro + Tema E-Ink + + + + Autom. + Oscuro + Claro + Adaptado + + + + Predeterminado + 1 min + 2 min + 3 min + Siempre + + + + @string/screen_unspecified + @string/screen_portrait + @string/screen_landscape + @string/screen_sensor + + + + íconePrincipal + ícone1 + ícone2 + ícone3 + ícone4 + ícone5 + ícone6 + + + + Desativado + Tradicional a Simplificado + Simplificado a Tradicional + + + + Fuente predeterminado + Fuente Serif + Fuente Monoespaciado + + + + En blanco + Título + Tiempo + Batería + Páginas + Avance + Páginas y avance + Nombre del libro + Tiempo y Batería + + + + Normal + Oscuro + Claro + + + + Autom. + Chino simplificado + Chino tradicional + Inglés + + + + FuenteLibro + FuenteRSS + SustituirRegla + + + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml new file mode 100644 index 000000000..7deb85506 --- /dev/null +++ b/app/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,850 @@ + + + + Legado + Legado·buscador + Legado necesita de acceso de almacenamineto para buscar y leer libros. Por favor, diríjase a los "Ajustes de la aplicación" para conceder el "Permiso de almacenamineto". + + + Inicio + Restaurar + Importar datos de Legado + Crear carpeta + Crea una carpeta de respaldo bajo el nombre de Legado. + Respaldo de caché de libros sin conexión + Exporta localmente lo respalda para su exportación + Respaldar para + Por favor, seleccione una carpeta de respaldo. + Importar desde legado + Importar datos de Github + Reemplazo + Enviar + + Aviso + Cancelar + Confirmar + Ir a Ajustes + No se puede ir a los ajustes. + + Reintentar + Cargando + Advertencia + Editar + Borrar + Borrar todo + Reemplazar + Reemplazo + Configurar reglas de reemplazo + No disponible + Activar + Buscar reemplazo + Estantería + Favoritos + Favorito + en Favoritos + No está en Favoritos + Suscripción + Todo + Lecturas recientes + Última lectura + Novedades + La estantería está vacía. ¡Busque libros o añádalos desde una fuente pública! + Buscar + Descargar + Lista + Grid-3 + Grid-4 + Grid-5 + Grid-6 + Layout + Vista + Biblioteca + Importar libros + Fuente de libros + Gestionar fuentes + Crear/Importar/Editar/Gestionar fuentes de libros + Ajustes + Ajustes de Temas + Configuraciones relacionadas a los temas visuales + Otros ajustes + Algunas configuraciones relacionadas a su funcionamiento + Acerca de + Donaciones + Sair + Todavía se guardó. ¿Desea continuar editando? + Tipos de libros + Versión + Local + Buscar + Origen: %s + Origen: %s + Título + Última vez: %s + ¿Desea añadir %s a su Estantería? + %s archivos(s) de texto en total + Cargando… + Reintentar + Servicio Web + Fuente de edición web y lectura de libros + Editar fuentes de libros en la web + Caché sin conexión + Caché sin conexión + precarga el o los capítulos(s) seleccionado(s) en Almacenamiento en caché + Cambiar origen + + \u3000\u3000 Este es una aplicación de lectura bajo software libre, desarrollado en Kotlin, en que podrás participar. ¡Síguelos en la cuenta oficial de WeChat! + + + Legado (YueDu 3.0) disponible para descargar en:\n https://play.google.com/store/apps/details?id=io.legado.play.release + + Versión %s + Verificación en segundo plano + podrá utilizar con libertad cuando se verifique la fuente del libro + Actualización automática + Actualiza los libros automáticamente al abrir la App + Descarga automática + Baja los últimos capítulos automaticamente al actualizar los libros + Respaldo y restauración + Ajustes de WebDav + Ajustes de WebDave importación de datos anteriores + Respaldar + Restaurar + El respaldo necesita de permisos de almacenamineto + La restauración necesita de permisos de almacenamineto + Aceptar + Cancelar + Confirmar respaldo + Los nuevos archivos de respaldo serán reemplazados con los anteriores.\n Carpeta de respaldo: YueDu + Confirmar restauración + La restauración de los datos de la estantería reemplazará a los datos de la actual. + Respaldo completado + Error de respaldo + Restaurando + Restauración completada + Restauración fallida + Orientación de pantalla + Auto(sensor) + Paisaje + Retrato + Adaptado del sistema + Descargo de responsabilidad + %d capítulos + Interfaz + Brillo + Capítulos + Próximo + Anterior + Ocultar barra de estado + Oculta la barra de navegación del sistema durante lectura + Voz + Hablando + Clic para abrir la lectura + Reproducir + Reproduciendo + Clic para abrir la reproducción + Pausar + Regresar + Actualizar + Iniciar + Detener + Pausar + Continuar + Cronómetro + Voz pausada + Hablando (%d min restantes) + Reproduciendo (%d min restantes) + Ocultar botones virtuales durante lectura + Oculta barra de navegación + Color de barra de navegación + Evaluación + E-mail + Error al abrir + Error al compartir + Sin capítulos + Añadir Url + Añadir Url de libro + Segundo plano + Autor + Autor: %s + Voz detelendia + Limpiar caché + Se limpió la caché + Guardar + Editar fuente + Editar fuente de libro + Desactivar fuente de libro + Añadir fuente de libro + Añadir fuente de suscripción + Añadir libros + Buscar + Copiar fuente + Pegar fuente + Descripción de las reglas de fuente + Comprobar actualizaciones + Digitalizar código QR + Digitalizar imágenes locales + Descripción de las reglas + Compartir + Compartir vía + Adaptar del sistema + Añadir + Importar fuentes del libro + Importar localmente + Importar en línea + Reemplazo + Editar regla de reemplazo + Modelo + Reemplazo + Portada + Libro + Botones de volumen para pasar página + Toque la pantalla para pasar página + Animación de hojeada + Animación de hojeada (libro) + Mantener pantalla encendida + Regresar + Menú + Ajuste + Barra de deslizamiento + Limpiar la caché borrará todos los capítulos guardados. ¿Está seguro de limpiarlos? + Compartir fuentes de libros + Nombre de regla + La regla está vacía o es incompatible con los lineamientos de Regex. + Acción de selección + Seleccionar todo + Selecionar todo (%1$d/%2$d) + Dejar de seleccionar todo (%1$d/%2$d) + Modo oscuro + Página de bienvenida + Iniciar descarga + Cancelar descarga + Excluir de descarga + Descargando %1$d/%2$d + Importar libro(s) seleccionado(s) + Número de tareas en simultáneo + Cambiar ícono + Quitar + Iniciar lectura + Cargando… + Error cargando, toque para volver a iniciar + Descripción de libro + Descripción:%s + Descripción: sin introducción + Abrir libro externo + Origen: %s + Importar reglas replace + Importar reglas en línea + Intervalo de actualizaciones + Lista de recientes + Fecha de actualización + Título del libro + Ordenar manualmente + Estrategia de lectura + Tipografía + Quitar selecionados + ¿Desea quitarlos? + Limpiar fuente + Descubrir + Descubrir + Sin contenido. Diríjase al Administrador de fuentes para añadirlo. + Quitar todo + Historial de búsqueda + Limpiar + Mostrar título de libro en texto + Sincronización de fuentes de libros + Sin último capítulo. + Mostrar tiempo y batería + Divisor de pantalla + Oscurecer ícono de barra de estado + Contenido + Copiar + Bajar todo + Este es un texto de prueba, \n\u3000\u3000 sólo para mostrar el resultado + Color de fondo (mantén pulsado para personalizar) + Barra de estado inmersiva + %d capítulo(s) restante(s) + Ninguno seleccionado + Mantén pulsado para introducir el valor de color + Cargando… + Preparando + Preparando algo más + Favoritos + Añadir a Favoritos + Quitar + Tiempo límite de carga + Siga:%s + Copiado con éxito + Organización de estantería + Esto borrará a todos los libros. Por favor, tenga cuidado. + Buscar fuentes de libros + Buscar fuentes de suscripción + Búsqueda (%d fuentes en total) + Capítulos (%d) + Negrito + Fuente + Texto + Página inicial + Derecha + Izquierda + Parte inferior + Parte superior + Espaciado + Espacio superior + Espacio inferior + Espacio derecho + Espacio izquiedo + Verificar fuentes del libro + Verificar fuente seleccionada + %1$s Avance %2$d/%3$d + ¡Por favor, instale y seleccione un TTS en chino! + ¡Error al inicializar TTS! + Conversión a simplificada + Desactivado + Simplificado a tradicional + Tradicional a simplificado + Modo de hojeada + %1$d elementos + Almacenamiento: + Adicionar ao Estante + Añadir a Estantería (%1$d) + %1$d libros añadidos con éxito + Por favor, coloque sus archivos de fuente al almacenamiento raíz y vuelve a seleccionar + Fuente predeterminada + Seleccionar fuentes + Tamaño de texto + Espacio entre líneas + Espacio entre párrafos + Encima + Selection To Top + Debajo + Selection To Bottom + Autoexpandir Descubrir + Expande por defecto la primera parte de Descubrir. + Líneas actuales %s + Velocidad de voz + Deslizamiento automático + Impedir deslizamiento automático + Velocidad de deslizamiento automático + Información sobre libro + Editar información del libro + Establecer la estantería como página de inicio + Ir automáticamente a la lista de Recientes + Objeto sustituto. El nombre del libro o la URL de origen están disponibles + Grupos + Carpeta de caché + Selector de archivos del sistema + Nueva versión + Descargar actualizaciones + Botones de volumen para pasar las páginas mientras lee + Ajuste de margen + Activar actualizaciones + Desactivar actualizaciones + Invertir + Buscar libro por nombre o autor + Nombre del libro, autor, URL + Preguntas frecuentes + Mostrar todos los buscados + Mostrar la fuente de búsqueda si Descubrir fue cerrado + Actualizar capítulos + Capítulos de Txt Regex + Codificación de texto + Orden ascendente/descendente + Ordenar + Ordenar automáticamente + Ordenar manualmente + Ordenar por nombre + Ir al principio + Ir al final + Leído: %s + Awaiting update + Preparando un poco más + Listo + Todo + Preparando actualización de libros + Preparando más actualizaciones de libros + Libros concluidos + Libros locales + El color de la barra de estado se vuelve transparente + barra de navegación de inmersión + La barra de navegación se vuelve transparente + Agregar a la estantería + Seguir leyendo + Carpeta de portada + Portada + Diapositiva + Simulación + Desplazado + Ninguno + La fuente de este libro utiliza funciones avanzadas, para desbloquear diríjase a Donaciones y toque el código de búsqueda del sobre rojo de Alipay para recibir ese sobre. + Actualiza el último capítulo después del cambio de fuente en segundo plano + si está habilitado, la actualización comenzará 1 minuto después de que se abra la aplicación + Ocultar automáticamente la barra de herramientas + La barra de herramientas se ocultará automáticamente cuando deslice la Librería + Iniciar sesión + Iniciar sesión% s + Completado + La fuente actual no se ha configurado con una dirección de inicio de sesión + Sin página anterior + Sin página siguiente + + + 源名称(sourceName) + 源URL(sourceUrl) + 源分组(sourceGroup) + 自定义源分组 + 输入自定义源分组名称 + 分类Url + 登录URL(loginUrl) + 源注释(sourceComment) + 搜索地址(url) + 发现地址规则(url) + 书籍列表规则(bookList) + 书名规则(name) + 详情页url规则(bookUrl) + 作者规则(author) + 分类规则(kind) + 简介规则(intro) + 封面规则(coverUrl) + 最新章节规则(lastChapter) + 字数规则(wordCount) + 书籍URL正则(bookUrlPattern) + 预处理规则(bookInfoInit) + 目录URL规则(tocUrl) + 允许修改书名作者(canReName) + 目录下一页规则(nextTocUrl) + 目录列表规则(chapterList) + 章节名称规则(ChapterName) + 章节URL规则(chapterUrl) + VIP标识(isVip) + 更新时间(ChapterInfo) + 正文规则(content) + 正文下一页URL规则(nextContentUrl) + WebViewJs(webJs) + 资源正则(sourceRegex) + 替换规则(replaceRegex) + 图片样式(imageStyle) + + 图标(sourceIcon) + 列表规则(ruleArticles) + 列表下一页规则(ruleArticles) + 标题规则(ruleTitle) + guid规则(ruleGuid) + 时间规则(rulePubDate) + 类别规则(ruleCategories) + 描述规则(ruleDescription) + 图片url规则(ruleImage) + 内容规则(ruleContent) + 样式(style) + 链接规则(ruleLink) + + + + Sin fuentes + Error al obtener la información del libro + Error al obtener el contenido + Error al obtener la lista de capítulos + Error al acceder al sitio:% s + Error al leer el archivo + Error al cargar la lista de capítulos + Error al obtener los datos + Error al cargar\n%s + Sin conexión + Tiempo de espera de conexión en línea + Error en el procesamiento de datos + + + Encabezado HTTP + Fuente de depuración + Importar desde código QR + Compartir fuentes seleccionadas + Escanear código QR + Toque para mostrar el menú cuando esté seleccionado + Tema + Tipo de tema + Seleccione el tema que prefiera + Únase al grupo QQ + Para establecer la imagen de fondo requiere del permiso de almacenamiento + Ingrese la dirección de origen del libro + Borrar archivo + Archivo borrado + ¿Está seguro de que desea borrar este archivo? + Carpeta + Importación inteligente + Descubrir + Cambiar modo de visualización + La importación de libros locales requiere permiso de almacenamiento + Tema nocturno + E-Ink + Optimización para dispositivos E-ink + Esta aplicación requiere permiso de almacenamiento para respaldar la información del libro + Toque nuevamente para salir de la aplicación + La importación de libros locales requiere permiso de almacenamiento + La conexión en línea no está disponible + + No + Aceptar + ¿Está seguro que desea borrar? + ¿Está seguro de que desea borra %s? + Está seguro de que desea eliminar todos los libros? + ¿También desea eliminar los capítulos de libros descargados? + Escanear el código QR requiere permiso de la cámara + La voz se está ejecutando, no se puede pasar las páginas automáticamente + Codificación de entrada + Capítulos de Txt Regex + Para abrir libros locales, se requiere permiso de almacenamiento + Libro sin nombre + Ingrese la URL de la regla de reemplazo + La lista de búsqueda se obtuvo con éxito%d + el nombre y la URL no pueden estar vacíos + Galería + obtenga sobres rojos de AliPay + Sin dirección de actualización + Abra la página de inicio, volverá automáticamente después de completarse + Después de iniciar sesión correctamente, toque el icono en la esquina superior derecha para probar el acceso a la página de inicio + Capítulo + Para + Usando Regex + Sangría + Ninguno + Sangría con 1 caracter + Sangría con 2 caracteres + Sangría con 3 caracteres + Sangría con 4 caracteres + Seleccione una carpeta + Seleccione un archivo + Sin Descubrir, puede agregarlo a Fuentes de libros + Restaurar valores predeterminados + La carpeta de caché personalizada requiere permiso de almacenamiento + Negro + Sin contenido + Cambiando fuente, espere + Capítulos sin contenido + Espaciado de palabras + + Básico + Buscar + Descubrir + Información + Capítulos + Contenido + + Modo E-Ink + Elimina animaciones y optimiza la experiencia para libros en papel electrónico + Servicio web + Puerto web + Puerto actual %s + Compartir código QR + Compartir cadenas + Compartir Wifi + Conceda el permiso de almacenamiento + Rebobinado rápido + Avance rápido + Anterior + Siguiente + Música + Audio + Activar + Activar js + Cargar URL base + Todas las fuentes + El contenido introducido no puede estar vacío + Limpiar la caché de búsqueda + Editar búsqueda + Cambie el icono de software que se muestra en el escritorio + Ayuda + Mío + Lecturas + %d%% + %d min + Leer por páginas + Auto-Brightness %s + Motor de voz + Imágenes de fondo + Color de fondo + Color del texto + Seleccione una foto + Gestión de grupo + Selección de grupo + Edición de grupo + Mover al grupo + Añadir a grupos + Quitar de grupos + Nuevo reemplazo + Grupo + Grupo: %s + Capítulos: %s + Activar Descubrir + Desactivar Descubrir + Activar selección + Desactivar selección + Exportar seleccionados + Exportar + Cargar capítulos + Cargar detalles del libro + TTS + Contraseña de WebDav + Ingrese su contraseña de WebDav + Ingrese la dirección de su servidor + Dirección del servidor WebDav + Cuenta WebDav + Ingrese su cuenta WebDav + Fuente de suscripción + Editar fuente de suscripción + Filtro + Buscar fuentes de Descubrir + Ubicación actual: + Búsqueda precisa + Iniciando servicio + Vacío + Selección de archivos + Selección de carpetas + ¡Finalizado! + Error en la dirección de Uri + Actualizar portada + Cambiar fuente + Imagen local + Tipo: + Fondo + Importando + Exportando + Defina qué botones pasan de página + Botón de página anterior + Botón de página siguiente + Primero agregue este libro a Estantería + Sin grupo + Oración anterior + Siguiente oración + Otra carpeta + Demasiadas palabras para crear un código QR + Compartir fuentes de suscripción + Compartir fuentes de libros + Cambio automático al modo oscuro + Sigue el modo oscuro del sistema + Atrás + Tono de voz en línea + (%1$d/%2$d) + Mostrar suscripción + Servicio detenido + Iniciando el servicio\nComprobando la barra de notificaciones para más detalles + Carpeta predeterminada + Selector de carpetas del sistema + Selector de carpetas de aplicaciones + Selector de archivos de la aplicación + En Android 10+ no puede leer ni guardar archivos debido a restricciones de permisos + Mantenga pulsado para mostrar Legado·Búsqueda en el menú de operaciones + Buscar en pantalla de operación de texto + Registro + Registro + Conversión a simplificada + El ícono es un ícono vectorial, que no era compatible antes de Android 8.0 + Speech settings + Start page + Long Tap to select text + Header + Content + Footer + Select end + Select start + Shared layout + Browser + Import default rules + Name + Regex + More menu + Minus + Plus + System typeface + Delete source file + Default-1 + Default-2 + Default-3 + Title + Left + Center + Hide + Add to Group + Save image + No default path + Group settings + View Chapters + Navigation bar shadow + Current shadow size(elevation): %s + Default + Main menu + Tap to grant permission + Legado needs Storage permission, please tap the "Grant Permission" button below, or go to "Settings"-"Application Permissions"-to open the required permission. If the permission is still not work, please tap "Select Folder" in the upper right corner to use the system folder picker. + The selected text cannot be spoken in full text speech + Extend to cutout + Updating Chapters + Headset buttons are always available + Headset buttons are available even exit the app. + Contributors + Contact + License + Other + 开源阅读 + Follow WeChat Official Accounts + WeChat + Supporting me will be appreciated + Official Accounts[开源阅读] + Changing source + Tap to join + Middle + Information + Switch Layout + Text font weight switching + Full screen gestures support + + + Primario + Resalte + Color de fondo + Color de la barra de navegación + Día + Día, primario + Día, resalte + Día, color de fondo + Día, color de la barra de navegación + Noche + Noche, primario + Noche, resalte + Noche, color de fondo + Noche, color de la barra de navegación + Cambiar la fuente automáticamente + Texto justificado + Texto alineado en la parte inferior + Velocidad de desplazamiento automático + Ordenar por URL + Respalde en simultáneo vía local y WebDav + Restaure primero desde WebDAV, haga clic en restaurar desde la ubicación del respaldo + Seleccione una carpeta de respaldo heredada + Activado + Desactivado + Iniciando descarga + Este libro ya está en la lista de descargas + Haga clic para abrir + Siga [开源阅读] para obtener ayuda haciendo clic en los anuncios + Código de sugerencia de WeChat + AliPay + Sobre rojo de AliPay, código de búsqueda + 537954522 Haga clic para copiar + Código QR del sobre rojo de AliPay + Código QR de AliPay + Código QR de la Colección QQ + gedoor, Invinciblelee, etc. Para obtener más detalles, vaya a Github + Limpiar la caché de libros y fuentes descargados + Portada predeterminada + Lista de descartados + Ignore parte del contenido durante la restauración + Leer ajustes de interfaz + Nombre del grupo + Sección de notas + Activar regla de reemplazo por defecto + Para libros recién agregados + Seleccione el archivo de restauración + ¡El fondo diurno no puede ser demasiado oscuro! + ¡El fondo inferior diurno no puede ser demasiado oscuro! + ¡El fondo nocturno no puede ser demasiado brillante! + ¡El fondo inferior nocturno no puede ser demasiado brillante! + Debe haber un contraste entre el color de resalte y el de fondo + Debe haber un contraste entre el resalte y el color del texto + Formato incorrecto + Error + Mostrar widget de brillo + Idioma + Importar fuente RSS + Su donación mejora esta aplicación + Cuenta oficial de Wechat [开源阅读软件] + Leer historial + Leer resumen del historial + TTS local + Recuento de subprocesos + Tiempo total de lectura + Deseleccionar todo + Importar + Exportar + Guardar configuración del tema + Guardar configuración del tema del día + Guardar configuración del tema nocturno + Lista de temas + Guardar, importar, compartir tema + Cambiar tema predeterminado + Ordenar por hora de actualización + Contenido de búsqueda + ¡Siga [开源阅读] en WeChat para obtener más feeds RSS! + Actualmente no hay una fuente para Descubrir, siga el número público [开源 阅读] para agregar una fuente de libro a través de Descubrir. + 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. + Nombre del tema + Limpiar automáticamente los datos de búsqueda caducados + Historial de búsqueda de más de un día + Volver a segmentar + Formato de nombre: + Haga clic en el icono de carpeta en la esquina superior derecha y seleccione la carpeta + Búsqueda inteligente + Nombre de archivo importado + No hay libros + Conservar el nombre original + El control de la pantalla táctil + Cerrar + Página siguiente + Página anterior + Sin acciones + Título + Mostrar/ocultar + pie de página encabezado + Suscripción de reglsa + 添加大佬们提供的规则导入地址\n添加后点击可导入规则 + Obtener progreso en la nube + El progreso actual excede el progreso de la nube. ¿Quieres sincronizar? + Progreso de lectura sincronizado + Sincronizar el progreso de la lectura al entrar y salir de la pantalla de lectura + No se pudo marcar + URL única + Exportar lista de libros + Importar lista de libros + Descarga anterior + Descargue% s capítulos antes + Está habilitado + Imagen de fondo + Copiar URL del libro + Copiar URL del capítulo + Carpeta de exportación + Codificación de texto exportado + Exportar a WebDav + Contenido inverso + Depurar + Registro de fallos + Uso de ramas chinas personalizadas + Formato de imagen + Sistema TTS + Formato de exportación + Verificar por autor + Esta URL ha sido reemplazada + Frecuencia de actualización de pantalla alta + Utilice la frecuencia de actualización de pantalla más alta + Exportar todo + Listo + Mostrar no leídos + Mostrar siempre la portada predeterminada + Mostrar siempre la cobertura predeterminada, no mostrar la cobertura de la red + Código fuente de búsqueda + Código fuente del libro + Código fuente de los capítulos + Código fuente del contenido + Listar el código fuente + Tamaño de fuente + Borde superior + Borde inferior + Mostrar + Ocultar + Ocultar cuando se muestra la barra de estado + TOC inverso + Mostrar + Formato + Formato de grupo + Exportar nombre de archivo + Restablecer + URL nula + 字典 + 未知错误 + diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml new file mode 100644 index 000000000..42cb3aa38 --- /dev/null +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -0,0 +1,850 @@ + + + + Legado + Legado·search + Legado needs storage access to find and read books. please go "App Settings" to allow "Storage permission". + + + Home + Restore + Import Legado data + Create a subfolder + Create a folder named Legado as the backup folder. + Offline cache book backup + Export to local and back up to the exports directory under the legado folder + Backup to + Please select a backup path. + Import legacy data + Import github data + Replacement + Send + + Prompt + Cancel + Confirm + Go to Settings + Cannot jump to Settings. + + Retry + Loading + Warning + Edit + Delete + Delete all + Replace + Replacement + Configure replacement rules + Not available now + Enable + Search Replacement + Bookshelf + Favorites + Favorite + in Favorites + Not in Favorites + Subscription + All + Recent reading + Last reading + What\'s new + The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please pay attention to the public account [开源阅读] to get the book source! + Search + Download + List + Grid-3 + Grid-4 + Grid-5 + Grid-6 + Layout + View + Library + Import books + Book sources + Sources management + Create/Import/Edit/Manage Book sources + Settings + Theme settings + Some settings related to interface or color + Other settings + Some function-related settings + About + Donations + Exit + It has not been saved. Do you want to continue editing? + Book styles + Version + Local + Search + Origin: %s + Origin: %s + Title + Latest: %s + Would you like to add %s to your Bookshelf? + %s text file(s) in total + Loading… + Retry + Web service + Web edit source and read book + Edit book sources on the web + Offline cache + Offline cache + cache the selected chapter(s) to Storage + Change Origin + + \u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us. Follow the WeChat Official Account [开源阅读]! + + + Legado (YueDu 3.0) download link:\n https://play.google.com/store/apps/details?id=io.legado.play.release + + Version %s + Background-verification + you can operate freely when verifying the book source + Auto-refresh + Update books automatically when opening the software + Auto-download + Download the latest chapters automatically when updating books + Backup and restore + WebDav settings + WebDav settings/Import legacy data + Backup + Restore + Backup needs storage permission + Restore needs storage permission + OK + Cancel + Backup confirmation + The new backup files will replace the original.\n Backup folder: YueDu + Restore confirmation + Restoring the bookshelf data will overwrite the current Bookshelf. + Backup succeed + Backup failed + Restoring + Restore succeed + Backup failed + Screen orientation + Auto(sensor) + Landscape + Portrait + Follow system + Disclaimer + %d chapters + Interface + Brightness + Chapters + Next + Prior + Hide status bar + Hide system navigation bar when reading + Speech + Speaking + Click to open the Reading + Play + Playing + Click to open the Playing + Pause + Back + Refresh + Start + Stop + Pause + Resume + Timer + Speak Paused + Speaking(%d min left) + Playing(%d min left) + Hide virtual buttons when reading + Hide navigation bar + Navigation bar color + Rating + Email + Open failed + Share failed + No chapters + Add url + Add book url + Background + Author + Author: %s + Speak Stopped + Clear cache + Cache cleared + Save + Edit source + Edit Book source + Disable Book source + Add Book source + Add subscription source + Add books + Scan + Copy source + Paste source + Source rules description + Check for Updates + Scan QR code + Scan local images + Rules description + Share + Share to + Follow system + Add + Import book sources + Import local + Import online + Replacement + Edit replacement rule + Pattern + Replacement + Cover + Book + Volume keys to turn page + Tap screen to turn page + Flip animation + Flip animation (book) + Keep screen awake + Back + Menu + Adjust + Scroll bar + Clearing the cache will delete all saved chapters. Are you sure to delete it? + Book sources sharing + Rule name + Pattern rule is empty or does not conform the regex specification. + Selection action + Select all + Select all(%1$d/%2$d) + Cancel select all(%1$d/%2$d) + Dark mode + Welcome page + Download start + Download cancel + No download + Downloaded %1$d/%2$d + Import selected book(s) + Number of concurrent tasks + Change icon + Remove + Start reading + Loading… + Load failed, tap to retry + Book description + Description:%s + Description: no introduction + Open external book + Origin: %s + Import replace rules + Import online rules + Check interval for updates + By recent list + By update time + By book title + By sort manually + Reading strategy + Typesetting + Delete selected + Are you sure to delete? + Default font + Discovery + Discovery + No content.Go to Sources management to add it! + Delete all + Search history + Clear + Display book title on text + Book Sources sync + No latest chapter. + Display time and battery + Display divider + Darken the status bar\'s icon color + Content + Copy + Download all + This is a test text, \n\u3000\u3000 just to show you the effect + Color and background (long tap to customize) + Immersive status bar + %d chapter(s) left + No selected + Long tap to input color value + Loading… + Awaiting + Awaiting more + Bookmarks + Add to Bookmarks + Delete + Loading timeout + Follow:%s + Copied successfully + Bookshelf arrangement + It will delete all books. Be careful,please. + Search book sources + Search subscription sources + Search( %d sources in total) + Chapters(%d) + Bold + Font + Text + Home page + Right + Left + Bottom + Top + Padding + Padding top + Padding bottom + Padding left + Padding right + Check book sources + Check the selected source + %1$s Progress %2$d/%3$d + Please install and select Chinese TTS! + TTS initialization failed! + Simplified conversion + Off + Simplified to traditional + Traditional to simplified + Flipping mode + %1$d items + Storage: + Add to Bookshelf + Add to Bookshelf(%1$d) + %1$d books added successfully + Please put the font files in the Fonts folder of the storage root directory and reselect + Default font + Select fonts + Text size + Line spacing + Paragraph spacing + To Top + Selection To Top + To Bottom + Selection To Bottom + Auto expand Discovery + Default expand the first Discovery. + Current threads %s + Speech rate + Auto scroll + Stop Auto scroll + Auto scroll speed + Book information + Edit book information + Use Bookshelf as start page + Auto jump to Recent list + Replacement object. Book name or source url is available + Groups + Cache path + System file picker + New version + Download updates + Volume keys to turn page when reading + Margin adjustment + Enable update + Disable update + Inverse + Search book name/author + Book name,Author,URL + FAQ + Display all Discovery + Display the selected origin\'s Discovery if closed + Update chapters + Txt Chapters Regex + Text encoding + Ascending/Descending order + Sort + Sort automatically + Sort manually + Sort by name + Scroll to the top + Scroll to the bottom + Read: %s + Awaiting update + Awaiting more + Finished + All + Awaiting update books + Awaiting more chapters books + Finished books + Local books + The status bar color becomes transparent + immersion navigation bar + The navigation bar becomes transparent + Add to Bookshelf + Continue reading + Cover path + Cover + Slide + Simulation + Scroll + None + This book source uses advanced features, please go to the Donations and tap the Alipay red envelope search code to receive the red envelope,then you can use it. + Update the latest chapter after changed origin in the background + if enabled,the update will start 1 minute later when the software is run + Auto hide ToolBar + The toolbar will be hidden automatically when scroll the Bookshelf + Login + Login%s + Success + The current source has not configured with a login address + No prior page + No next page + + + 源名称(sourceName) + 源URL(sourceUrl) + 源分组(sourceGroup) + 自定义源分组 + 输入自定义源分组名称 + 分类Url + 登录URL(loginUrl) + 源注释(sourceComment) + 搜索地址(url) + 发现地址规则(url) + 书籍列表规则(bookList) + 书名规则(name) + 详情页url规则(bookUrl) + 作者规则(author) + 分类规则(kind) + 简介规则(intro) + 封面规则(coverUrl) + 最新章节规则(lastChapter) + 字数规则(wordCount) + 书籍URL正则(bookUrlPattern) + 预处理规则(bookInfoInit) + 目录URL规则(tocUrl) + 允许修改书名作者(canReName) + 目录下一页规则(nextTocUrl) + 目录列表规则(chapterList) + 章节名称规则(ChapterName) + 章节URL规则(chapterUrl) + VIP标识(isVip) + 更新时间(ChapterInfo) + 正文规则(content) + 正文下一页URL规则(nextContentUrl) + WebViewJs(webJs) + 资源正则(sourceRegex) + 替换规则(replaceRegex) + 图片样式(imageStyle) + + 图标(sourceIcon) + 列表规则(ruleArticles) + 列表下一页规则(ruleArticles) + 标题规则(ruleTitle) + guid规则(ruleGuid) + 时间规则(rulePubDate) + 类别规则(ruleCategories) + 描述规则(ruleDescription) + 图片url规则(ruleImage) + 内容规则(ruleContent) + 样式(style) + 链接规则(ruleLink) + + + + No source + Failed to obtain book information + Failed to obtain content + Failed to obtain chapters list + Failed to access website:%s + Failed to read file + Failed to load chapters list + Failed to get data + Failed to load\n%s + No network + Network connection timeout + Data parsing failed + + + HTTP Header + Debug source + Import from QR code + Share selected sources + Scan QR code + Tap to display Menu when selected + Theme + Theme mode + Select a theme you want + Join QQ group + Set the background image requires storage permission + Input book source address + Delete file + Deleted file + Are you sure to delete this file? + Directory + Intelligent import + Discovery + Switch display styles + Import local books requires storage permission + Night Theme + E-Ink + Optimization for E-ink devices + This software requires storage permission to store the book information to be backed up + Tap again to exit the program + Import local books requires storage permission + Network connection is not available + Yes + No + OK + Are you sure to delete it? + Are you sure to delete %s? + Are you sure to delete all books? + Do you want to delete the downloaded book chapters at the same time? + Scan QR code requires Camera permissions + Speech is running, cannot turn pages automatically + Input encoding + Txt Chapters Regex + Open local books requires storage permission + No bookName + Input replacement rule URL + Search list obtained successfully%d + name and URL cannot be empty + Gallery + get AliPay red envelopes + No update address + Opening the homepage, it will return to start page automatically after success + After successful login, please tap the icon on the upper right corner to test the homepage access + Chapter + To + Using Regex + Indent + None + Indent with 1 chars + Indent with 2 chars + Indent with 3 chars + Indent with 4 chars + Select a folder + Select a file + No Discovery, you can add it in BookSource + Restore default + Custom cache path requires Storage permission + Black + No content + Changing source, wait please + Chapters is empty + Word spacing + + Basic + Search + Discovery + Information + Chapters + Content + + E-Ink mode + Remove animations and optimize the experience of using E-paper books + Web service + Web port + Current port %s + QR code sharing + Strings sharing + Wifi sharing + Please grant Storage Permission + Speed down + Speed up + Prior + Next + Music + Audio + Enable + Enable js + Load BaseUrl + All Sources + The input content cannot be empty + Clear Discovery cache + Edit Discovery + Switch the software icon displayed on the desktop + Help + Me + Read + %d%% + %d min + Auto-Brightness %s + Speak by pages + Speak Engine + Background images + Background color + Text color + Select a picture + Group management + Group selection + Group editing + Move to group + Add to Groups + Remove from Groups + New replacement + Group + Group: %s + Chapters: %s + Enable Discovery + Disable Discovery + Enable selected + Disable selected + Export selected + Export + Load chapters + Load book detail + TTS + WebDav password + Input you WebDav authorized password + Input you server address + WebDav server address + WebDav account + Input your WebDav account + Subscription source + Edit Subscription source + Filter + Search Discovery sources + Current location: + Precise search + Starting service + Empty + File selection + Folder selection + I AM OVER! + Uri To Path failed + Refresh cover + Change origin + Local image + Type: + Background + Importing + Exporting + Set page-turning buttons + Page up button + Page down button + Add this book to Bookshelf first + Ungrouped + Prior sentence + Next sentence + Other folder + There are too many words to create a QR code + Subscription sources sharing + Book sources sharing + Automatic switching dark mode + Following system dark mode + Go back + Online Speech tone + (%1$d/%2$d) + Display Subscription + Service stopped + Starting service\nChecking notification bar for details + Default path + System folder picker + App folder picker + App file picker + Android 10+ unable to read and write file due to permission restrictions + Long tap to display Legado·Search in the operation menu + Text operation display Search + Log + Log + Simplified conversion + The icon is a vector icon, which was not supported before Android 8.0 + Speech settings + Start page + Long Tap to select text + Header + Content + Footer + Select end + Select start + Shared layout + Browser + Import default rules + Name + Regex + More menu + Minus + Plus + System typeface + Delete source file + Default-1 + Default-2 + Default-3 + Title + Left + Center + Hide + Add to Group + Save image + No default path + Group settings + View Chapters + Navigation bar shadow + Current shadow size(elevation): %s + Default + Main menu + Tap to grant permission + Legado needs Storage permission, please tap the "Grant Permission" button below, or go to "Settings"-"Application Permissions"-to open the required permission. If the permission is still not work, please tap "Select Folder" in the upper right corner to use the system folder picker. + The selected text cannot be spoken in full text speech + Extend to cutout + Updating Chapters + Headset buttons are always available + Headset buttons are available even exit the app. + Contributors + Contact + License + Other + 开源阅读 + Follow WeChat Official Accounts + WeChat + Supporting me will be appreciated + Official Accounts[开源阅读] + Changing source + Tap to join + Middle + Information + Switch Layout + Text font weight switching + Full screen gestures support + + + Primary + Accent + Background color + NavBar color + Day + Day,Primary + Day,Accent + Day,Background color + Day,NavBar color + Night + Night,Primary + Night,Accent + Night,Background color + Night,NavBar color + Change source automatically + Text justified + Text align bottom + Auto scroll speed + Sort by URL + Backup the local and WebDav simultaneously + Restore from WebDAV first, Restore form the local backup on long click + Select a legacy backup folder + Enabled + Disabled + Starting download + This book is already in Download list + Click to open + Follow [开源阅读] to support me by clicking on ads + WeChat Tipping Code + AliPay + AliPay red envelope search code + 537954522 Click to copy + AliPay red envelope QR code + AliPay QR code + QQ Collection QR code + gedoor,Invinciblelee etc. Checking in github for details + Clear the cache of the downloaded books and fonts + Default cover + Bypass list + Ignore some contents while restoring + Reading interface settings + Group name + Remarks section + Enable replace rule by default + For new added books + Select restore file + Day background can not be too dark! + Day bottom can not be too dark! + Night background can not be too bright! + Night bottom can not be too bright! + Need Difference between accent and background color + Need Difference between accent and text color + Wrong format + Error + Show brightness widget + Language + Import rss source + Your donation makes this app better + Wechat official account [开源阅读软件] + Read record + Read record summary + Local TTS + Thread count + Total read time + Unselect all + Import + Export + Save theme config + Save day theme config + Save night theme config + Theme list + Save, Import, Share theme + Switch default theme + Sort by update time + Search content + Follow Wechat official account [开源阅读] to get Subscription source + Empty now,Follow Wechat official account [开源阅读] to get Discovery source + 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. + Theme name + "Clear expired search histories automatically " + Search histories more than one day + Re-segment + Style name: + Click the folder icon in the upper right corner and select the folder + Intelligent scanning + Imported-file name + No books + Keep the original name + Screen touch control + Close + Next page + Prior page + None + Title + Show/Hide + footer header + Rule Subscription + 添加大佬们提供的规则导入地址\n添加后点击可导入规则 + Pull the cloud progress + The current progress exceeds the cloud progress. Do you want to synchronize? + Synchronous reading progress + Synchronize reading progress when entering / exiting the reading interface + Failed to create bookmark + Single URL + Export the list of books + Import the list of books + Download in advance + Download %s chapters in advance + Is enabled + Background image + Copy book URL + Copy chapters URL + Export to a folder + Exported text coding + Export to WebDav + Reverse content + Debug + Crash log + Custom Chinese line feed + Style of Images + System tts + Exported file format + Check by author + This URL has subscribed + High screen refresh rate + Use maximum screen refresh rate + Export all + Finished + Show unread flag + Always show default cover + Always show the default cover, do not show the network cover + Search source code + Book source code + Chapters source code + Content source code + List source code + Font size + Margin top + Marigin bottom + Show + Hide + Hide when status bar show + Reverse toc + 显示发现 + 样式 + 分组样式 + 导出文件名 + 重置 + url为空 + 字典 + 未知错误 + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 879ce0647..bc6e12b19 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -4,6 +4,8 @@ @color/md_blue_grey_700 @color/md_deep_orange_800 + @color/md_dark_disabled + @color/md_grey_900 @color/md_grey_850 @color/md_grey_800 @@ -12,6 +14,7 @@ #69000000 #10ffffff + #20ffffff #30ffffff #50ffffff diff --git a/app/src/main/res/values-night/strings.xml b/app/src/main/res/values-night/strings.xml deleted file mode 100644 index 0d2c4cc40..000000000 --- a/app/src/main/res/values-night/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/arrays.xml b/app/src/main/res/values-pt-rBR/arrays.xml new file mode 100644 index 000000000..132358ee0 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/arrays.xml @@ -0,0 +1,115 @@ + + + + Texto + Áudio + + + + Aba + Pasta + + + + @string/indent_0 + @string/indent_1 + @string/indent_2 + @string/indent_3 + @string/indent_4 + + + + .txt + .json + .xml + + + + @string/jf_convert_o + @string/jf_convert_j + @string/jf_convert_f + + + + Adaptar do sistema + Tema Claro + Tema Escuro + Tema E-Ink + + + + Autom. + Escuro + Claro + Adaptado + + + + Padrão + 1 min + 2 min + 3 min + Sempre + + + + @string/screen_unspecified + @string/screen_portrait + @string/screen_landscape + @string/screen_sensor + + + + íconePrincipal + ícone1 + ícone2 + ícone3 + ícone4 + ícone5 + ícone6 + + + + Desativado + Tradicional a Simplificado + Simplificado a Tradicional + + + + Fonte padrão + Fonte Serif + Fonte Monospaced + + + + Em branco + Título + Tempo + Bateria + Páginas + Progresso + Páginas e progresso + Nome do livro + Tempo e Bateria + + + + Normal + Negrito + Claro + + + + Autom. + Chinês simplificado + Chinês tradicional + Inglês + + + + FonteLivro + FonteRSS + SubstituirRegra + + + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..b3f2e2eb1 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,850 @@ + + + + Legado + Legado·pesquisa + Legado precisa de acesso ao armazenamento para encontrar e ler livros. Por favor, vá às "Configurações do App" para conceder "Permissão de armazenamento". + + + Início + Restaurar + Importar dados ao Legado + Criar uma subpasta + Criar uma pasta de Backup com o nome Legado. + Backup do cache dos livros para leitura off-line + Exportar localmente e fazer Backup à pasta de exportação + Backup para + Por favor, selecione uma pasta de Backup. + Importar dados de Legado + Importar dados de Github + Substituição + Enviar + + Aviso + Cancelar + Confirmar + Vá até às Configurações + Não foi possível abrir Configurações. + + Tentar novamente + Carregando + Aviso + Editar + Excluir + Excluir tudo + Substituir + Substituição + Configurar regras de substituição + Indisponível + Ativar + Procurar o substituto + Estante + Favoritos + Favorito + em Favoritos + Não está em Favoritos + Assinatura + Tudo + Leitura recente + Última leitura + Novidades + A estante está vazia. Procure por livros ou adicione-os via descoberta! \n se você o usa pela primeira vez, por favor, preste atenção à conta pública [开源阅读] para obter a fonte do livro! + Pesquisar + Download + Lista + Grade-3 + Grade-4 + Grade-5 + Grade-6 + Layout + Vista + Biblioteca + Importar livros + Fontes de livros + Gerenciar fontes + Criar/Importar/Editar/Gerenciar fontes de livros + Configurações + Configurações de Temas + Alguns ajustes relacionados à interface ou cor + Outras configurações + Algumas configurações relacionadas à funcionamento + Sobre + Doações + Sair + Não foi salvo. Você quer continuar editando? + Tipos de livros + Versão + Local + Procurar + Origem: %s + Origem: %s + Título + Último: %s + Você gostaria de adicionar %s à sua Estante? + %s arquivo(s) de texto em total + Carregando… + Tentar novamente + Serviço Web + Fonte de edição web e livro lidos + Editar fontes de livros na web + Cache off-line + Cache off-line + salvar o(s) capítulo(s) selecionado(s) no Armazenamento em cache + Alterar a origem + + \u3000\u3000 Este é um App de leitura de software livre, desenvolvido em Kotlin, você é bem-vindo a participar em projeto. Siga a conta oficial de WeChat [开源阅读]! + + + Legado (YueDu 3.0) link de download:\n https://play.google.com/store/apps/details?id=io.legado.play.release + + Versão %s + Verificação em segundo plano + Poderá usar a vontade ao verificar a fonte do livro + Atualização automática + Atualizar os livros automaticamente ao abrir o App + Download automático + Baixar automaticamente os últimos capítulos ao atualizar os livros + Backup e restauração + Configurações de WebDav + Configurações/importação WebDav de dados Legado + Fazer Backup + Restaurar + O Backup precisa de permissão de armazenamento + Para restaurar é necessária a permissão de armazenamento + OK + Cancelar + Confirmação do Backup + Os novos arquivos de Backup substituirão os originais.\n Pasta de Backup: YueDu + Confirmação da Restauração + A restauração dos dados da estante irá substituir a estante atual. + Backup bem sucedido + O Backup falhou + Restaurando + Restauração bem sucedida + A restauração falhou + Orientação da tela + Auto(sensor) + Paisagem + Retrato + Adaptar do sistema + Isenção de responsabilidade + %d capítulos + Interface + Brilho + Capítulos + Próximo + Anterior + Ocultar barra de status + Ocultar a barra de navegação do sistema durante leitura + Voz + Falando + Clique para abrir a leitura + Reproduzir + Reproduzindo + Clique para abrir a reprodução + Pausar + Anterior + Atualizar + Começar + Parar + Pausar + Retomar + Timer + Voz pausada + Falando(%d min restantes) + Reproduzindo(%d min restantes) + Esconder botões virtuais durante a leitura + Ocultar a barra de navegação + Cor da barra de navegação + Avaliação + E-mail + Falha ao abrir + Falha ao compartilhar + Sem capítulos + Adicionar Url + Adicionar Url de livro + Segundo plano + Autor + Autor: %s + Voz parada + Limpar o cache + O cache foi removido + Salvar + Editar fonte + Editar fonte do livro + Desativar fonte do livro + Adicionar fonte de livro + Adicionar fonte de assinatura + Adicionar livros + Pesquisar + Copiar a fonte + Colar a fonte + A descrição de fonte das regras + Verificar por atualizações + Digitalizar código QR + Digitalizar imagens locais + Descrição das regras + Compartilhar + Compartilhar via + Adaptar do sistema + Adicionar + Importar as fontes de livro + Importar localmente + Importar on-line + Substituição + Editar regra de substituição + Modelo + Substituição + Capa + Livro + Botões de volume para virar páginas + Toque a tela para virar página + Virar animação + Virar animação (livro) + Manter a tela ligada + Voltar + Menu + Ajuste + Barra de deslize + Ao limpar o cache você excluirá todos os capítulos salvos. Você tem certeza disso? + Compartilhamento de fontes de livros + Nome da regra + A regra padrão está vazia ou não está em conformidade com as especificações de Regex. + Ação de seleção + Selecionar tudo + Selecionar tudo(%1$d/%2$d) + Cancelar a seleção de todos(%1$d/%2$d) + Modo escuro + Página de boas-vindas + Iníciar o download + Cancelar o download + Nenhum download + Baixado %1$d/%2$d + Importar livro(s) selecionado(s) + Número de tarefas simultâneas + Alterar o ícone + Excluir + Começar a leitura + Carregando… + Falha ao carregar, toque para tentar novamente + Descrição do livro + Descrição:%s + Descrição: sem introdução + Abrir o livro externo + Origem: %s + Import replace rule + Importar regras on-line + Intervalo de atualizações + Por recentes + Por data de atualização + Por título do livro + Ordenar manualmente + Estratégia de leitura + Tipografia + Excluir selecionados + Você quer excluir? + Fonte padrão + Descoberta + Descoberta + Sem conteúdo. Vá ao Gerenciador de fontes para adicioná-lo! + Excluir tudo + Histórico de busca + Limpar + Mostrar título do livro no texto + Sincronização de fontes dos livros + Não há último capítulo. + Mostrar o tempo e a bateria + Divisor da tela + Escurecer a cor do ícone na barra de status + Conteúdo + Copiar + Baixar tudo + Este é um texto de teste, \n\u3000\u3000 apenas para mostrar o resultado + Cor e fundo (toque longo para personalizar) + Barra de status imersiva + %d capítulo(s) restante(s) + Nenhum selecionado + Clique longo, para introduzir o valor da cor + Carregando… + Aguardando + Quase lá + Favoritos + Adicionar aos Favoritos + Excluir + Tempo limite de carregamento + Siga:%s + Copiado com sucesso + A organização da estante + Isto excluirá todos os livros. Por favor, tenha cuidado. + Pesquisar fontes de livros + Buscar fontes de assinatura + Pesquisa( %d fontes no total) + Capítulos(%d) + Negrito + Fonte + Texto + Página inicial + Direita + Esquerda + Parte inferior + Parte superior + Com espaço + Espaço superior + Espaço inferior + Espaço da esquerda + Espaço da direita + Verificar fontes do livro + Verificar a fonte selecionada + %1$s Progresso %2$d/%3$d + Favor instalar e selecionar o TTS chinês! + A inicialização do TTS falhou! + Conversão simplificada + Desligado + Simplificado ao tradicional + Tradicional ao simplificado + Modo de virar + %1$d itens + Armazenamento: + Adicionar ao Estante + Adicionar ao Estante(%1$d) + %1$d livros adicionados com sucesso + Favor colocar os arquivos de fontes na pasta Root de Fontes no armazenamento e selecionar novamente + Fonte padrão + Selecionar fontes + Tamanho do texto + Espaço entre linhas + Espaço entre parágrafos + Na parte superior + Seleção no topo + Na parte inferior + Seleção no fundo + Expansão automática de Descoberta + A expansão padrão da primeira Descoberta. + Linhas atuais %s + Velocidade da voz + Deslize automático + Impedir deslize automático + Velocidade automática ao deslizar + Informações sobre livro + Editar informações do livro + Definir a Estante como página inicial + Vá automaticamente à lista de Recentes + Objeto substituto. Nome do livro ou Url de origem estão disponíveis + Grupos + Pasta de Cache + Seletor de arquivos do sistema + Nova versão + Baixar atualizações + Botões de volume para virar páginas durante leitura + Ajuste de margem + Ativar atualizações + Desativar atualizações + Inverso + Pesquisar livro por nome/autor + Nome do livro,Autor,URL + FAQ + Mostrar todas as Descobertas + Mostrar a fonte da Descoberta selecionada se encerrado + Atualizar os capítulos + Capítulos de Txt Regex + Codificação de texto + Ordem ascendente/descendente + Ordenar + Ordenar automaticamente + Ordenar manualmente + Ordenar por nome + Vá ao topo + Vá ao final + Ler: %s + Aguardando atualização + Quase lá + Concluído + Tudo + Aguardando atualização dos livros + Aguardando mais capítulos de livros + Livros concluídos + Livros locais + A cor da barra de status fica transparente + barra de navegação de imersão + A barra de navegação fica transparente + Adicionar à Estante + Continuar lendo + Pasta das capas + Capa + Deslizar + Simulação + Deslizar + Nenhum + Esta fonte de livro usa recursos avançados, por favor vá até Doações e toque no código de busca do envelope vermelho Alipay para receber o envelope vermelho, depois você pode usá-lo. + Atualizar o último capítulo após a alteração de origem em segundo plano + se ativado, a atualização será iniciada 1 minuto depois o App for aberto + Auto esconder a barra de ferramentas + A barra de ferramentas será escondida automaticamente ao deslizar a Estante + Login + Login%s + Sucesso + A fonte atual não foi configurada com um endereço de login + Nenhuma página anterior + Nenhuma página seguinte + + + 源名称(Nome da fonte) + 源URL(fonteUrl) + 源分组(fonteGrupo) + 自定义源分组 + 输入自定义源分组名称 + 分类Url + 登录URL(loginUrl) + 源注释(fonteComentário) + 搜索地址(url) + 发现地址规则(url) + 书籍列表规则(ListaLivros) + 书名规则(nome) + 详情页url规则(livroUrl) + 作者规则(autor) + 分类规则(tipo) + 简介规则(intro) + 封面规则(capaUrl) + 最新章节规则(últimoCapítulo) + 字数规则(contagemPalavras) + 书籍URL正则(LivroModeloUrl) + 预处理规则(livroInfoInit) + 目录URL规则(tocUrl) + 允许修改书名作者(podeRenomear) + 目录下一页规则(proxTocUrl) + 目录列表规则(capítuloLista) + 章节名称规则(capítuloNome) + 章节URL规则(capítuloUrl) + VIP标识(éVip) + 更新时间(capítuloInfo) + 正文规则(conteúdo) + 正文下一页URL规则(proxConteúdoUrl) + WebViewJs(webJs) + 资源正则(fonteRegex) + 替换规则(substRegex) + 图片样式(imagemFormato) + + 图标(fonteÍcone) + 列表规则(regrasArtigos) + 列表下一页规则(regrasArtigos) + 标题规则(regrasTítulo) + guid规则(regraGuid) + 时间规则(regraPubData) + 类别规则(regraCategorias) + 描述规则(regraDescrição) + 图片url规则(regraImagem) + 内容规则(regraConteúdo) + 样式(formato) + 链接规则(regraLink) + + + + Nenhum fonte + Falha ao obter informações sobre os livros + Falha ao obter o conteúdo + Falha ao obter a lista de capítulos + Falha ao acessar o site:%s + Falha na leitura do arquivo + Falha ao carregar a lista de capítulos + Falha ao obter dados + Falha ao carregar\n%s + Sem internet + Tempo limite da conexão à rede + O processamento de dados falhou + + + Cabeçalho HTTP + Fonte de depuração + Importar de código QR + Compartilhar fontes selecionadas + Digitalizar código QR + Toque para exibir o menu quando for selecionado + Tema + Tipo de tema + Selecionar tema que você preferir + Junte-se ao grupo QQ + Para definir a imagem de fundo é necessária permissão de armazenamento + Digite o endereço da fonte de livro + Excluir arquivo + Arquivo excluído + Tem certeza que quer excluir este arquivo? + Pasta + Importação inteligente + Descoberta + Alterar o modo de exibição + Importação de livros locais requer permissão de armazenamento + Tema Noturno + E-Ink + Otimização para dispositivos E-ink + Este App requer permissão de armazenamento para salvar as informações do livro no Backup + Toque novamente para sair do App + Importação de livros locais requer permissão de armazenamento + A conexão de internet não está disponível + Sim + Não + OK + Realmente quer excluir? + Tem certeza que quer excluir %s? + Você realmente quer excluir todos os livros? + Você também quer excluir os capítulos de livros baixados? + Para digitalizar o código QR é necessário ter permissão da câmera + A voz está em execução, não pode virar páginas automaticamente + Codificação de entrada + Capítulos de Txt Regex + Para abrir livros locais é necessária permissão de armazenamento + Livro sem nome + URL da regra de substituição de entrada + A lista procurada obtida com sucesso%d + nome e URL não podem estar vazios + Galeria + obter envelopes vermelhos de AliPay + Nenhuma atualização no endereço + Abrindo a página inicial, ela voltará automaticamente após o sucesso + Após o login bem sucedido, por favor toque no ícone no canto superior direito para testar o acesso à página inicial + Capítulo + Para + Usando Regex + Indentação + Nenhum + Indentação com 1 caract. + Indentação com 2 caract. + Indentação com 3 caract. + Indentação com 4 caract. + Selecionar uma pasta + Selecionar um arquivo + Nenhuma Descoberta, você pode adicioná-la em Fontes de Livros + Restaurar padrão + A pasta personalizada de cache requer permissão de armazenamento + Preto + Sem conteúdo + Alteração de fonte, aguarde por favor + Os capítulos sem conteúdo + Espaço entre palavras + + Básico + Pesquisar + Descoberta + Informação + Capítulos + Conteúdo + + Modo E-Ink + Remover animações e otimizar a experiência com livros E-papel + Serviço Web + Porta Web + Porta atual %s + Compartilhar o código QR + Compartilhar sequências + Compartilhar Wifi + Por favor, conceda permissão de armazenamento + Retroceder rápido + Avançar rápido + Anterior + Próximo + Musica + Áudio + Ativar + Ativar js + Carregar a Url-base + Todas as fontes + O conteúdo de entrada não pode estar vazio + Limpar o cache da Descoberta + Editar Descoberta + Alternar o ícone do software exibido na área de trabalho + Ajuda + Minhas + Leituras + %d%% + %d min + Brilho autom. %s + Ler páginas em voz alta + Mecanismo de voz + Imagens de fundo + Cor de fundo + Cor de texto + Selecione uma foto + Administração de grupo + Seleção de grupo + Edição de grupo + Mover ao grupo + Adicionar aos grupos + Remover dos grupos + Novo substituto + Grupo + Grupo: %s + Capítulos: %s + Ativar a Descoberta + Desativar a Descoberta + Ativar selecionados + Desativar selecionados + Exportar selecionados + Exportar + Carregar os capítulos + Carregar os detalhes do livro + TTS + Senha WebDav + Digite sua senha de autorização para WebDav + Digite o endereço do seu servidor + Endereço do servidor WebDav + Conta WebDav + Digite sua conta WebDav + Fonte de assinatura + Editar a fonte de assinatura + Filtrar + Pesquisar as fontes da Descoberta + Localização atual: + Busca precisa + Iniciação do serviço + Vazio + Seletor de arquivos + Seletor de pastas + Concluído! + Endereço Uri falhou + Atualizar capa + Alterar a fonte + Imagem local + Tipo: + Segundo plano + Importando + Exportando + Definir que botões viram as páginas + Botão de página anterior + Botão de página seguinte + Primeiro adicionar este livro à Estante + Sem grupo + Frase anterior + Frase seguinte + Outra pasta + Demasiadas palavras para criar um código QR + Compartilhar fontes de assinatura + Compartilhar fontes de livros + Alteração automática ao modo escuro + Seguir o modo escuro do sistema + Voltar + Tom de voz online + (%1$d/%2$d) + Exibir assinatura + Serviço parou + Iniciando o serviço\nVerificando a barra de notificações para detalhes + Pasta padrão + Seletor de pastas do sistema + Seletor de pastas do App + Seletor de arquivos do App + Android 10+ não é capaz de ler e salvar arquivos devido a restrições de permissão + Clique longo para exibir o Legado·Pesquisa no menu de operação + Visualização da operação de texto na Pesquisa + Log + Log + Conversão simplificada + O ícone é um ícone vetorial, que não era suportado antes de Android 8.0 + Configurações da voz + Página inicial + Clique longo para selecionar o texto + Cabeçalho + Conteúdo + Rodapé + Selecionar o fim + Selecionar o início + Layout compartilhado + Navegador + Importar regras padrão + Nome + Regex + Mais menu + Menos + Mais + Tamanho e tipo da fonte + Excluir arquivo fonte + Padrão-1 + Padrão-2 + Padrão-3 + Título + Esquerda + Centro + Esconder + Adicionar ao grupo + Salvar imagem + Nenhum pasta padrão + Configurações de grupo + Ver os capítulos + Sombra na barra de navegação + Tamanho da sombra atual(elevação): %s + Padrão + Menu principal + Toque para conceder permissão + Legado precisa de permissão de armazenamento, por favor toque abaixo no botão "Conceder Permissão", ou vá às "Configurações"-"Permissões do Apps"-para abrir a permissão necessária. Se a permissão não funcionar, por favor, toque no "Selecionar Pasta" no canto superior direito para usar o seletor de pastas do sistema. + O texto selecionado não pode ser falado como texto completo + Ampliar para recortar + Atualização de capítulos + Os botões de fones de ouvido estão sempre disponíveis + Os botões de fones de ouvido estão disponíveis mesmo ao sair do App. + Colaboradores + Contato + Licença + Outros + 开源阅读 + Siga a conta oficial de WeChat + WeChat + Apoio ao meu trabalho é bem-vindo + Conta oficial[开源阅读] + Alteração de fonte + Toque para participar + Médio + Informação + Alterar o layout + Alteração de tamanho da fonte de texto + Suporte aos gestos em tela completa + + + Primário + Destaque + Cor de fundo + Cor de NavBar + Dia + Dia,Principal + Dia,Destaque + Dia,Cor de fundo + Dia,Cor de NavBar + Noite + Noite,Principal + Noite,Destaque + Noite,Cor de fundo + Noite,Cor de NavBar + Alterar a fonte automaticamente + Texto justificado + Texto alinhado com parte inferior + Velocidade de rolagem automática + Ordenar por URL + Faça backup local e de WebDav simultaneamente + Primeiro restaurar de WebDAV, ao clicar longo restaurar do local backup + Selecione uma pasta Legado de Backup + Ativado + Desativado + Iniciando o download + Este livro já está na lista de download + Clique para abrir + Siga [开源阅读] para ajudar, clicando nos anúncios + Código de gorjeta no WeChat + AliPay + Envelope vermelho de AliPay, código de busca + 537954522 Clique para copiar + Envelope vermelho de AliPay código QR + AliPay código QR + Coleção QQ Código QR + gedoor,Invinciblelee etc. Para mais detalhes vá ao Github + Limpar o cache dos livros e fontes baixados + Capa padrão + Lista de ignorados + Ignore alguns conteúdos durante restauração + Configuração da interface de leitura + Nome de grupo + Seção de observações + Ativar regra de substituição por padrão + Para livros recém adicionados + Selecione o arquivo de restauração + O fundo diurno não pode ser muito escuro! + A parte inferior diurno não pode ser muito escuro! + O fundo noturno não pode ser muito brilhante! + A parte inferior noturno não pode ser muito brilhante! + Tem que haver diferença entre destaque e cor de fundo + Tem que haver diferença entre destaque e cor de texto + Formato incorreto + Erro + Mostrar o widget de brilho + Idioma + Importar fonte de RSS + Sua doação tornará este App ainda melhor + Conta oficial de Wechat [开源阅读软件] + Histórico de leitura + Leia o resumo do histórico + TTS local + Contagem de tópicos + Tempo total da leitura + Desmarcar todos + Importar + Exportar + Salvar configuração do tema + Salvar a configuração do tema diurno + Salvar a configuração do tema noturno + Lista de temas + Salvar, Importar, Compartilhar tema + Alterar o tema padrão + Ordenar por tempo de atualização + Pesquisar conteúdo + Siga [开源阅读] no WeChat para mais fontes de RSS! + Atualmente não há nenhuma fonte, siga o número público [开源阅读] para adicionar uma fonte de livro via Descoberta + 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. + Nome de tema + Excluir automaticamente dados vencidos da busca + Histórico de busca mais de um dia + Re-segmentar + Formato do nome: + Clique no ícone da pasta no canto superior direito e selecione a pasta + Busca inteligente + Nome do arquivo importado + Sem livros + Manter o nome original + O controle de toque da tela + Fechar + Próxima página + Página anterior + Nenhuma Ação + Título + Mostrar/ocultar + rodapé cabeçalho + Assinatura de regras + 添加大佬们提供的规则导入地址\n添加后点击可导入规则 + Obter o progresso da nuvem + O progresso atual excede o progresso da nuvem. Você quer sincronizar? + Progresso de leitura sincronizada + Sincronizar o progresso da leitura ao entrar e sair da tela de leitura + Falha ao marcar como favorito + URL única + Exportar a lista de livros + Importar a lista de livros + Baixar antecipadamente + Baixar %s capítulos antecipadamente + Está ativado + Imagem de fundo + Copiar a URL do livro + Copiar a URL dos capítulos + Exportar a pasta + Codificação de texto exportada + Exportar para WebDav + Conteúdo reverso + Depuração + Registro de falhas + Utilização de layout chinesas personalizadas + Formato de imagem + Sistema TTS + Formato de exportação + Verificar por autor + Esta URL já foi assinada + Alta taxa de atualização da tela + Use a maior taxa de atualização da tela + Exportar tudo + Concluído + Mostrar não lido + Sempre mostrar capa padrão + Sempre mostrar capa padrão, não mostrar capa da rede + Procurar código fonte + Código fonte do livro + Código fonte dos capítulos + Código fonte do conteúdo + Listar código fonte + Tamanho de letra + Borda superior + Borda inferior + Mostrar + Ocultar + Ocultar quando a barra de status é mostrada + TOC reverso + Mostrar + Formato + Formato de grupo + Exportar o nome do arquivo + Resetar + Nenhuma url + Dicionários + 未知错误 + diff --git a/app/src/main/res/values-zh-rHK/arrays.xml b/app/src/main/res/values-zh-rHK/arrays.xml index 4e75577e4..a16e661f9 100644 --- a/app/src/main/res/values-zh-rHK/arrays.xml +++ b/app/src/main/res/values-zh-rHK/arrays.xml @@ -1,11 +1,21 @@ + + 文本 + 音頻 + + + + 標籤 + 文件夾 + + 跟隨系統 亮色主題 暗色主題 - E-Ink(墨水屏) + E-Ink(墨水螢幕) @@ -35,6 +45,18 @@ 系統等寬字體 + + + 標題 + 時間 + 電量 + 頁數 + 進度 + 頁數同埋進度 + 書名 + 時間同埋電量 + + 正常 粗體 @@ -42,10 +64,16 @@ - 跟随系统 - 简体中文 - 繁体中文 + 跟隨系統 + 簡體中文 + 繁體中文 英文 - \ No newline at end of file + + 書源 + 訂閲源 + 替換規則 + + + diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 0e6ae2b2d..5d4812042 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -10,6 +10,8 @@ 導入閲讀數據 創建子文件夾 創建 legado 文件夾作爲備份路徑 + 離線緩存書籍備份 + 導出本地同時備份到legado文件夾下exports目錄 備份路徑 導入舊版數據 導入 Github 數據 @@ -43,7 +45,7 @@ 最近閲讀 最後閲讀 更新日誌 - 書架還空着,快去添加吧! + 書架還空著,先去搜索書籍或從發現裏添加吧!\n如果初次使用請先關註公眾號[开源阅读]獲取書源! 搜尋 下載 列表 @@ -80,12 +82,11 @@ 載入中… 重試 Web 服務 - 啟用 Web 服務 + 瀏覽器寫源,看書 web 編輯書源 - http://%1$s:%2$d - 離線下載 - 離線下載 - 下載已選擇的章節到本地 + 離線緩存 + 離線緩存 + 緩存已選擇的章節到本地 換源 \u3000\u3000這是一款使用 Kotlin 全新開發的開源的閲讀應用程式,歡迎你的加入。關注公眾號[legado-top]! @@ -94,6 +95,8 @@ 閲讀3.0下載地址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release Version %s + 後臺校驗書源 + 打開后可以在校驗書源時自由操作 自動刷新 打開程式時自動更新書輯 自動下載最新章節 @@ -145,11 +148,11 @@ 繼續 定時 朗讀暫停 - 正在朗讀(剩餘 %d 分鐘) + 讀緊(剩餘 %d 分鐘) + 播緊(剩餘 %d 分鐘) 閲讀介面隱藏導航欄 隱藏導航欄 導航欄顏色 - GitHub 評分 發送電子郵件 無法打開 @@ -183,8 +186,8 @@ 跟隨系統 添加 導入書源 - 本地導入 - 網絡導入 + 本地導入 + 網絡導入 替換淨化 替換規則編輯 替換規則 @@ -193,8 +196,8 @@ 音量鍵翻頁 點擊翻頁 - 點擊總是翻下一頁 翻頁動畫 + 翻頁動畫(本書) 屏幕超時 返回 菜單 @@ -207,7 +210,7 @@ 選擇操作 全選 全選 (%1$d/%2$d) - 取消 (%1$d/%2$d) + 取消全選 (%1$d/%2$d) 深色模式 啟動頁 開始下載 @@ -223,9 +226,10 @@ 載入失敗,點擊重試 內容簡介 簡介: %s + 簡介: 暫無簡介 打開外部書籍 來源: %s - 本地導入 + 導入替換規則 導入在線規則 檢查更新間隔 按閲讀時間 @@ -256,7 +260,7 @@ 文字顏色和背景(長按自定義) 沉浸式狀態欄 還剩 %d 章未下載 - 沒有選擇 + 仲未揀 長按輸入顏色值 加載中… 追更區 @@ -266,7 +270,7 @@ 刪除 加載超時 關注: %s - 已拷貝 + 拷貝咗 整理書架 這將會刪除所有書籍,請謹慎操作。 搜索書源 @@ -288,7 +292,7 @@ 右邊距 校驗書源 校驗所選 - 進度 %1$d/%2$d + %1$s 進度 %2$d/%3$d 請安裝並選擇中文 TTS! TTS 初始化失敗! 簡繁轉換 @@ -297,7 +301,7 @@ 繁轉簡 翻頁模式 %1$d 項 - 存儲卡: + 存儲咭: 加入書架 加入書架 (%1$d) 成功添加 %1$d 本書 @@ -308,6 +312,9 @@ 行距 段距 置頂 + 置頂所選 + 置底 + 置底所選 自動展開發現 默認展開第一組發現 當前線程數 %s @@ -336,13 +343,13 @@ 顯示所有發現 關閉則只顯示勾選源的發現 更新目錄 - Txt目錄正則 + TXT目錄正則 設置編碼 倒序-順序 排序 智能排序 手動排序 - 拼音排序 + 名稱排序 滾動到頂部 滾動到底部 已讀: %s @@ -355,8 +362,8 @@ 完結書籍 本地書籍 狀態欄顏色透明 - 導航欄變色 - 導航欄根據夜間模式變化 + 沉浸式导航栏 + 导航栏颜色透明 放入書架 繼續閲讀 封面地址 @@ -398,6 +405,7 @@ 書籍 URL 正則 (bookUrlPattern) 預處理規則 (bookInfoInit) 目錄 URL 規則 (tocUrl) + 允許修改書名作者 (canReName) 目錄下一頁規則 (nextTocUrl) 目錄列表規則 (chapterList) 章節名稱規則 (ChapterName) @@ -406,9 +414,8 @@ 更新時間 (ChapterInfo) 正文規則 (content) 正文下一頁 URL 規則 (nextContentUrl) - webJs + WebViewJs (webJs) 資源正則 (sourceRegex) - 圖標 (sourceIcon) 列表規則 (ruleArticles) 列表下一頁規則 (ruleArticles) @@ -454,7 +461,7 @@ 確定刪除文件嗎? 手機目錄 智能導入 - 發現 + 發現 切換顯示樣式 導入本地書籍需存儲權限 夜間模式 @@ -464,10 +471,11 @@ 再按一次退出程式 導入本地書籍需存儲權限 網絡連接不可用 - - + + 唔係 確認 是否確認刪除? + 是否確認刪除 %s? 是否刪除全部書籍? 是否同時刪除已下載的書籍目錄? 掃描二維碼需相機權限 @@ -478,7 +486,7 @@ 未獲取到書名 輸入替換規則網址 搜索列表獲取成功%d - 書源名稱和 URL 不能為空 + 名稱和 URL 不能為空 圖庫 領支付寶紅包 沒有獲取到更新地址 @@ -563,6 +571,7 @@ 導出所選 導出 加載目錄 + 加載詳情頁 TTS WebDav 密碼 輸入你的 WebDav 授權密碼 @@ -586,8 +595,6 @@ 封面換源 選擇本地圖片 類型: - 文本 - 音頻 後台 正在導入 正在導出 @@ -612,7 +619,8 @@ 正在啟動服務\n具體信息查看通知欄 默認路徑 系統文件夾選擇器 - 自帶選擇器\n(Android10 以上因權限限制可能無法使用) + 自帶文件夾選擇器 + 自帶文件選擇器 Android10 以上因權限限制可能無法讀寫文件 長按文字在操作菜單中顯示閲讀·搜索 文字操作顯示搜索 @@ -664,15 +672,16 @@ 開發人員 聯繫我們 開源許可 - 關注公衆號 + 關注公眾號 WeChat 你的支持是我更新的動力 - 公众号[开源阅读] + 公眾號[开源阅读] 正在自動換源 點擊加入 信息 切換佈局 + 全面屏手勢優化 主色調 @@ -689,10 +698,7 @@ 夜間,強調色 夜間,背景色 夜間,導航欄顏色 - 隱藏頁眉 - 隱藏頁脚 自動換源 - 置底 文字兩端對齊 自動翻頁速度 地址排序 @@ -710,54 +716,134 @@ 該書已在下載列表 點擊打開 關注[legado-top]點擊廣告支持我 - 微信赞赏码 + 微信讚賞碼 支付寶 支付寶紅包搜索碼 537954522 點擊複製 支付寶紅包二維碼 支付寶收款二維碼 QQ收款二維碼 - gedoor,Invinciblelee等,详情请在github中查看 + gedoor,Invinciblelee等,詳情請在github中查看 清除已下載書籍和字體緩存 默認封面 恢復忽略列表 恢復時忽略一些內容不恢復,方便不同手機配置不同 閱讀界面設置 - 图片样式(imageStyle) - 替换规则(replaceRegex) + 圖片樣式 (imageStyle) + 替換規則 (replaceRegex) 分組名稱 備註內容 - E-Ink模式下只有白纸黑字,不显示其它背景 - 默认启用替换净化 - 新加入书架的书是否启用替换净化 - 选择恢复文件 + 默認啟用替換淨化 + 新加入書架的書是否啟用替換淨化 + 選擇恢復文件 白天背景不能太暗 - 白天底栏不能太暗 - 夜间背景不能太亮 - 夜间底栏不能太亮 - 强调色不能和背景颜色相似 - 强调色不能和文字颜色相似 - 格式不对 - 错误 - 显示亮度调节控件 - 语言 - 导入订阅源 - 您的支持是我更新的动力 - 公众号[开源阅读软件] - 阅读记录 - 阅读时间记录 + 白天底欄不能太暗 + 夜間背景不能太亮 + 夜間底欄不能太亮 + 強調色不能和背景顏色相似 + 強調色不能和文字顏色相似 + 格式不對 + 錯誤 + 顯示亮度調節控制項 + 語言 + 匯入訂閱源 + 您嘅支援喺我更新嘅動力 + 公眾號[开源阅读软件] + 閲讀記錄 + 閱讀時間記錄 本地TTS - 线程数 - 总阅读时间 - 全不选 - 删除所有 - 导入 - 导出 - 保存主题配置 - 保存白天主题配置以共调用和分享 - 保存夜间主题配置以共调用和分享 - 主题列表 - 使用保存主题,导入,分享主题 + 線程數 + 總閲讀時間 + 全部唔要 + 刪除所有 + 導入 + 導出 + 儲存主題配置 + 儲存白天主題配置以供使用同埋分享 + 儲存夜間主題配置以供使用同埋分享 + 主題列表 + 使用儲存主題,匯入,分享主題 切換默認主題 + 分享選中源 + 時間排序 + 全文搜索 + 關注公眾號[开源阅读]獲取訂閲源! + 目前沒有發現源,關注公眾號[开源阅读]添加包含發現的書源! + 將焦點放到輸入框按下物理按鍵會自動輸入鍵值,多個按鍵會自動用英文逗號隔開. + 主題名 + 自動清除過期搜尋資料 + 超過一天的搜尋資料 + 重新分段 + 樣式名稱: + 點擊右上角資料夾圖示,選擇資料夾 + 智能掃描 + 導入文件名 + 複製書籍URL + 複製目錄URL + 冇書 + 保留原名 + 點擊區域設定 + 閂咗 + 下一頁 + 上一頁 + 無操作 + 正文標題 + 顯示/隱藏 + 頁眉頁腳 + 規則訂閱 + 添加大佬們提供的規則匯入地址 添加後點擊可匯入規則 + 拉取雲端進度 + 目前進度超過雲端進度,係咪同步? + 同步閱讀進度 + 進入退出閱讀介面時同步閱讀進度 + 建立書籤失敗 + 單URL + 導出書單 + 導入書單 + 預下載 + 預先下載%s章正文 + 係咪啟用 + 背景圖片 + 導出資料夾 + 導出編碼 + 導出到WebDav + 反轉內容 + 調試 + 崩潰日誌 + 使用自訂中文分行 + 圖片樣式 + 系統TTS + 導出格式 + 校驗作者 + 搜尋源碼 + 書輯源碼 + 目錄源碼 + 正文源碼 + 列表源碼 + 此url訂閱咗 + 高刷 + 使用螢幕最高刷新率 + 導出所有 + 完成 + 顯示未讀標誌 + 總是使用默認封面 + 總是使用默認封面,唔顯示網路封面 + 字號 + 上邊距 + 下邊距 + 顯示 + 隱藏 + 狀態欄顯示時隱藏 + 自訂源分組 + 輸入自訂源分組名稱 + 反轉目錄 + 顯示發現 + 樣式 + 分組樣式 + 導出文件名 + 重置 + url為空 + 字典 + 未知錯誤 diff --git a/app/src/main/res/values-zh-rTW/arrays.xml b/app/src/main/res/values-zh-rTW/arrays.xml index 23909ad85..d88800ca3 100644 --- a/app/src/main/res/values-zh-rTW/arrays.xml +++ b/app/src/main/res/values-zh-rTW/arrays.xml @@ -1,16 +1,13 @@ - @string/book_type_text - @string/book_type_audio + 文字 + 音訊 - - @string/indent_0 - @string/indent_1 - @string/indent_2 - @string/indent_3 - @string/indent_4 + + 標籤 + 文件夾 @@ -19,12 +16,6 @@ .xml - - @string/jf_convert_o - @string/jf_convert_j - @string/jf_convert_f - - 跟隨系統 亮色主題 @@ -47,18 +38,6 @@ 常亮 - - @string/sys_folder_picker - @string/app_folder_picker - - - - @string/screen_unspecified - @string/screen_portrait - @string/screen_landscape - @string/screen_sensor - - iconMain icon1 @@ -89,6 +68,8 @@ 頁數 進度 頁數及進度 + 書名 + 時間及電量 @@ -104,4 +85,10 @@ 英文 + + 書源 + 訂閲源 + 替換規則 + + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7be3d0924..cb5621f19 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2,19 +2,22 @@ 閱讀 閱讀·搜尋 - 閱讀需要存取記憶卡權限,請前往“設定”—“應用程式權限”—打開所需權限 + 閱讀需要存取記憶卡權限,請前往「設定」—「應用程式權限」—打開所需權限 - Home + 備份 復原 匯入閱讀資料 建立子資料夾 建立legado資料夾作為備份資料夾 + 離線快取書籍備份 + 匯出本機同時備份到legado資料夾下exports目錄 備份路徑 + 請選擇備份路徑 匯入舊版資料 匯入Github資料 淨化取代 - Send + 傳送 提示 取消 @@ -27,6 +30,7 @@ 提醒 編輯 刪除 + 刪除所有 取代 取代淨化 配置取代淨化規則 @@ -43,7 +47,7 @@ 最近閱讀 最後閱讀 更新日誌 - 書架還空著,先去添加吧! + 書架還空著,先去搜尋書籍或從發現裡添加吧!\n如果初次使用請先關注公眾號[开源阅读]獲取書源! 搜尋 下載 列表 @@ -54,7 +58,7 @@ 書架布局 檢視 書城 - 添加本機 + 新增本機 書源 書源管理 建立/匯入/編輯/管理書源 @@ -80,12 +84,11 @@ 載入中… 重試 Web 服務 - 啟用Web服務 + 瀏覽器寫源,看書 web編輯書源 - http://%1$s:%2$d - 離線下載 - 離線下載 - 下載選擇的章節到本機 + 離線快取 + 離線快取 + 快取選擇的章節到本機 換源 \u3000\u3000這是一款使用Kotlin全新開發的開源的閱讀軟體,歡迎您的加入。關注公眾號[legado-top]! @@ -94,6 +97,8 @@ 閱讀3.0下載網址:\nhttps://play.google.com/store/apps/details?id=io.legado.play.release Version %s + 後臺校驗書源 + 打開後可以在校驗書源時自由操作 自動重新整理 打開軟體時自動更新書籍 自動下載最新章節 @@ -103,8 +108,8 @@ WebDav設定/匯入舊版本資料 備份 復原 - 備份請給與儲存權限 - 復原請給與儲存權限 + 備份請給予儲存權限 + 復原請給予儲存權限 確認 取消 確認備份嗎? @@ -146,17 +151,17 @@ 定時 朗讀暫停 正在朗讀(還剩%d分鐘) + 正在朗播放(還剩%d分鐘) 閱讀介面隱藏虛擬按鍵 隱藏導航欄 導航欄顏色 - GitHub 評分 發送郵件 無法打開 分享失敗 無章節 - 添加網址 - 添加書籍網址 + 新增網址 + 新增書籍網址 背景 作者 作者: %s @@ -169,7 +174,7 @@ 禁用書源 建立書源 建立訂閱源 - 添加書籍 + 新增書籍 掃描 複製源 貼上源 @@ -181,10 +186,10 @@ 分享 軟體分享 跟隨系統 - 添加 + 新增 匯入書源 - 本機匯入 - 網路匯入 + 本機匯入 + 網路匯入 取代淨化 取代規則編輯 取代規則 @@ -193,8 +198,8 @@ 音量鍵翻頁 點擊翻頁 - 點擊總是翻下一頁 翻頁動畫 + 翻頁動畫(本書) 螢幕超時 返回 選單 @@ -207,7 +212,7 @@ 選擇操作 全選 全選(%1$d/%2$d) - 取消(%1$d/%2$d) + 取消全選(%1$d/%2$d) 深色模式 啟動頁 開始下載 @@ -223,9 +228,10 @@ 載入失敗,點擊重試 內容簡介 簡介:%s + 簡介: 暫無簡介 打開外部書籍 來源: %s - 本機匯入 + 匯入替換規則 匯入線上規則 檢查更新間隔 按閱讀時間 @@ -288,7 +294,7 @@ 右邊距 校驗書源 校驗所選 - 進度 %1$d/%2$d + %1$s 進度 %2$d/%3$d 請安裝並選擇中文TTS! TTS初始化失敗! 簡繁轉換 @@ -308,7 +314,9 @@ 行距 段距 置頂 + 置頂所選 置底 + 置底所選 自動展開發現 預設展開第一組發現 目前執行緒數 %s @@ -337,13 +345,13 @@ 顯示所有發現 關閉則只顯示勾選源的發現 更新目錄 - Txt目錄正則 + TXT目錄正則 設定編碼 倒序-順序 排序 智慧排序 手動排序 - 拼音排序 + 名稱排序 滾動到頂部 滾動到底部 已讀: %s @@ -356,8 +364,8 @@ 完結書籍 本機書籍 狀態欄顏色透明 - 導航欄變色 - 導航欄根據夜間模式變化 + 沉浸式導航欄 + 導航欄顏色透明 放入書架 繼續閱讀 封面地址 @@ -382,11 +390,13 @@ 源名稱(sourceName) 源URL(sourceUrl) 源分組(sourceGroup) + 自訂源分組 + 輸入自訂源分組名稱 分類Url 登入URL(loginUrl) + 源注釋(sourceComment) 搜尋地址(url) 發現地址規則(url) - 源注釋(sourceComment) 書籍列表規則(bookList) 書名規則(name) 詳情頁url規則(bookUrl) @@ -399,6 +409,7 @@ 書籍URL正則(bookUrlPattern) 預處理規則(bookInfoInit) 目錄URL規則(tocUrl) + 允許修改書名作者(canReName) 目錄下一頁規則(nextTocUrl) 目錄列表規則(chapterList) 章節名稱規則(ChapterName) @@ -407,7 +418,9 @@ 更新時間(ChapterInfo) 正文規則(content) 正文下一頁URL規則(nextContentUrl) - webJs + WebViewJs(webJs) + 圖片樣式(imageStyle) + 取代規則(replaceRegex) 資源正則(sourceRegex) 圖示(sourceIcon) @@ -442,6 +455,7 @@ 請求頭(header) 除錯源 二維碼匯入 + 分享選取源 掃描二維碼 選中時點擊可彈出選單 主題 @@ -455,7 +469,7 @@ 確定刪除文件嗎? 手機目錄 智慧匯入 - 發現 + 發現 切換顯示樣式 匯入本機書籍需儲存權限 夜間模式 @@ -469,6 +483,7 @@ 確認 是否確認刪除? + 是否確認刪除 %s? 是否刪除全部書籍? 是否同時刪除已下載的書籍目錄? 掃描二維碼需相機權限 @@ -479,7 +494,7 @@ 未獲取到書名 輸入取代規則網址 搜尋列表獲取成功%d - 書源名稱和URL不能為空 + 名稱和URL不能為空 圖庫 領支付寶紅包 沒有獲取到更新地址 @@ -564,7 +579,8 @@ 匯出所選 匯出 載入目錄 - TTS + 載入詳情頁 + 文字轉語音 WebDav 密碼 輸入你的WebDav授權密碼 輸入你的伺服器地址 @@ -587,8 +603,6 @@ 封面換源 選擇本機圖片 類型: - 文字 - 音訊 後台 正在匯入 正在匯出 @@ -613,7 +627,8 @@ 正在啟動服務\n具體訊息查看通知欄 預設路徑 系統資料夾選擇器 - 自帶選擇器\n(Android10以上因權限限制可能無法使用) + 自帶資料夾選擇器 + 自帶資料夾選擇器 Android10以上因權限限制可能無法讀寫文件 長按文字在操作選單中顯示閱讀·搜尋 文字操作顯示搜尋 @@ -670,15 +685,15 @@ 關注公眾號 微信 您的支援是我更新的動力 - 公眾號[開源閱讀] + 公眾號[开源阅读] 正在自動換源 點擊加入 訊息 - 隱藏頁首 - 隱藏頁尾 切換布局 - + 文章字重轉換 + 全面屏手勢最佳化 + 主色調 強調色 @@ -696,21 +711,19 @@ 夜間,底欄色 自動換源 文字兩端對齊 + 文字底部對齊 自動翻頁速度 地址排序 - 文章字體轉換 - 請選擇備份路徑 本機和 WebDav 一起備份 優先從 WebDav 復原,長按從本機復原 選擇舊版備份資料夾 已啟用 已禁用 - 文字底部對齊 正在啟動下載 該書已在下載列表 點擊打開 關注[legado-top]點擊廣告支持我 - 微信贊賞碼 + 微信讚賞碼 支付寶 支付寶紅包搜尋碼 537954522 點擊複製 @@ -723,11 +736,8 @@ 復原忽略列表 復原時忽略一些內容不復原,方便不同手機配置不同 閱讀介面設定 - 圖片樣式(imageStyle) - 取代規則(replaceRegex) 分組名稱 備註內容 - E-Ink模式下只有白紙黑字,不顯示其它背景 預設啟用取代淨化 新加入書架的書是否啟用取代淨化 選擇復原文件 @@ -743,22 +753,98 @@ 語言 匯入訂閱源 您的支援是我更新的動力 - 公眾號[開源閱讀軟體] + 公眾號[开源阅读软件] 閱讀記錄 閱讀時間記錄 本機TTS 執行緒數 總閱讀時間 全不選 - 刪除所有 - 导入 - 导出 - 保存主题配置 - 保存白天主题配置以共调用和分享 - 保存夜间主题配置以共调用和分享 - 主题列表 - 使用保存主题,导入,分享主题 - 切換默認主題 - + 匯入 + 匯出 + 儲存主題配置 + 儲存白天主題配置以供呼叫和分享 + 儲存夜間主題配置以供呼叫和分享 + 主題列表 + 使用儲存主題,匯入,分享主題 + 切換預設主題 + 時間排序 + 全文搜尋 + 關注公眾號[开源阅读]獲取訂閱源! + 目前沒有發現源,關注公眾號[开源阅读]添加包含發現的書源! + 將焦點放到輸入框按下物理按鍵會自動輸入鍵值,多個按鍵會自動用英文逗號隔開. + 主題名稱 + 自動清除過期搜尋資料 + 超過一天的搜尋資料 + 重新分段 + 樣式名稱: + 點擊右上角資料夾圖示,選擇資料夾 + 智慧掃描 + 匯入檔案名 + 複製書籍URL + 複製目錄URL + 沒有書籍 + 保留原名 + 點擊區域設定 + 關閉 + 下一頁 + 上一頁 + 無操作 + 正文標題 + 顯示/隱藏 + 頁首頁尾 + 規則訂閱 + 添加大佬們提供的規則匯入地址\n添加後點擊可匯入規則 + 拉取雲端進度 + 目前進度超過雲端進度,是否同步? + 同步閱讀進度 + 進入退出閱讀介面時同步閱讀進度 + 建立書籤失敗 + 單URL + 匯出書單 + 匯入書單 + 預下載 + 預先下載%s章正文 + 是否啟用 + 背景圖片 + 匯出資料夾 + 匯出編碼 + 匯出到WebDav + 反轉內容 + 除錯 + 當機日誌 + 使用自訂中文分行 + 圖片樣式 + 系統TTS + 匯出格式 + 校驗作者 + 搜尋原始碼 + 書籍原始碼 + 目錄原始碼 + 正文原始碼 + 列表原始碼 + 此url已訂閱 + 高刷 + 使用螢幕最高刷新率 + 匯出所有 + 完成 + 顯示未讀標誌 + 總是使用預設封面 + 總是顯示預設封面,不顯示網路封面 + 字號 + 上邊距 + 下邊距 + 顯示 + 隱藏 + 狀態欄顯示時隱藏 + 反轉目錄 + 顯示發現 + 樣式 + 分組樣式 + 匯出檔案名 + 重設 + url為空 + 字典 + 未知錯誤 diff --git a/app/src/main/res/values-zh/arrays.xml b/app/src/main/res/values-zh/arrays.xml index 5fdf42b1d..dd54deede 100644 --- a/app/src/main/res/values-zh/arrays.xml +++ b/app/src/main/res/values-zh/arrays.xml @@ -1,16 +1,13 @@ - @string/book_type_text - @string/book_type_audio + 文本 + 音频 - - @string/indent_0 - @string/indent_1 - @string/indent_2 - @string/indent_3 - @string/indent_4 + + 标签 + 文件夹 @@ -19,12 +16,6 @@ .xml - - @string/jf_convert_o - @string/jf_convert_j - @string/jf_convert_f - - 跟随系统 亮色主题 @@ -47,18 +38,6 @@ 常亮 - - @string/sys_folder_picker - @string/app_folder_picker - - - - @string/screen_unspecified - @string/screen_portrait - @string/screen_landscape - @string/screen_sensor - - iconMain icon1 @@ -89,6 +68,8 @@ 页数 进度 页数及进度 + 书名 + 时间及电量 @@ -104,4 +85,10 @@ 英文 + + 书源 + 订阅源 + 替换规则 + + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index fe359bf67..910663504 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -10,6 +10,8 @@ 导入阅读数据 创建子文件夹 创建legado文件夹作为备份文件夹 + 离线缓存书籍备份 + 导出本地同时备份到legado文件夹下exports目录 备份路径 请选择备份路径 导入旧版数据 @@ -45,7 +47,7 @@ 最近阅读 最后阅读 更新日志 - 书架还空着,先去添加吧! + 书架还空着,先去搜索书籍或从发现里添加吧!\n初次使用请关注公众号[开源阅读]获取书源和教程! 搜索 下载 列表 @@ -82,12 +84,11 @@ 加载中… 重试 Web 服务 - 启用Web服务 + 浏览器写源,看书 web编辑书源 - http://%1$s:%2$d - 离线下载 - 离线下载 - 下载选择的章节到本地 + 离线缓存 + 离线缓存 + 缓存选择的章节到本地 换源 \u3000\u3000这是一款使用Kotlin全新开发的开源的阅读软件,欢迎您的加入。关注公众号[开源阅读]! @@ -96,6 +97,8 @@ 阅读3.0下载地址:\nhttps://www.coolapk.com/apk/256030 Version %s + 后台校验书源 + 打开后可以在校验书源时自由操作 自动刷新 打开软件时自动更新书籍 自动下载最新章节 @@ -148,10 +151,10 @@ 定时 朗读暂停 正在朗读(还剩%d分钟) + 正在播放(还剩%d分钟) 阅读界面隐藏虚拟按键 隐藏导航栏 导航栏颜色 - GitHub 评分 发送邮件 无法打开 @@ -185,8 +188,8 @@ 跟随系统 添加 导入书源 - 本地导入 - 网络导入 + 本地导入 + 网络导入 替换净化 替换规则编辑 替换规则 @@ -195,8 +198,8 @@ 音量键翻页 点击翻页 - 点击总是翻下一页 翻页动画 + 翻页动画(本书) 屏幕超时 返回 菜单 @@ -209,7 +212,7 @@ 选择操作 全选 全选(%1$d/%2$d) - 取消(%1$d/%2$d) + 取消全选(%1$d/%2$d) 深色模式 启动页 开始下载 @@ -225,9 +228,10 @@ 加载失败,点击重试 内容简介 简介:%s + 简介: 暂无简介 打开外部书籍 来源: %s - 本地导入 + 导入替换规则 导入在线规则 检查更新间隔 按阅读时间 @@ -290,7 +294,7 @@ 右边距 校验书源 校验所选 - 进度 %1$d/%2$d + %1$s 进度 %2$d/%3$d 请安装并选择中文TTS! TTS初始化失败! 简繁转换 @@ -310,7 +314,9 @@ 行距 段距 置顶 + 置顶所选 置底 + 置底所选 自动展开发现 默认展开第一组发现 当前线程数 %s @@ -345,7 +351,7 @@ 排序 智能排序 手动排序 - 拼音排序 + 名称排序 滚动到顶部 滚动到底部 已读: %s @@ -358,8 +364,8 @@ 完结书籍 本地书籍 状态栏颜色透明 - 导航栏变色 - 导航栏根据夜间模式变化 + 沉浸式导航栏 + 导航栏颜色透明 放入书架 继续阅读 封面地址 @@ -384,6 +390,8 @@ 源名称(sourceName) 源URL(sourceUrl) 源分组(sourceGroup) + 自定义源分组 + 输入自定义源分组名称 分类Url 登录URL(loginUrl) 源注释(sourceComment) @@ -401,6 +409,7 @@ 书籍URL正则(bookUrlPattern) 预处理规则(bookInfoInit) 目录URL规则(tocUrl) + 允许修改书名作者(canReName) 目录下一页规则(nextTocUrl) 目录列表规则(chapterList) 章节名称规则(ChapterName) @@ -409,7 +418,9 @@ 更新时间(ChapterInfo) 正文规则(content) 正文下一页URL规则(nextContentUrl) - webJs + WebViewJs(webJs) + 图片样式(imageStyle) + 替换规则(replaceRegex) 资源正则(sourceRegex) 图标(sourceIcon) @@ -444,6 +455,7 @@ 请求头(header) 调试源 二维码导入 + 分享选中源 扫描二维码 选中时点击可弹出菜单 主题 @@ -457,7 +469,7 @@ 确定删除文件吗? 手机目录 智能导入 - 发现 + 发现 切换显示样式 导入本地书籍需存储权限 夜间模式 @@ -471,6 +483,7 @@ 确认 是否确认删除? + 是否确认删除 %s? 是否删除全部书籍? 是否同时删除已下载的书籍目录? 扫描二维码需相机权限 @@ -481,7 +494,7 @@ 未获取到书名 输入替换规则网址 搜索列表获取成功%d - 书源名称和URL不能为空 + 名称和URL不能为空 图库 领支付宝红包 没有获取到更新地址 @@ -566,6 +579,7 @@ 导出所选 导出 加载目录 + 加载详情页 TTS WebDav 密码 输入你的WebDav授权密码 @@ -589,8 +603,6 @@ 封面换源 选择本地图片 类型: - 文本 - 音频 后台 正在导入 正在导出 @@ -615,7 +627,8 @@ 正在启动服务\n具体信息查看通知栏 默认路径 系统文件夹选择器 - 自带选择器\n(Android10以上因权限限制可能无法使用) + 自带文件夹选择器 + 自带文件选择器 Android10以上因权限限制可能无法读写文件 长按文字在操作菜单中显示阅读·搜索 文字操作显示搜索 @@ -677,10 +690,9 @@ 点击加入 信息 - 隐藏页眉 - 隐藏页脚 切换布局 文章字重切换 + 全面屏手势优化 主色调 @@ -724,11 +736,8 @@ 恢复忽略列表 恢复时忽略一些内容不恢复,方便不同手机配置不同 阅读界面设置 - 图片样式(imageStyle) - 替换规则(replaceRegex) 分组名称 备注内容 - E-Ink模式下只有白纸黑字,不显示其它背景 默认启用替换净化 新加入书架的书是否启用替换净化 选择恢复文件 @@ -754,10 +763,89 @@ 导入 导出 保存主题配置 - 保存白天主题配置以共调用和分享 - 保存夜间主题配置以共调用和分享 + 保存白天主题配置以供调用和分享 + 保存夜间主题配置以供调用和分享 主题列表 使用保存主题,导入,分享主题 切换默认主题 + 时间排序 + 全文搜索 + 关注公众号[开源阅读]获取订阅源! + 当前没有发现源,关注公众号[开源阅读]添加带发现的书源! + 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. + 主题名称 + 自动清除过期搜索数据 + 超过一天的搜索数据 + 重新分段 + 样式名称: + 点击右上角文件夹图标,选择文件夹 + 智能扫描 + 导入文件名 + 拷贝书籍URL + 拷贝目录URL + 没有书籍 + 保留原名 + 点击区域设置 + 关闭 + 下一页 + 上一页 + 无操作 + 正文标题 + 显示/隐藏 + 页眉页脚 + 规则订阅 + 添加大佬们提供的规则导入地址\n添加后点击可导入规则 + 拉取云端进度 + 当前进度超过云端进度,是否同步? + 同步阅读进度 + 进入退出阅读界面时同步阅读进度 + 创建书签失败 + 单URL + 导出书单 + 导入书单 + 预下载 + 预先下载%s章正文 + 是否启用 + 背景图片 + 导出文件夹 + 导出编码 + 导出到WebDav + 反转内容 + 调试 + 崩溃日志 + 使用自定义中文分行 + 图片样式 + 系统TTS + 导出格式 + 校验作者 + 搜索源码 + 书籍源码 + 目录源码 + 正文源码 + 列表源码 + 此url已订阅 + 高刷 + 使用屏幕最高刷新率 + 导出所有 + 完成 + 显示未读标志 + 总是使用默认封面 + 总是显示默认封面,不显示网络封面 + 字号 + 上边距 + 下边距 + 显示 + 隐藏 + 状态栏显示时隐藏 + 反转目录 + 显示发现 + 样式 + 分组样式 + 导出文件名 + 重置 + url为空 + 字典 + 未知错误 + 自动备份失败 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 5847f7765..2633c8003 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,8 +1,13 @@ - @string/book_type_text - @string/book_type_audio + Text + Audio + + + + Tab + Folder @@ -47,11 +52,6 @@ Always - - @string/sys_folder_picker - @string/app_folder_picker - - @string/screen_unspecified @string/screen_portrait @@ -89,6 +89,8 @@ Pages Progress Pages and progress + Book name + Time and Battery @@ -98,10 +100,16 @@ - 跟随系统 - 简体中文 - 繁体中文 - 英文 + Auto + Simplified_Chinese + Traditional_Chinese + English + + + + BookSource + RssSource + ReplaceRule \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index bdb8fd0b1..fd11b974c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -167,6 +167,11 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 30e85cda3..81726a580 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,6 +3,9 @@ @color/md_light_blue_600 @color/md_light_blue_700 @color/md_pink_800 + + @color/md_light_disabled + #66666666 #FF578FCC @@ -16,9 +19,11 @@ @color/md_grey_100 @color/md_grey_200 #7fffffff + #00000000 #10000000 + #20000000 #30000000 #50000000 diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cc2d9c170..c80e30395 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -5,4 +5,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/non_translat.xml b/app/src/main/res/values/non_translat.xml new file mode 100644 index 000000000..defba67b0 --- /dev/null +++ b/app/src/main/res/values/non_translat.xml @@ -0,0 +1,8 @@ + + + + http://%1$s:%2$d + GitHub + 【%s】 + + \ No newline at end of file diff --git a/app/src/main/res/values/pref_key_value.xml b/app/src/main/res/values/pref_key_value.xml index a2a2611cb..904a4e441 100644 --- a/app/src/main/res/values/pref_key_value.xml +++ b/app/src/main/res/values/pref_key_value.xml @@ -1,23 +1,15 @@ - auto_refresh - list_screen_direction - full_screen - threads_num - user_agent bookshelf_px - read_type - expandGroupFind - defaultToRead - autoDownload - checkUpdate 开源阅读 - https://celeter.github.io/ + https://alanskycn.gitee.io/teachme/ https://github.com/gedoor/legado https://github.com/gedoor/legado/graphs/contributors https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/ https://github.com/gedoor/legado/releases/latest https://api.github.com/repos/gedoor/legado/releases/latest + https://t.me/yueduguanfang + https://discord.gg/qDE52P5xGW diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ccc85629a..92954b0a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ + Legado @@ -10,6 +11,8 @@ Import Legado data Create a subfolder Create a folder named Legado as the backup folder. + Offline cache book backup + Export to local and back up to the exports directory under the legado folder Backup to Please select a backup path. Import legacy data @@ -45,7 +48,7 @@ Recent reading Last reading What\'s new - Still empty,please add it first. + The bookshelf is still empty. Search for books or add them from discovery! \n if you use it for the first time, please pay attention to the public account [开源阅读] to get the book source! Search Download List @@ -82,12 +85,11 @@ Loading… Retry Web service - Enable web service + Web edit source and read book Edit book sources on the web - http://%1$s:%2$d - Offline download - Offline download - Download the selected chapter(s) to Storage + Offline cache + Offline cache + cache the selected chapter(s) to Storage Change Origin \u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us. Follow the WeChat Official Account [开源阅读]! @@ -96,6 +98,8 @@ Legado (YueDu 3.0) download link:\n https://play.google.com/store/apps/details?id=io.legado.play.release Version %s + Background-verification + you can operate freely when verifying the book source Auto-refresh Update books automatically when opening the software Auto-download @@ -148,10 +152,10 @@ Timer Speak Paused Speaking(%d min left) + Playing(%d min left) Hide virtual buttons when reading Hide navigation bar Navigation bar color - GitHub Rating Email Open failed @@ -185,8 +189,8 @@ Follow system Add Import book sources - Import local sources - Import online sources + Import local + Import online Replacement Edit replacement rule Pattern @@ -195,8 +199,8 @@ Book Volume keys to turn page Tap screen to turn page - Tap screen always to next page Flip animation + Flip animation (book) Keep screen awake Back Menu @@ -209,7 +213,7 @@ Selection action Select all Select all(%1$d/%2$d) - Cancel(%1$d/%2$d) + Cancel select all(%1$d/%2$d) Dark mode Welcome page Download start @@ -225,9 +229,10 @@ Load failed, tap to retry Book description Description:%s + Description: no introduction Open external book Origin: %s - Import local rules + Import replace rules Import online rules Check interval for updates By recent list @@ -290,8 +295,7 @@ Padding right Check book sources Check the selected source - Debug the selected source - Progress %1$d/%2$d + %1$s Progress %2$d/%3$d Please install and select Chinese TTS! TTS initialization failed! Simplified conversion @@ -311,7 +315,9 @@ Line spacing Paragraph spacing To Top + Selection To Top To Bottom + Selection To Bottom Auto expand Discovery Default expand the first Discovery. Current threads %s @@ -319,8 +325,8 @@ Auto scroll Stop Auto scroll Auto scroll speed - Book infomation - Edit book infomation + Book information + Edit book information Use Bookshelf as start page Auto jump to Recent list Replacement object. Book name or source url is available @@ -346,7 +352,7 @@ Sort Sort automatically Sort manually - Sort phonetically + Sort by name Scroll to the top Scroll to the bottom Read: %s @@ -359,8 +365,8 @@ Finished books Local books The status bar color becomes transparent - Discolored navigation bar - The navigation bar changes color following the dark mode + immersion navigation bar + The navigation bar becomes transparent Add to Bookshelf Continue reading Cover path @@ -385,6 +391,9 @@ 源名称(sourceName) 源URL(sourceUrl) 源分组(sourceGroup) + 自定义源分组 + 输入自定义源分组名称 + 分类Url 登录URL(loginUrl) 源注释(sourceComment) @@ -402,6 +411,7 @@ 书籍URL正则(bookUrlPattern) 预处理规则(bookInfoInit) 目录URL规则(tocUrl) + 允许修改书名作者(canReName) 目录下一页规则(nextTocUrl) 目录列表规则(chapterList) 章节名称规则(ChapterName) @@ -410,7 +420,7 @@ 更新时间(ChapterInfo) 正文规则(content) 正文下一页URL规则(nextContentUrl) - webJs + WebViewJs(webJs) 资源正则(sourceRegex) 替换规则(replaceRegex) 图片样式(imageStyle) @@ -444,9 +454,10 @@ Data parsing failed - Source http header + HTTP Header Debug source Import from QR code + Share selected sources Scan QR code Tap to display Menu when selected Theme @@ -460,7 +471,7 @@ Are you sure to delete this file? Directory Intelligent import - Discovery + Discovery Switch display styles Import local books requires storage permission Night Theme @@ -474,6 +485,7 @@ No OK Are you sure to delete it? + Are you sure to delete %s? Are you sure to delete all books? Do you want to delete the downloaded book chapters at the same time? Scan QR code requires Camera permissions @@ -484,7 +496,7 @@ No bookName Input replacement rule URL Search list obtained successfully%d - Source name and URL cannot be empty + name and URL cannot be empty Gallery get AliPay red envelopes No update address @@ -569,6 +581,7 @@ Export selected Export Load chapters + Load book detail TTS WebDav password Input you WebDav authorized password @@ -592,8 +605,6 @@ Change origin Local image Type: - Text - Audio Background Importing Exporting @@ -618,7 +629,8 @@ Starting service\nChecking notification bar for details Default path System folder picker - App folder picker\n(Android 10+ May not be available due to permission restrictions) + App folder picker + App file picker Android 10+ unable to read and write file due to permission restrictions Long tap to display Legado·Search in the operation menu Text operation display Search @@ -680,11 +692,10 @@ Tap to join Middle Information - Hide header - Hide footer Switch Layout Text font weight switching - + Full screen gestures support + Primary Accent @@ -710,10 +721,9 @@ Select a legacy backup folder Enabled Disabled - Starting download This book is already in Download list - click to open + Click to open Follow [开源阅读] to support me by clicking on ads WeChat Tipping Code AliPay @@ -730,11 +740,9 @@ Reading interface settings Group name Remarks section - Only white page black text in E-Ink mode Enable replace rule by default For new added books Select restore file - Switch default theme Day background can not be too dark! Day bottom can not be too dark! Night background can not be too bright! @@ -754,12 +762,92 @@ Thread count Total read time Unselect all - Import str - Export str + Import + Export Save theme config Save day theme config Save night theme config Theme list Save, Import, Share theme + Switch default theme + Sort by update time + Search content + Follow Wechat official account [开源阅读] to get Subscription source + Empty now,Follow Wechat official account [开源阅读] to get Discovery source + 将焦点放到输入框按下物理按键会自动录入键值,多个按键会自动用英文逗号隔开. + Theme name + "Clear expired search histories automatically " + Search histories more than one day + Re-segment + Style name: + Click the folder icon in the upper right corner and select the folder + Intelligent scanning + Imported-file name + No books + Keep the original name + Screen touch control + Close + Next page + Prior page + None + Title + Show/Hide + footer header + Rule Subscription + 添加大佬们提供的规则导入地址\n添加后点击可导入规则 + Pull the cloud progress + The current progress exceeds the cloud progress. Do you want to synchronize? + Synchronous reading progress + Synchronize reading progress when entering / exiting the reading interface + Failed to create bookmark + Single URL + Export the list of books + Import the list of books + Download in advance + Download %s chapters in advance + Is enabled + Background image + Copy book URL + Copy chapters URL + Export folder + Exported text coding + Export to WebDav + Reverse content + Debug + Crash log + Custom Chinese line feed + Style of Images + System tts + Exported file format + Check by author + This URL has subscribed + High screen refresh rate + Use maximum screen refresh rate + Export all + Finished + Show unread flag + Always show default cover + Always show the default cover, do not show the network cover + Search source code + Book source code + Chapters source code + Content source code + List source code + Font size + Margin top + Marigin bottom + Show + Hide + Hide when status bar show + Reverse toc + Show + Style + Group style + Export file name + Reset + Null url + 字典 + 未知错误 + Autobackup failed - \ No newline at end of file + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b01529784..c397d6ed4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -52,8 +52,7 @@ - //**************************************************************Widget - Style******************************************************************************// + //*******************Widget Style**********************************// + diff --git a/app/src/main/res/xml/about.xml b/app/src/main/res/xml/about.xml index 89bc507eb..15a6c1523 100644 --- a/app/src/main/res/xml/about.xml +++ b/app/src/main/res/xml/about.xml @@ -9,13 +9,15 @@ app:iconSpaceReserved="false" /> @@ -51,6 +53,18 @@ android:summary="@string/this_github_url" app:iconSpaceReserved="false" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index 2c950a5b6..99b392d5a 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -3,4 +3,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_config_backup.xml b/app/src/main/res/xml/pref_config_backup.xml index b16c6f32b..6a57dcc87 100644 --- a/app/src/main/res/xml/pref_config_backup.xml +++ b/app/src/main/res/xml/pref_config_backup.xml @@ -36,6 +36,15 @@ app:allowDividerBelow="false" app:iconSpaceReserved="false" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/xml/pref_config_other.xml b/app/src/main/res/xml/pref_config_other.xml index 0e3930546..28360973f 100644 --- a/app/src/main/res/xml/pref_config_other.xml +++ b/app/src/main/res/xml/pref_config_other.xml @@ -18,27 +18,28 @@ - @@ -51,6 +52,21 @@ app:iconSpaceReserved="false" app:layout="@layout/view_preference_category"> + + + + + + - - + + + + + + - - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_config_theme.xml b/app/src/main/res/xml/pref_config_theme.xml index 673332a25..8fb8143be 100644 --- a/app/src/main/res/xml/pref_config_theme.xml +++ b/app/src/main/res/xml/pref_config_theme.xml @@ -20,10 +20,10 @@ app:iconSpaceReserved="false" /> + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/spen_remote_actions.xml b/app/src/main/res/xml/spen_remote_actions.xml new file mode 100644 index 000000000..f38cfebe8 --- /dev/null +++ b/app/src/main/res/xml/spen_remote_actions.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/avd.bat b/avd.bat new file mode 100644 index 000000000..9c6c97af0 --- /dev/null +++ b/avd.bat @@ -0,0 +1 @@ +emulator -avd %1 -dns-server 8.8.8.8 -no-snapshot-load \ No newline at end of file diff --git a/build.gradle b/build.gradle index b9930dac3..537e02b97 100644 --- a/build.gradle +++ b/build.gradle @@ -1,33 +1,27 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.0' + ext.kotlin_version = '1.5.30-M1' repositories { google() - jcenter() - maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } - maven { url 'https://s3.amazonaws.com/fabric-artifacts/public' } - maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } - maven { url 'https://maven.fabric.io/public' } + mavenCentral() maven { url 'https://plugins.gradle.org/m2/' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:7.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.3' - classpath 'com.google.gms:google-services:4.3.3' - classpath 'io.fabric.tools:gradle:1.31.2' + classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.4' } } allprojects { repositories { google() - jcenter() - maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } - maven { url "https://jitpack.io" } - maven { url 'https://maven.google.com/' } - maven { url 'https://github.com/psiegman/mvn-repo/raw/master/releases' } + mavenCentral() + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://jitpack.io' } } } diff --git a/epublib/.gitignore b/epublib/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/epublib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/epublib/build.gradle b/epublib/build.gradle new file mode 100644 index 000000000..27330e83e --- /dev/null +++ b/epublib/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 30 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + android { + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + +} \ No newline at end of file diff --git a/epublib/consumer-rules.pro b/epublib/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/epublib/proguard-rules.pro b/epublib/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/epublib/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/epublib/src/main/AndroidManifest.xml b/epublib/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f8f3d7bea --- /dev/null +++ b/epublib/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/Constants.java b/epublib/src/main/java/me/ag2s/epublib/Constants.java new file mode 100644 index 000000000..d9f873e41 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/Constants.java @@ -0,0 +1,13 @@ +package me.ag2s.epublib; + + +public interface Constants { + + String CHARACTER_ENCODING = "UTF-8"; + String DOCTYPE_XHTML = ""; + String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml"; + String EPUB_GENERATOR_NAME = "Ag2S EpubLib"; + String EPUB_DUOKAN_NAME = "DK-SONGTI"; + char FRAGMENT_SEPARATOR_CHAR = '#'; + String DEFAULT_TOC_ID = "toc"; +} diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java new file mode 100644 index 000000000..9b52e01cb --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java @@ -0,0 +1,158 @@ +package me.ag2s.epublib.browsersupport; + +import java.util.EventObject; + +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.util.StringUtil; + +/** + * Used to tell NavigationEventListener just what kind of navigation action + * the user just did. + * + * @author paul + * + */ +@SuppressWarnings("unused") +public class NavigationEvent extends EventObject { + + private static final long serialVersionUID = -6346750144308952762L; + + private Resource oldResource; + private int oldSpinePos; + private Navigator navigator; + private EpubBook oldBook; + private int oldSectionPos; + private String oldFragmentId; + + public NavigationEvent(Object source) { + super(source); + } + + public NavigationEvent(Object source, Navigator navigator) { + super(source); + this.navigator = navigator; + this.oldBook = navigator.getBook(); + this.oldFragmentId = navigator.getCurrentFragmentId(); + this.oldSectionPos = navigator.getCurrentSectionPos(); + this.oldResource = navigator.getCurrentResource(); + this.oldSpinePos = navigator.getCurrentSpinePos(); + } + + /** + * The previous position within the section. + * + * @return The previous position within the section. + */ + public int getOldSectionPos() { + return oldSectionPos; + } + + public Navigator getNavigator() { + return navigator; + } + + public String getOldFragmentId() { + return oldFragmentId; + } + + // package + void setOldFragmentId(String oldFragmentId) { + this.oldFragmentId = oldFragmentId; + } + + public EpubBook getOldBook() { + return oldBook; + } + + // package + void setOldPagePos(int oldPagePos) { + this.oldSectionPos = oldPagePos; + } + + public int getCurrentSectionPos() { + return navigator.getCurrentSectionPos(); + } + + public int getOldSpinePos() { + return oldSpinePos; + } + + public int getCurrentSpinePos() { + return navigator.getCurrentSpinePos(); + } + + public String getCurrentFragmentId() { + return navigator.getCurrentFragmentId(); + } + + public boolean isBookChanged() { + if (oldBook == null) { + return true; + } + return oldBook != navigator.getBook(); + } + + public boolean isSpinePosChanged() { + return getOldSpinePos() != getCurrentSpinePos(); + } + + public boolean isFragmentChanged() { + return StringUtil.equals(getOldFragmentId(), getCurrentFragmentId()); + } + + public Resource getOldResource() { + return oldResource; + } + + public Resource getCurrentResource() { + return navigator.getCurrentResource(); + } + + public void setOldResource(Resource oldResource) { + this.oldResource = oldResource; + } + + + public void setOldSpinePos(int oldSpinePos) { + this.oldSpinePos = oldSpinePos; + } + + + public void setNavigator(Navigator navigator) { + this.navigator = navigator; + } + + + public void setOldBook(EpubBook oldBook) { + this.oldBook = oldBook; + } + + public EpubBook getCurrentBook() { + return getNavigator().getBook(); + } + + public boolean isResourceChanged() { + return oldResource != getCurrentResource(); + } + + @SuppressWarnings("NullableProblems") + public String toString() { + return StringUtil.toString( + "oldSectionPos", oldSectionPos, + "oldResource", oldResource, + "oldBook", oldBook, + "oldFragmentId", oldFragmentId, + "oldSpinePos", oldSpinePos, + "currentPagePos", getCurrentSectionPos(), + "currentResource", getCurrentResource(), + "currentBook", getCurrentBook(), + "currentFragmentId", getCurrentFragmentId(), + "currentSpinePos", getCurrentSpinePos() + ); + } + + public boolean isSectionPosChanged() { + return oldSectionPos != getCurrentSectionPos(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java new file mode 100644 index 000000000..a856347ce --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java @@ -0,0 +1,18 @@ +package me.ag2s.epublib.browsersupport; + +/** + * Implemented by classes that want to be notified if the user moves to + * another location in the book. + * + * @author paul + * + */ +public interface NavigationEventListener { + + /** + * Called whenever the user navigates to another position in the book. + * + * @param navigationEvent f + */ + void navigationPerformed(NavigationEvent navigationEvent); +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java new file mode 100644 index 000000000..710f0240c --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java @@ -0,0 +1,207 @@ +package me.ag2s.epublib.browsersupport; + +import java.util.ArrayList; +import java.util.List; + +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Resource; + +/** + * A history of the user's locations with the epub. + * + * @author paul.siegmann + */ +public class NavigationHistory implements NavigationEventListener { + + public static final int DEFAULT_MAX_HISTORY_SIZE = 1000; + private static final long DEFAULT_HISTORY_WAIT_TIME = 1000; + + private static class Location { + + private String href; + + public Location(String href) { + super(); + this.href = href; + } + + @SuppressWarnings("unused") + public void setHref(String href) { + this.href = href; + } + + public String getHref() { + return href; + } + } + + private long lastUpdateTime = 0; + private List locations = new ArrayList<>(); + private final Navigator navigator; + private int currentPos = -1; + private int currentSize = 0; + private int maxHistorySize = DEFAULT_MAX_HISTORY_SIZE; + private long historyWaitTime = DEFAULT_HISTORY_WAIT_TIME; + + public NavigationHistory(Navigator navigator) { + this.navigator = navigator; + navigator.addNavigationEventListener(this); + initBook(navigator.getBook()); + } + + public int getCurrentPos() { + return currentPos; + } + + + public int getCurrentSize() { + return currentSize; + } + + public void initBook(EpubBook book) { + if (book == null) { + return; + } + locations = new ArrayList<>(); + currentPos = -1; + currentSize = 0; + if (navigator.getCurrentResource() != null) { + addLocation(navigator.getCurrentResource().getHref()); + } + } + + /** + * If the time between a navigation event is less than the historyWaitTime + * then the new location is not added to the history. + * + * When a user is rapidly viewing many pages using the slider we do not + * want all of them to be added to the history. + * + * @return the time we wait before adding the page to the history + */ + public long getHistoryWaitTime() { + return historyWaitTime; + } + + public void setHistoryWaitTime(long historyWaitTime) { + this.historyWaitTime = historyWaitTime; + } + + public void addLocation(Resource resource) { + if (resource == null) { + return; + } + addLocation(resource.getHref()); + } + + /** + * Adds the location after the current position. + * If the currentposition is not the end of the list then the elements + * between the current element and the end of the list will be discarded. + * + * Does nothing if the new location matches the current location. + *
    + * If this nr of locations becomes larger then the historySize then the + * first item(s) will be removed. + *v + * @param location d + */ + public void addLocation(Location location) { + // do nothing if the new location matches the current location + if (!(locations.isEmpty()) && + location.getHref().equals(locations.get(currentPos).getHref())) { + return; + } + currentPos++; + if (currentPos != currentSize) { + locations.set(currentPos, location); + } else { + locations.add(location); + checkHistorySize(); + } + currentSize = currentPos + 1; + } + + /** + * Removes all elements that are too much for the maxHistorySize + * out of the history. + */ + private void checkHistorySize() { + while (locations.size() > maxHistorySize) { + locations.remove(0); + currentSize--; + currentPos--; + } + } + + public void addLocation(String href) { + addLocation(new Location(href)); + } + + private String getLocationHref(int pos) { + if (pos < 0 || pos >= locations.size()) { + return null; + } + return locations.get(currentPos).getHref(); + } + + /** + * Moves the current positions delta positions. + * + * move(-1) to go one position back in history.
    + * move(1) to go one position forward.
    发 + * + * @param delta f + * + * @return Whether we actually moved. If the requested value is illegal + * it will return false, true otherwise. + */ + public boolean move(int delta) { + if (((currentPos + delta) < 0) + || ((currentPos + delta) >= currentSize)) { + return false; + } + currentPos += delta; + navigator.gotoResource(getLocationHref(currentPos), this); + return true; + } + + + /** + * If this is not the source of the navigationEvent then the addLocation + * will be called with the href of the currentResource in the navigationEvent. + */ + @Override + public void navigationPerformed(NavigationEvent navigationEvent) { + if (this == navigationEvent.getSource()) { + return; + } + if (navigationEvent.getCurrentResource() == null) { + return; + } + + if ((System.currentTimeMillis() - this.lastUpdateTime) > historyWaitTime) { + // if the user scrolled rapidly through the pages then the last page + // will not be added to the history. We fix that here: + addLocation(navigationEvent.getOldResource()); + + addLocation(navigationEvent.getCurrentResource().getHref()); + } + lastUpdateTime = System.currentTimeMillis(); + } + + public String getCurrentHref() { + if (currentPos < 0 || currentPos >= locations.size()) { + return null; + } + return locations.get(currentPos).getHref(); + } + + public void setMaxHistorySize(int maxHistorySize) { + this.maxHistorySize = maxHistorySize; + } + + public int getMaxHistorySize() { + return maxHistorySize; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java new file mode 100644 index 000000000..84319e65e --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java @@ -0,0 +1,220 @@ +package me.ag2s.epublib.browsersupport; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Resource; + +/** + * A helper class for epub browser applications. + *

    + * It helps moving from one resource to the other, from one resource + * to the other and keeping other elements of the application up-to-date + * by calling the NavigationEventListeners. + * + * @author paul + */ +public class Navigator implements Serializable { + + private static final long serialVersionUID = 1076126986424925474L; + private EpubBook book; + private int currentSpinePos; + private Resource currentResource; + private int currentPagePos; + private String currentFragmentId; + + private final List eventListeners = new ArrayList<>(); + + public Navigator() { + this(null); + } + + public Navigator(EpubBook book) { + this.book = book; + this.currentSpinePos = 0; + if (book != null) { + this.currentResource = book.getCoverPage(); + } + this.currentPagePos = 0; + } + + private synchronized void handleEventListeners( + NavigationEvent navigationEvent) { + for (int i = 0; i < eventListeners.size(); i++) { + NavigationEventListener navigationEventListener = eventListeners.get(i); + navigationEventListener.navigationPerformed(navigationEvent); + } + } + + public boolean addNavigationEventListener( + NavigationEventListener navigationEventListener) { + return this.eventListeners.add(navigationEventListener); + } + + public boolean removeNavigationEventListener( + NavigationEventListener navigationEventListener) { + return this.eventListeners.remove(navigationEventListener); + } + + public int gotoFirstSpineSection(Object source) { + return gotoSpineSection(0, source); + } + + public int gotoPreviousSpineSection(Object source) { + return gotoPreviousSpineSection(0, source); + } + + public int gotoPreviousSpineSection(int pagePos, Object source) { + if (currentSpinePos < 0) { + return gotoSpineSection(0, pagePos, source); + } else { + return gotoSpineSection(currentSpinePos - 1, pagePos, source); + } + } + + public boolean hasNextSpineSection() { + return (currentSpinePos < (book.getSpine().size() - 1)); + } + + public boolean hasPreviousSpineSection() { + return (currentSpinePos > 0); + } + + public int gotoNextSpineSection(Object source) { + if (currentSpinePos < 0) { + return gotoSpineSection(0, source); + } else { + return gotoSpineSection(currentSpinePos + 1, source); + } + } + + public int gotoResource(String resourceHref, Object source) { + Resource resource = book.getResources().getByHref(resourceHref); + return gotoResource(resource, source); + } + + + public int gotoResource(Resource resource, Object source) { + return gotoResource(resource, 0, null, source); + } + + public int gotoResource(Resource resource, String fragmentId, Object source) { + return gotoResource(resource, 0, fragmentId, source); + } + + public int gotoResource(Resource resource, int pagePos, Object source) { + return gotoResource(resource, pagePos, null, source); + } + + public int gotoResource(Resource resource, int pagePos, String fragmentId, + Object source) { + if (resource == null) { + return -1; + } + NavigationEvent navigationEvent = new NavigationEvent(source, this); + this.currentResource = resource; + this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); + this.currentPagePos = pagePos; + this.currentFragmentId = fragmentId; + handleEventListeners(navigationEvent); + + return currentSpinePos; + } + + public int gotoResourceId(String resourceId, Object source) { + return gotoSpineSection(book.getSpine().findFirstResourceById(resourceId), + source); + } + + public int gotoSpineSection(int newSpinePos, Object source) { + return gotoSpineSection(newSpinePos, 0, source); + } + + /** + * Go to a specific section. + * Illegal spine positions are silently ignored. + * + * @param newSpinePos f + * @param source f + * @return The current position within the spine + */ + public int gotoSpineSection(int newSpinePos, int newPagePos, Object source) { + if (newSpinePos == currentSpinePos) { + return currentSpinePos; + } + if (newSpinePos < 0 || newSpinePos >= book.getSpine().size()) { + return currentSpinePos; + } + NavigationEvent navigationEvent = new NavigationEvent(source, this); + currentSpinePos = newSpinePos; + currentPagePos = newPagePos; + currentResource = book.getSpine().getResource(currentSpinePos); + handleEventListeners(navigationEvent); + return currentSpinePos; + } + + public int gotoLastSpineSection(Object source) { + return gotoSpineSection(book.getSpine().size() - 1, source); + } + + public void gotoBook(EpubBook book, Object source) { + NavigationEvent navigationEvent = new NavigationEvent(source, this); + this.book = book; + this.currentFragmentId = null; + this.currentPagePos = 0; + this.currentResource = null; + this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); + handleEventListeners(navigationEvent); + } + + /** + * The current position within the spine. + * + * @return something < 0 if the current position is not within the spine. + */ + public int getCurrentSpinePos() { + return currentSpinePos; + } + + public Resource getCurrentResource() { + return currentResource; + } + + /** + * Sets the current index and resource without calling the eventlisteners. + * + * If you want the eventListeners called use gotoSection(index); + * + * @param currentIndex f + */ + public void setCurrentSpinePos(int currentIndex) { + this.currentSpinePos = currentIndex; + this.currentResource = book.getSpine().getResource(currentIndex); + } + + public EpubBook getBook() { + return book; + } + + /** + * Sets the current index and resource without calling the eventlisteners. + * + * If you want the eventListeners called use gotoSection(index); + * + */ + public int setCurrentResource(Resource currentResource) { + this.currentSpinePos = book.getSpine().getResourceIndex(currentResource); + this.currentResource = currentResource; + return currentSpinePos; + } + + public String getCurrentFragmentId() { + return currentFragmentId; + } + + public int getCurrentSectionPos() { + return currentPagePos; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/browsersupport/package-info.java b/epublib/src/main/java/me/ag2s/epublib/browsersupport/package-info.java new file mode 100644 index 000000000..1ca74f1e4 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/browsersupport/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides classes that help make an epub reader application. + * + * These classes have no dependencies on graphic toolkits, they're purely + * to help with the browsing/navigation logic. + */ +package me.ag2s.epublib.browsersupport; diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Author.java b/epublib/src/main/java/me/ag2s/epublib/domain/Author.java new file mode 100644 index 000000000..a604a5d08 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Author.java @@ -0,0 +1,89 @@ +package me.ag2s.epublib.domain; + + + +import me.ag2s.epublib.util.StringUtil; + +import java.io.Serializable; + +/** + * Represents one of the authors of the book + * + * @author paul + */ +public class Author implements Serializable { + + private static final long serialVersionUID = 6663408501416574200L; + + private String firstname; + private String lastname; + private Relator relator = Relator.AUTHOR; + + public Author(String singleName) { + this("", singleName); + } + + public Author(String firstname, String lastname) { + this.firstname = firstname; + this.lastname = lastname; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + + @Override + @SuppressWarnings("NullableProblems") + public String toString() { + return this.lastname + ", " + this.firstname; + } + + public int hashCode() { + return StringUtil.hashCode(firstname, lastname); + } + + public boolean equals(Object authorObject) { + if (!(authorObject instanceof Author)) { + return false; + } + Author other = (Author) authorObject; + return StringUtil.equals(firstname, other.firstname) + && StringUtil.equals(lastname, other.lastname); + } + + /** + * 设置贡献者的角色 + * + * @param code 角色编号 + */ + + public void setRole(String code) { + Relator result = Relator.byCode(code); + if (result == null) { + result = Relator.AUTHOR; + } + this.relator = result; + } + + public Relator getRelator() { + return relator; + } + + + public void setRelator(Relator relator) { + this.relator = relator; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Date.java b/epublib/src/main/java/me/ag2s/epublib/domain/Date.java new file mode 100644 index 000000000..8e25aba5d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Date.java @@ -0,0 +1,112 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Locale; + +import me.ag2s.epublib.epub.PackageDocumentBase; + +/** + * A Date used by the book's metadata. + *

    + * Examples: creation-date, modification-date, etc + * + * @author paul + */ +public class Date implements Serializable { + + private static final long serialVersionUID = 7533866830395120136L; + + public enum Event { + PUBLICATION("publication"), + MODIFICATION("modification"), + CREATION("creation"); + + private final String value; + + Event(String v) { + value = v; + } + + public static Event fromValue(String v) { + for (Event c : Event.values()) { + if (c.value.equals(v)) { + return c; + } + } + return null; + } + + @Override + @SuppressWarnings("NullableProblems") + public String toString() { + return value; + } + } + + + private Event event; + private String dateString; + + public Date() { + this(new java.util.Date(), Event.CREATION); + } + + public Date(java.util.Date date) { + this(date, (Event) null); + } + + public Date(String dateString) { + this(dateString, (Event) null); + } + + public Date(java.util.Date date, Event event) { + this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date), + event); + } + + public Date(String dateString, Event event) { + this.dateString = dateString; + this.event = event; + } + + public Date(java.util.Date date, String event) { + this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date), + event); + } + + public Date(String dateString, String event) { + this(checkDate(dateString), Event.fromValue(event)); + this.dateString = dateString; + } + + private static String checkDate(String dateString) { + if (dateString == null) { + throw new IllegalArgumentException( + "Cannot create a date from a blank string"); + } + return dateString; + } + + public String getValue() { + return dateString; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + @Override + @SuppressWarnings("NullableProblems") + public String toString() { + if (event == null) { + return dateString; + } + return "" + event + ":" + dateString; + } +} + diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java b/epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java new file mode 100644 index 000000000..bae1ede47 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/EpubBook.java @@ -0,0 +1,323 @@ +package me.ag2s.epublib.domain; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Representation of a Book. + *

    + * All resources of a Book (html, css, xml, fonts, images) are represented + * as Resources. See getResources() for access to these.
    + * A Book as 3 indexes into these Resources, as per the epub specification.
    + *

    + *
    Spine
    + *
    these are the Resources to be shown when a user reads the book from + * start to finish.
    + *
    Table of Contents
    + *
    The table of contents. Table of Contents references may be in a + * different order and contain different Resources than the spine, and often do. + *
    Guide
    + *
    The Guide has references to a set of special Resources like the + * cover page, the Glossary, the copyright page, etc. + *
    + *

    + * The complication is that these 3 indexes may and usually do point to + * different pages. + * A chapter may be split up in 2 pieces to fit it in to memory. Then the + * spine will contain both pieces, but the Table of Contents only the first. + *

    + * The Content page may be in the Table of Contents, the Guide, but not + * in the Spine. + * Etc. + *

    + *

    + * Please see the illustration at: doc/schema.svg + * + * @author paul + * @author jake + */ +public class EpubBook implements Serializable { + + private static final long serialVersionUID = 2068355170895770100L; + + private Resources resources = new Resources(); + private Metadata metadata = new Metadata(); + private Spine spine = new Spine(); + private TableOfContents tableOfContents = new TableOfContents(); + private final Guide guide = new Guide(); + private Resource opfResource; + private Resource ncxResource; + private Resource coverImage; + + + private String version = "2.0"; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isEpub3() { + return this.version.startsWith("3."); + } + + @SuppressWarnings("UnusedReturnValue") + public TOCReference addSection( + TOCReference parentSection, String sectionTitle, Resource resource) { + return addSection(parentSection, sectionTitle, resource, null); + } + + /** + * Adds the resource to the table of contents of the book as a child + * section of the given parentSection + * + * @param parentSection parentSection + * @param sectionTitle sectionTitle + * @param resource resource + * @param fragmentId fragmentId + * @return The table of contents + */ + public TOCReference addSection( + TOCReference parentSection, String sectionTitle, Resource resource, + String fragmentId) { + getResources().add(resource); + if (spine.findFirstResourceById(resource.getId()) < 0) { + spine.addSpineReference(new SpineReference(resource)); + } + return parentSection.addChildSection( + new TOCReference(sectionTitle, resource, fragmentId)); + } + + public TOCReference addSection(String title, Resource resource) { + return addSection(title, resource, null); + } + + /** + * Adds a resource to the book's set of resources, table of contents and + * if there is no resource with the id in the spine also adds it to the spine. + * + * @param title title + * @param resource resource + * @param fragmentId fragmentId + * @return The table of contents + */ + public TOCReference addSection( + String title, Resource resource, String fragmentId) { + getResources().add(resource); + TOCReference tocReference = tableOfContents + .addTOCReference(new TOCReference(title, resource, fragmentId)); + if (spine.findFirstResourceById(resource.getId()) < 0) { + spine.addSpineReference(new SpineReference(resource)); + } + return tocReference; + } + + @SuppressWarnings("unused") + public void generateSpineFromTableOfContents() { + Spine spine = new Spine(tableOfContents); + + // in case the tocResource was already found and assigned + spine.setTocResource(this.spine.getTocResource()); + + this.spine = spine; + } + + /** + * The Book's metadata (titles, authors, etc) + * + * @return The Book's metadata (titles, authors, etc) + */ + public Metadata getMetadata() { + return metadata; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + + + public void setResources(Resources resources) { + this.resources = resources; + } + + @SuppressWarnings("unused") + public Resource addResource(Resource resource) { + return resources.add(resource); + } + + /** + * The collection of all images, chapters, sections, xhtml files, + * stylesheets, etc that make up the book. + * + * @return The collection of all images, chapters, sections, xhtml files, + * stylesheets, etc that make up the book. + */ + public Resources getResources() { + return resources; + } + + + /** + * The sections of the book that should be shown if a user reads the book + * from start to finish. + * + * @return The Spine + */ + public Spine getSpine() { + return spine; + } + + + public void setSpine(Spine spine) { + this.spine = spine; + } + + + /** + * The Table of Contents of the book. + * + * @return The Table of Contents of the book. + */ + public TableOfContents getTableOfContents() { + return tableOfContents; + } + + + public void setTableOfContents(TableOfContents tableOfContents) { + this.tableOfContents = tableOfContents; + } + + /** + * The book's cover page as a Resource. + * An XHTML document containing a link to the cover image. + * + * @return The book's cover page as a Resource + */ + public Resource getCoverPage() { + Resource coverPage = guide.getCoverPage(); + if (coverPage == null) { + coverPage = spine.getResource(0); + } + return coverPage; + } + + + public void setCoverPage(Resource coverPage) { + if (coverPage == null) { + return; + } + if (resources.notContainsByHref(coverPage.getHref())) { + resources.add(coverPage); + } + guide.setCoverPage(coverPage); + } + + /** + * Gets the first non-blank title from the book's metadata. + * + * @return the first non-blank title from the book's metadata. + */ + public String getTitle() { + return getMetadata().getFirstTitle(); + } + + + /** + * The book's cover image. + * + * @return The book's cover image. + */ + public Resource getCoverImage() { + return coverImage; + } + + public void setCoverImage(Resource coverImage) { + if (coverImage == null) { + return; + } + if (resources.notContainsByHref(coverImage.getHref())) { + resources.add(coverImage); + } + this.coverImage = coverImage; + } + + /** + * The guide; contains references to special sections of the book like + * colophon, glossary, etc. + * + * @return The guide; contains references to special sections of the book + * like colophon, glossary, etc. + */ + public Guide getGuide() { + return guide; + } + + /** + * All Resources of the Book that can be reached via the Spine, the + * TableOfContents or the Guide. + *

    + * Consists of a list of "reachable" resources: + *

      + *
    • The coverpage
    • + *
    • The resources of the Spine that are not already in the result
    • + *
    • The resources of the Table of Contents that are not already in the + * result
    • + *
    • The resources of the Guide that are not already in the result
    • + *
    + * To get all html files that make up the epub file use + * {@link #getResources()} + * + * @return All Resources of the Book that can be reached via the Spine, + * the TableOfContents or the Guide. + */ + public List getContents() { + Map result = new LinkedHashMap<>(); + addToContentsResult(getCoverPage(), result); + + for (SpineReference spineReference : getSpine().getSpineReferences()) { + addToContentsResult(spineReference.getResource(), result); + } + + for (Resource resource : getTableOfContents().getAllUniqueResources()) { + addToContentsResult(resource, result); + } + + for (GuideReference guideReference : getGuide().getReferences()) { + addToContentsResult(guideReference.getResource(), result); + } + + return new ArrayList<>(result.values()); + } + + private static void addToContentsResult(Resource resource, + Map allReachableResources) { + if (resource != null && (!allReachableResources + .containsKey(resource.getHref()))) { + allReachableResources.put(resource.getHref(), resource); + } + } + + public Resource getOpfResource() { + return opfResource; + } + + public void setOpfResource(Resource opfResource) { + this.opfResource = opfResource; + } + + public void setNcxResource(Resource ncxResource) { + this.ncxResource = ncxResource; + } + + public Resource getNcxResource() { + return ncxResource; + } +} + diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java b/epublib/src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java new file mode 100644 index 000000000..5b38e39f6 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java @@ -0,0 +1,33 @@ +package me.ag2s.epublib.domain; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * @author jake + */ +public class EpubResourceProvider implements LazyResourceProvider { + + private final String epubFilename; + + /** + * @param epubFilename the file name for the epub we're created from. + */ + public EpubResourceProvider(String epubFilename) { + this.epubFilename = epubFilename; + } + + @Override + public InputStream getResourceStream(String href) throws IOException { + ZipFile zipFile = new ZipFile(epubFilename); + ZipEntry zipEntry = zipFile.getEntry(href); + if (zipEntry == null) { + zipFile.close(); + throw new IllegalStateException( + "Cannot find entry " + href + " in epub file " + epubFilename); + } + return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java b/epublib/src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java new file mode 100644 index 000000000..e273712d6 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java @@ -0,0 +1,44 @@ +package me.ag2s.epublib.domain; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 用于创建epub,添加大文件(如大量图片)时容易OOM,使用LazyResource,避免OOM. + * + */ + +public class FileResourceProvider implements LazyResourceProvider { + //需要导入资源的父目录 + String dir; + + /** + * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 + * @param parentDir 文件的目录 + */ + public FileResourceProvider(String parentDir) { + this.dir = parentDir; + } + + /** + * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 + * @param parentFile 文件夹 + */ + @SuppressWarnings("unused") + public FileResourceProvider(File parentFile) { + this.dir = parentFile.getPath(); + } + + /** + * 根据子文件名href,再父目录下读取文件获取FileInputStream + * @param href 子文件名href + * @return 对应href的FileInputStream + * @throws IOException 抛出IOException + */ + @Override + public InputStream getResourceStream(String href) throws IOException { + return new FileInputStream(new File(dir, href)); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Guide.java b/epublib/src/main/java/me/ag2s/epublib/domain/Guide.java new file mode 100644 index 000000000..57c0467a9 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Guide.java @@ -0,0 +1,128 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * The guide is a selection of special pages of the book. + * Examples of these are the cover, list of illustrations, etc. + * + * It is an optional part of an epub, and support for the various types + * of references varies by reader. + * + * The only part of this that is heavily used is the cover page. + * + * @author paul + * + */ +public class Guide implements Serializable { + + /** + * + */ + private static final long serialVersionUID = -6256645339915751189L; + + public static final String DEFAULT_COVER_TITLE = GuideReference.COVER; + + private List references = new ArrayList<>(); + private static final int COVERPAGE_NOT_FOUND = -1; + private static final int COVERPAGE_UNITIALIZED = -2; + + private int coverPageIndex = -1; + + public List getReferences() { + return references; + } + + public void setReferences(List references) { + this.references = references; + uncheckCoverPage(); + } + + private void uncheckCoverPage() { + coverPageIndex = COVERPAGE_UNITIALIZED; + } + + public GuideReference getCoverReference() { + checkCoverPage(); + if (coverPageIndex >= 0) { + return references.get(coverPageIndex); + } + return null; + } + @SuppressWarnings("UnusedReturnValue") + public int setCoverReference(GuideReference guideReference) { + if (coverPageIndex >= 0) { + references.set(coverPageIndex, guideReference); + } else { + references.add(0, guideReference); + coverPageIndex = 0; + } + return coverPageIndex; + } + + private void checkCoverPage() { + if (coverPageIndex == COVERPAGE_UNITIALIZED) { + initCoverPage(); + } + } + + + private void initCoverPage() { + int result = COVERPAGE_NOT_FOUND; + for (int i = 0; i < references.size(); i++) { + GuideReference guideReference = references.get(i); + if (guideReference.getType().equals(GuideReference.COVER)) { + result = i; + break; + } + } + coverPageIndex = result; + } + + /** + * The coverpage of the book. + * + * @return The coverpage of the book. + */ + public Resource getCoverPage() { + GuideReference guideReference = getCoverReference(); + if (guideReference == null) { + return null; + } + return guideReference.getResource(); + } + + public void setCoverPage(Resource coverPage) { + GuideReference coverpageGuideReference = new GuideReference(coverPage, + GuideReference.COVER, DEFAULT_COVER_TITLE); + setCoverReference(coverpageGuideReference); + } + + @SuppressWarnings("UnusedReturnValue") + public ResourceReference addReference(GuideReference reference) { + this.references.add(reference); + uncheckCoverPage(); + return reference; + } + + /** + * A list of all GuideReferences that have the given + * referenceTypeName (ignoring case). + * + * @param referenceTypeName referenceTypeName + * @return A list of all GuideReferences that have the given + * referenceTypeName (ignoring case). + */ + public List getGuideReferencesByType( + String referenceTypeName) { + List result = new ArrayList<>(); + for (GuideReference guideReference : references) { + if (referenceTypeName.equalsIgnoreCase(guideReference.getType())) { + result.add(guideReference); + } + } + return result; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/GuideReference.java b/epublib/src/main/java/me/ag2s/epublib/domain/GuideReference.java new file mode 100644 index 000000000..89bd3ff12 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/GuideReference.java @@ -0,0 +1,102 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.util.StringUtil; +import java.io.Serializable; + + +/** + * These are references to elements of the book's guide. + * + * @see Guide + * + * @author paul + * + */ +public class GuideReference extends TitledResourceReference + implements Serializable { + + private static final long serialVersionUID = -316179702440631834L; + + /** + * the book cover(s), jacket information, etc. + */ + public static final String COVER = "cover"; + + /** + * human-readable page with title, author, publisher, and other metadata + */ + public static String TITLE_PAGE = "title-page"; + + /** + * Human-readable table of contents. + * Not to be confused the epub file table of contents + * + */ + public static String TOC = "toc"; + + /** + * back-of-book style index + */ + public static String INDEX = "index"; + public static String GLOSSARY = "glossary"; + public static String ACKNOWLEDGEMENTS = "acknowledgements"; + public static String BIBLIOGRAPHY = "bibliography"; + public static String COLOPHON = "colophon"; + public static String COPYRIGHT_PAGE = "copyright-page"; + public static String DEDICATION = "dedication"; + + /** + * an epigraph is a phrase, quotation, or poem that is set at the + * beginning of a document or component. + * + * source: http://en.wikipedia.org/wiki/Epigraph_%28literature%29 + */ + public static String EPIGRAPH = "epigraph"; + + public static String FOREWORD = "foreword"; + + /** + * list of illustrations + */ + public static String LOI = "loi"; + + /** + * list of tables + */ + public static String LOT = "lot"; + public static String NOTES = "notes"; + public static String PREFACE = "preface"; + + /** + * A page of content (e.g. "Chapter 1") + */ + public static String TEXT = "text"; + + private String type; + + public GuideReference(Resource resource) { + this(resource, null); + } + + public GuideReference(Resource resource, String title) { + super(resource, title); + } + + public GuideReference(Resource resource, String type, String title) { + this(resource, type, title, null); + } + + public GuideReference(Resource resource, String type, String title, + String fragmentId) { + super(resource, title, fragmentId); + this.type = StringUtil.isNotBlank(type) ? type.toLowerCase() : null; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Identifier.java b/epublib/src/main/java/me/ag2s/epublib/domain/Identifier.java new file mode 100644 index 000000000..ccb46186f --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Identifier.java @@ -0,0 +1,130 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.util.StringUtil; +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + +/** + * A Book's identifier. + * + * Defaults to a random UUID and scheme "UUID" + * + * @author paul + */ +public class Identifier implements Serializable { + + private static final long serialVersionUID = 955949951416391810L; + @SuppressWarnings("unused") + public interface Scheme { + + String UUID = "UUID"; + String ISBN = "ISBN"; + String URL = "URL"; + String URI = "URI"; + } + + private boolean bookId = false; + private String scheme; + private String value; + + /** + * Creates an Identifier with as value a random UUID and scheme "UUID" + */ + public Identifier() { + this(Scheme.UUID, UUID.randomUUID().toString()); + } + + + public Identifier(String scheme, String value) { + this.scheme = scheme; + this.value = value; + } + + /** + * The first identifier for which the bookId is true is made the + * bookId identifier. + * + * If no identifier has bookId == true then the first bookId identifier + * is written as the primary. + * + * @param identifiers i + * @return The first identifier for which the bookId is true is made + * the bookId identifier. + */ + public static Identifier getBookIdIdentifier(List identifiers) { + if (identifiers == null || identifiers.isEmpty()) { + return null; + } + + Identifier result = null; + for (Identifier identifier : identifiers) { + if (identifier.isBookId()) { + result = identifier; + break; + } + } + + if (result == null) { + result = identifiers.get(0); + } + + return result; + } + + public String getScheme() { + return scheme; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + + public void setBookId(boolean bookId) { + this.bookId = bookId; + } + + + /** + * This bookId property allows the book creator to add multiple ids and + * tell the epubwriter which one to write out as the bookId. + * + * The Dublin Core metadata spec allows multiple identifiers for a Book. + * The epub spec requires exactly one identifier to be marked as the book id. + * + * @return whether this is the unique book id. + */ + public boolean isBookId() { + return bookId; + } + + public int hashCode() { + return StringUtil.defaultIfNull(scheme).hashCode() ^ StringUtil + .defaultIfNull(value).hashCode(); + } + + public boolean equals(Object otherIdentifier) { + if (!(otherIdentifier instanceof Identifier)) { + return false; + } + return StringUtil.equals(scheme, ((Identifier) otherIdentifier).scheme) + && StringUtil.equals(value, ((Identifier) otherIdentifier).value); + } + @SuppressWarnings("NullableProblems") + @Override + public String toString() { + if (StringUtil.isBlank(scheme)) { + return "" + value; + } + return "" + scheme + ":" + value; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/LazyResource.java b/epublib/src/main/java/me/ag2s/epublib/domain/LazyResource.java new file mode 100644 index 000000000..a0faf070e --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/LazyResource.java @@ -0,0 +1,143 @@ +package me.ag2s.epublib.domain; + +import android.util.Log; + +import me.ag2s.epublib.util.IOUtil; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A Resource that loads its data only on-demand from a EPUB book file. + * This way larger books can fit into memory and can be opened faster. + */ +public class LazyResource extends Resource { + + private static final long serialVersionUID = 5089400472352002866L; + private final String TAG= getClass().getName(); + + private final LazyResourceProvider resourceProvider; + private final long cachedSize; + + /** + * Creates a lazy resource, when the size is unknown. + * + * @param resourceProvider The resource provider loads data on demand. + * @param href The resource's href within the epub. + */ + public LazyResource(LazyResourceProvider resourceProvider, String href) { + this(resourceProvider, -1, href); + } + public LazyResource(LazyResourceProvider resourceProvider, String href, String originalHref) { + this(resourceProvider, -1, href, originalHref); + } + + /** + * Creates a Lazy resource, by not actually loading the data for this entry. + * + * The data will be loaded on the first call to getData() + * + * @param resourceProvider The resource provider loads data on demand. + * @param size The size of this resource. + * @param href The resource's href within the epub. + */ + public LazyResource( + LazyResourceProvider resourceProvider, long size, String href) { + super(null, null, href, MediaTypes.determineMediaType(href)); + this.resourceProvider = resourceProvider; + this.cachedSize = size; + } + public LazyResource( + LazyResourceProvider resourceProvider, long size, String href, String originalHref) { + super(null, null, href, originalHref, MediaTypes.determineMediaType(href)); + this.resourceProvider = resourceProvider; + this.cachedSize = size; + } + + /** + * Gets the contents of the Resource as an InputStream. + * + * @return The contents of the Resource. + * + * @throws IOException IOException + */ + public InputStream getInputStream() throws IOException { + if (isInitialized()) { + return new ByteArrayInputStream(getData()); + } else { + return resourceProvider.getResourceStream(this.originalHref); + } + } + + /** + * Initializes the resource by loading its data into memory. + * + * @throws IOException IOException + */ + public void initialize() throws IOException { + getData(); + } + + /** + * The contents of the resource as a byte[] + * + * If this resource was lazy-loaded and the data was not yet loaded, + * it will be loaded into memory at this point. + * This included opening the zip file, so expect a first load to be slow. + * + * @return The contents of the resource + */ + public byte[] getData() throws IOException { + + if (data == null) { + + Log.d(TAG, "Initializing lazy resource: " + this.getHref()); + + InputStream in = resourceProvider.getResourceStream(this.originalHref); + byte[] readData = IOUtil.toByteArray(in, (int) this.cachedSize); + if (readData == null) { + throw new IOException( + "Could not load the contents of resource: " + this.getHref()); + } else { + this.data = readData; + } + + in.close(); + } + + return data; + } + + /** + * Tells this resource to release its cached data. + * + * If this resource was not lazy-loaded, this is a no-op. + */ + public void close() { + if (this.resourceProvider != null) { + this.data = null; + } + } + + /** + * Returns if the data for this resource has been loaded into memory. + * + * @return true if data was loaded. + */ + public boolean isInitialized() { + return data != null; + } + + /** + * Returns the size of this resource in bytes. + * + * @return the size. + */ + public long getSize() { + if (data != null) { + return data.length; + } + + return cachedSize; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java b/epublib/src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java new file mode 100644 index 000000000..09b60e596 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java @@ -0,0 +1,12 @@ +package me.ag2s.epublib.domain; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author jake + */ +public interface LazyResourceProvider { + + InputStream getResourceStream(String href) throws IOException; +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java new file mode 100644 index 000000000..dd21eead5 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java @@ -0,0 +1,21 @@ +package me.ag2s.epublib.domain; +@SuppressWarnings("unused") +public enum ManifestItemProperties implements ManifestProperties { + COVER_IMAGE("cover-image"), + MATHML("mathml"), + NAV("nav"), + REMOTE_RESOURCES("remote-resources"), + SCRIPTED("scripted"), + SVG("svg"), + SWITCH("switch"); + + private final String name; + + ManifestItemProperties(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java new file mode 100644 index 000000000..b67d63f91 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java @@ -0,0 +1,16 @@ +package me.ag2s.epublib.domain; +@SuppressWarnings("unused") +public enum ManifestItemRefProperties implements ManifestProperties { + PAGE_SPREAD_LEFT("page-spread-left"), + PAGE_SPREAD_RIGHT("page-spread-right"); + + private final String name; + + ManifestItemRefProperties(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/ManifestProperties.java b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestProperties.java new file mode 100644 index 000000000..26892d7b0 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/ManifestProperties.java @@ -0,0 +1,6 @@ +package me.ag2s.epublib.domain; + +public interface ManifestProperties { + + String getName(); +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/MediaType.java b/epublib/src/main/java/me/ag2s/epublib/domain/MediaType.java new file mode 100644 index 000000000..9a6817996 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/MediaType.java @@ -0,0 +1,73 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; + +/** + * MediaType is used to tell the type of content a resource is. + * + * Examples of mediatypes are image/gif, text/css and application/xhtml+xml + * + * All allowed mediaTypes are maintained bye the MediaTypeService. + * + * @see MediaTypes + * + * @author paul + */ +public class MediaType implements Serializable { + + private static final long serialVersionUID = -7256091153727506788L; + private final String name; + private final String defaultExtension; + private final Collection extensions; + + public MediaType(String name, String defaultExtension) { + this(name, defaultExtension, new String[]{defaultExtension}); + } + + public MediaType(String name, String defaultExtension, + String[] extensions) { + this(name, defaultExtension, Arrays.asList(extensions)); + } + + public int hashCode() { + if (name == null) { + return 0; + } + return name.hashCode(); + } + + public MediaType(String name, String defaultExtension, + Collection mextensions) { + super(); + this.name = name; + this.defaultExtension = defaultExtension; + this.extensions = mextensions; + } + + public String getName() { + return name; + } + + + public String getDefaultExtension() { + return defaultExtension; + } + + + public Collection getExtensions() { + return extensions; + } + + public boolean equals(Object otherMediaType) { + if (!(otherMediaType instanceof MediaType)) { + return false; + } + return name.equals(((MediaType) otherMediaType).getName()); + } + @SuppressWarnings("NullableProblems") + public String toString() { + return name; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/MediaTypes.java b/epublib/src/main/java/me/ag2s/epublib/domain/MediaTypes.java new file mode 100644 index 000000000..61d51ad64 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/MediaTypes.java @@ -0,0 +1,94 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.util.StringUtil; +import java.util.HashMap; +import java.util.Map; + + +/** + * Manages mediatypes that are used by epubs + * + * @author paul + */ +public class MediaTypes { + + public static final MediaType XHTML = new MediaType("application/xhtml+xml", + ".xhtml", new String[]{".htm", ".html", ".xhtml"}); + public static final MediaType EPUB = new MediaType("application/epub+zip", + ".epub"); + public static final MediaType NCX = new MediaType("application/x-dtbncx+xml", + ".ncx"); + + public static final MediaType JAVASCRIPT = new MediaType("text/javascript", + ".js"); + public static final MediaType CSS = new MediaType("text/css", ".css"); + + // images + public static final MediaType JPG = new MediaType("image/jpeg", ".jpg", + new String[]{".jpg", ".jpeg"}); + public static final MediaType PNG = new MediaType("image/png", ".png"); + public static final MediaType GIF = new MediaType("image/gif", ".gif"); + + public static final MediaType SVG = new MediaType("image/svg+xml", ".svg"); + + // fonts + public static final MediaType TTF = new MediaType( + "application/x-truetype-font", ".ttf"); + public static final MediaType OPENTYPE = new MediaType( + "application/vnd.ms-opentype", ".otf"); + public static final MediaType WOFF = new MediaType("application/font-woff", + ".woff"); + + // audio + public static final MediaType MP3 = new MediaType("audio/mpeg", ".mp3"); + public static final MediaType OGG = new MediaType("audio/ogg", ".ogg"); + + // video + public static final MediaType MP4 = new MediaType("video/mp4", ".mp4"); + + public static final MediaType SMIL = new MediaType("application/smil+xml", + ".smil"); + public static final MediaType XPGT = new MediaType( + "application/adobe-page-template+xml", ".xpgt"); + public static final MediaType PLS = new MediaType("application/pls+xml", + ".pls"); + + public static final MediaType[] mediaTypes = new MediaType[]{ + XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF, + SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG + }; + + public static final Map mediaTypesByName = new HashMap<>(); + + static { + for (MediaType mediaType : mediaTypes) { + mediaTypesByName.put(mediaType.getName(), mediaType); + } + } + + public static boolean isBitmapImage(MediaType mediaType) { + return mediaType == JPG || mediaType == PNG || mediaType == GIF; + } + + /** + * Gets the MediaType based on the file extension. + * Null of no matching extension found. + * + * @param filename filename + * @return the MediaType based on the file extension. + */ + public static MediaType determineMediaType(String filename) { + for (MediaType mediaType : mediaTypesByName.values()) { + for (String extension : mediaType.getExtensions()) { + if (StringUtil.endsWithIgnoreCase(filename, extension)) { + return mediaType; + } + } + } + return null; + } + + public static MediaType getMediaTypeByName(String mediaTypeName) { + return mediaTypesByName.get(mediaTypeName); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Metadata.java b/epublib/src/main/java/me/ag2s/epublib/domain/Metadata.java new file mode 100644 index 000000000..71c995dbe --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Metadata.java @@ -0,0 +1,241 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.util.StringUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +/** + * A Book's collection of Metadata. + * In the future it should contain all Dublin Core attributes, for now + * it contains a set of often-used ones. + * + * @author paul + */ +public class Metadata implements Serializable { + + private static final long serialVersionUID = -2437262888962149444L; + + public static final String DEFAULT_LANGUAGE = "en"; + + private boolean autoGeneratedId;//true; + private List authors = new ArrayList<>(); + private List contributors = new ArrayList<>(); + private List dates = new ArrayList<>(); + private String language = DEFAULT_LANGUAGE; + private Map otherProperties = new HashMap<>(); + private List rights = new ArrayList<>(); + private List titles = new ArrayList<>(); + private List identifiers = new ArrayList<>(); + private List subjects = new ArrayList<>(); + private String format = MediaTypes.EPUB.getName(); + private List types = new ArrayList<>(); + private List descriptions = new ArrayList<>(); + private List publishers = new ArrayList<>(); + private Map metaAttributes = new HashMap<>(); + + public Metadata() { + identifiers.add(new Identifier()); + autoGeneratedId = true; + } + + @SuppressWarnings("unused") + public boolean isAutoGeneratedId() { + return autoGeneratedId; + } + + /** + * Metadata properties not hard-coded like the author, title, etc. + * + * @return Metadata properties not hard-coded like the author, title, etc. + */ + public Map getOtherProperties() { + return otherProperties; + } + + public void setOtherProperties(Map otherProperties) { + this.otherProperties = otherProperties; + } + + @SuppressWarnings("unused") + public Date addDate(Date date) { + this.dates.add(date); + return date; + } + + public List getDates() { + return dates; + } + + public void setDates(List dates) { + this.dates = dates; + } + + @SuppressWarnings("UnusedReturnValue") + public Author addAuthor(Author author) { + authors.add(author); + return author; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + @SuppressWarnings("UnusedReturnValue") + public Author addContributor(Author contributor) { + contributors.add(contributor); + return contributor; + } + + public List getContributors() { + return contributors; + } + + public void setContributors(List contributors) { + this.contributors = contributors; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public List getSubjects() { + return subjects; + } + + public void setSubjects(List subjects) { + this.subjects = subjects; + } + + public void setRights(List rights) { + this.rights = rights; + } + + public List getRights() { + return rights; + } + + + /** + * Gets the first non-blank title of the book. + * Will return "" if no title found. + * + * @return the first non-blank title of the book. + */ + public String getFirstTitle() { + if (titles == null || titles.isEmpty()) { + return ""; + } + for (String title : titles) { + if (StringUtil.isNotBlank(title)) { + return title; + } + } + return ""; + } + + public String addTitle(String title) { + this.titles.add(title); + return title; + } + + public void setTitles(List titles) { + this.titles = titles; + } + + public List getTitles() { + return titles; + } + + @SuppressWarnings("UnusedReturnValue") + public String addPublisher(String publisher) { + this.publishers.add(publisher); + return publisher; + } + + public void setPublishers(List publishers) { + this.publishers = publishers; + } + + public List getPublishers() { + return publishers; + } + + @SuppressWarnings("UnusedReturnValue") + public String addDescription(String description) { + this.descriptions.add(description); + return description; + } + + public void setDescriptions(List descriptions) { + this.descriptions = descriptions; + } + + public List getDescriptions() { + return descriptions; + } + + @SuppressWarnings("unused") + public Identifier addIdentifier(Identifier identifier) { + if (autoGeneratedId && (!(identifiers.isEmpty()))) { + identifiers.set(0, identifier); + } else { + identifiers.add(identifier); + } + autoGeneratedId = false; + return identifier; + } + + public void setIdentifiers(List identifiers) { + this.identifiers = identifiers; + autoGeneratedId = false; + } + + public List getIdentifiers() { + return identifiers; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } + + @SuppressWarnings("UnusedReturnValue") + public String addType(String type) { + this.types.add(type); + return type; + } + + public List getTypes() { + return types; + } + + public void setTypes(List types) { + this.types = types; + } + + @SuppressWarnings("unused") + public String getMetaAttribute(String name) { + return metaAttributes.get(name); + } + + public void setMetaAttributes(Map metaAttributes) { + this.metaAttributes = metaAttributes; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Relator.java b/epublib/src/main/java/me/ag2s/epublib/domain/Relator.java new file mode 100644 index 000000000..b9637588d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Relator.java @@ -0,0 +1,1144 @@ +package me.ag2s.epublib.domain; + + +/** + * A relator denotes which role a certain individual had in the creation/modification of the ebook. + * + * Examples are 'creator', 'blurb writer', etc. + * + * This is contains the complete Library of Concress relator list. + * + * @see MARC Code List for Relators + * + * @author paul + */ +public enum Relator { + + /** + * Use for a person or organization who principally exhibits acting skills in a musical or dramatic presentation or entertainment. + */ + ACTOR("act", "Actor"), + + /** + * Use for a person or organization who 1) reworks a musical composition, usually for a different medium, or 2) rewrites novels or stories for motion pictures or other audiovisual medium. + */ + ADAPTER("adp", "Adapter"), + + /** + * Use for a person or organization that reviews, examines and interprets data or information in a specific area. + */ + ANALYST("anl", "Analyst"), + + /** + * Use for a person or organization who draws the two-dimensional figures, manipulates the three dimensional objects and/or also programs the computer to move objects and images for the purpose of animated film processing. Animation cameras, stands, celluloid screens, transparencies and inks are some of the tools of the animator. + */ + ANIMATOR("anm", "Animator"), + + /** + * Use for a person who writes manuscript annotations on a printed item. + */ + ANNOTATOR("ann", "Annotator"), + + /** + * Use for a person or organization responsible for the submission of an application or who is named as eligible for the results of the processing of the application (e.g., bestowing of rights, reward, title, position). + */ + APPLICANT("app", "Applicant"), + + /** + * Use for a person or organization who designs structures or oversees their construction. + */ + ARCHITECT("arc", "Architect"), + + /** + * Use for a person or organization who transcribes a musical composition, usually for a different medium from that of the original; in an arrangement the musical substance remains essentially unchanged. + */ + ARRANGER("arr", "Arranger"), + + /** + * Use for a person (e.g., a painter or sculptor) who makes copies of works of visual art. + */ + ART_COPYIST("acp", "Art copyist"), + + /** + * Use for a person (e.g., a painter) or organization who conceives, and perhaps also implements, an original graphic design or work of art, if specific codes (e.g., [egr], [etr]) are not desired. For book illustrators, prefer Illustrator [ill]. + */ + ARTIST("art", "Artist"), + + /** + * Use for a person responsible for controlling the development of the artistic style of an entire production, including the choice of works to be presented and selection of senior production staff. + */ + ARTISTIC_DIRECTOR("ard", "Artistic director"), + + /** + * Use for a person or organization to whom a license for printing or publishing has been transferred. + */ + ASSIGNEE("asg", "Assignee"), + + /** + * Use for a person or organization associated with or found in an item or collection, which cannot be determined to be that of a Former owner [fmo] or other designated relator indicative of provenance. + */ + ASSOCIATED_NAME("asn", "Associated name"), + + /** + * Use for an author, artist, etc., relating him/her to a work for which there is or once was substantial authority for designating that person as author, creator, etc. of the work. + */ + ATTRIBUTED_NAME("att", "Attributed name"), + + /** + * Use for a person or organization in charge of the estimation and public auctioning of goods, particularly books, artistic works, etc. + */ + AUCTIONEER("auc", "Auctioneer"), + + /** + * Use for a person or organization chiefly responsible for the intellectual or artistic content of a work, usually printed text. This term may also be used when more than one person or body bears such responsibility. + */ + AUTHOR("aut", "Author"), + + /** + * Use for a person or organization whose work is largely quoted or extracted in works to which he or she did not contribute directly. Such quotations are found particularly in exhibition catalogs, collections of photographs, etc. + */ + AUTHOR_IN_QUOTATIONS_OR_TEXT_EXTRACTS("aqt", + "Author in quotations or text extracts"), + + /** + * Use for a person or organization responsible for an afterword, postface, colophon, etc. but who is not the chief author of a work. + */ + AUTHOR_OF_AFTERWORD_COLOPHON_ETC("aft", + "Author of afterword, colophon, etc."), + + /** + * Use for a person or organization responsible for the dialog or spoken commentary for a screenplay or sound recording. + */ + AUTHOR_OF_DIALOG("aud", "Author of dialog"), + + /** + * Use for a person or organization responsible for an introduction, preface, foreword, or other critical introductory matter, but who is not the chief author. + */ + AUTHOR_OF_INTRODUCTION_ETC("aui", "Author of introduction, etc."), + + /** + * Use for a person or organization responsible for a motion picture screenplay, dialog, spoken commentary, etc. + */ + AUTHOR_OF_SCREENPLAY_ETC("aus", "Author of screenplay, etc."), + + /** + * Use for a person or organization responsible for a work upon which the work represented by the catalog record is based. This may be appropriate for adaptations, sequels, continuations, indexes, etc. + */ + BIBLIOGRAPHIC_ANTECEDENT("ant", "Bibliographic antecedent"), + + /** + * Use for a person or organization responsible for the binding of printed or manuscript materials. + */ + BINDER("bnd", "Binder"), + + /** + * Use for a person or organization responsible for the binding design of a book, including the type of binding, the type of materials used, and any decorative aspects of the binding. + */ + BINDING_DESIGNER("bdd", "Binding designer"), + + /** + * Use for the named entity responsible for writing a commendation or testimonial for a work, which appears on or within the publication itself, frequently on the back or dust jacket of print publications or on advertising material for all media. + */ + BLURB_WRITER("blw", "Blurb writer"), + + /** + * Use for a person or organization responsible for the entire graphic design of a book, including arrangement of type and illustration, choice of materials, and process used. + */ + BOOK_DESIGNER("bkd", "Book designer"), + + /** + * Use for a person or organization responsible for the production of books and other print media, if specific codes (e.g., [bkd], [egr], [tyd], [prt]) are not desired. + */ + BOOK_PRODUCER("bkp", "Book producer"), + + /** + * Use for a person or organization responsible for the design of flexible covers designed for or published with a book, including the type of materials used, and any decorative aspects of the bookjacket. + */ + BOOKJACKET_DESIGNER("bjd", "Bookjacket designer"), + + /** + * Use for a person or organization responsible for the design of a book owner's identification label that is most commonly pasted to the inside front cover of a book. + */ + BOOKPLATE_DESIGNER("bpd", "Bookplate designer"), + + /** + * Use for a person or organization who makes books and other bibliographic materials available for purchase. Interest in the materials is primarily lucrative. + */ + BOOKSELLER("bsl", "Bookseller"), + + /** + * Use for a person or organization who writes in an artistic hand, usually as a copyist and or engrosser. + */ + CALLIGRAPHER("cll", "Calligrapher"), + + /** + * Use for a person or organization responsible for the creation of maps and other cartographic materials. + */ + CARTOGRAPHER("ctg", "Cartographer"), + + /** + * Use for a censor, bowdlerizer, expurgator, etc., official or private. + */ + CENSOR("cns", "Censor"), + + /** + * Use for a person or organization who composes or arranges dances or other movements (e.g., "master of swords") for a musical or dramatic presentation or entertainment. + */ + CHOREOGRAPHER("chr", "Choreographer"), + + /** + * Use for a person or organization who is in charge of the images captured for a motion picture film. The cinematographer works under the supervision of a director, and may also be referred to as director of photography. Do not confuse with videographer. + */ + CINEMATOGRAPHER("cng", "Cinematographer"), + + /** + * Use for a person or organization for whom another person or organization is acting. + */ + CLIENT("cli", "Client"), + + /** + * Use for a person or organization that takes a limited part in the elaboration of a work of another person or organization that brings complements (e.g., appendices, notes) to the work. + */ + COLLABORATOR("clb", "Collaborator"), + + /** + * Use for a person or organization who has brought together material from various sources that has been arranged, described, and cataloged as a collection. A collector is neither the creator of the material nor a person to whom manuscripts in the collection may have been addressed. + */ + COLLECTOR("col", "Collector"), + + /** + * Use for a person or organization responsible for the production of photographic prints from film or other colloid that has ink-receptive and ink-repellent surfaces. + */ + COLLOTYPER("clt", "Collotyper"), + + /** + * Use for the named entity responsible for applying color to drawings, prints, photographs, maps, moving images, etc. + */ + COLORIST("clr", "Colorist"), + + /** + * Use for a person or organization who provides interpretation, analysis, or a discussion of the subject matter on a recording, motion picture, or other audiovisual medium. + */ + COMMENTATOR("cmm", "Commentator"), + + /** + * Use for a person or organization responsible for the commentary or explanatory notes about a text. For the writer of manuscript annotations in a printed book, use Annotator [ann]. + */ + COMMENTATOR_FOR_WRITTEN_TEXT("cwt", "Commentator for written text"), + + /** + * Use for a person or organization who produces a work or publication by selecting and putting together material from the works of various persons or bodies. + */ + COMPILER("com", "Compiler"), + + /** + * Use for the party who applies to the courts for redress, usually in an equity proceeding. + */ + COMPLAINANT("cpl", "Complainant"), + + /** + * Use for a complainant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. + */ + COMPLAINANT_APPELLANT("cpt", "Complainant-appellant"), + + /** + * Use for a complainant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. + */ + COMPLAINANT_APPELLEE("cpe", "Complainant-appellee"), + + /** + * Use for a person or organization who creates a musical work, usually a piece of music in manuscript or printed form. + */ + COMPOSER("cmp", "Composer"), + + /** + * Use for a person or organization responsible for the creation of metal slug, or molds made of other materials, used to produce the text and images in printed matter. + */ + COMPOSITOR("cmt", "Compositor"), + + /** + * Use for a person or organization responsible for the original idea on which a work is based, this includes the scientific author of an audio-visual item and the conceptor of an advertisement. + */ + CONCEPTOR("ccp", "Conceptor"), + + /** + * Use for a person who directs a performing group (orchestra, chorus, opera, etc.) in a musical or dramatic presentation or entertainment. + */ + CONDUCTOR("cnd", "Conductor"), + + /** + * Use for the named entity responsible for documenting, preserving, or treating printed or manuscript material, works of art, artifacts, or other media. + */ + CONSERVATOR("con", "Conservator"), + + /** + * Use for a person or organization relevant to a resource, who is called upon for professional advice or services in a specialized field of knowledge or training. + */ + CONSULTANT("csl", "Consultant"), + + /** + * Use for a person or organization relevant to a resource, who is engaged specifically to provide an intellectual overview of a strategic or operational task and by analysis, specification, or instruction, to create or propose a cost-effective course of action or solution. + */ + CONSULTANT_TO_A_PROJECT("csp", "Consultant to a project"), + + /** + * Use for the party who opposes, resists, or disputes, in a court of law, a claim, decision, result, etc. + */ + CONTESTANT("cos", "Contestant"), + + /** + * Use for a contestant who takes an appeal from one court of law or jurisdiction to another to reverse the judgment. + */ + CONTESTANT_APPELLANT("cot", "Contestant-appellant"), + + /** + * Use for a contestant against whom an appeal is taken from one court of law or jurisdiction to another to reverse the judgment. + */ + CONTESTANT_APPELLEE("coe", "Contestant-appellee"), + + /** + * Use for the party defending a claim, decision, result, etc. being opposed, resisted, or disputed in a court of law. + */ + CONTESTEE("cts", "Contestee"), + + /** + * Use for a contestee who takes an appeal from one court or jurisdiction to another to reverse the judgment. + */ + CONTESTEE_APPELLANT("ctt", "Contestee-appellant"), + + /** + * Use for a contestee against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment. + */ + CONTESTEE_APPELLEE("cte", "Contestee-appellee"), + + /** + * Use for a person or organization relevant to a resource, who enters into a contract with another person or organization to perform a specific task. + */ + CONTRACTOR("ctr", "Contractor"), + + /** + * Use for a person or organization one whose work has been contributed to a larger work, such as an anthology, serial publication, or other compilation of individual works. Do not use if the sole function in relation to a work is as author, editor, compiler or translator. + */ + CONTRIBUTOR("ctb", "Contributor"), + + /** + * Use for a person or organization listed as a copyright owner at the time of registration. Copyright can be granted or later transferred to another person or organization, at which time the claimant becomes the copyright holder. + */ + COPYRIGHT_CLAIMANT("cpc", "Copyright claimant"), + + /** + * Use for a person or organization to whom copy and legal rights have been granted or transferred for the intellectual content of a work. The copyright holder, although not necessarily the creator of the work, usually has the exclusive right to benefit financially from the sale and use of the work to which the associated copyright protection applies. + */ + COPYRIGHT_HOLDER("cph", "Copyright holder"), + + /** + * Use for a person or organization who is a corrector of manuscripts, such as the scriptorium official who corrected the work of a scribe. For printed matter, use Proofreader. + */ + CORRECTOR("crr", "Corrector"), + + /** + * Use for a person or organization who was either the writer or recipient of a letter or other communication. + */ + CORRESPONDENT("crp", "Correspondent"), + + /** + * Use for a person or organization who designs or makes costumes, fixes hair, etc., for a musical or dramatic presentation or entertainment. + */ + COSTUME_DESIGNER("cst", "Costume designer"), + + /** + * Use for a person or organization responsible for the graphic design of a book cover, album cover, slipcase, box, container, etc. For a person or organization responsible for the graphic design of an entire book, use Book designer; for book jackets, use Bookjacket designer. + */ + COVER_DESIGNER("cov", "Cover designer"), + + /** + * Use for a person or organization responsible for the intellectual or artistic content of a work. + */ + CREATOR("cre", "Creator"), + + /** + * Use for a person or organization responsible for conceiving and organizing an exhibition. + */ + CURATOR_OF_AN_EXHIBITION("cur", "Curator of an exhibition"), + + /** + * Use for a person or organization who principally exhibits dancing skills in a musical or dramatic presentation or entertainment. + */ + DANCER("dnc", "Dancer"), + + /** + * Use for a person or organization that submits data for inclusion in a database or other collection of data. + */ + DATA_CONTRIBUTOR("dtc", "Data contributor"), + + /** + * Use for a person or organization responsible for managing databases or other data sources. + */ + DATA_MANAGER("dtm", "Data manager"), + + /** + * Use for a person or organization to whom a book, manuscript, etc., is dedicated (not the recipient of a gift). + */ + DEDICATEE("dte", "Dedicatee"), + + /** + * Use for the author of a dedication, which may be a formal statement or in epistolary or verse form. + */ + DEDICATOR("dto", "Dedicator"), + + /** + * Use for the party defending or denying allegations made in a suit and against whom relief or recovery is sought in the courts, usually in a legal action. + */ + DEFENDANT("dfd", "Defendant"), + + /** + * Use for a defendant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal action. + */ + DEFENDANT_APPELLANT("dft", "Defendant-appellant"), + + /** + * Use for a defendant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal action. + */ + DEFENDANT_APPELLEE("dfe", "Defendant-appellee"), + + /** + * Use for the organization granting a degree for which the thesis or dissertation described was presented. + */ + DEGREE_GRANTOR("dgg", "Degree grantor"), + + /** + * Use for a person or organization executing technical drawings from others' designs. + */ + DELINEATOR("dln", "Delineator"), + + /** + * Use for an entity depicted or portrayed in a work, particularly in a work of art. + */ + DEPICTED("dpc", "Depicted"), + + /** + * Use for a person or organization placing material in the physical custody of a library or repository without transferring the legal title. + */ + DEPOSITOR("dpt", "Depositor"), + + /** + * Use for a person or organization responsible for the design if more specific codes (e.g., [bkd], [tyd]) are not desired. + */ + DESIGNER("dsr", "Designer"), + + /** + * Use for a person or organization who is responsible for the general management of a work or who supervises the production of a performance for stage, screen, or sound recording. + */ + DIRECTOR("drt", "Director"), + + /** + * Use for a person who presents a thesis for a university or higher-level educational degree. + */ + DISSERTANT("dis", "Dissertant"), + + /** + * Use for the name of a place from which a resource, e.g., a serial, is distributed. + */ + DISTRIBUTION_PLACE("dbp", "Distribution place"), + + /** + * Use for a person or organization that has exclusive or shared marketing rights for an item. + */ + DISTRIBUTOR("dst", "Distributor"), + + /** + * Use for a person or organization who is the donor of a book, manuscript, etc., to its present owner. Donors to previous owners are designated as Former owner [fmo] or Inscriber [ins]. + */ + DONOR("dnr", "Donor"), + + /** + * Use for a person or organization who prepares artistic or technical drawings. + */ + DRAFTSMAN("drm", "Draftsman"), + + /** + * Use for a person or organization to which authorship has been dubiously or incorrectly ascribed. + */ + DUBIOUS_AUTHOR("dub", "Dubious author"), + + /** + * Use for a person or organization who prepares for publication a work not primarily his/her own, such as by elucidating text, adding introductory or other critical matter, or technically directing an editorial staff. + */ + EDITOR("edt", "Editor"), + + /** + * Use for a person responsible for setting up a lighting rig and focusing the lights for a production, and running the lighting at a performance. + */ + ELECTRICIAN("elg", "Electrician"), + + /** + * Use for a person or organization who creates a duplicate printing surface by pressure molding and electrodepositing of metal that is then backed up with lead for printing. + */ + ELECTROTYPER("elt", "Electrotyper"), + + /** + * Use for a person or organization that is responsible for technical planning and design, particularly with construction. + */ + ENGINEER("eng", "Engineer"), + + /** + * Use for a person or organization who cuts letters, figures, etc. on a surface, such as a wooden or metal plate, for printing. + */ + ENGRAVER("egr", "Engraver"), + + /** + * Use for a person or organization who produces text or images for printing by subjecting metal, glass, or some other surface to acid or the corrosive action of some other substance. + */ + ETCHER("etr", "Etcher"), + + /** + * Use for the name of the place where an event such as a conference or a concert took place. + */ + EVENT_PLACE("evp", "Event place"), + + /** + * Use for a person or organization in charge of the description and appraisal of the value of goods, particularly rare items, works of art, etc. + */ + EXPERT("exp", "Expert"), + + /** + * Use for a person or organization that executed the facsimile. + */ + FACSIMILIST("fac", "Facsimilist"), + + /** + * Use for a person or organization that manages or supervises the work done to collect raw data or do research in an actual setting or environment (typically applies to the natural and social sciences). + */ + FIELD_DIRECTOR("fld", "Field director"), + + /** + * Use for a person or organization who is an editor of a motion picture film. This term is used regardless of the medium upon which the motion picture is produced or manufactured (e.g., acetate film, video tape). + */ + FILM_EDITOR("flm", "Film editor"), + + /** + * Use for a person or organization who is identified as the only party or the party of the first part. In the case of transfer of right, this is the assignor, transferor, licensor, grantor, etc. Multiple parties can be named jointly as the first party + */ + FIRST_PARTY("fpy", "First party"), + + /** + * Use for a person or organization who makes or imitates something of value or importance, especially with the intent to defraud. + */ + FORGER("frg", "Forger"), + + /** + * Use for a person or organization who owned an item at any time in the past. Includes those to whom the material was once presented. A person or organization giving the item to the present owner is designated as Donor [dnr] + */ + FORMER_OWNER("fmo", "Former owner"), + + /** + * Use for a person or organization that furnished financial support for the production of the work. + */ + FUNDER("fnd", "Funder"), + + /** + * Use for a person responsible for geographic information system (GIS) development and integration with global positioning system data. + */ + GEOGRAPHIC_INFORMATION_SPECIALIST("gis", "Geographic information specialist"), + + /** + * Use for a person or organization in memory or honor of whom a book, manuscript, etc. is donated. + */ + HONOREE("hnr", "Honoree"), + + /** + * Use for a person who is invited or regularly leads a program (often broadcast) that includes other guests, performers, etc. (e.g., talk show host). + */ + HOST("hst", "Host"), + + /** + * Use for a person or organization responsible for the decoration of a work (especially manuscript material) with precious metals or color, usually with elaborate designs and motifs. + */ + ILLUMINATOR("ilu", "Illuminator"), + + /** + * Use for a person or organization who conceives, and perhaps also implements, a design or illustration, usually to accompany a written text. + */ + ILLUSTRATOR("ill", "Illustrator"), + + /** + * Use for a person who signs a presentation statement. + */ + INSCRIBER("ins", "Inscriber"), + + /** + * Use for a person or organization who principally plays an instrument in a musical or dramatic presentation or entertainment. + */ + INSTRUMENTALIST("itr", "Instrumentalist"), + + /** + * Use for a person or organization who is interviewed at a consultation or meeting, usually by a reporter, pollster, or some other information gathering agent. + */ + INTERVIEWEE("ive", "Interviewee"), + + /** + * Use for a person or organization who acts as a reporter, pollster, or other information gathering agent in a consultation or meeting involving one or more individuals. + */ + INTERVIEWER("ivr", "Interviewer"), + + /** + * Use for a person or organization who first produces a particular useful item, or develops a new process for obtaining a known item or result. + */ + INVENTOR("inv", "Inventor"), + + /** + * Use for an institution that provides scientific analyses of material samples. + */ + LABORATORY("lbr", "Laboratory"), + + /** + * Use for a person or organization that manages or supervises work done in a controlled setting or environment. + */ + LABORATORY_DIRECTOR("ldr", "Laboratory director"), + + /** + * Use for a person or organization whose work involves coordinating the arrangement of existing and proposed land features and structures. + */ + LANDSCAPE_ARCHITECT("lsa", "Landscape architect"), + + /** + * Use to indicate that a person or organization takes primary responsibility for a particular activity or endeavor. Use with another relator term or code to show the greater importance this person or organization has regarding that particular role. If more than one relator is assigned to a heading, use the Lead relator only if it applies to all the relators. + */ + LEAD("led", "Lead"), + + /** + * Use for a person or organization permitting the temporary use of a book, manuscript, etc., such as for photocopying or microfilming. + */ + LENDER("len", "Lender"), + + /** + * Use for the party who files a libel in an ecclesiastical or admiralty case. + */ + LIBELANT("lil", "Libelant"), + + /** + * Use for a libelant who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment. + */ + LIBELANT_APPELLANT("lit", "Libelant-appellant"), + + /** + * Use for a libelant against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment. + */ + LIBELANT_APPELLEE("lie", "Libelant-appellee"), + + /** + * Use for a party against whom a libel has been filed in an ecclesiastical court or admiralty. + */ + LIBELEE("lel", "Libelee"), + + /** + * Use for a libelee who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment. + */ + LIBELEE_APPELLANT("let", "Libelee-appellant"), + + /** + * Use for a libelee against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment. + */ + LIBELEE_APPELLEE("lee", "Libelee-appellee"), + + /** + * Use for a person or organization who is a writer of the text of an opera, oratorio, etc. + */ + LIBRETTIST("lbt", "Librettist"), + + /** + * Use for a person or organization who is an original recipient of the right to print or publish. + */ + LICENSEE("lse", "Licensee"), + + /** + * Use for person or organization who is a signer of the license, imprimatur, etc. + */ + LICENSOR("lso", "Licensor"), + + /** + * Use for a person or organization who designs the lighting scheme for a theatrical presentation, entertainment, motion picture, etc. + */ + LIGHTING_DESIGNER("lgd", "Lighting designer"), + + /** + * Use for a person or organization who prepares the stone or plate for lithographic printing, including a graphic artist creating a design directly on the surface from which printing will be done. + */ + LITHOGRAPHER("ltg", "Lithographer"), + + /** + * Use for a person or organization who is a writer of the text of a song. + */ + LYRICIST("lyr", "Lyricist"), + + /** + * Use for a person or organization that makes an artifactual work (an object made or modified by one or more persons). Examples of artifactual works include vases, cannons or pieces of furniture. + */ + MANUFACTURER("mfr", "Manufacturer"), + + /** + * Use for the named entity responsible for marbling paper, cloth, leather, etc. used in construction of a resource. + */ + MARBLER("mrb", "Marbler"), + + /** + * Use for a person or organization performing the coding of SGML, HTML, or XML markup of metadata, text, etc. + */ + MARKUP_EDITOR("mrk", "Markup editor"), + + /** + * Use for a person or organization primarily responsible for compiling and maintaining the original description of a metadata set (e.g., geospatial metadata set). + */ + METADATA_CONTACT("mdc", "Metadata contact"), + + /** + * Use for a person or organization responsible for decorations, illustrations, letters, etc. cut on a metal surface for printing or decoration. + */ + METAL_ENGRAVER("mte", "Metal-engraver"), + + /** + * Use for a person who leads a program (often broadcast) where topics are discussed, usually with participation of experts in fields related to the discussion. + */ + MODERATOR("mod", "Moderator"), + + /** + * Use for a person or organization that supervises compliance with the contract and is responsible for the report and controls its distribution. Sometimes referred to as the grantee, or controlling agency. + */ + MONITOR("mon", "Monitor"), + + /** + * Use for a person who transcribes or copies musical notation + */ + MUSIC_COPYIST("mcp", "Music copyist"), + + /** + * Use for a person responsible for basic music decisions about a production, including coordinating the work of the composer, the sound editor, and sound mixers, selecting musicians, and organizing and/or conducting sound for rehearsals and performances. + */ + MUSICAL_DIRECTOR("msd", "Musical director"), + + /** + * Use for a person or organization who performs music or contributes to the musical content of a work when it is not possible or desirable to identify the function more precisely. + */ + MUSICIAN("mus", "Musician"), + + /** + * Use for a person who is a speaker relating the particulars of an act, occurrence, or course of events. + */ + NARRATOR("nrt", "Narrator"), + + /** + * Use for a person or organization responsible for opposing a thesis or dissertation. + */ + OPPONENT("opn", "Opponent"), + + /** + * Use for a person or organization responsible for organizing a meeting for which an item is the report or proceedings. + */ + ORGANIZER_OF_MEETING("orm", "Organizer of meeting"), + + /** + * Use for a person or organization performing the work, i.e., the name of a person or organization associated with the intellectual content of the work. This category does not include the publisher or personal affiliation, or sponsor except where it is also the corporate author. + */ + ORIGINATOR("org", "Originator"), + + /** + * Use for relator codes from other lists which have no equivalent in the MARC list or for terms which have not been assigned a code. + */ + OTHER("oth", "Other"), + + /** + * Use for a person or organization that currently owns an item or collection. + */ + OWNER("own", "Owner"), + + /** + * Use for a person or organization responsible for the production of paper, usually from wood, cloth, or other fibrous material. + */ + PAPERMAKER("ppm", "Papermaker"), + + /** + * Use for a person or organization that applied for a patent. + */ + PATENT_APPLICANT("pta", "Patent applicant"), + + /** + * Use for a person or organization that was granted the patent referred to by the item. + */ + PATENT_HOLDER("pth", "Patent holder"), + + /** + * Use for a person or organization responsible for commissioning a work. Usually a patron uses his or her means or influence to support the work of artists, writers, etc. This includes those who commission and pay for individual works. + */ + PATRON("pat", "Patron"), + + /** + * Use for a person or organization who exhibits musical or acting skills in a musical or dramatic presentation or entertainment, if specific codes for those functions ([act], [dnc], [itr], [voc], etc.) are not used. If specific codes are used, [prf] is used for a person whose principal skill is not known or specified. + */ + PERFORMER("prf", "Performer"), + + /** + * Use for an authority (usually a government agency) that issues permits under which work is accomplished. + */ + PERMITTING_AGENCY("pma", "Permitting agency"), + + /** + * Use for a person or organization responsible for taking photographs, whether they are used in their original form or as reproductions. + */ + PHOTOGRAPHER("pht", "Photographer"), + + /** + * Use for the party who complains or sues in court in a personal action, usually in a legal proceeding. + */ + PLAINTIFF("ptf", "Plaintiff"), + + /** + * Use for a plaintiff who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding. + */ + PLAINTIFF_APPELLANT("ptt", "Plaintiff-appellant"), + + /** + * Use for a plaintiff against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding. + */ + PLAINTIFF_APPELLEE("pte", "Plaintiff-appellee"), + + /** + * Use for a person or organization responsible for the production of plates, usually for the production of printed images and/or text. + */ + PLATEMAKER("plt", "Platemaker"), + + /** + * Use for a person or organization who prints texts, whether from type or plates. + */ + PRINTER("prt", "Printer"), + + /** + * Use for a person or organization who prints illustrations from plates. + */ + PRINTER_OF_PLATES("pop", "Printer of plates"), + + /** + * Use for a person or organization who makes a relief, intaglio, or planographic printing surface. + */ + PRINTMAKER("prm", "Printmaker"), + + /** + * Use for a person or organization primarily responsible for performing or initiating a process, such as is done with the collection of metadata sets. + */ + PROCESS_CONTACT("prc", "Process contact"), + + /** + * Use for a person or organization responsible for the making of a motion picture, including business aspects, management of the productions, and the commercial success of the work. + */ + PRODUCER("pro", "Producer"), + + /** + * Use for a person responsible for all technical and business matters in a production. + */ + PRODUCTION_MANAGER("pmn", "Production manager"), + + /** + * Use for a person or organization associated with the production (props, lighting, special effects, etc.) of a musical or dramatic presentation or entertainment. + */ + PRODUCTION_PERSONNEL("prd", "Production personnel"), + + /** + * Use for a person or organization responsible for the creation and/or maintenance of computer program design documents, source code, and machine-executable digital files and supporting documentation. + */ + PROGRAMMER("prg", "Programmer"), + + /** + * Use for a person or organization with primary responsibility for all essential aspects of a project, or that manages a very large project that demands senior level responsibility, or that has overall responsibility for managing projects, or provides overall direction to a project manager. + */ + PROJECT_DIRECTOR("pdr", "Project director"), + + /** + * Use for a person who corrects printed matter. For manuscripts, use Corrector [crr]. + */ + PROOFREADER("pfr", "Proofreader"), + + /** + * Use for the name of the place where a resource is published. + */ + PUBLICATION_PLACE("pup", "Publication place"), + + /** + * Use for a person or organization that makes printed matter, often text, but also printed music, artwork, etc. available to the public. + */ + PUBLISHER("pbl", "Publisher"), + + /** + * Use for a person or organization who presides over the elaboration of a collective work to ensure its coherence or continuity. This includes editors-in-chief, literary editors, editors of series, etc. + */ + PUBLISHING_DIRECTOR("pbd", "Publishing director"), + + /** + * Use for a person or organization who manipulates, controls, or directs puppets or marionettes in a musical or dramatic presentation or entertainment. + */ + PUPPETEER("ppt", "Puppeteer"), + + /** + * Use for a person or organization to whom correspondence is addressed. + */ + RECIPIENT("rcp", "Recipient"), + + /** + * Use for a person or organization who supervises the technical aspects of a sound or video recording session. + */ + RECORDING_ENGINEER("rce", "Recording engineer"), + + /** + * Use for a person or organization who writes or develops the framework for an item without being intellectually responsible for its content. + */ + REDACTOR("red", "Redactor"), + + /** + * Use for a person or organization who prepares drawings of architectural designs (i.e., renderings) in accurate, representational perspective to show what the project will look like when completed. + */ + RENDERER("ren", "Renderer"), + + /** + * Use for a person or organization who writes or presents reports of news or current events on air or in print. + */ + REPORTER("rpt", "Reporter"), + + /** + * Use for an agency that hosts data or material culture objects and provides services to promote long term, consistent and shared use of those data or objects. + */ + REPOSITORY("rps", "Repository"), + + /** + * Use for a person who directed or managed a research project. + */ + RESEARCH_TEAM_HEAD("rth", "Research team head"), + + /** + * Use for a person who participated in a research project but whose role did not involve direction or management of it. + */ + RESEARCH_TEAM_MEMBER("rtm", "Research team member"), + + /** + * Use for a person or organization responsible for performing research. + */ + RESEARCHER("res", "Researcher"), + + /** + * Use for the party who makes an answer to the courts pursuant to an application for redress, usually in an equity proceeding. + */ + RESPONDENT("rsp", "Respondent"), + + /** + * Use for a respondent who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. + */ + RESPONDENT_APPELLANT("rst", "Respondent-appellant"), + + /** + * Use for a respondent against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding. + */ + RESPONDENT_APPELLEE("rse", "Respondent-appellee"), + + /** + * Use for a person or organization legally responsible for the content of the published material. + */ + RESPONSIBLE_PARTY("rpy", "Responsible party"), + + /** + * Use for a person or organization, other than the original choreographer or director, responsible for restaging a choreographic or dramatic work and who contributes minimal new content. + */ + RESTAGER("rsg", "Restager"), + + /** + * Use for a person or organization responsible for the review of a book, motion picture, performance, etc. + */ + REVIEWER("rev", "Reviewer"), + + /** + * Use for a person or organization responsible for parts of a work, often headings or opening parts of a manuscript, that appear in a distinctive color, usually red. + */ + RUBRICATOR("rbr", "Rubricator"), + + /** + * Use for a person or organization who is the author of a motion picture screenplay. + */ + SCENARIST("sce", "Scenarist"), + + /** + * Use for a person or organization who brings scientific, pedagogical, or historical competence to the conception and realization on a work, particularly in the case of audio-visual items. + */ + SCIENTIFIC_ADVISOR("sad", "Scientific advisor"), + + /** + * Use for a person who is an amanuensis and for a writer of manuscripts proper. For a person who makes pen-facsimiles, use Facsimilist [fac]. + */ + SCRIBE("scr", "Scribe"), + + /** + * Use for a person or organization who models or carves figures that are three-dimensional representations. + */ + SCULPTOR("scl", "Sculptor"), + + /** + * Use for a person or organization who is identified as the party of the second part. In the case of transfer of right, this is the assignee, transferee, licensee, grantee, etc. Multiple parties can be named jointly as the second party. + */ + SECOND_PARTY("spy", "Second party"), + + /** + * Use for a person or organization who is a recorder, redactor, or other person responsible for expressing the views of a organization. + */ + SECRETARY("sec", "Secretary"), + + /** + * Use for a person or organization who translates the rough sketches of the art director into actual architectural structures for a theatrical presentation, entertainment, motion picture, etc. Set designers draw the detailed guides and specifications for building the set. + */ + SET_DESIGNER("std", "Set designer"), + + /** + * Use for a person whose signature appears without a presentation or other statement indicative of provenance. When there is a presentation statement, use Inscriber [ins]. + */ + SIGNER("sgn", "Signer"), + + /** + * Use for a person or organization who uses his/her/their voice with or without instrumental accompaniment to produce music. A performance may or may not include actual words. + */ + SINGER("sng", "Singer"), + + /** + * Use for a person who produces and reproduces the sound score (both live and recorded), the installation of microphones, the setting of sound levels, and the coordination of sources of sound for a production. + */ + SOUND_DESIGNER("sds", "Sound designer"), + + /** + * Use for a person who participates in a program (often broadcast) and makes a formalized contribution or presentation generally prepared in advance. + */ + SPEAKER("spk", "Speaker"), + + /** + * Use for a person or organization that issued a contract or under the auspices of which a work has been written, printed, published, etc. + */ + SPONSOR("spn", "Sponsor"), + + /** + * Use for a person who is in charge of everything that occurs on a performance stage, and who acts as chief of all crews and assistant to a director during rehearsals. + */ + STAGE_MANAGER("stm", "Stage manager"), + + /** + * Use for an organization responsible for the development or enforcement of a standard. + */ + STANDARDS_BODY("stn", "Standards body"), + + /** + * Use for a person or organization who creates a new plate for printing by molding or copying another printing surface. + */ + STEREOTYPER("str", "Stereotyper"), + + /** + * Use for a person relaying a story with creative and/or theatrical interpretation. + */ + STORYTELLER("stl", "Storyteller"), + + /** + * Use for a person or organization that supports (by allocating facilities, staff, or other resources) a project, program, meeting, event, data objects, material culture objects, or other entities capable of support. + */ + SUPPORTING_HOST("sht", "Supporting host"), + + /** + * Use for a person or organization who does measurements of tracts of land, etc. to determine location, forms, and boundaries. + */ + SURVEYOR("srv", "Surveyor"), + + /** + * Use for a person who, in the context of a resource, gives instruction in an intellectual subject or demonstrates while teaching physical skills. + */ + TEACHER("tch", "Teacher"), + + /** + * Use for a person who is ultimately in charge of scenery, props, lights and sound for a production. + */ + TECHNICAL_DIRECTOR("tcd", "Technical director"), + + /** + * Use for a person under whose supervision a degree candidate develops and presents a thesis, mémoire, or text of a dissertation. + */ + THESIS_ADVISOR("ths", "Thesis advisor"), + + /** + * Use for a person who prepares a handwritten or typewritten copy from original material, including from dictated or orally recorded material. For makers of pen-facsimiles, use Facsimilist [fac]. + */ + TRANSCRIBER("trc", "Transcriber"), + + /** + * Use for a person or organization who renders a text from one language into another, or from an older form of a language into the modern form. + */ + TRANSLATOR("trl", "Translator"), + + /** + * Use for a person or organization who designed the type face used in a particular item. + */ + TYPE_DESIGNER("tyd", "Type designer"), + + /** + * Use for a person or organization primarily responsible for choice and arrangement of type used in an item. If the typographer is also responsible for other aspects of the graphic design of a book (e.g., Book designer [bkd]), codes for both functions may be needed. + */ + TYPOGRAPHER("tyg", "Typographer"), + + /** + * Use for the name of a place where a university that is associated with a resource is located, for example, a university where an academic dissertation or thesis was presented. + */ + UNIVERSITY_PLACE("uvp", "University place"), + + /** + * Use for a person or organization in charge of a video production, e.g. the video recording of a stage production as opposed to a commercial motion picture. The videographer may be the camera operator or may supervise one or more camera operators. Do not confuse with cinematographer. + */ + VIDEOGRAPHER("vdg", "Videographer"), + + /** + * Use for a person or organization who principally exhibits singing skills in a musical or dramatic presentation or entertainment. + */ + VOCALIST("voc", "Vocalist"), + + /** + * Use for a person who verifies the truthfulness of an event or action. + */ + WITNESS("wit", "Witness"), + + /** + * Use for a person or organization who makes prints by cutting the image in relief on the end-grain of a wood block. + */ + WOOD_ENGRAVER("wde", "Wood-engraver"), + + /** + * Use for a person or organization who makes prints by cutting the image in relief on the plank side of a wood block. + */ + WOODCUTTER("wdc", "Woodcutter"), + + /** + * Use for a person or organization who writes significant material which accompanies a sound recording or other audiovisual material. + */ + WRITER_OF_ACCOMPANYING_MATERIAL("wam", "Writer of accompanying material"); + + private final String code; + private final String name; + + Relator(String code, String name) { + this.code = code; + this.name = name; + } + + public String getCode() { + return code; + } + + public String getName() { + return name; + } + + public static Relator byCode(String code) { + for (Relator relator : Relator.values()) { + if (relator.getCode().equalsIgnoreCase(code)) { + return relator; + } + } + return null; + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Resource.java b/epublib/src/main/java/me/ag2s/epublib/domain/Resource.java new file mode 100644 index 000000000..2349c9e91 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Resource.java @@ -0,0 +1,339 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.util.IOUtil; +import me.ag2s.epublib.util.StringUtil; +import me.ag2s.epublib.util.commons.io.XmlStreamReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.Serializable; + +/** + * Represents a resource that is part of the epub. + * A resource can be a html file, image, xml, etc. + * + * @author paul + * + */ +public class Resource implements Serializable { + + private static final long serialVersionUID = 1043946707835004037L; + private String id; + private String title; + private String href; + + + + private String properties; + protected final String originalHref; + private MediaType mediaType; + private String inputEncoding; + protected byte[] data; + + /** + * Creates an empty Resource with the given href. + * + * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 + * + * @param href The location of the resource within the epub. Example: "chapter1.html". + */ + public Resource(String href) { + this(null, new byte[0], href, MediaTypes.determineMediaType(href)); + } + + /** + * Creates a Resource with the given data and MediaType. + * The href will be automatically generated. + * + * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 + * + * @param data The Resource's contents + * @param mediaType The MediaType of the Resource + */ + public Resource(byte[] data, MediaType mediaType) { + this(null, data, null, mediaType); + } + + /** + * Creates a resource with the given data at the specified href. + * The MediaType will be determined based on the href extension. + * + * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 + * + * @see MediaTypes#determineMediaType(String) + * + * @param data The Resource's contents + * @param href The location of the resource within the epub. Example: "chapter1.html". + */ + public Resource(byte[] data, String href) { + this(null, data, href, MediaTypes.determineMediaType(href), + Constants.CHARACTER_ENCODING); + } + + /** + * Creates a resource with the data from the given Reader at the specified href. + * The MediaType will be determined based on the href extension. + * + * @see MediaTypes#determineMediaType(String) + * + * @param in The Resource's contents + * @param href The location of the resource within the epub. Example: "cover.jpg". + */ + public Resource(Reader in, String href) throws IOException { + this(null, IOUtil.toByteArray(in, Constants.CHARACTER_ENCODING), href, + MediaTypes.determineMediaType(href), + Constants.CHARACTER_ENCODING); + } + + /** + * Creates a resource with the data from the given InputStream at the specified href. + * The MediaType will be determined based on the href extension. + * + * @see MediaTypes#determineMediaType(String) + * + * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 + * + * It is recommended to us the {@link #Resource(Reader, String)} method for creating textual + * (html/css/etc) resources to prevent encoding problems. + * Use this method only for binary Resources like images, fonts, etc. + * + * + * @param in The Resource's contents + * @param href The location of the resource within the epub. Example: "cover.jpg". + */ + public Resource(InputStream in, String href) throws IOException { + this(null, IOUtil.toByteArray(in), href, + MediaTypes.determineMediaType(href)); + } + + /** + * Creates a resource with the given id, data, mediatype at the specified href. + * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8 + * + * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value. + * @param data The Resource's contents + * @param href The location of the resource within the epub. Example: "chapter1.html". + * @param mediaType The resources MediaType + */ + public Resource(String id, byte[] data, String href, MediaType mediaType) { + this(id, data, href, mediaType, Constants.CHARACTER_ENCODING); + } + public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType) { + this(id, data, href, originalHref, mediaType, Constants.CHARACTER_ENCODING); + } + + + /** + * Creates a resource with the given id, data, mediatype at the specified href. + * If the data is of a text type (html/css/etc) then it will use the given inputEncoding. + * + * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value. + * @param data The Resource's contents + * @param href The location of the resource within the epub. Example: "chapter1.html". + * @param mediaType The resources MediaType + * @param inputEncoding If the data is of a text type (html/css/etc) then it will use the given inputEncoding. + */ + public Resource(String id, byte[] data, String href, MediaType mediaType, + String inputEncoding) { + this.id = id; + this.href = href; + this.originalHref = href; + this.mediaType = mediaType; + this.inputEncoding = inputEncoding; + this.data = data; + } + public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType, + String inputEncoding) { + this.id = id; + this.href = href; + this.originalHref = originalHref; + this.mediaType = mediaType; + this.inputEncoding = inputEncoding; + this.data = data; + } + + /** + * Gets the contents of the Resource as an InputStream. + * + * @return The contents of the Resource. + * + * @throws IOException IOException + */ + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(getData()); + } + + /** + * The contents of the resource as a byte[] + * + * @return The contents of the resource + */ + public byte[] getData() throws IOException { + return data; + } + + /** + * Tells this resource to release its cached data. + * + * If this resource was not lazy-loaded, this is a no-op. + */ + public void close() { + } + + /** + * Sets the data of the Resource. + * If the data is a of a different type then the original data then make sure to change the MediaType. + * + * @param data the data of the Resource + */ + public void setData(byte[] data) { + this.data = data; + } + + /** + * Returns the size of this resource in bytes. + * + * @return the size. + */ + public long getSize() { + return data.length; + } + + /** + * If the title is found by scanning the underlying html document then it is cached here. + * + * @return the title + */ + public String getTitle() { + return title; + } + + /** + * Sets the Resource's id: Make sure it is unique and a valid identifier. + * + * @param id Resource's id + */ + public void setId(String id) { + this.id = id; + } + + /** + * The resources Id. + * + * Must be both unique within all the resources of this book and a valid identifier. + * @return The resources Id. + */ + public String getId() { + return id; + } + + /** + * The location of the resource within the contents folder of the epub file. + * + * Example:
    + * images/cover.jpg
    + * content/chapter1.xhtml
    + * + * @return The location of the resource within the contents folder of the epub file. + */ + public String getHref() { + return href; + } + + /** + * Sets the Resource's href. + * + * @param href Resource's href. + */ + public void setHref(String href) { + this.href = href; + } + + /** + * The character encoding of the resource. + * Is allowed to be null for non-text resources like images. + * + * @return The character encoding of the resource. + */ + public String getInputEncoding() { + return inputEncoding; + } + + /** + * Sets the Resource's input character encoding. + * + * @param encoding Resource's input character encoding. + */ + public void setInputEncoding(String encoding) { + this.inputEncoding = encoding; + } + + /** + * Gets the contents of the Resource as Reader. + * + * Does all sorts of smart things (courtesy of apache commons io XMLStreamREader) to handle encodings, byte order markers, etc. + * + * @return the contents of the Resource as Reader. + * @throws IOException IOException + */ + public Reader getReader() throws IOException { + return new XmlStreamReader(new ByteArrayInputStream(getData()), + getInputEncoding()); + } + + /** + * Gets the hashCode of the Resource's href. + * + */ + public int hashCode() { + return href.hashCode(); + } + + /** + * Checks to see of the given resourceObject is a resource and whether its href is equal to this one. + * + * @return whether the given resourceObject is a resource and whether its href is equal to this one. + */ + public boolean equals(Object resourceObject) { + if (!(resourceObject instanceof Resource)) { + return false; + } + return href.equals(((Resource) resourceObject).getHref()); + } + + /** + * This resource's mediaType. + * + * @return This resource's mediaType. + */ + public MediaType getMediaType() { + return mediaType; + } + + public void setMediaType(MediaType mediaType) { + this.mediaType = mediaType; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getProperties() { + return properties; + } + + public void setProperties(String properties) { + this.properties = properties; + } + @SuppressWarnings("NullableProblems") + public String toString() { + return StringUtil.toString("id", id, + "title", title, + "encoding", inputEncoding, + "mediaType", mediaType, + "href", href, + "size", (data == null ? 0 : data.length)); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java b/epublib/src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java new file mode 100644 index 000000000..d6aa68f86 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java @@ -0,0 +1,36 @@ +package me.ag2s.epublib.domain; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipFile; + +/** + * A wrapper class for closing a ZipFile object when the InputStream derived + * from it is closed. + * + * @author ttopalov + */ +public class ResourceInputStream extends FilterInputStream { + + private final ZipFile zipFile; + + /** + * Constructor. + * + * @param in + * The InputStream object. + * @param zipFile + * The ZipFile object. + */ + public ResourceInputStream(InputStream in, ZipFile zipFile) { + super(in); + this.zipFile = zipFile; + } + + @Override + public void close() throws IOException { + super.close(); + zipFile.close(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/ResourceReference.java b/epublib/src/main/java/me/ag2s/epublib/domain/ResourceReference.java new file mode 100644 index 000000000..f09c97643 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/ResourceReference.java @@ -0,0 +1,43 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; + +public class ResourceReference implements Serializable { + + private static final long serialVersionUID = 2596967243557743048L; + + protected Resource resource; + + public ResourceReference(Resource resource) { + this.resource = resource; + } + + + public Resource getResource() { + return resource; + } + + /** + * Besides setting the resource it also sets the fragmentId to null. + * + * @param resource resource + */ + public void setResource(Resource resource) { + this.resource = resource; + } + + + /** + * The id of the reference referred to. + * + * null of the reference is null or has a null id itself. + * + * @return The id of the reference referred to. + */ + public String getResourceId() { + if (resource != null) { + return resource.getId(); + } + return null; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Resources.java b/epublib/src/main/java/me/ag2s/epublib/domain/Resources.java new file mode 100644 index 000000000..27d512700 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Resources.java @@ -0,0 +1,402 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.util.StringUtil; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * All the resources that make up the book. + * XHTML files, images and epub xml documents must be here. + * + * @author paul + */ +public class Resources implements Serializable { + + private static final long serialVersionUID = 2450876953383871451L; + private static final String IMAGE_PREFIX = "image_"; + private static final String ITEM_PREFIX = "item_"; + private int lastId = 1; + + private Map resources = new HashMap<>(); + + /** + * Adds a resource to the resources. + *

    + * Fixes the resources id and href if necessary. + * + * @param resource resource + * @return the newly added resource + */ + public Resource add(Resource resource) { + fixResourceHref(resource); + fixResourceId(resource); + this.resources.put(resource.getHref(), resource); + return resource; + } + + /** + * Checks the id of the given resource and changes to a unique identifier if it isn't one already. + * + * @param resource resource + */ + public void fixResourceId(Resource resource) { + String resourceId = resource.getId(); + + // first try and create a unique id based on the resource's href + if (StringUtil.isBlank(resource.getId())) { + resourceId = StringUtil.substringBeforeLast(resource.getHref(), '.'); + resourceId = StringUtil.substringAfterLast(resourceId, '/'); + } + + resourceId = makeValidId(resourceId, resource); + + // check if the id is unique. if not: create one from scratch + if (StringUtil.isBlank(resourceId) || containsId(resourceId)) { + resourceId = createUniqueResourceId(resource); + } + resource.setId(resourceId); + } + + /** + * Check if the id is a valid identifier. if not: prepend with valid identifier + * + * @param resource resource + * @return a valid id + */ + private String makeValidId(String resourceId, Resource resource) { + if (StringUtil.isNotBlank(resourceId) && !Character + .isJavaIdentifierStart(resourceId.charAt(0))) { + resourceId = getResourceItemPrefix(resource) + resourceId; + } + return resourceId; + } + + private String getResourceItemPrefix(Resource resource) { + String result; + if (MediaTypes.isBitmapImage(resource.getMediaType())) { + result = IMAGE_PREFIX; + } else { + result = ITEM_PREFIX; + } + return result; + } + + /** + * Creates a new resource id that is guaranteed to be unique for this set of Resources + * + * @param resource resource + * @return a new resource id that is guaranteed to be unique for this set of Resources + */ + private String createUniqueResourceId(Resource resource) { + int counter = lastId; + if (counter == Integer.MAX_VALUE) { + if (resources.size() == Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Resources contains " + Integer.MAX_VALUE + + " elements: no new elements can be added"); + } else { + counter = 1; + } + } + String prefix = getResourceItemPrefix(resource); + String result = prefix + counter; + while (containsId(result)) { + result = prefix + (++counter); + } + lastId = counter; + return result; + } + + /** + * Whether the map of resources already contains a resource with the given id. + * + * @param id id + * @return Whether the map of resources already contains a resource with the given id. + */ + public boolean containsId(String id) { + if (StringUtil.isBlank(id)) { + return false; + } + for (Resource resource : resources.values()) { + if (id.equals(resource.getId())) { + return true; + } + } + return false; + } + + /** + * Gets the resource with the given id. + * + * @param id id + * @return null if not found + */ + public Resource getById(String id) { + if (StringUtil.isBlank(id)) { + return null; + } + for (Resource resource : resources.values()) { + if (id.equals(resource.getId())) { + return resource; + } + } + return null; + } + + public Resource getByProperties(String properties) { + if (StringUtil.isBlank(properties)) { + return null; + } + for (Resource resource : resources.values()) { + if (properties.equals(resource.getProperties())) { + return resource; + } + } + return null; + } + + /** + * Remove the resource with the given href. + * + * @param href href + * @return the removed resource, null if not found + */ + public Resource remove(String href) { + return resources.remove(href); + } + + private void fixResourceHref(Resource resource) { + if (StringUtil.isNotBlank(resource.getHref()) + && !resources.containsKey(resource.getHref())) { + return; + } + if (StringUtil.isBlank(resource.getHref())) { + if (resource.getMediaType() == null) { + throw new IllegalArgumentException( + "Resource must have either a MediaType or a href"); + } + int i = 1; + String href = createHref(resource.getMediaType(), i); + while (resources.containsKey(href)) { + href = createHref(resource.getMediaType(), (++i)); + } + resource.setHref(href); + } + } + + private String createHref(MediaType mediaType, int counter) { + if (MediaTypes.isBitmapImage(mediaType)) { + return IMAGE_PREFIX + counter + mediaType.getDefaultExtension(); + } else { + return ITEM_PREFIX + counter + mediaType.getDefaultExtension(); + } + } + + + public boolean isEmpty() { + return resources.isEmpty(); + } + + /** + * The number of resources + * + * @return The number of resources + */ + public int size() { + return resources.size(); + } + + /** + * The resources that make up this book. + * Resources can be xhtml pages, images, xml documents, etc. + * + * @return The resources that make up this book. + */ + @SuppressWarnings("unused") + public Map getResourceMap() { + return resources; + } + + public Collection getAll() { + return resources.values(); + } + + + /** + * Whether there exists a resource with the given href + * + * @param href href + * @return Whether there exists a resource with the given href + */ + public boolean notContainsByHref(String href) { + if (StringUtil.isBlank(href)) { + return true; + } else { + return !resources.containsKey( + StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR)); + } + } + /** + * Whether there exists a resource with the given href + * + * @param href href + * @return Whether there exists a resource with the given href + */ + @SuppressWarnings("unused") + public boolean containsByHref(String href) { + return !notContainsByHref(href); + } + + /** + * Sets the collection of Resources to the given collection of resources + * + * @param resources resources + */ + public void set(Collection resources) { + this.resources.clear(); + addAll(resources); + } + + /** + * Adds all resources from the given Collection of resources to the existing collection. + * + * @param resources resources + */ + public void addAll(Collection resources) { + for (Resource resource : resources) { + fixResourceHref(resource); + this.resources.put(resource.getHref(), resource); + } + } + + /** + * Sets the collection of Resources to the given collection of resources + * + * @param resources A map with as keys the resources href and as values the Resources + */ + public void set(Map resources) { + this.resources = new HashMap<>(resources); + } + + + /** + * First tries to find a resource with as id the given idOrHref, if that + * fails it tries to find one with the idOrHref as href. + * + * @param idOrHref idOrHref + * @return the found Resource + */ + public Resource getByIdOrHref(String idOrHref) { + Resource resource = getById(idOrHref); + if (resource == null) { + resource = getByHref(idOrHref); + } + return resource; + } + + + /** + * Gets the resource with the given href. + * If the given href contains a fragmentId then that fragment id will be ignored. + * + * @param href href + * @return null if not found. + */ + public Resource getByHref(String href) { + if (StringUtil.isBlank(href)) { + return null; + } + href = StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR); + return resources.get(href); + } + + /** + * Gets the first resource (random order) with the give mediatype. + *

    + * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. + * + * @param mediaType mediaType + * @return the first resource (random order) with the give mediatype. + */ + public Resource findFirstResourceByMediaType(MediaType mediaType) { + return findFirstResourceByMediaType(resources.values(), mediaType); + } + + /** + * Gets the first resource (random order) with the give mediatype. + *

    + * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype. + * + * @param mediaType mediaType + * @return the first resource (random order) with the give mediatype. + */ + public static Resource findFirstResourceByMediaType( + Collection resources, MediaType mediaType) { + for (Resource resource : resources) { + if (resource.getMediaType() == mediaType) { + return resource; + } + } + return null; + } + + /** + * All resources that have the given MediaType. + * + * @param mediaType mediaType + * @return All resources that have the given MediaType. + */ + public List getResourcesByMediaType(MediaType mediaType) { + List result = new ArrayList<>(); + if (mediaType == null) { + return result; + } + for (Resource resource : getAll()) { + if (resource.getMediaType() == mediaType) { + result.add(resource); + } + } + return result; + } + + /** + * All Resources that match any of the given list of MediaTypes + * + * @param mediaTypes mediaType + * @return All Resources that match any of the given list of MediaTypes + */ + @SuppressWarnings("unused") + public List getResourcesByMediaTypes(MediaType[] mediaTypes) { + List result = new ArrayList<>(); + if (mediaTypes == null) { + return result; + } + + // this is the fastest way of doing this according to + // http://stackoverflow.com/questions/1128723/in-java-how-can-i-test-if-an-array-contains-a-certain-value + List mediaTypesList = Arrays.asList(mediaTypes); + for (Resource resource : getAll()) { + if (mediaTypesList.contains(resource.getMediaType())) { + result.add(resource); + } + } + return result; + } + + + /** + * All resource hrefs + * + * @return all resource hrefs + */ + public Collection getAllHrefs() { + return resources.keySet(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/Spine.java b/epublib/src/main/java/me/ag2s/epublib/domain/Spine.java new file mode 100644 index 000000000..293837086 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/Spine.java @@ -0,0 +1,191 @@ +package me.ag2s.epublib.domain; + +import me.ag2s.epublib.util.StringUtil; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * The spine sections are the sections of the book in the order in which the book should be read. + * + * This contrasts with the Table of Contents sections which is an index into the Book's sections. + * + * @see TableOfContents + * + * @author paul + */ +public class Spine implements Serializable { + + private static final long serialVersionUID = 3878483958947357246L; + private Resource tocResource; + private List spineReferences; + + public Spine() { + this(new ArrayList<>()); + } + + /** + * Creates a spine out of all the resources in the table of contents. + * + * @param tableOfContents tableOfContents + */ + public Spine(TableOfContents tableOfContents) { + this.spineReferences = createSpineReferences( + tableOfContents.getAllUniqueResources()); + } + + public Spine(List spineReferences) { + this.spineReferences = spineReferences; + } + + public static List createSpineReferences( + Collection resources) { + List result = new ArrayList<>( + resources.size()); + for (Resource resource : resources) { + result.add(new SpineReference(resource)); + } + return result; + } + + public List getSpineReferences() { + return spineReferences; + } + + public void setSpineReferences(List spineReferences) { + this.spineReferences = spineReferences; + } + + /** + * Gets the resource at the given index. + * Null if not found. + * + * @param index index + * @return the resource at the given index. + */ + public Resource getResource(int index) { + if (index < 0 || index >= spineReferences.size()) { + return null; + } + return spineReferences.get(index).getResource(); + } + + /** + * Finds the first resource that has the given resourceId. + * + * Null if not found. + * + * @param resourceId resourceId + * @return the first resource that has the given resourceId. + */ + public int findFirstResourceById(String resourceId) { + if (StringUtil.isBlank(resourceId)) { + return -1; + } + + for (int i = 0; i < spineReferences.size(); i++) { + SpineReference spineReference = spineReferences.get(i); + if (resourceId.equals(spineReference.getResourceId())) { + return i; + } + } + return -1; + } + + /** + * Adds the given spineReference to the spine references and returns it. + * + * @param spineReference spineReference + * @return the given spineReference + */ + public SpineReference addSpineReference(SpineReference spineReference) { + if (spineReferences == null) { + this.spineReferences = new ArrayList<>(); + } + spineReferences.add(spineReference); + return spineReference; + } + + /** + * Adds the given resource to the spine references and returns it. + * + * @return the given spineReference + */ + @SuppressWarnings("unused") + public SpineReference addResource(Resource resource) { + return addSpineReference(new SpineReference(resource)); + } + + /** + * The number of elements in the spine. + * + * @return The number of elements in the spine. + */ + public int size() { + return spineReferences.size(); + } + + /** + * As per the epub file format the spine officially maintains a reference to the Table of Contents. + * The epubwriter will look for it here first, followed by some clever tricks to find it elsewhere if not found. + * Put it here to be sure of the expected behaviours. + * + * @param tocResource tocResource + */ + public void setTocResource(Resource tocResource) { + this.tocResource = tocResource; + } + + /** + * The resource containing the XML for the tableOfContents. + * When saving an epub file this resource needs to be in this place. + * + * @return The resource containing the XML for the tableOfContents. + */ + public Resource getTocResource() { + return tocResource; + } + + /** + * The position within the spine of the given resource. + * + * @param currentResource currentResource + * @return something < 0 if not found. + * + */ + public int getResourceIndex(Resource currentResource) { + if (currentResource == null) { + return -1; + } + return getResourceIndex(currentResource.getHref()); + } + + /** + * The first position within the spine of a resource with the given href. + * + * @return something < 0 if not found. + * + */ + public int getResourceIndex(String resourceHref) { + int result = -1; + if (StringUtil.isBlank(resourceHref)) { + return result; + } + for (int i = 0; i < spineReferences.size(); i++) { + if (resourceHref.equals(spineReferences.get(i).getResource().getHref())) { + result = i; + break; + } + } + return result; + } + + /** + * Whether the spine has any references + * @return Whether the spine has any references + */ + public boolean isEmpty() { + return spineReferences.isEmpty(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/SpineReference.java b/epublib/src/main/java/me/ag2s/epublib/domain/SpineReference.java new file mode 100644 index 000000000..135e4e6bf --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/SpineReference.java @@ -0,0 +1,52 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; + + +/** + * A Section of a book. + * Represents both an item in the package document and a item in the index. + * + * @author paul + */ +public class SpineReference extends ResourceReference implements Serializable { + + private static final long serialVersionUID = -7921609197351510248L; + private boolean linear;//default = true; + + public SpineReference(Resource resource) { + this(resource, true); + } + + + public SpineReference(Resource resource, boolean linear) { + super(resource); + this.linear = linear; + } + + /** + * Linear denotes whether the section is Primary or Auxiliary. + * Usually the cover page has linear set to false and all the other sections + * have it set to true. + *

    + * It's an optional property that readers may also ignore. + * + *

    primary or auxiliary is useful for Reading Systems which + * opt to present auxiliary content differently than primary content. + * For example, a Reading System might opt to render auxiliary content in + * a popup window apart from the main window which presents the primary + * content. (For an example of the types of content that may be considered + * auxiliary, refer to the example below and the subsequent discussion.)
    + * + * @return whether the section is Primary or Auxiliary. + * @see OPF Spine specification + */ + public boolean isLinear() { + return linear; + } + + public void setLinear(boolean linear) { + this.linear = linear; + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/TOCReference.java b/epublib/src/main/java/me/ag2s/epublib/domain/TOCReference.java new file mode 100644 index 000000000..5fe9c71bb --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/TOCReference.java @@ -0,0 +1,56 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * An item in the Table of Contents. + * + * @see TableOfContents + * + * @author paul + */ +public class TOCReference extends TitledResourceReference + implements Serializable { + + private static final long serialVersionUID = 5787958246077042456L; + private List children; + private static final Comparator COMPARATOR_BY_TITLE_IGNORE_CASE = (tocReference1, tocReference2) -> String.CASE_INSENSITIVE_ORDER.compare(tocReference1.getTitle(), tocReference2.getTitle()); + @Deprecated + public TOCReference() { + this(null, null, null); + } + + public TOCReference(String name, Resource resource) { + this(name, resource, null); + } + + public TOCReference(String name, Resource resource, String fragmentId) { + this(name, resource, fragmentId, new ArrayList<>()); + } + + public TOCReference(String title, Resource resource, String fragmentId, + List children) { + super(resource, title, fragmentId); + this.children = children; + } + @SuppressWarnings("unused") + public static Comparator getComparatorByTitleIgnoreCase() { + return COMPARATOR_BY_TITLE_IGNORE_CASE; + } + + public List getChildren() { + return children; + } + + public TOCReference addChildSection(TOCReference childSection) { + this.children.add(childSection); + return childSection; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/TableOfContents.java b/epublib/src/main/java/me/ag2s/epublib/domain/TableOfContents.java new file mode 100644 index 000000000..6c268358d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/TableOfContents.java @@ -0,0 +1,260 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The table of contents of the book. + * The TableOfContents is a tree structure at the root it is a list of TOCReferences, each if which may have as children another list of TOCReferences. + * + * The table of contents is used by epub as a quick index to chapters and sections within chapters. + * It may contain duplicate entries, may decide to point not to certain chapters, etc. + * + * See the spine for the complete list of sections in the order in which they should be read. + * + * @see Spine + * + * @author paul + */ +public class TableOfContents implements Serializable { + + private static final long serialVersionUID = -3147391239966275152L; + + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private List tocReferences; + + public TableOfContents() { + this(new ArrayList<>()); + } + + public TableOfContents(List tocReferences) { + this.tocReferences = tocReferences; + } + + public List getTocReferences() { + return tocReferences; + } + + public void setTocReferences(List tocReferences) { + this.tocReferences = tocReferences; + } + + /** + * Calls addTOCReferenceAtLocation after splitting the path using the DEFAULT_PATH_SEPARATOR. + * @return the new TOCReference + */ + @SuppressWarnings("unused") + public TOCReference addSection(Resource resource, String path) { + return addSection(resource, path, DEFAULT_PATH_SEPARATOR); + } + + /** + * Calls addTOCReferenceAtLocation after splitting the path using the given pathSeparator. + * + * @param resource resource + * @param path path + * @param pathSeparator pathSeparator + * @return the new TOCReference + */ + public TOCReference addSection(Resource resource, String path, + String pathSeparator) { + String[] pathElements = path.split(pathSeparator); + return addSection(resource, pathElements); + } + + /** + * Finds the first TOCReference in the given list that has the same title as the given Title. + * + * @param title title + * @param tocReferences tocReferences + * @return null if not found. + */ + private static TOCReference findTocReferenceByTitle(String title, + List tocReferences) { + for (TOCReference tocReference : tocReferences) { + if (title.equals(tocReference.getTitle())) { + return tocReference; + } + } + return null; + } + + /** + * Adds the given Resources to the TableOfContents at the location specified by the pathElements. + * + * Example: + * Calling this method with a Resource and new String[] {"chapter1", "paragraph1"} will result in the following: + *
      + *
    • a TOCReference with the title "chapter1" at the root level.
      + * If this TOCReference did not yet exist it will have been created and does not point to any resource
    • + *
    • A TOCReference that has the title "paragraph1". This TOCReference will be the child of TOCReference "chapter1" and + * will point to the given Resource
    • + *
    + * + * @param resource resource + * @param pathElements pathElements + * @return the new TOCReference + */ + public TOCReference addSection(Resource resource, String[] pathElements) { + if (pathElements == null || pathElements.length == 0) { + return null; + } + TOCReference result = null; + List currentTocReferences = this.tocReferences; + for (String currentTitle : pathElements) { + result = findTocReferenceByTitle(currentTitle, currentTocReferences); + if (result == null) { + result = new TOCReference(currentTitle, null); + currentTocReferences.add(result); + } + currentTocReferences = result.getChildren(); + } + result.setResource(resource); + return result; + } + + /** + * Adds the given Resources to the TableOfContents at the location specified by the pathElements. + * + * Example: + * Calling this method with a Resource and new int[] {0, 0} will result in the following: + *
      + *
    • a TOCReference at the root level.
      + * If this TOCReference did not yet exist it will have been created with a title of "" and does not point to any resource
    • + *
    • A TOCReference that points to the given resource and is a child of the previously created TOCReference.
      + * If this TOCReference didn't exist yet it will be created and have a title of ""
    • + *
    + * + * @param resource resource + * @param pathElements pathElements + * @return the new TOCReference + */ + @SuppressWarnings("unused") + public TOCReference addSection(Resource resource, int[] pathElements, + String sectionTitlePrefix, String sectionNumberSeparator) { + if (pathElements == null || pathElements.length == 0) { + return null; + } + TOCReference result = null; + List currentTocReferences = this.tocReferences; + for (int i = 0; i < pathElements.length; i++) { + int currentIndex = pathElements[i]; + if (currentIndex > 0 && currentIndex < (currentTocReferences.size() + - 1)) { + result = currentTocReferences.get(currentIndex); + } else { + result = null; + } + if (result == null) { + paddTOCReferences(currentTocReferences, pathElements, i, + sectionTitlePrefix, sectionNumberSeparator); + result = currentTocReferences.get(currentIndex); + } + currentTocReferences = result.getChildren(); + } + result.setResource(resource); + return result; + } + + private void paddTOCReferences(List currentTocReferences, + int[] pathElements, int pathPos, String sectionPrefix, + String sectionNumberSeparator) { + for (int i = currentTocReferences.size(); i <= pathElements[pathPos]; i++) { + String sectionTitle = createSectionTitle(pathElements, pathPos, i, + sectionPrefix, + sectionNumberSeparator); + currentTocReferences.add(new TOCReference(sectionTitle, null)); + } + } + + private String createSectionTitle(int[] pathElements, int pathPos, + int lastPos, + String sectionPrefix, String sectionNumberSeparator) { + StringBuilder title = new StringBuilder(sectionPrefix); + for (int i = 0; i < pathPos; i++) { + if (i > 0) { + title.append(sectionNumberSeparator); + } + title.append(pathElements[i] + 1); + } + if (pathPos > 0) { + title.append(sectionNumberSeparator); + } + title.append(lastPos + 1); + return title.toString(); + } + + public TOCReference addTOCReference(TOCReference tocReference) { + if (tocReferences == null) { + tocReferences = new ArrayList<>(); + } + tocReferences.add(tocReference); + return tocReference; + } + + /** + * All unique references (unique by href) in the order in which they are referenced to in the table of contents. + * + * @return All unique references (unique by href) in the order in which they are referenced to in the table of contents. + */ + public List getAllUniqueResources() { + Set uniqueHrefs = new HashSet<>(); + List result = new ArrayList<>(); + getAllUniqueResources(uniqueHrefs, result, tocReferences); + return result; + } + + private static void getAllUniqueResources(Set uniqueHrefs, + List result, List tocReferences) { + for (TOCReference tocReference : tocReferences) { + Resource resource = tocReference.getResource(); + if (resource != null && !uniqueHrefs.contains(resource.getHref())) { + uniqueHrefs.add(resource.getHref()); + result.add(resource); + } + getAllUniqueResources(uniqueHrefs, result, tocReference.getChildren()); + } + } + + /** + * The total number of references in this table of contents. + * + * @return The total number of references in this table of contents. + */ + public int size() { + return getTotalSize(tocReferences); + } + + private static int getTotalSize(Collection tocReferences) { + int result = tocReferences.size(); + for (TOCReference tocReference : tocReferences) { + result += getTotalSize(tocReference.getChildren()); + } + return result; + } + + /** + * The maximum depth of the reference tree + * @return The maximum depth of the reference tree + */ + public int calculateDepth() { + return calculateDepth(tocReferences, 0); + } + + private int calculateDepth(List tocReferences, + int currentDepth) { + int maxChildDepth = 0; + for (TOCReference tocReference : tocReferences) { + int childDepth = calculateDepth(tocReference.getChildren(), 1); + if (childDepth > maxChildDepth) { + maxChildDepth = childDepth; + } + } + return currentDepth + maxChildDepth; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java b/epublib/src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java new file mode 100644 index 000000000..e7fe769ad --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java @@ -0,0 +1,91 @@ +package me.ag2s.epublib.domain; + +import java.io.Serializable; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.util.StringUtil; + +public class TitledResourceReference extends ResourceReference + implements Serializable { + + private static final long serialVersionUID = 3918155020095190080L; + private String fragmentId; + private String title; + + /** + * 这会使title为null + * + * @param resource resource + */ + @Deprecated + @SuppressWarnings("unused") + public TitledResourceReference(Resource resource) { + this(resource, null); + } + + public TitledResourceReference(Resource resource, String title) { + this(resource, title, null); + } + + public TitledResourceReference(Resource resource, String title, + String fragmentId) { + super(resource); + this.title = title; + this.fragmentId = fragmentId; + } + + public String getFragmentId() { + return fragmentId; + } + + public void setFragmentId(String fragmentId) { + this.fragmentId = fragmentId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + + /** + * If the fragmentId is blank it returns the resource href, otherwise + * it returns the resource href + '#' + the fragmentId. + * + * @return If the fragmentId is blank it returns the resource href, + * otherwise it returns the resource href + '#' + the fragmentId. + */ + public String getCompleteHref() { + if (StringUtil.isBlank(fragmentId)) { + return resource.getHref(); + } else { + return resource.getHref() + Constants.FRAGMENT_SEPARATOR_CHAR + + fragmentId; + } + } + + @Override + public Resource getResource() { + //resource为null时不设置标题 + if(this.resource!=null&&this.title!=null){ + resource.setTitle(title); + } + + return resource; + } + + public void setResource(Resource resource, String fragmentId) { + super.setResource(resource); + this.fragmentId = fragmentId; + } + + /** + * Sets the resource to the given resource and sets the fragmentId to null. + */ + public void setResource(Resource resource) { + setResource(resource, null); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java new file mode 100644 index 000000000..d45b9d891 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessor.java @@ -0,0 +1,20 @@ +package me.ag2s.epublib.epub; + +import me.ag2s.epublib.domain.EpubBook; + +/** + * Post-processes a book. + * + * Can be used to clean up a book after reading or before writing. + * + * @author paul + */ +public interface BookProcessor { + + /** + * A BookProcessor that returns the input book unchanged. + */ + BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; + + EpubBook processBook(EpubBook book); +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java new file mode 100644 index 000000000..35256370c --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java @@ -0,0 +1,73 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import me.ag2s.epublib.domain.EpubBook; + +/** + * A book processor that combines several other bookprocessors + *

    + * Fixes coverpage/coverimage. + * Cleans up the XHTML. + * + * @author paul.siegmann + */ +@SuppressWarnings("unused declaration") +public class BookProcessorPipeline implements BookProcessor { + + private static final String TAG= BookProcessorPipeline.class.getName(); + private List bookProcessors; + + public BookProcessorPipeline() { + this(null); + } + + public BookProcessorPipeline(List bookProcessingPipeline) { + this.bookProcessors = bookProcessingPipeline; + } + + @Override + public EpubBook processBook(EpubBook book) { + if (bookProcessors == null) { + return book; + } + for (BookProcessor bookProcessor : bookProcessors) { + try { + book = bookProcessor.processBook(book); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + } + return book; + } + + public void addBookProcessor(BookProcessor bookProcessor) { + if (this.bookProcessors == null) { + bookProcessors = new ArrayList<>(); + } + this.bookProcessors.add(bookProcessor); + } + + public void addBookProcessors(Collection bookProcessors) { + if (this.bookProcessors == null) { + this.bookProcessors = new ArrayList<>(); + } + this.bookProcessors.addAll(bookProcessors); + } + + + public List getBookProcessors() { + return bookProcessors; + } + + + public void setBookProcessingPipeline( + List bookProcessingPipeline) { + this.bookProcessors = bookProcessingPipeline; + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/DOMUtil.java b/epublib/src/main/java/me/ag2s/epublib/epub/DOMUtil.java new file mode 100644 index 000000000..e6ae05b3d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/DOMUtil.java @@ -0,0 +1,177 @@ +package me.ag2s.epublib.epub; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; + +import java.util.ArrayList; +import java.util.List; + +import me.ag2s.epublib.util.StringUtil; + +/** + * Utility methods for working with the DOM. + * + * @author paul + */ +// package +class DOMUtil { + + /** + * First tries to get the attribute value by doing an getAttributeNS on the element, if that gets an empty element it does a getAttribute without namespace. + * + * @param element element + * @param namespace namespace + * @param attribute attribute + * @return String Attribute + */ + public static String getAttribute(Element element, String namespace, + String attribute) { + String result = element.getAttributeNS(namespace, attribute); + if (StringUtil.isEmpty(result)) { + result = element.getAttribute(attribute); + } + return result; + } + + /** + * Gets all descendant elements of the given parentElement with the given namespace and tagname and returns their text child as a list of String. + * + * @param parentElement parentElement + * @param namespace namespace + * @param tagName tagName + * @return List + */ + public static List getElementsTextChild(Element parentElement, + String namespace, String tagName) { + NodeList elements = parentElement + .getElementsByTagNameNS(namespace, tagName); + //ArrayList 初始化时指定长度提高性能 + List result = new ArrayList<>(elements.getLength()); + for (int i = 0; i < elements.getLength(); i++) { + result.add(getTextChildrenContent((Element) elements.item(i))); + } + return result; + } + + /** + * Finds in the current document the first element with the given namespace and elementName and with the given findAttributeName and findAttributeValue. + * It then returns the value of the given resultAttributeName. + * + * @param document document + * @param namespace namespace + * @param elementName elementName + * @param findAttributeName findAttributeName + * @param findAttributeValue findAttributeValue + * @param resultAttributeName resultAttributeName + * @return String value + */ + public static String getFindAttributeValue(Document document, + String namespace, String elementName, String findAttributeName, + String findAttributeValue, String resultAttributeName) { + NodeList metaTags = document.getElementsByTagNameNS(namespace, elementName); + for (int i = 0; i < metaTags.getLength(); i++) { + Element metaElement = (Element) metaTags.item(i); + if (findAttributeValue + .equalsIgnoreCase(metaElement.getAttribute(findAttributeName)) + && StringUtil + .isNotBlank(metaElement.getAttribute(resultAttributeName))) { + return metaElement.getAttribute(resultAttributeName); + } + } + return null; + } + + /** + * Gets the first element that is a child of the parentElement and has the given namespace and tagName + * + * @param parentElement parentElement + * @param namespace namespace + * @param tagName tagName + * @return Element + */ + public static NodeList getElementsByTagNameNS(Element parentElement, + String namespace, String tagName) { + NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); + if (nodes.getLength() != 0) { + return nodes; + } + nodes = parentElement.getElementsByTagName(tagName); + if (nodes.getLength() == 0) { + return null; + } + return nodes; + } + /** + * Gets the first element that is a child of the parentElement and has the given namespace and tagName + * + * @param parentElement parentElement + * @param namespace namespace + * @param tagName tagName + * @return Element + */ + public static NodeList getElementsByTagNameNS(Document parentElement, + String namespace, String tagName) { + NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); + if (nodes.getLength() != 0) { + return nodes; + } + nodes = parentElement.getElementsByTagName(tagName); + if (nodes.getLength() == 0) { + return null; + } + return nodes; + } + + /** + * Gets the first element that is a child of the parentElement and has the given namespace and tagName + * + * @param parentElement parentElement + * @param namespace namespace + * @param tagName tagName + * @return Element + */ + public static Element getFirstElementByTagNameNS(Element parentElement, + String namespace, String tagName) { + NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName); + if (nodes.getLength() != 0) { + return (Element) nodes.item(0); + } + nodes = parentElement.getElementsByTagName(tagName); + if (nodes.getLength() == 0) { + return null; + } + return (Element) nodes.item(0); + } + + /** + * The contents of all Text nodes that are children of the given parentElement. + * The result is trim()-ed. + *

    + * The reason for this more complicated procedure instead of just returning the data of the firstChild is that + * when the text is Chinese characters then on Android each Characater is represented in the DOM as + * an individual Text node. + * + * @param parentElement parentElement + * @return String value + */ + public static String getTextChildrenContent(Element parentElement) { + if (parentElement == null) { + return null; + } + StringBuilder result = new StringBuilder(); + NodeList childNodes = parentElement.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if ((node == null) || + (node.getNodeType() != Node.TEXT_NODE)) { + continue; + } + result.append(((Text) node).getData()); + } + return result.toString().trim(); + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java new file mode 100644 index 000000000..dd741cc4c --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java @@ -0,0 +1,139 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import me.ag2s.epublib.Constants; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URL; +import java.util.Objects; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +/** + * Various low-level support methods for reading/writing epubs. + * + * @author paul.siegmann + */ +public class EpubProcessorSupport { + + private static final String TAG = EpubProcessorSupport.class.getName(); + + protected static DocumentBuilderFactory documentBuilderFactory; + + static { + init(); + } + + static class EntityResolverImpl implements EntityResolver { + + private String previousLocation; + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws IOException { + String resourcePath; + if (systemId.startsWith("http:")) { + URL url = new URL(systemId); + resourcePath = "dtd/" + url.getHost() + url.getPath(); + previousLocation = resourcePath + .substring(0, resourcePath.lastIndexOf('/')); + } else { + resourcePath = + previousLocation + systemId.substring(systemId.lastIndexOf('/')); + } + + if (Objects.requireNonNull(this.getClass().getClassLoader()).getResource(resourcePath) == null) { + throw new RuntimeException( + "remote resource is not cached : [" + systemId + + "] cannot continue"); + } + + InputStream in = Objects.requireNonNull(EpubProcessorSupport.class.getClassLoader()) + .getResourceAsStream(resourcePath); + return new InputSource(in); + } + } + + + private static void init() { + EpubProcessorSupport.documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilderFactory.setValidating(false); + } + + public static XmlSerializer createXmlSerializer(OutputStream out) + throws UnsupportedEncodingException { + return createXmlSerializer( + new OutputStreamWriter(out, Constants.CHARACTER_ENCODING)); + } + + public static XmlSerializer createXmlSerializer(Writer out) { + XmlSerializer result = null; + try { + /* + * Disable XmlPullParserFactory here before it doesn't work when + * building native image using GraalVM + */ + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setValidating(true); + result = factory.newSerializer(); + + //result = new KXmlSerializer(); + result.setFeature( + "http://xmlpull.org/v1/doc/features.html#indent-output", true); + result.setOutput(out); + } catch (Exception e) { + Log.e(TAG, + "When creating XmlSerializer: " + e.getClass().getName() + ": " + e + .getMessage()); + } + return result; + } + + /** + * Gets an EntityResolver that loads dtd's and such from the epub4j classpath. + * In order to enable the loading of relative urls the given EntityResolver contains the previousLocation. + * Because of a new EntityResolver is created every time this method is called. + * Fortunately the EntityResolver created uses up very little memory per instance. + * + * @return an EntityResolver that loads dtd's and such from the epub4j classpath. + */ + public static EntityResolver getEntityResolver() { + return new EntityResolverImpl(); + } + + @SuppressWarnings("unused") + public DocumentBuilderFactory getDocumentBuilderFactory() { + return documentBuilderFactory; + } + + /** + * Creates a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath. + * + * @return a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath. + */ + public static DocumentBuilder createDocumentBuilder() { + DocumentBuilder result = null; + try { + result = documentBuilderFactory.newDocumentBuilder(); + result.setEntityResolver(getEntityResolver()); + } catch (ParserConfigurationException e) { + Log.e(TAG, e.getMessage()); + } + return result; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java new file mode 100644 index 000000000..78fd6ab94 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubReader.java @@ -0,0 +1,173 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Resources; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +/** + * Reads an epub file. + * + * @author paul + */ +@SuppressWarnings("ALL") +public class EpubReader { + + private static final String TAG = EpubReader.class.getName(); + private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR; + + public EpubBook readEpub(InputStream in) throws IOException { + return readEpub(in, Constants.CHARACTER_ENCODING); + } + + public EpubBook readEpub(ZipInputStream in) throws IOException { + return readEpub(in, Constants.CHARACTER_ENCODING); + } + + public EpubBook readEpub(ZipFile zipfile) throws IOException { + return readEpub(zipfile, Constants.CHARACTER_ENCODING); + } + + /** + * Read epub from inputstream + * + * @param in the inputstream from which to read the epub + * @param encoding the encoding to use for the html files within the epub + * @return the Book as read from the inputstream + * @throws IOException IOException + */ + public EpubBook readEpub(InputStream in, String encoding) throws IOException { + return readEpub(new ZipInputStream(in), encoding); + } + + + /** + * Reads this EPUB without loading any resources into memory. + * + * @param zipFile the file to load + * @param encoding the encoding for XHTML files + * @return this Book without loading all resources into memory. + * @throws IOException IOException + */ + public EpubBook readEpubLazy(ZipFile zipFile, String encoding) + throws IOException { + return readEpubLazy(zipFile, encoding, + Arrays.asList(MediaTypes.mediaTypes)); + } + + public EpubBook readEpub(ZipInputStream in, String encoding) throws IOException { + return readEpub(ResourcesLoader.loadResources(in, encoding)); + } + + public EpubBook readEpub(ZipFile in, String encoding) throws IOException { + return readEpub(ResourcesLoader.loadResources(in, encoding)); + } + + /** + * Reads this EPUB without loading all resources into memory. + * + * @param zipFile the file to load + * @param encoding the encoding for XHTML files + * @param lazyLoadedTypes a list of the MediaType to load lazily + * @return this Book without loading all resources into memory. + * @throws IOException IOException + */ + public EpubBook readEpubLazy(ZipFile zipFile, String encoding, + List lazyLoadedTypes) throws IOException { + Resources resources = ResourcesLoader + .loadResources(zipFile, encoding, lazyLoadedTypes); + return readEpub(resources); + } + + public EpubBook readEpub(Resources resources) { + return readEpub(resources, new EpubBook()); + } + + public EpubBook readEpub(Resources resources, EpubBook result) { + if (result == null) { + result = new EpubBook(); + } + handleMimeType(result, resources); + String packageResourceHref = getPackageResourceHref(resources); + Resource packageResource = processPackageResource(packageResourceHref, + result, resources); + result.setOpfResource(packageResource); + Resource ncxResource = processNcxResource(packageResource, result); + result.setNcxResource(ncxResource); + result = postProcessBook(result); + return result; + } + + + private EpubBook postProcessBook(EpubBook book) { + if (bookProcessor != null) { + book = bookProcessor.processBook(book); + } + return book; + } + + private Resource processNcxResource(Resource packageResource, EpubBook book) { + Log.d(TAG, "OPF:getHref()" + packageResource.getHref()); + if (book.isEpub3()) { + return NCXDocumentV3.read(book, this); + } else { + return NCXDocumentV2.read(book, this); + } + + } + + private Resource processPackageResource(String packageResourceHref, EpubBook book, + Resources resources) { + Resource packageResource = resources.remove(packageResourceHref); + try { + PackageDocumentReader.read(packageResource, this, book, resources); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + return packageResource; + } + + private String getPackageResourceHref(Resources resources) { + String defaultResult = "OEBPS/content.opf"; + String result = defaultResult; + + Resource containerResource = resources.remove("META-INF/container.xml"); + if (containerResource == null) { + return result; + } + try { + Document document = ResourceUtil.getAsDocument(containerResource); + Element rootFileElement = (Element) ((Element) document + .getDocumentElement().getElementsByTagName("rootfiles").item(0)) + .getElementsByTagName("rootfile").item(0); + result = rootFileElement.getAttribute("full-path"); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + if (StringUtil.isBlank(result)) { + result = defaultResult; + } + return result; + } + + private void handleMimeType(EpubBook result, Resources resources) { + resources.remove("mimetype"); + //result.setResources(resources); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java new file mode 100644 index 000000000..c421d5a9f --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java @@ -0,0 +1,190 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.util.IOUtil; + +/** + * Generates an epub file. Not thread-safe, single use object. + * + * @author paul + */ +public class EpubWriter { + + private static final String TAG= EpubWriter.class.getName(); + + // package + static final String EMPTY_NAMESPACE_PREFIX = ""; + + private BookProcessor bookProcessor; + + public EpubWriter() { + this(BookProcessor.IDENTITY_BOOKPROCESSOR); + } + + + public EpubWriter(BookProcessor bookProcessor) { + this.bookProcessor = bookProcessor; + } + + + public void write(EpubBook book, OutputStream out) throws IOException { + book = processBook(book); + ZipOutputStream resultStream = new ZipOutputStream(out); + writeMimeType(resultStream); + writeContainer(resultStream); + initTOCResource(book); + writeResources(book, resultStream); + writePackageDocument(book, resultStream); + resultStream.close(); + } + + private EpubBook processBook(EpubBook book) { + if (bookProcessor != null) { + book = bookProcessor.processBook(book); + } + return book; + } + + private void initTOCResource(EpubBook book) { + Resource tocResource; + try { + if (book.isEpub3()) { + tocResource = NCXDocumentV3.createNCXResource(book); + } else { + tocResource = NCXDocumentV2.createNCXResource(book); + } + + Resource currentTocResource = book.getSpine().getTocResource(); + if (currentTocResource != null) { + book.getResources().remove(currentTocResource.getHref()); + } + book.getSpine().setTocResource(tocResource); + book.getResources().add(tocResource); + } catch (Exception ex) { + Log.e(TAG, + "Error writing table of contents: " + + ex.getClass().getName() + ": " + ex.getMessage(), ex); + } + } + + + private void writeResources(EpubBook book, ZipOutputStream resultStream) { + for (Resource resource : book.getResources().getAll()) { + writeResource(resource, resultStream); + } + } + + /** + * Writes the resource to the resultStream. + * + * @param resource resource + * @param resultStream resultStream + */ + private void writeResource(Resource resource, ZipOutputStream resultStream) { + if (resource == null) { + return; + } + try { + resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref())); + InputStream inputStream = resource.getInputStream(); + + IOUtil.copy(inputStream, resultStream); + inputStream.close(); + } catch (Exception e) { + Log.e(TAG,e.getMessage(), e); + } + } + + + private void writePackageDocument(EpubBook book, ZipOutputStream resultStream) + throws IOException { + resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf")); + XmlSerializer xmlSerializer = EpubProcessorSupport + .createXmlSerializer(resultStream); + PackageDocumentWriter.write(this, xmlSerializer, book); + xmlSerializer.flush(); +// String resultAsString = result.toString(); +// resultStream.write(resultAsString.getBytes(Constants.ENCODING)); + } + + /** + * Writes the META-INF/container.xml file. + * + * @param resultStream resultStream + * @throws IOException IOException + */ + private void writeContainer(ZipOutputStream resultStream) throws IOException { + resultStream.putNextEntry(new ZipEntry("META-INF/container.xml")); + Writer out = new OutputStreamWriter(resultStream); + out.write("\n"); + out.write( + "\n"); + out.write("\t\n"); + out.write( + "\t\t\n"); + out.write("\t\n"); + out.write(""); + out.flush(); + } + + /** + * Stores the mimetype as an uncompressed file in the ZipOutputStream. + * + * @param resultStream resultStream + * @throws IOException IOException + */ + private void writeMimeType(ZipOutputStream resultStream) throws IOException { + ZipEntry mimetypeZipEntry = new ZipEntry("mimetype"); + mimetypeZipEntry.setMethod(ZipEntry.STORED); + byte[] mimetypeBytes = MediaTypes.EPUB.getName().getBytes(); + mimetypeZipEntry.setSize(mimetypeBytes.length); + mimetypeZipEntry.setCrc(calculateCrc(mimetypeBytes)); + resultStream.putNextEntry(mimetypeZipEntry); + resultStream.write(mimetypeBytes); + } + + private long calculateCrc(byte[] data) { + CRC32 crc = new CRC32(); + crc.update(data); + return crc.getValue(); + } + + String getNcxId() { + return "ncx"; + } + + String getNcxHref() { + return "toc.ncx"; + } + + String getNcxMediaType() { + return MediaTypes.NCX.getName(); + } + + + @SuppressWarnings("unused") + public BookProcessor getBookProcessor() { + return bookProcessor; + } + + @SuppressWarnings("unused") + public void setBookProcessor(BookProcessor bookProcessor) { + this.bookProcessor = bookProcessor; + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java b/epublib/src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java new file mode 100644 index 000000000..7780f6b47 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java @@ -0,0 +1,9 @@ +package me.ag2s.epublib.epub; + +import me.ag2s.epublib.domain.Resource; +import java.io.OutputStream; +@SuppressWarnings("unused") +public interface HtmlProcessor { + + void processHtmlResource(Resource resource, OutputStream out); +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java new file mode 100644 index 000000000..2bf47de51 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java @@ -0,0 +1,343 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.Author; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Identifier; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.TOCReference; +import me.ag2s.epublib.domain.TableOfContents; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +/** + * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/ + * + * @author paul + */ +public class NCXDocumentV2 { + + public static final String NAMESPACE_NCX = "http://www.daisy.org/z3986/2005/ncx/"; + @SuppressWarnings("unused") + public static final String PREFIX_NCX = "ncx"; + public static final String NCX_ITEM_ID = "ncx"; + public static final String DEFAULT_NCX_HREF = "toc.ncx"; + public static final String PREFIX_DTB = "dtb"; + + private static final String TAG = NCXDocumentV2.class.getName(); + + private interface NCXTags { + + String ncx = "ncx"; + String meta = "meta"; + String navPoint = "navPoint"; + String navMap = "navMap"; + String navLabel = "navLabel"; + String content = "content"; + String text = "text"; + String docTitle = "docTitle"; + String docAuthor = "docAuthor"; + String head = "head"; + } + + private interface NCXAttributes { + + String src = "src"; + String name = "name"; + String content = "content"; + String id = "id"; + String playOrder = "playOrder"; + String clazz = "class"; + String version = "version"; + } + + private interface NCXAttributeValues { + + String chapter = "chapter"; + String version = "2005-1"; + + } + + @SuppressWarnings("unused") + public static Resource read(EpubBook book, EpubReader epubReader) { + Resource ncxResource = null; + if (book.getSpine().getTocResource() == null) { + Log.e(TAG, "Book does not contain a table of contents file"); + return null; + } + try { + ncxResource = book.getSpine().getTocResource(); + if (ncxResource == null) { + return null; + } + Log.d(TAG, ncxResource.getHref()); + Document ncxDocument = ResourceUtil.getAsDocument(ncxResource); + Element navMapElement = DOMUtil + .getFirstElementByTagNameNS(ncxDocument.getDocumentElement(), + NAMESPACE_NCX, NCXTags.navMap); + if (navMapElement == null) { + return null; + } + + TableOfContents tableOfContents = new TableOfContents( + readTOCReferences(navMapElement.getChildNodes(), book)); + book.setTableOfContents(tableOfContents); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + return ncxResource; + } + + static List readTOCReferences(NodeList navpoints, + EpubBook book) { + if (navpoints == null) { + return new ArrayList<>(); + } + List result = new ArrayList<>( + navpoints.getLength()); + for (int i = 0; i < navpoints.getLength(); i++) { + Node node = navpoints.item(i); + if (node.getNodeType() != Document.ELEMENT_NODE) { + continue; + } + if (!(node.getLocalName().equals(NCXTags.navPoint))) { + continue; + } + TOCReference tocReference = readTOCReference((Element) node, book); + result.add(tocReference); + } + return result; + } + + static TOCReference readTOCReference(Element navpointElement, EpubBook book) { + String label = readNavLabel(navpointElement); + //Log.d(TAG,"label:"+label); + String tocResourceRoot = StringUtil + .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); + if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref() + .length()) { + tocResourceRoot = ""; + } else { + tocResourceRoot = tocResourceRoot + "/"; + } + String reference = StringUtil + .collapsePathDots(tocResourceRoot + readNavReference(navpointElement)); + String href = StringUtil + .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR); + String fragmentId = StringUtil + .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR); + Resource resource = book.getResources().getByHref(href); + if (resource == null) { + Log.e(TAG, "Resource with href " + href + " in NCX document not found"); + } + Log.v(TAG, "label:" + label); + Log.v(TAG, "href:" + href); + Log.v(TAG, "fragmentId:" + fragmentId); + TOCReference result = new TOCReference(label, resource, fragmentId); + List childTOCReferences = readTOCReferences( + navpointElement.getChildNodes(), book); + result.setChildren(childTOCReferences); + return result; + } + + private static String readNavReference(Element navpointElement) { + Element contentElement = DOMUtil + .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX, + NCXTags.content); + if (contentElement == null) { + return null; + } + String result = DOMUtil + .getAttribute(contentElement, NAMESPACE_NCX, NCXAttributes.src); + try { + result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + } + return result; + } + + private static String readNavLabel(Element navpointElement) { + //Log.d(TAG,navpointElement.getTagName()); + Element navLabel = DOMUtil + .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX, + NCXTags.navLabel); + assert navLabel != null; + return DOMUtil.getTextChildrenContent(DOMUtil + .getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text)); + } + + @SuppressWarnings("unused") + public static void write(EpubWriter epubWriter, EpubBook book, + ZipOutputStream resultStream) throws IOException { + resultStream + .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref())); + XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream); + write(out, book); + out.flush(); + } + + + /** + * Generates a resource containing an xml document containing the table of contents of the book in ncx format. + * + * @param xmlSerializer the serializer used + * @param book the book to serialize + * @throws IOException IOException + * @throws IllegalStateException IllegalStateException + * @throws IllegalArgumentException IllegalArgumentException + */ + public static void write(XmlSerializer xmlSerializer, EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { + write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), + book.getMetadata().getAuthors(), book.getTableOfContents()); + } + + public static Resource createNCXResource(EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { + return createNCXResource(book.getMetadata().getIdentifiers(), + book.getTitle(), book.getMetadata().getAuthors(), + book.getTableOfContents()); + } + + public static Resource createNCXResource(List identifiers, + String title, List authors, TableOfContents tableOfContents) + throws IllegalArgumentException, IllegalStateException, IOException { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data); + write(out, identifiers, title, authors, tableOfContents); + return new Resource(NCX_ITEM_ID, data.toByteArray(), + DEFAULT_NCX_HREF, MediaTypes.NCX); + } + + public static void write(XmlSerializer serializer, + List identifiers, String title, List authors, + TableOfContents tableOfContents) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startDocument(Constants.CHARACTER_ENCODING, false); + serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_NCX); + serializer.startTag(NAMESPACE_NCX, NCXTags.ncx); +// serializer.writeNamespace("ncx", NAMESPACE_NCX); +// serializer.attribute("xmlns", NAMESPACE_NCX); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.version, + NCXAttributeValues.version); + serializer.startTag(NAMESPACE_NCX, NCXTags.head); + + for (Identifier identifier : identifiers) { + writeMetaElement(identifier.getScheme(), identifier.getValue(), + serializer); + } + + writeMetaElement("generator", Constants.EPUB_GENERATOR_NAME, serializer); + writeMetaElement("depth", String.valueOf(tableOfContents.calculateDepth()), + serializer); + writeMetaElement("totalPageCount", "0", serializer); + writeMetaElement("maxPageNumber", "0", serializer); + + serializer.endTag(NAMESPACE_NCX, "head"); + + serializer.startTag(NAMESPACE_NCX, NCXTags.docTitle); + serializer.startTag(NAMESPACE_NCX, NCXTags.text); + // write the first title + serializer.text(StringUtil.defaultIfNull(title)); + serializer.endTag(NAMESPACE_NCX, NCXTags.text); + serializer.endTag(NAMESPACE_NCX, NCXTags.docTitle); + + for (Author author : authors) { + serializer.startTag(NAMESPACE_NCX, NCXTags.docAuthor); + serializer.startTag(NAMESPACE_NCX, NCXTags.text); + serializer.text(author.getLastname() + ", " + author.getFirstname()); + serializer.endTag(NAMESPACE_NCX, NCXTags.text); + serializer.endTag(NAMESPACE_NCX, NCXTags.docAuthor); + } + + serializer.startTag(NAMESPACE_NCX, NCXTags.navMap); + writeNavPoints(tableOfContents.getTocReferences(), 1, serializer); + serializer.endTag(NAMESPACE_NCX, NCXTags.navMap); + + serializer.endTag(NAMESPACE_NCX, "ncx"); + serializer.endDocument(); + } + + + private static void writeMetaElement(String dtbName, String content, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_NCX, NCXTags.meta); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.name, + PREFIX_DTB + ":" + dtbName); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.content, + content); + serializer.endTag(NAMESPACE_NCX, NCXTags.meta); + } + + private static int writeNavPoints(List tocReferences, + int playOrder, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + for (TOCReference tocReference : tocReferences) { + if (tocReference.getResource() == null) { + playOrder = writeNavPoints(tocReference.getChildren(), playOrder, + serializer); + continue; + } + writeNavPointStart(tocReference, playOrder, serializer); + playOrder++; + if (!tocReference.getChildren().isEmpty()) { + playOrder = writeNavPoints(tocReference.getChildren(), playOrder, + serializer); + } + writeNavPointEnd(tocReference, serializer); + } + return playOrder; + } + + + private static void writeNavPointStart(TOCReference tocReference, + int playOrder, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_NCX, NCXTags.navPoint); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.id, + "navPoint-" + playOrder); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.playOrder, + String.valueOf(playOrder)); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.clazz, + NCXAttributeValues.chapter); + serializer.startTag(NAMESPACE_NCX, NCXTags.navLabel); + serializer.startTag(NAMESPACE_NCX, NCXTags.text); + serializer.text(tocReference.getTitle()); + serializer.endTag(NAMESPACE_NCX, NCXTags.text); + serializer.endTag(NAMESPACE_NCX, NCXTags.navLabel); + serializer.startTag(NAMESPACE_NCX, NCXTags.content); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.src, + tocReference.getCompleteHref()); + serializer.endTag(NAMESPACE_NCX, NCXTags.content); + } + @SuppressWarnings("unused") + private static void writeNavPointEnd(TOCReference tocReference, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.endTag(NAMESPACE_NCX, NCXTags.navPoint); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java new file mode 100644 index 000000000..845b999d1 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java @@ -0,0 +1,461 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xmlpull.v1.XmlSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.Author; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Identifier; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.TOCReference; +import me.ag2s.epublib.domain.TableOfContents; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +/** + * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/ + * + * @author Ag2S20150909 + */ + +public class NCXDocumentV3 { + public static final String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml"; + public static final String NAMESPACE_EPUB = "http://www.idpf.org/2007/ops"; + public static final String LANGUAGE = "en"; + @SuppressWarnings("unused") + public static final String PREFIX_XHTML = "html"; + public static final String NCX_ITEM_ID = "htmltoc"; + public static final String DEFAULT_NCX_HREF = "toc.xhtml"; + public static final String V3_NCX_PROPERTIES = "nav"; + public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML; + + private static final String TAG = NCXDocumentV3.class.getName(); + + private interface XHTMLTgs { + String html = "html"; + String head = "head"; + String title = "title"; + String meta = "meta"; + String link = "link"; + String body = "body"; + String h1 = "h1"; + String h2 = "h2"; + String nav = "nav"; + String ol = "ol"; + String li = "li"; + String a = "a"; + String span = "span"; + } + + private interface XHTMLAttributes { + String xmlns = "xmlns"; + String xmlns_epub = "xmlns:epub"; + String lang = "lang"; + String xml_lang = "xml:lang"; + String rel = "rel"; + String type = "type"; + String epub_type = "epub:type";//nav的必须属性 + String id = "id"; + String role = "role"; + String href = "href"; + String http_equiv = "http-equiv"; + String content = "content"; + + } + + private interface XHTMLAttributeValues { + String Content_Type = "Content-Type"; + String HTML_UTF8 = "text/html; charset=utf-8"; + String lang = "en"; + String epub_type = "toc"; + String role_toc = "doc-toc"; + + } + + + /** + * 解析epub的目录文件 + * + * @param book Book + * @param epubReader epubreader + * @return Resource + */ + @SuppressWarnings("unused") + public static Resource read(EpubBook book, EpubReader epubReader) { + Resource ncxResource = null; + if (book.getSpine().getTocResource() == null) { + Log.e(TAG, "Book does not contain a table of contents file"); + return null; + } + try { + ncxResource = book.getSpine().getTocResource(); + if (ncxResource == null) { + return null; + } + //一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件 + if (ncxResource.getHref().endsWith(".ncx")){ + Log.v(TAG,"该epub文件不标准,使用了epub2的目录文件"); + return NCXDocumentV2.read(book, epubReader); + } + Log.d(TAG, ncxResource.getHref()); + + Document ncxDocument = ResourceUtil.getAsDocument(ncxResource); + Log.d(TAG, ncxDocument.getNodeName()); + + Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0); + if(navMapElement==null){ + Log.d(TAG,"epub3目录文件未发现nav节点,尝试使用epub2的规则解析"); + return NCXDocumentV2.read(book, epubReader); + } + navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0); + Log.d(TAG, navMapElement.getTagName()); + + TableOfContents tableOfContents = new TableOfContents( + readTOCReferences(navMapElement.getChildNodes(), book)); + Log.d(TAG, tableOfContents.toString()); + book.setTableOfContents(tableOfContents); + } catch (Exception e) { + Log.e(TAG, e.getMessage(), e); + } + return ncxResource; + } + + private static List doToc(Node n, EpubBook book) { + List result = new ArrayList<>(); + + if (n == null || n.getNodeType() != Document.ELEMENT_NODE) { + return result; + } else { + Element el = (Element) n; + NodeList nodeList = el.getElementsByTagName(XHTMLTgs.li); + for (int i = 0; i < nodeList.getLength(); i++) { + result.add(readTOCReference((Element) nodeList.item(i), book)); + } + } + return result; + } + + + static List readTOCReferences(NodeList navpoints, + EpubBook book) { + if (navpoints == null) { + return new ArrayList<>(); + } + //Log.d(TAG, "readTOCReferences:navpoints.getLength()" + navpoints.getLength()); + List result = new ArrayList<>(navpoints.getLength()); + for (int i = 0; i < navpoints.getLength(); i++) { + Node node = navpoints.item(i); + //如果该node是null,或者不是Element,跳出本次循环 + if (node == null || node.getNodeType() != Document.ELEMENT_NODE) { + continue; + } + + Element el = (Element) node; + //如果该Element的name为”li“,将其添加到目录结果 + if (el.getTagName().equals(XHTMLTgs.li)) { + result.add(readTOCReference(el, book)); + } + + } + + + return result; + } + + + static TOCReference readTOCReference(Element navpointElement, EpubBook book) { + //章节的名称 + String label = readNavLabel(navpointElement); + //Log.d(TAG, "label:" + label); + String tocResourceRoot = StringUtil + .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/'); + if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref() + .length()) { + tocResourceRoot = ""; + } else { + tocResourceRoot = tocResourceRoot + "/"; + } + + String reference = StringUtil + .collapsePathDots(tocResourceRoot + readNavReference(navpointElement)); + String href = StringUtil + .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR); + String fragmentId = StringUtil + .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR); + Resource resource = book.getResources().getByHref(href); + if (resource == null) { + Log.e(TAG, "Resource with href " + href + " in NCX document not found"); + } + Log.v(TAG, "label:" + label); + Log.v(TAG, "href:" + href); + Log.v(TAG, "fragmentId:" + fragmentId); + + //父级目录 + TOCReference result = new TOCReference(label, resource, fragmentId); + //解析子级目录 + List childTOCReferences = doToc(navpointElement, book); + //readTOCReferences( + //navpointElement.getChildNodes(), book); + result.setChildren(childTOCReferences); + return result; + } + + /** + * 获取目录节点的href + * + * @param navpointElement navpointElement + * @return String + */ + private static String readNavReference(Element navpointElement) { + //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav + //父级节点必须是 "li" + //Log.d(TAG, "readNavReference:" + navpointElement.getTagName()); + + Element contentElement = DOMUtil + .getFirstElementByTagNameNS(navpointElement, "", XHTMLTgs.a); + if (contentElement == null) { + return null; + } + String result = DOMUtil + .getAttribute(contentElement, "", XHTMLAttributes.href); + try { + result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + } + + return result; + + } + + /** + * 获取目录节点里面的章节名 + * + * @param navpointElement navpointElement + * @return String + */ + private static String readNavLabel(Element navpointElement) { + //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav + //父级节点必须是 "li" + //Log.d(TAG, "readNavLabel:" + navpointElement.getTagName()); + String label; + Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "a"); + assert labelElement != null; + label = labelElement.getTextContent(); + if (StringUtil.isNotBlank(label)) { + return label; + } else { + labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "span"); + } + assert labelElement != null; + label = labelElement.getTextContent(); + //如果通过 a 标签无法获取章节列表,则是无href章节名 + return label; + + } + + public static Resource createNCXResource(EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { + return createNCXResource(book.getMetadata().getIdentifiers(), + book.getTitle(), book.getMetadata().getAuthors(), + book.getTableOfContents()); + } + + public static Resource createNCXResource(List identifiers, + String title, List authors, TableOfContents tableOfContents) + throws IllegalArgumentException, IllegalStateException, IOException { + ByteArrayOutputStream data = new ByteArrayOutputStream(); + XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data); + write(out, identifiers, title, authors, tableOfContents); + + Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(), + DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE); + resource.setProperties(V3_NCX_PROPERTIES); + return resource; + } + + /** + * Generates a resource containing an xml document containing the table of contents of the book in ncx format. + * + * @param xmlSerializer the serializer used + * @param book the book to serialize + * @throws IOException IOException + * @throws IllegalStateException IllegalStateException + * @throws IllegalArgumentException IllegalArgumentException + */ + public static void write(XmlSerializer xmlSerializer, EpubBook book) + throws IllegalArgumentException, IllegalStateException, IOException { + write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(), + book.getMetadata().getAuthors(), book.getTableOfContents()); + } + + /** + * 写入 + * + * @param serializer serializer + * @param identifiers identifiers + * @param title title + * @param authors authors + * @param tableOfContents tableOfContents + */ + @SuppressWarnings("unused") + public static void write(XmlSerializer serializer, + List identifiers, String title, List authors, + TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startDocument(Constants.CHARACTER_ENCODING, false); + serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML); + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE); + //写入头部head标签 + writeHead(title, serializer); + //body开始 + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body); + //h1开始 + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1); + serializer.text(title); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1); + //h1关闭 + //nav开始 + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav); + serializer.attribute("", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type); + serializer.attribute("", XHTMLAttributes.id, XHTMLAttributeValues.epub_type); + serializer.attribute("", XHTMLAttributes.role, XHTMLAttributeValues.role_toc); + //h2开始 + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2); + serializer.text("目录"); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2); + + + writeNavPoints(tableOfContents.getTocReferences(), 1, serializer); + + + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav); + + //body关闭 + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body); + + + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html); + serializer.endDocument(); + + } + + private static int writeNavPoints(List tocReferences, + int playOrder, + XmlSerializer serializer) throws IOException { + writeOlStart(serializer); + for (TOCReference tocReference : tocReferences) { + if (tocReference.getResource() == null) { + playOrder = writeNavPoints(tocReference.getChildren(), playOrder, + serializer); + continue; + } + + + writeNavPointStart(tocReference, serializer); + + playOrder++; + if (!tocReference.getChildren().isEmpty()) { + playOrder = writeNavPoints(tocReference.getChildren(), playOrder, + serializer); + } + + writeNavPointEnd(tocReference, serializer); + } + writeOlSEnd(serializer); + return playOrder; + } + + private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException { + writeLiStart(serializer); + String title = tocReference.getTitle(); + String href = tocReference.getCompleteHref(); + if (StringUtil.isNotBlank(href)) { + writeLabel(title, href, serializer); + } else { + writeLabel(title, serializer); + } + } + + @SuppressWarnings("unused") + private static void writeNavPointEnd(TOCReference tocReference, + XmlSerializer serializer) throws IOException { + writeLiEnd(serializer); + } + + protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException { + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a); + serializer.attribute("", XHTMLAttributes.href, href); + //attribute必须在Text之前设置。 + serializer.text(title); + //serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a); + } + + protected static void writeLabel(String title, XmlSerializer serializer) throws IOException { + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span); + serializer.text(title); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span); + } + + private static void writeLiStart(XmlSerializer serializer) throws IOException { + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li); + Log.d(TAG, "writeLiStart"); + } + + private static void writeLiEnd(XmlSerializer serializer) throws IOException { + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li); + Log.d(TAG, "writeLiEND"); + } + + private static void writeOlStart(XmlSerializer serializer) throws IOException { + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol); + Log.d(TAG, "writeOlStart"); + } + + private static void writeOlSEnd(XmlSerializer serializer) throws IOException { + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol); + Log.d(TAG, "writeOlEnd"); + } + + private static void writeHead(String title, XmlSerializer serializer) throws IOException { + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head); + //title + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title); + serializer.text(StringUtil.defaultIfNull(title)); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title); + //link + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link); + serializer.attribute("", XHTMLAttributes.rel, "stylesheet"); + serializer.attribute("", XHTMLAttributes.type, "text/css"); + serializer.attribute("", XHTMLAttributes.href, "css/style.css"); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link); + + //meta + serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta); + serializer.attribute("", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type); + serializer.attribute("", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8); + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta); + + serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head); + } + + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java new file mode 100644 index 000000000..a172ce32d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java @@ -0,0 +1,97 @@ +package me.ag2s.epublib.epub; + + +/** + * Functionality shared by the PackageDocumentReader and the PackageDocumentWriter + * + * @author paul + * + */ +public class PackageDocumentBase { + + public static final String BOOK_ID_ID = "duokan-book-id"; + public static final String NAMESPACE_OPF = "http://www.idpf.org/2007/opf"; + public static final String NAMESPACE_DUBLIN_CORE = "http://purl.org/dc/elements/1.1/"; + public static final String PREFIX_DUBLIN_CORE = "dc"; + //public static final String PREFIX_OPF = "opf"; + //在EPUB3标准中,packge前面没有opf头,一些epub阅读器也不支持opf头。 + //Some Epub Reader not reconize op:packge,So just let it empty; + public static final String PREFIX_OPF = ""; + //添加 version 变量来区分Epub文件的版本 + //Add the version field to distinguish the version of EPUB file + public static final String version="version"; + public static final String dateFormat = "yyyy-MM-dd"; + + protected interface DCTags { + + String title = "title"; + String creator = "creator"; + String subject = "subject"; + String description = "description"; + String publisher = "publisher"; + String contributor = "contributor"; + String date = "date"; + String type = "type"; + String format = "format"; + String identifier = "identifier"; + String source = "source"; + String language = "language"; + String relation = "relation"; + String coverage = "coverage"; + String rights = "rights"; + } + + protected interface DCAttributes { + + String scheme = "scheme"; + String id = "id"; + } + + protected interface OPFTags { + + String metadata = "metadata"; + String meta = "meta"; + String manifest = "manifest"; + String packageTag = "package"; + String itemref = "itemref"; + String spine = "spine"; + String reference = "reference"; + String guide = "guide"; + String item = "item"; + } + + protected interface OPFAttributes { + + String uniqueIdentifier = "unique-identifier"; + String idref = "idref"; + String name = "name"; + String content = "content"; + String type = "type"; + String href = "href"; + String linear = "linear"; + String event = "event"; + String role = "role"; + String file_as = "file-as"; + String id = "id"; + String media_type = "media-type"; + String title = "title"; + String toc = "toc"; + String version = "version"; + String scheme = "scheme"; + String property = "property"; + //add for epub3 + /** + * add for epub3 + */ + String properties="properties"; + } + + protected interface OPFValues { + + String meta_cover = "cover"; + String reference_cover = "cover"; + String no = "no"; + String generator = "generator"; + String duokan = "duokan-body-font"; + } +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java new file mode 100644 index 000000000..86f3f95c0 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java @@ -0,0 +1,225 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +import me.ag2s.epublib.domain.Author; +import me.ag2s.epublib.domain.Date; +import me.ag2s.epublib.domain.Identifier; +import me.ag2s.epublib.domain.Metadata; +import me.ag2s.epublib.util.StringUtil; + +/** + * Reads the package document metadata. + *

    + * In its own separate class because the PackageDocumentReader became a bit large and unwieldy. + * + * @author paul + */ +// package +class PackageDocumentMetadataReader extends PackageDocumentBase { + + private static final String TAG = PackageDocumentMetadataReader.class.getName(); + + public static Metadata readMetadata(Document packageDocument) { + Metadata result = new Metadata(); + Element metadataElement = DOMUtil + .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), + NAMESPACE_OPF, OPFTags.metadata); + if (metadataElement == null) { + Log.e(TAG, "Package does not contain element " + OPFTags.metadata); + return result; + } + result.setTitles(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.title)); + result.setPublishers(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.publisher)); + result.setDescriptions(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.description)); + result.setRights(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.rights)); + result.setTypes(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.type)); + result.setSubjects(DOMUtil + .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.subject)); + result.setIdentifiers(readIdentifiers(metadataElement)); + result.setAuthors(readCreators(metadataElement)); + result.setContributors(readContributors(metadataElement)); + result.setDates(readDates(metadataElement)); + result.setOtherProperties(readOtherProperties(metadataElement)); + result.setMetaAttributes(readMetaProperties(metadataElement)); + Element languageTag = DOMUtil + .getFirstElementByTagNameNS(metadataElement, NAMESPACE_DUBLIN_CORE, + DCTags.language); + if (languageTag != null) { + result.setLanguage(DOMUtil.getTextChildrenContent(languageTag)); + } + + return result; + } + + /** + * consumes meta tags that have a property attribute as defined in the standard. For example: + * <meta property="rendition:layout">pre-paginated</meta> + * + * @param metadataElement metadataElement + * @return Map + */ + private static Map readOtherProperties( + Element metadataElement) { + Map result = new HashMap<>(); + + NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta); + for (int i = 0; i < metaTags.getLength(); i++) { + Node metaNode = metaTags.item(i); + Node property = metaNode.getAttributes() + .getNamedItem(OPFAttributes.property); + if (property != null) { + String name = property.getNodeValue(); + String value = metaNode.getTextContent(); + result.put(new QName(name), value); + } + } + + return result; + } + + /** + * consumes meta tags that have a property attribute as defined in the standard. For example: + * <meta property="rendition:layout">pre-paginated</meta> + * + * @param metadataElement metadataElement + * @return Map + */ + private static Map readMetaProperties( + Element metadataElement) { + Map result = new HashMap<>(); + + NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta); + for (int i = 0; i < metaTags.getLength(); i++) { + Element metaElement = (Element) metaTags.item(i); + String name = metaElement.getAttribute(OPFAttributes.name); + String value = metaElement.getAttribute(OPFAttributes.content); + result.put(name, value); + } + + return result; + } + + private static String getBookIdId(Document document) { + Element packageElement = DOMUtil + .getFirstElementByTagNameNS(document.getDocumentElement(), + NAMESPACE_OPF, OPFTags.packageTag); + if (packageElement == null) { + return null; + } + return DOMUtil.getAttribute(packageElement, NAMESPACE_OPF, OPFAttributes.uniqueIdentifier); + + } + + private static List readCreators(Element metadataElement) { + return readAuthors(DCTags.creator, metadataElement); + } + + private static List readContributors(Element metadataElement) { + return readAuthors(DCTags.contributor, metadataElement); + } + + private static List readAuthors(String authorTag, + Element metadataElement) { + NodeList elements = metadataElement + .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, authorTag); + List result = new ArrayList<>(elements.getLength()); + for (int i = 0; i < elements.getLength(); i++) { + Element authorElement = (Element) elements.item(i); + Author author = createAuthor(authorElement); + if (author != null) { + result.add(author); + } + } + return result; + + } + + private static List readDates(Element metadataElement) { + NodeList elements = metadataElement + .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.date); + List result = new ArrayList<>(elements.getLength()); + for (int i = 0; i < elements.getLength(); i++) { + Element dateElement = (Element) elements.item(i); + Date date; + try { + date = new Date(DOMUtil.getTextChildrenContent(dateElement), + DOMUtil.getAttribute(dateElement, NAMESPACE_OPF, OPFAttributes.event)); + result.add(date); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.getMessage()); + } + } + return result; + + } + + private static Author createAuthor(Element authorElement) { + String authorString = DOMUtil.getTextChildrenContent(authorElement); + if (StringUtil.isBlank(authorString)) { + return null; + } + int spacePos = authorString.lastIndexOf(' '); + Author result; + if (spacePos < 0) { + result = new Author(authorString); + } else { + result = new Author(authorString.substring(0, spacePos), + authorString.substring(spacePos + 1)); + } + result.setRole( + DOMUtil.getAttribute(authorElement, NAMESPACE_OPF, OPFAttributes.role)); + return result; + } + + + private static List readIdentifiers(Element metadataElement) { + NodeList identifierElements = metadataElement + .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.identifier); + if (identifierElements.getLength() == 0) { + Log.e(TAG, "Package does not contain element " + DCTags.identifier); + return new ArrayList<>(); + } + String bookIdId = getBookIdId(metadataElement.getOwnerDocument()); + List result = new ArrayList<>( + identifierElements.getLength()); + for (int i = 0; i < identifierElements.getLength(); i++) { + Element identifierElement = (Element) identifierElements.item(i); + String schemeName = DOMUtil.getAttribute(identifierElement, NAMESPACE_OPF, DCAttributes.scheme); + String identifierValue = DOMUtil + .getTextChildrenContent(identifierElement); + if (StringUtil.isBlank(identifierValue)) { + continue; + } + Identifier identifier = new Identifier(schemeName, identifierValue); + if (identifierElement.getAttribute("id").equals(bookIdId)) { + identifier.setBookId(true); + } + result.add(identifier); + } + return result; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java new file mode 100644 index 000000000..fc758bc03 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java @@ -0,0 +1,187 @@ +package me.ag2s.epublib.epub; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.Author; +import me.ag2s.epublib.domain.Date; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Identifier; +import me.ag2s.epublib.util.StringUtil; + +public class PackageDocumentMetadataWriter extends PackageDocumentBase { + + /** + * Writes the book's metadata. + * + * @param book book + * @param serializer serializer + * @throws IOException IOException + * @throws IllegalStateException IllegalStateException + * @throws IllegalArgumentException IllegalArgumentException + */ + public static void writeMetaData(EpubBook book, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_OPF, OPFTags.metadata); + serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); + serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); + + writeIdentifiers(book.getMetadata().getIdentifiers(), serializer); + writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(), + serializer); + writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(), + serializer); + writeSimpleMetdataElements(DCTags.description, + book.getMetadata().getDescriptions(), serializer); + writeSimpleMetdataElements(DCTags.publisher, + book.getMetadata().getPublishers(), serializer); + writeSimpleMetdataElements(DCTags.type, book.getMetadata().getTypes(), + serializer); + writeSimpleMetdataElements(DCTags.rights, book.getMetadata().getRights(), + serializer); + + // write authors + for (Author author : book.getMetadata().getAuthors()) { + serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.creator); + serializer.attribute(NAMESPACE_OPF, OPFAttributes.role, + author.getRelator().getCode()); + serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as, + author.getLastname() + ", " + author.getFirstname()); + serializer.text(author.getFirstname() + " " + author.getLastname()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.creator); + } + + // write contributors + for (Author author : book.getMetadata().getContributors()) { + serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor); + serializer.attribute(NAMESPACE_OPF, OPFAttributes.role, + author.getRelator().getCode()); + serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as, + author.getLastname() + ", " + author.getFirstname()); + serializer.text(author.getFirstname() + " " + author.getLastname()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor); + } + + // write dates + for (Date date : book.getMetadata().getDates()) { + serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.date); + if (date.getEvent() != null) { + serializer.attribute(NAMESPACE_OPF, OPFAttributes.event, + date.getEvent().toString()); + } + serializer.text(date.getValue()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.date); + } + + // write language + if (StringUtil.isNotBlank(book.getMetadata().getLanguage())) { + serializer.startTag(NAMESPACE_DUBLIN_CORE, "language"); + serializer.text(book.getMetadata().getLanguage()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, "language"); + } + + // write other properties + if (book.getMetadata().getOtherProperties() != null) { + for (Map.Entry mapEntry : book.getMetadata() + .getOtherProperties().entrySet()) { + serializer.startTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, + OPFAttributes.property, mapEntry.getKey().getLocalPart()); + serializer.text(mapEntry.getValue()); + serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta); + + } + } + + // write coverimage + if (book.getCoverImage() != null) { // write the cover image + serializer.startTag(NAMESPACE_OPF, OPFTags.meta); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, + OPFValues.meta_cover); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, + book.getCoverImage().getId()); + serializer.endTag(NAMESPACE_OPF, OPFTags.meta); + } + + // write generator + serializer.startTag(NAMESPACE_OPF, OPFTags.meta); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, + OPFValues.generator); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, + Constants.EPUB_GENERATOR_NAME); + serializer.endTag(NAMESPACE_OPF, OPFTags.meta); + + // write duokan + serializer.startTag(NAMESPACE_OPF, OPFTags.meta); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name, + OPFValues.duokan); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content, + Constants.EPUB_DUOKAN_NAME); + serializer.endTag(NAMESPACE_OPF, OPFTags.meta); + + serializer.endTag(NAMESPACE_OPF, OPFTags.metadata); + } + + private static void writeSimpleMetdataElements(String tagName, + List values, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + for (String value : values) { + if (StringUtil.isBlank(value)) { + continue; + } + serializer.startTag(NAMESPACE_DUBLIN_CORE, tagName); + serializer.text(value); + serializer.endTag(NAMESPACE_DUBLIN_CORE, tagName); + } + } + + + /** + * Writes out the complete list of Identifiers to the package document. + * The first identifier for which the bookId is true is made the bookId identifier. + * If no identifier has bookId == true then the first bookId identifier is written as the primary. + * + * @param identifiers identifiers + * @param serializer serializer + * @throws IllegalStateException e + * @throws IllegalArgumentException e + * @ + */ + private static void writeIdentifiers(List identifiers, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + Identifier bookIdIdentifier = Identifier.getBookIdIdentifier(identifiers); + if (bookIdIdentifier == null) { + return; + } + + serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, DCAttributes.id, + BOOK_ID_ID); + serializer.attribute(NAMESPACE_OPF, OPFAttributes.scheme, + bookIdIdentifier.getScheme()); + serializer.text(bookIdIdentifier.getValue()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); + + for (Identifier identifier : identifiers.subList(1, identifiers.size())) { + if (identifier == bookIdIdentifier) { + continue; + } + serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); + serializer.attribute(NAMESPACE_OPF, "scheme", identifier.getScheme()); + serializer.text(identifier.getValue()); + serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier); + } + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java new file mode 100644 index 000000000..eaf0c4687 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java @@ -0,0 +1,431 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Guide; +import me.ag2s.epublib.domain.GuideReference; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Resources; +import me.ag2s.epublib.domain.Spine; +import me.ag2s.epublib.domain.SpineReference; +import me.ag2s.epublib.util.ResourceUtil; +import me.ag2s.epublib.util.StringUtil; + +/** + * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf + * + * @author paul + */ +public class PackageDocumentReader extends PackageDocumentBase { + + private static final String TAG = PackageDocumentReader.class.getName(); + private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[]{"toc", + "ncx", "ncxtoc", "htmltoc"}; + + + public static void read( + Resource packageResource, EpubReader epubReader, EpubBook book, + Resources resources) + throws SAXException, IOException { + Document packageDocument = ResourceUtil.getAsDocument(packageResource); + String packageHref = packageResource.getHref(); + resources = fixHrefs(packageHref, resources); + readGuide(packageDocument, epubReader, book, resources); + + // Books sometimes use non-identifier ids. We map these here to legal ones + Map idMapping = new HashMap<>(); + String version = DOMUtil.getAttribute(packageDocument.getDocumentElement(), PREFIX_OPF, PackageDocumentBase.version); + + resources = readManifest(packageDocument, packageHref, epubReader, + resources, idMapping); + book.setResources(resources); + book.setVersion(version); + readCover(packageDocument, book); + book.setMetadata( + PackageDocumentMetadataReader.readMetadata(packageDocument)); + book.setSpine(readSpine(packageDocument, book.getResources(), idMapping)); + + // if we did not find a cover page then we make the first page of the book the cover page + if (book.getCoverPage() == null && book.getSpine().size() > 0) { + book.setCoverPage(book.getSpine().getResource(0)); + } + } + +// private static Resource readCoverImage(Element metadataElement, Resources resources) { +// String coverResourceId = DOMUtil.getFindAttributeValue(metadataElement.getOwnerDocument(), NAMESPACE_OPF, OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, OPFAttributes.content); +// if (StringUtil.isBlank(coverResourceId)) { +// return null; +// } +// Resource coverResource = resources.getByIdOrHref(coverResourceId); +// return coverResource; +// } + + + /** + * Reads the manifest containing the resource ids, hrefs and mediatypes. + * + * @param packageDocument e + * @param packageHref e + * @param epubReader e + * @param resources e + * @param idMapping e + * @return a Map with resources, with their id's as key. + */ + @SuppressWarnings("unused") + private static Resources readManifest(Document packageDocument, + String packageHref, + EpubReader epubReader, Resources resources, + Map idMapping) { + Element manifestElement = DOMUtil + .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), + NAMESPACE_OPF, OPFTags.manifest); + Resources result = new Resources(); + if (manifestElement == null) { + Log.e(TAG, + "Package document does not contain element " + OPFTags.manifest); + return result; + } + NodeList itemElements = manifestElement + .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.item); + for (int i = 0; i < itemElements.getLength(); i++) { + Element itemElement = (Element) itemElements.item(i); + String id = DOMUtil + .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.id); + String href = DOMUtil + .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href); + + try { + href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + } + String mediaTypeName = DOMUtil + .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.media_type); + Resource resource = resources.remove(href); + if (resource == null) { + Log.e(TAG, "resource with href '" + href + "' not found"); + continue; + } + resource.setId(id); + //for epub3 + String properties = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.properties); + resource.setProperties(properties); + + MediaType mediaType = MediaTypes.getMediaTypeByName(mediaTypeName); + if (mediaType != null) { + resource.setMediaType(mediaType); + } + result.add(resource); + idMapping.put(id, resource.getId()); + } + return result; + } + + + /** + * Reads the book's guide. + * Here some more attempts are made at finding the cover page. + * + * @param packageDocument r + * @param epubReader r + * @param book r + * @param resources g + */ + @SuppressWarnings("unused") + private static void readGuide(Document packageDocument, + EpubReader epubReader, EpubBook book, Resources resources) { + Element guideElement = DOMUtil + .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), + NAMESPACE_OPF, OPFTags.guide); + if (guideElement == null) { + return; + } + Guide guide = book.getGuide(); + NodeList guideReferences = guideElement + .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); + for (int i = 0; i < guideReferences.getLength(); i++) { + Element referenceElement = (Element) guideReferences.item(i); + String resourceHref = DOMUtil + .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href); + if (StringUtil.isBlank(resourceHref)) { + continue; + } + Resource resource = resources.getByHref(StringUtil + .substringBefore(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); + if (resource == null) { + Log.e(TAG, "Guide is referencing resource with href " + resourceHref + + " which could not be found"); + continue; + } + String type = DOMUtil + .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.type); + if (StringUtil.isBlank(type)) { + Log.e(TAG, "Guide is referencing resource with href " + resourceHref + + " which is missing the 'type' attribute"); + continue; + } + String title = DOMUtil + .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.title); + if (GuideReference.COVER.equalsIgnoreCase(type)) { + continue; // cover is handled elsewhere + } + GuideReference reference = new GuideReference(resource, type, title, + StringUtil + .substringAfter(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); + guide.addReference(reference); + } + } + + + /** + * Strips off the package prefixes up to the href of the packageHref. + *

    + * Example: + * If the packageHref is "OEBPS/content.opf" then a resource href like "OEBPS/foo/bar.html" will be turned into "foo/bar.html" + * + * @param packageHref f + * @param resourcesByHref g + * @return The stripped package href + */ + static Resources fixHrefs(String packageHref, + Resources resourcesByHref) { + int lastSlashPos = packageHref.lastIndexOf('/'); + if (lastSlashPos < 0) { + return resourcesByHref; + } + Resources result = new Resources(); + for (Resource resource : resourcesByHref.getAll()) { + if (StringUtil.isNotBlank(resource.getHref()) + && resource.getHref().length() > lastSlashPos) { + resource.setHref(resource.getHref().substring(lastSlashPos + 1)); + } + result.add(resource); + } + return result; + } + + /** + * Reads the document's spine, containing all sections in reading order. + * + * @param packageDocument b + * @param resources b + * @param idMapping b + * @return the document's spine, containing all sections in reading order. + */ + private static Spine readSpine(Document packageDocument, Resources resources, + Map idMapping) { + + Element spineElement = DOMUtil + .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), + NAMESPACE_OPF, OPFTags.spine); + if (spineElement == null) { + Log.e(TAG, "Element " + OPFTags.spine + + " not found in package document, generating one automatically"); + return generateSpineFromResources(resources); + } + Spine result = new Spine(); + String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc); + Log.v(TAG,tocResourceId); + result.setTocResource(findTableOfContentsResource(tocResourceId, resources)); + NodeList spineNodes = DOMUtil.getElementsByTagNameNS(packageDocument, NAMESPACE_OPF, OPFTags.itemref); + if(spineNodes==null){ + Log.e(TAG,"spineNodes is null"); + return result; + } + List spineReferences = new ArrayList<>(spineNodes.getLength()); + for (int i = 0; i < spineNodes.getLength(); i++) { + Element spineItem = (Element) spineNodes.item(i); + String itemref = DOMUtil.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.idref); + if (StringUtil.isBlank(itemref)) { + Log.e(TAG, "itemref with missing or empty idref"); // XXX + continue; + } + String id = idMapping.get(itemref); + if (id == null) { + id = itemref; + } + + Resource resource = resources.getByIdOrHref(id); + if (resource == null) { + Log.e(TAG, "resource with id '" + id + "' not found"); + continue; + } + + SpineReference spineReference = new SpineReference(resource); + if (OPFValues.no.equalsIgnoreCase(DOMUtil + .getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.linear))) { + spineReference.setLinear(false); + } + spineReferences.add(spineReference); + } + result.setSpineReferences(spineReferences); + return result; + } + + /** + * Creates a spine out of all resources in the resources. + * The generated spine consists of all XHTML pages in order of their href. + * + * @param resources f + * @return a spine created out of all resources in the resources. + */ + private static Spine generateSpineFromResources(Resources resources) { + Spine result = new Spine(); + List resourceHrefs = new ArrayList<>(resources.getAllHrefs()); + Collections.sort(resourceHrefs, String.CASE_INSENSITIVE_ORDER); + for (String resourceHref : resourceHrefs) { + Resource resource = resources.getByHref(resourceHref); + if (resource.getMediaType() == MediaTypes.NCX) { + result.setTocResource(resource); + } else if (resource.getMediaType() == MediaTypes.XHTML) { + result.addSpineReference(new SpineReference(resource)); + } + } + return result; + } + + + /** + * The spine tag should contain a 'toc' attribute with as value the resource id of the table of contents resource. + *

    + * Here we try several ways of finding this table of contents resource. + * We try the given attribute value, some often-used ones and finally look through all resources for the first resource with the table of contents mimetype. + * + * @param tocResourceId g + * @param resources g + * @return the Resource containing the table of contents + */ + static Resource findTableOfContentsResource(String tocResourceId, + Resources resources) { + Resource tocResource; + //一些epub3的文件为了兼容epub2,保留的epub2的目录文件,这里优先选择epub3的xml目录 + tocResource = resources.getByProperties("nav"); + if (tocResource != null) { + return tocResource; + } + + if (StringUtil.isNotBlank(tocResourceId)) { + tocResource = resources.getByIdOrHref(tocResourceId); + } + + if (tocResource != null) { + return tocResource; + } + + // get the first resource with the NCX mediatype + tocResource = resources.findFirstResourceByMediaType(MediaTypes.NCX); + + if (tocResource == null) { + for (String possibleNcxItemId : POSSIBLE_NCX_ITEM_IDS) { + tocResource = resources.getByIdOrHref(possibleNcxItemId); + if (tocResource != null) { + break; + } + tocResource = resources + .getByIdOrHref(possibleNcxItemId.toUpperCase()); + if (tocResource != null) { + break; + } + } + } + + + if (tocResource == null) { + Log.e(TAG, + "Could not find table of contents resource. Tried resource with id '" + + tocResourceId + "', " + Constants.DEFAULT_TOC_ID + ", " + + Constants.DEFAULT_TOC_ID.toUpperCase() + + " and any NCX resource."); + } + return tocResource; + } + + + /** + * Find all resources that have something to do with the coverpage and the cover image. + * Search the meta tags and the guide references + * + * @param packageDocument s + * @return all resources that have something to do with the coverpage and the cover image. + */ + // package + static Set findCoverHrefs(Document packageDocument) { + + Set result = new HashSet<>(); + + // try and find a meta tag with name = 'cover' and a non-blank id + String coverResourceId = DOMUtil + .getFindAttributeValue(packageDocument, NAMESPACE_OPF, + OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, + OPFAttributes.content); + + if (StringUtil.isNotBlank(coverResourceId)) { + String coverHref = DOMUtil + .getFindAttributeValue(packageDocument, NAMESPACE_OPF, + OPFTags.item, OPFAttributes.id, coverResourceId, + OPFAttributes.href); + if (StringUtil.isNotBlank(coverHref)) { + result.add(coverHref); + } else { + result.add( + coverResourceId); // maybe there was a cover href put in the cover id attribute + } + } + // try and find a reference tag with type is 'cover' and reference is not blank + String coverHref = DOMUtil + .getFindAttributeValue(packageDocument, NAMESPACE_OPF, + OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover, + OPFAttributes.href); + if (StringUtil.isNotBlank(coverHref)) { + result.add(coverHref); + } + return result; + } + + /** + * Finds the cover resource in the packageDocument and adds it to the book if found. + * Keeps the cover resource in the resources map + * + * @param packageDocument s + * @param book x + */ + private static void readCover(Document packageDocument, EpubBook book) { + + Collection coverHrefs = findCoverHrefs(packageDocument); + for (String coverHref : coverHrefs) { + Resource resource = book.getResources().getByHref(coverHref); + if (resource == null) { + Log.e(TAG, "Cover resource " + coverHref + " not found"); + continue; + } + if (resource.getMediaType() == MediaTypes.XHTML) { + book.setCoverPage(resource); + } else if (MediaTypes.isBitmapImage(resource.getMediaType())) { + book.setCoverImage(resource); + } + } + } + + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java new file mode 100644 index 000000000..49db82ae6 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java @@ -0,0 +1,253 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.EpubBook; +import me.ag2s.epublib.domain.Guide; +import me.ag2s.epublib.domain.GuideReference; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Spine; +import me.ag2s.epublib.domain.SpineReference; +import me.ag2s.epublib.util.StringUtil; + +/** + * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf + * + * @author paul + */ +public class PackageDocumentWriter extends PackageDocumentBase { + + private static final String TAG = PackageDocumentWriter.class.getName(); + + public static void write(EpubWriter epubWriter, XmlSerializer serializer, + EpubBook book) { + try { + serializer.startDocument(Constants.CHARACTER_ENCODING, false); + serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF); + serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE); + serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version, + book.getVersion()); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, + OPFAttributes.uniqueIdentifier, BOOK_ID_ID); + + PackageDocumentMetadataWriter.writeMetaData(book, serializer); + + writeManifest(book, epubWriter, serializer); + writeSpine(book, epubWriter, serializer); + writeGuide(book, epubWriter, serializer); + + serializer.endTag(NAMESPACE_OPF, OPFTags.packageTag); + serializer.endDocument(); + serializer.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Writes the package's spine. + * + * @param book e + * @param epubWriter g + * @param serializer g + * @throws IOException g + * @throws IllegalStateException g + * @throws IllegalArgumentException 1@throws XMLStreamException + */ + @SuppressWarnings("unused") + private static void writeSpine(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_OPF, OPFTags.spine); + Resource tocResource = book.getSpine().getTocResource(); + String tocResourceId = tocResource.getId(); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc, + tocResourceId); + + if (book.getCoverPage() != null // there is a cover page + && book.getSpine().findFirstResourceById(book.getCoverPage().getId()) + < 0) { // cover page is not already in the spine + // write the cover html file + serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, + book.getCoverPage().getId()); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, + "no"); + serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); + } + writeSpineItems(book.getSpine(), serializer); + serializer.endTag(NAMESPACE_OPF, OPFTags.spine); + } + + + private static void writeManifest(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_OPF, OPFTags.manifest); + + serializer.startTag(NAMESPACE_OPF, OPFTags.item); + + //For EPUB3 + if (book.isEpub3()) { + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName()); + } else { + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, + epubWriter.getNcxId()); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, epubWriter.getNcxHref()); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType()); + } + + serializer.endTag(NAMESPACE_OPF, OPFTags.item); + +// writeCoverResources(book, serializer); + + for (Resource resource : getAllResourcesSortById(book)) { + writeItem(book, resource, serializer); + } + + serializer.endTag(NAMESPACE_OPF, OPFTags.manifest); + } + + private static List getAllResourcesSortById(EpubBook book) { + List allResources = new ArrayList<>( + book.getResources().getAll()); + Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId())); + return allResources; + } + + /** + * Writes a resources as an item element + * + * @param resource g + * @param serializer g + * @throws IOException g + * @throws IllegalStateException g + * @throws IllegalArgumentException 1@throws XMLStreamException + */ + private static void writeItem(EpubBook book, Resource resource, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + if (resource == null || + (resource.getMediaType() == MediaTypes.NCX + && book.getSpine().getTocResource() != null)) { + return; + } + if (StringUtil.isBlank(resource.getId())) { +// log.error("resource id must not be empty (href: " + resource.getHref() +// + ", mediatype:" + resource.getMediaType() + ")"); + Log.e(TAG, "resource id must not be empty (href: " + resource.getHref() + + ", mediatype:" + resource.getMediaType() + ")"); + return; + } + if (StringUtil.isBlank(resource.getHref())) { +// log.error("resource href must not be empty (id: " + resource.getId() +// + ", mediatype:" + resource.getMediaType() + ")"); + Log.e(TAG, "resource href must not be empty (id: " + resource.getId() + + ", mediatype:" + resource.getMediaType() + ")"); + return; + } + if (resource.getMediaType() == null) { +// log.error("resource mediatype must not be empty (id: " + resource.getId() +// + ", href:" + resource.getHref() + ")"); + Log.e(TAG, "resource mediatype must not be empty (id: " + resource.getId() + + ", href:" + resource.getHref() + ")"); + return; + } + serializer.startTag(NAMESPACE_OPF, OPFTags.item); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, + resource.getId()); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, + resource.getHref()); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, + resource.getMediaType().getName()); + serializer.endTag(NAMESPACE_OPF, OPFTags.item); + } + + /** + * List all spine references + * + * @throws IOException f + * @throws IllegalStateException f + * @throws IllegalArgumentException f + */ + @SuppressWarnings("unused") + private static void writeSpineItems(Spine spine, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + for (SpineReference spineReference : spine.getSpineReferences()) { + serializer.startTag(NAMESPACE_OPF, OPFTags.itemref); + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref, + spineReference.getResourceId()); + if (!spineReference.isLinear()) { + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear, + OPFValues.no); + } + serializer.endTag(NAMESPACE_OPF, OPFTags.itemref); + } + } + + private static void writeGuide(EpubBook book, EpubWriter epubWriter, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + serializer.startTag(NAMESPACE_OPF, OPFTags.guide); + ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter, + serializer); + for (GuideReference reference : book.getGuide().getReferences()) { + writeGuideReference(reference, serializer); + } + serializer.endTag(NAMESPACE_OPF, OPFTags.guide); + } + + @SuppressWarnings("unused") + private static void ensureCoverPageGuideReferenceWritten(Guide guide, + EpubWriter epubWriter, XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + if (!(guide.getGuideReferencesByType(GuideReference.COVER).isEmpty())) { + return; + } + Resource coverPage = guide.getCoverPage(); + if (coverPage != null) { + writeGuideReference( + new GuideReference(guide.getCoverPage(), GuideReference.COVER, + GuideReference.COVER), serializer); + } + } + + + private static void writeGuideReference(GuideReference reference, + XmlSerializer serializer) + throws IllegalArgumentException, IllegalStateException, IOException { + if (reference == null) { + return; + } + serializer.startTag(NAMESPACE_OPF, OPFTags.reference); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.type, + reference.getType()); + serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, + reference.getCompleteHref()); + if (StringUtil.isNotBlank(reference.getTitle())) { + serializer + .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.title, + reference.getTitle()); + } + serializer.endTag(NAMESPACE_OPF, OPFTags.reference); + } +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java b/epublib/src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java new file mode 100644 index 000000000..5da77b8d2 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java @@ -0,0 +1,185 @@ +package me.ag2s.epublib.epub; + +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import me.ag2s.epublib.domain.EpubResourceProvider; +import me.ag2s.epublib.domain.LazyResource; +import me.ag2s.epublib.domain.LazyResourceProvider; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.domain.Resources; +import me.ag2s.epublib.util.CollectionUtil; +import me.ag2s.epublib.util.ResourceUtil; + + +/** + * Loads Resources from inputStreams, ZipFiles, etc + * + * @author paul + */ +public class ResourcesLoader { + + private static final String TAG = ResourcesLoader.class.getName(); + + + /** + * Loads the entries of the zipFile as resources. + *

    + * The MediaTypes that are in the lazyLoadedTypes will not get their + * contents loaded, but are stored as references to entries into the + * ZipFile and are loaded on demand by the Resource system. + * + * @param zipFile import epub zipfile + * @param defaultHtmlEncoding epub xhtml default encoding + * @param lazyLoadedTypes lazyLoadedTypes + * @return Resources + * @throws IOException IOException + */ + public static Resources loadResources(ZipFile zipFile, + String defaultHtmlEncoding, + List lazyLoadedTypes) throws IOException { + + LazyResourceProvider resourceProvider = + new EpubResourceProvider(zipFile.getName()); + + Resources result = new Resources(); + Enumeration entries = zipFile.entries(); + + while (entries.hasMoreElements()) { + ZipEntry zipEntry = entries.nextElement(); + + if (zipEntry == null || zipEntry.isDirectory()) { + continue; + } + + String href = zipEntry.getName(); + + Resource resource; + + if (shouldLoadLazy(href, lazyLoadedTypes)) { + resource = new LazyResource(resourceProvider, zipEntry.getSize(), href); + } else { + resource = ResourceUtil + .createResource(zipEntry, zipFile.getInputStream(zipEntry)); + /*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/ + if (href.endsWith("opf")) { + String string = new String(resource.getData()).replace("smlns=\"", "xmlns=\""); + resource.setData(string.getBytes()); + } + + } + + if (resource.getMediaType() == MediaTypes.XHTML) { + resource.setInputEncoding(defaultHtmlEncoding); + } + result.add(resource); + } + + return result; + } + + /** + * Whether the given href will load a mediaType that is in the + * collection of lazilyLoadedMediaTypes. + * + * @param href href + * @param lazilyLoadedMediaTypes lazilyLoadedMediaTypes + * @return Whether the given href will load a mediaType that is + * in the collection of lazilyLoadedMediaTypes. + */ + private static boolean shouldLoadLazy(String href, + Collection lazilyLoadedMediaTypes) { + if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) { + return false; + } + MediaType mediaType = MediaTypes.determineMediaType(href); + return lazilyLoadedMediaTypes.contains(mediaType); + } + + /** + * Loads all entries from the ZipInputStream as Resources. + *

    + * Loads the contents of all ZipEntries into memory. + * Is fast, but may lead to memory problems when reading large books + * on devices with small amounts of memory. + * + * @param zipInputStream zipInputStream + * @param defaultHtmlEncoding defaultHtmlEncoding + * @return Resources + * @throws IOException IOException + */ + public static Resources loadResources(ZipInputStream zipInputStream, + String defaultHtmlEncoding) throws IOException { + Resources result = new Resources(); + ZipEntry zipEntry; + do { + // get next valid zipEntry + zipEntry = getNextZipEntry(zipInputStream); + if ((zipEntry == null) || zipEntry.isDirectory()) { + continue; + } + String href = zipEntry.getName(); + + // store resource + Resource resource = ResourceUtil.createResource(zipEntry, zipInputStream); + ///*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/ + if (href.endsWith("opf")) { + String string = new String(resource.getData()).replace("smlns=\"", "xmlns=\""); + resource.setData(string.getBytes()); + } + if (resource.getMediaType() == MediaTypes.XHTML) { + resource.setInputEncoding(defaultHtmlEncoding); + } + result.add(resource); + } while (zipEntry != null); + + return result; + } + + + private static ZipEntry getNextZipEntry(ZipInputStream zipInputStream) + throws IOException { + try { + return zipInputStream.getNextEntry(); + } catch (ZipException e) { + //see Issue #122 Infinite loop. + //when reading a file that is not a real zip archive or a zero length file, zipInputStream.getNextEntry() + //throws an exception and does not advance, so loadResources enters an infinite loop + //log.error("Invalid or damaged zip file.", e); + Log.e(TAG, e.getLocalizedMessage()); + try { + zipInputStream.closeEntry(); + } catch (Exception ignored) { + } + throw e; + } + } + + /** + * Loads all entries from the ZipInputStream as Resources. + *

    + * Loads the contents of all ZipEntries into memory. + * Is fast, but may lead to memory problems when reading large books + * on devices with small amounts of memory. + * + * @param zipFile zipFile + * @param defaultHtmlEncoding defaultHtmlEncoding + * @return Resources + * @throws IOException IOException + */ + public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding) throws IOException { + List ls = new ArrayList<>(); + return loadResources(zipFile, defaultHtmlEncoding, ls); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/CollectionUtil.java b/epublib/src/main/java/me/ag2s/epublib/util/CollectionUtil.java new file mode 100644 index 000000000..e367fdc66 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/CollectionUtil.java @@ -0,0 +1,71 @@ +package me.ag2s.epublib.util; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +public class CollectionUtil { + + /** + * Wraps an Enumeration around an Iterator + * @author paul.siegmann + * + * @param + */ + private static class IteratorEnumerationAdapter implements Enumeration { + + private final Iterator iterator; + + public IteratorEnumerationAdapter(Iterator iter) { + this.iterator = iter; + } + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public T nextElement() { + return iterator.next(); + } + } + + /** + * Creates an Enumeration out of the given Iterator. + * @param g + * @param it g + * @return an Enumeration created out of the given Iterator. + */ + @SuppressWarnings("unused") + public static Enumeration createEnumerationFromIterator( + Iterator it) { + return new IteratorEnumerationAdapter<>(it); + } + + + /** + * Returns the first element of the list, null if the list is null or empty. + * + * @param f + * @param list f + * @return the first element of the list, null if the list is null or empty. + */ + public static T first(List list) { + if (list == null || list.isEmpty()) { + return null; + } + return list.get(0); + } + + /** + * Whether the given collection is null or has no elements. + * + * @param collection g + * @return Whether the given collection is null or has no elements. + */ + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java b/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java new file mode 100644 index 000000000..dcbda6830 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java @@ -0,0 +1,962 @@ +package me.ag2s.epublib.util; + +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.Charset; + +import me.ag2s.epublib.epub.PackageDocumentReader; +import me.ag2s.epublib.util.commons.io.IOConsumer; + +/** + * Most of the functions herein are re-implementations of the ones in + * apache io IOUtils. + *

    + * The reason for re-implementing this is that the functions are fairly simple + * and using my own implementation saves the inclusion of a 200Kb jar file. + */ +public class IOUtil { + private static final String TAG = IOUtil.class.getName(); + + /** + * Represents the end-of-file (or stream). + * + * @since 2.5 (made public) + */ + public static final int EOF = -1; + + + public static final int DEFAULT_BUFFER_SIZE = 1024 * 8; + private static final byte[] SKIP_BYTE_BUFFER = new byte[DEFAULT_BUFFER_SIZE]; + + // Allocated in the relevant skip method if necessary. + /* + * These buffers are static and are shared between threads. + * This is possible because the buffers are write-only - the contents are never read. + * + * N.B. there is no need to synchronize when creating these because: + * - we don't care if the buffer is created multiple times (the data is ignored) + * - we always use the same size buffer, so if it it is recreated it will still be OK + * (if the buffer size were variable, we would need to synch. to ensure some other thread + * did not create a smaller one) + */ + private static char[] SKIP_CHAR_BUFFER; + + /** + * Gets the contents of the Reader as a byte[], with the given character encoding. + * + * @param in g + * @param encoding g + * @return the contents of the Reader as a byte[], with the given character encoding. + * @throws IOException g + */ + public static byte[] toByteArray(Reader in, String encoding) + throws IOException { + StringWriter out = new StringWriter(); + copy(in, out); + out.flush(); + return out.toString().getBytes(encoding); + } + + /** + * Returns the contents of the InputStream as a byte[] + * + * @param in f + * @return the contents of the InputStream as a byte[] + * @throws IOException f + */ + public static byte[] toByteArray(InputStream in) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + copy(in, result); + result.flush(); + return result.toByteArray(); + } + + + /** + * Reads data from the InputStream, using the specified buffer size. + *

    + * This is meant for situations where memory is tight, since + * it prevents buffer expansion. + * + * @param in the stream to read data from + * @param size the size of the array to create + * @return the array, or null + * @throws IOException f + */ + public static byte[] toByteArray(InputStream in, int size) + throws IOException { + + try { + ByteArrayOutputStream result; + + if (size > 0) { + result = new ByteArrayOutputStream(size); + } else { + result = new ByteArrayOutputStream(); + } + + copy(in, result); + result.flush(); + return result.toByteArray(); + } catch (OutOfMemoryError error) { + //Return null so it gets loaded lazily. + return null; + } + + } + + + /** + * if totalNrRead < 0 then totalNrRead is returned, if + * (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead + * is returned, -1 otherwise. + * + * @param nrRead f + * @param totalNrNread f + * @return if totalNrRead < 0 then totalNrRead is returned, if + * (nrRead + totalNrRead) < Integer.MAX_VALUE then nrRead + totalNrRead + * is returned, -1 otherwise. + */ + protected static int calcNewNrReadSize(int nrRead, int totalNrNread) { + if (totalNrNread < 0) { + return totalNrNread; + } + if (totalNrNread > (Integer.MAX_VALUE - nrRead)) { + return -1; + } else { + return (totalNrNread + nrRead); + } + } + + // + public static void copy(InputStream in, OutputStream result) throws IOException { + copy(in, result,DEFAULT_BUFFER_SIZE); + } + + /** + * Copies bytes from an InputStream to an OutputStream using an internal buffer of the + * given size. + *

    + * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

    + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param bufferSize the bufferSize used to copy from the input to the output + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 2.5 + */ + public static long copy(final InputStream input, final OutputStream output, final int bufferSize) + throws IOException { + return copyLarge(input, output, new byte[bufferSize]); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the default character encoding of the platform. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead + */ + @Deprecated + public static void copy(final InputStream input, final Writer output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param inputCharset the charset to use for the input stream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(final InputStream input, final Writer output, final Charset inputCharset) + throws IOException { + final InputStreamReader in = new InputStreamReader(input, inputCharset.name()); + copy(in, output); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + * Character encoding names can be found at + * IANA. + *

    + * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io + * .UnsupportedEncodingException} in version 2.2 if the + * encoding is not supported. + * @since 1.1 + */ + public static void copy(final InputStream input, final Writer output, final String inputCharsetName) + throws IOException { + copy(input, output, Charset.forName(inputCharsetName)); + } + + /** + * Copies chars from a Reader to a Appendable. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Appendable to write to + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.7 + */ + public static long copy(final Reader input, final Appendable output) throws IOException { + return copy(input, output, CharBuffer.allocate(DEFAULT_BUFFER_SIZE)); + } + + /** + * Copies chars from a Reader to an Appendable. + *

    + * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

    + * + * @param input the Reader to read from + * @param output the Appendable to write to + * @param buffer the buffer to be used for the copy + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.7 + */ + public static long copy(final Reader input, final Appendable output, final CharBuffer buffer) throws IOException { + long count = 0; + int n; + while (EOF != (n = input.read(buffer))) { + buffer.flip(); + output.append(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copies chars from a Reader to bytes on an + * OutputStream using the default character encoding of the + * platform, and calling flush. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

    + * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead + */ + @Deprecated + public static void copy(final Reader input, final OutputStream output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copies chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + *

    + * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

    + *

    + * This method uses {@link OutputStreamWriter}. + *

    + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param outputCharset the charset to use for the OutputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(final Reader input, final OutputStream output, final Charset outputCharset) + throws IOException { + final OutputStreamWriter out = new OutputStreamWriter(output, outputCharset.name()); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, + // we have to flush here. + out.flush(); + } + + /** + * Copies chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * Character encoding names can be found at + * IANA. + *

    + * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

    + * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io + * .UnsupportedEncodingException} in version 2.2 if the + * encoding is not supported. + * @since 1.1 + */ + public static void copy(final Reader input, final OutputStream output, final String outputCharsetName) + throws IOException { + copy(input, output, Charset.forName(outputCharsetName)); + } + + /** + * Copies chars from a Reader to a Writer. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(final Reader input, final Writer output) throws IOException { + final long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copies bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + *

    + * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + *

    + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(final InputStream input, final OutputStream output) + throws IOException { + return copy(input, output, DEFAULT_BUFFER_SIZE); + } + + /** + * Copies bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

    + * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

    + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param buffer the buffer to use for the copy + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer) + throws IOException { + long count = 0; + if (input != null) { + int n; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + //input.close(); + } + return count; + } + + /** + * Copies some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

    + *

    + * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

    + * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, + final long length) throws IOException { + return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

    + * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

    + *

    + * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

    + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @param buffer the buffer to use for the copy + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, + final long inputOffset, final long length, final byte[] buffer) throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + final int bufferLength = buffer.length; + int bytesToRead = bufferLength; + if (length > 0 && length < bufferLength) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, bufferLength); + } + } + return totalRead; + } + + /** + * Copies chars from a large (over 2GB) Reader to a Writer. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(final Reader input, final Writer output) throws IOException { + return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies chars from a large (over 2GB) Reader to a Writer. + *

    + * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

    + * + * @param input the Reader to read from + * @param output the Writer to write to + * @param buffer the buffer to be used for the copy + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException { + long count = 0; + int n; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copies some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

    + * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

    + * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length) + throws IOException { + return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

    + * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

    + * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @param buffer the buffer to be used for the copy + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length, + final char[] buffer) + throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + int bytesToRead = buffer.length; + if (length > 0 && length < buffer.length) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, buffer.length); + } + } + return totalRead; + } + + /** + * Skips bytes from an input byte stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * skip() implementations in subclasses of {@link InputStream}. + *

    + * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather + * than delegating to {@link InputStream#skip(long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of bytes are skipped. + *

    + * + * @param input byte stream to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @see InputStream#skip(long) + * @see IO-203 - Add skipFully() method for InputStreams + * @since 2.0 + */ + public static long skip(final InputStream input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple + * times (the data is ignored) - we always use the same size buffer, so if it it is recreated it will still be + * OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a + * smaller one) + */ + long remain = toSkip; + while (remain > 0) { + // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() + final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips bytes from a ReadableByteChannel. + * This implementation guarantees that it will read as many bytes + * as possible before giving up. + * + * @param input ReadableByteChannel to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * @throws IOException if there is a problem reading the ReadableByteChannel + * @throws IllegalArgumentException if toSkip is negative + * @since 2.5 + */ + public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length)); + long remain = toSkip; + while (remain > 0) { + skipByteBuffer.position(0); + skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + final int n = input.read(skipByteBuffer); + if (n == EOF) { + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips characters from an input character stream. + * This implementation guarantees that it will read as many characters + * as possible before giving up; this may not always be the case for + * skip() implementations in subclasses of {@link Reader}. + *

    + * Note that the implementation uses {@link Reader#read(char[], int, int)} rather + * than delegating to {@link Reader#skip(long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

    + * + * @param input character stream to skip + * @param toSkip number of characters to skip. + * @return number of characters actually skipped. + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @see Reader#skip(long) + * @see IO-203 - Add skipFully() method for InputStreams + * @since 2.0 + */ + public static long skip(final Reader input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data + * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer + * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) + */ + if (SKIP_CHAR_BUFFER == null) { + SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length]; + } + long remain = toSkip; + while (remain > 0) { + // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() + final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips the requested number of bytes or fail if there are not enough left. + *

    + * This allows for the possibility that {@link InputStream#skip(long)} may + * not skip as many bytes as requested (most likely because of reaching EOF). + *

    + * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

    + * + * @param input stream to skip + * @param toSkip the number of bytes to skip + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @see InputStream#skip(long) + * @since 2.0 + */ + public static void skipFully(final InputStream input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Skips the requested number of bytes or fail if there are not enough left. + * + * @param input ReadableByteChannel to skip + * @param toSkip the number of bytes to skip + * @throws IOException if there is a problem reading the ReadableByteChannel + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @since 2.5 + */ + public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Skips the requested number of characters or fail if there are not enough left. + *

    + * This allows for the possibility that {@link Reader#skip(long)} may + * not skip as many characters as requested (most likely because of reaching EOF). + *

    + * Note that the implementation uses {@link #skip(Reader, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

    + * + * @param input stream to skip + * @param toSkip the number of characters to skip + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of characters skipped was incorrect + * @see Reader#skip(long) + * @since 2.0 + */ + public static void skipFully(final Reader input, final long toSkip) throws IOException { + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Returns the length of the given array in a null-safe manner. + * + * @param array an array or null + * @return the array length -- or 0 if the given array is null. + * @since 2.7 + */ + public static int length(final byte[] array) { + return array == null ? 0 : array.length; + } + + /** + * Returns the length of the given array in a null-safe manner. + * + * @param array an array or null + * @return the array length -- or 0 if the given array is null. + * @since 2.7 + */ + public static int length(final char[] array) { + return array == null ? 0 : array.length; + } + + /** + * Returns the length of the given CharSequence in a null-safe manner. + * + * @param csq a CharSequence or null + * @return the CharSequence length -- or 0 if the given CharSequence is null. + * @since 2.7 + */ + public static int length(final CharSequence csq) { + return csq == null ? 0 : csq.length(); + } + + /** + * Returns the length of the given array in a null-safe manner. + * + * @param array an array or null + * @return the array length -- or 0 if the given array is null. + * @since 2.7 + */ + public static int length(final Object[] array) { + return array == null ? 0 : array.length; + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeable The resource to close, may be null. + * @throws IOException if an I/O error occurs. + * @since 2.7 + */ + public static void close(final Closeable closeable) throws IOException { + if (closeable != null) { + closeable.close(); + } + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeables The resource(s) to close, may be null. + * @throws IOException if an I/O error occurs. + * @since 2.8.0 + */ + public static void close(final Closeable... closeables) throws IOException { + if (closeables != null) { + for (final Closeable closeable : closeables) { + close(closeable); + } + } + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeable The resource to close, may be null. + * @param consumer Consume the IOException thrown by {@link Closeable#close()}. + * @throws IOException if an I/O error occurs. + * @since 2.7 + */ + public static void close(final Closeable closeable, final IOConsumer consumer) throws IOException { + if (closeable != null) { + try { + closeable.close(); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + } + + /** + * Closes a URLConnection. + * + * @param conn the connection to close. + * @since 2.4 + */ + public static void close(final URLConnection conn) { + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).disconnect(); + } + } + + @SuppressWarnings("unused") + public static String Stream2String(InputStream inputStream) { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString(); + } catch (Exception e) { + return e.getLocalizedMessage(); + } + + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java b/epublib/src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java new file mode 100644 index 000000000..15a3893b6 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java @@ -0,0 +1,33 @@ +package me.ag2s.epublib.util; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * OutputStream with the close() disabled. + * We write multiple documents to a ZipOutputStream. + * Some of the formatters call a close() after writing their data. + * We don't want them to do that, so we wrap regular OutputStreams in this NoCloseOutputStream. + * + * @author paul + */ +@SuppressWarnings("unused") +public class NoCloseOutputStream extends OutputStream { + + private final OutputStream outputStream; + + public NoCloseOutputStream(OutputStream outputStream) { + this.outputStream = outputStream; + } + + @Override + public void write(int b) throws IOException { + outputStream.write(b); + } + + /** + * A close() that does not call it's parent's close() + */ + public void close() { + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/NoCloseWriter.java b/epublib/src/main/java/me/ag2s/epublib/util/NoCloseWriter.java new file mode 100644 index 000000000..ad1da9c7c --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/NoCloseWriter.java @@ -0,0 +1,36 @@ +package me.ag2s.epublib.util; + +import java.io.IOException; +import java.io.Writer; + +/** + * Writer with the close() disabled. + * We write multiple documents to a ZipOutputStream. + * Some of the formatters call a close() after writing their data. + * We don't want them to do that, so we wrap regular Writers in this NoCloseWriter. + * + * @author paul + */ +@SuppressWarnings("unused") +public class NoCloseWriter extends Writer { + + private final Writer writer; + + public NoCloseWriter(Writer writer) { + this.writer = writer; + } + + @Override + public void close() { + } + + @Override + public void flush() throws IOException { + writer.flush(); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + writer.write(cbuf, off, len); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java b/epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java new file mode 100644 index 000000000..853d1a5e4 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java @@ -0,0 +1,175 @@ +package me.ag2s.epublib.util; + +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.parsers.DocumentBuilder; + +import me.ag2s.epublib.Constants; +import me.ag2s.epublib.domain.MediaType; +import me.ag2s.epublib.domain.MediaTypes; +import me.ag2s.epublib.domain.Resource; +import me.ag2s.epublib.epub.EpubProcessorSupport; + +/** + * Various resource utility methods + * + * @author paul + */ +public class ResourceUtil { + /** + * 快速创建HTML类型的Resource + * + * @param title 章节的标题 + * @param txt 章节的正文 + * @param model html模板 + * @return 返回Resource + */ + public static Resource createChapterResource(String title, String txt, String model, String href) { + if (title.contains("\n")) { + title = "" + title.replaceFirst("\\s*\\n\\s*", "
    "); + } else { + title = title.replaceFirst("\\s+", "
    "); + if (title.contains("")) + title = "" + title; + } + String html = model.replace("{title}", title) + .replace("{content}", StringUtil.formatHtml(txt)); + return new Resource(html.getBytes(), href); + } + + public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) { + String html = model.replace("{name}", name) + .replace("{author}", author) + .replace("{kind}", kind) + .replace("{wordCount}", wordCount) + .replace("{intro}", StringUtil.formatHtml(intro)); + return new Resource(html.getBytes(), href); + } + + /** + * 快速从File创建Resource + * + * @param file File + * @return Resource + * @throws IOException IOException + */ + + @SuppressWarnings("unused") + public static Resource createResource(File file) throws IOException { + if (file == null) { + return null; + } + MediaType mediaType = MediaTypes.determineMediaType(file.getName()); + byte[] data = IOUtil.toByteArray(new FileInputStream(file)); + return new Resource(data, mediaType); + } + + + /** + * 创建一个只带标题的HTMl类型的Resource,常用于封面页,大卷页 + * + * @param title v + * @param href v + * @return a resource with as contents a html page with the given title. + */ + @SuppressWarnings("unused") + public static Resource createResource(String title, String href) { + String content = + "" + title + "

    " + title + + "

    "; + return new Resource(null, content.getBytes(), href, MediaTypes.XHTML, + Constants.CHARACTER_ENCODING); + } + + /** + * Creates a resource out of the given zipEntry and zipInputStream. + * + * @param zipEntry v + * @param zipInputStream v + * @return a resource created out of the given zipEntry and zipInputStream. + * @throws IOException v + */ + public static Resource createResource(ZipEntry zipEntry, + ZipInputStream zipInputStream) throws IOException { + return new Resource(zipInputStream, zipEntry.getName()); + + } + + public static Resource createResource(ZipEntry zipEntry, + InputStream zipInputStream) throws IOException { + return new Resource(zipInputStream, zipEntry.getName()); + + } + + /** + * Converts a given string from given input character encoding to the requested output character encoding. + * + * @param inputEncoding v + * @param outputEncoding v + * @param input v + * @return the string from given input character encoding converted to the requested output character encoding. + * @throws UnsupportedEncodingException v + */ + @SuppressWarnings("unused") + public static byte[] recode(String inputEncoding, String outputEncoding, + byte[] input) throws UnsupportedEncodingException { + return new String(input, inputEncoding).getBytes(outputEncoding); + } + + /** + * Gets the contents of the Resource as an InputSource in a null-safe manner. + */ + @SuppressWarnings("unused") + public static InputSource getInputSource(Resource resource) + throws IOException { + if (resource == null) { + return null; + } + Reader reader = resource.getReader(); + if (reader == null) { + return null; + } + return new InputSource(reader); + } + + + /** + * Reads parses the xml therein and returns the result as a Document + */ + public static Document getAsDocument(Resource resource) + throws SAXException, IOException { + return getAsDocument(resource, + EpubProcessorSupport.createDocumentBuilder()); + } + + /** + * Reads the given resources inputstream, parses the xml therein and returns the result as a Document + * + * @param resource v + * @param documentBuilder v + * @return the document created from the given resource + * @throws UnsupportedEncodingException v + * @throws SAXException v + * @throws IOException v + */ + public static Document getAsDocument(Resource resource, + DocumentBuilder documentBuilder) + throws UnsupportedEncodingException, SAXException, IOException { + InputSource inputSource = getInputSource(resource); + if (inputSource == null) { + return null; + } + return documentBuilder.parse(inputSource); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/StringUtil.java b/epublib/src/main/java/me/ag2s/epublib/util/StringUtil.java new file mode 100644 index 000000000..e9fff3136 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/StringUtil.java @@ -0,0 +1,291 @@ +package me.ag2s.epublib.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Various String utility functions. + *

    + * Most of the functions herein are re-implementations of the ones in apache + * commons StringUtils. The reason for re-implementing this is that the + * functions are fairly simple and using my own implementation saves the + * inclusion of a 200Kb jar file. + * + * @author paul.siegmann + */ +public class StringUtil { + + /** + * Changes a path containing '..', '.' and empty dirs into a path that + * doesn't. X/foo/../Y is changed into 'X/Y', etc. Does not handle invalid + * paths like "../". + * + * @param path path + * @return the normalized path + */ + public static String collapsePathDots(String path) { + String[] stringParts = path.split("/"); + List parts = new ArrayList<>(Arrays.asList(stringParts)); + for (int i = 0; i < parts.size() - 1; i++) { + String currentDir = parts.get(i); + if (currentDir.length() == 0 || currentDir.equals(".")) { + parts.remove(i); + i--; + } else if (currentDir.equals("..")) { + parts.remove(i - 1); + parts.remove(i - 1); + i -= 2; + } + } + StringBuilder result = new StringBuilder(); + if (path.startsWith("/")) { + result.append('/'); + } + for (int i = 0; i < parts.size(); i++) { + result.append(parts.get(i)); + if (i < (parts.size() - 1)) { + result.append('/'); + } + } + return result.toString(); + } + + /** + * Whether the String is not null, not zero-length and does not contain of + * only whitespace. + * + * @param text text + * @return Whether the String is not null, not zero-length and does not contain of + */ + public static boolean isNotBlank(String text) { + return !isBlank(text); + } + + /** + * Whether the String is null, zero-length and does contain only whitespace. + * + * @return Whether the String is null, zero-length and does contain only whitespace. + */ + public static boolean isBlank(String text) { + if (isEmpty(text)) { + return true; + } + for (int i = 0; i < text.length(); i++) { + if (!Character.isWhitespace(text.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Whether the given string is null or zero-length. + * + * @param text the input for this method + * @return Whether the given string is null or zero-length. + */ + public static boolean isEmpty(String text) { + return (text == null) || (text.length() == 0); + } + + /** + * Whether the given source string ends with the given suffix, ignoring + * case. + * + * @param source source + * @param suffix suffix + * @return Whether the given source string ends with the given suffix, ignoring case. + */ + public static boolean endsWithIgnoreCase(String source, String suffix) { + if (isEmpty(suffix)) { + return true; + } + if (isEmpty(source)) { + return false; + } + if (suffix.length() > source.length()) { + return false; + } + return source.substring(source.length() - suffix.length()) + .toLowerCase().endsWith(suffix.toLowerCase()); + } + + /** + * If the given text is null return "", the original text otherwise. + * + * @param text text + * @return If the given text is null "", the original text otherwise. + */ + public static String defaultIfNull(String text) { + return defaultIfNull(text, ""); + } + + /** + * If the given text is null return "", the given defaultValue otherwise. + * + * @param text d + * @param defaultValue d + * @return If the given text is null "", the given defaultValue otherwise. + */ + public static String defaultIfNull(String text, String defaultValue) { + if (text == null) { + return defaultValue; + } + return text; + } + + /** + * Null-safe string comparator + * + * @param text1 d + * @param text2 d + * @return whether the two strings are equal + */ + public static boolean equals(String text1, String text2) { + if (text1 == null) { + return (text2 == null); + } + return text1.equals(text2); + } + + /** + * Pretty toString printer. + * + * @param keyValues d + * @return a string representation of the input values + */ + public static String toString(Object... keyValues) { + StringBuilder result = new StringBuilder(); + result.append('['); + for (int i = 0; i < keyValues.length; i += 2) { + if (i > 0) { + result.append(", "); + } + result.append(keyValues[i]); + result.append(": "); + Object value = null; + if ((i + 1) < keyValues.length) { + value = keyValues[i + 1]; + } + if (value == null) { + result.append(""); + } else { + result.append('\''); + result.append(value); + result.append('\''); + } + } + result.append(']'); + return result.toString(); + } + + public static int hashCode(String... values) { + int result = 31; + for (String value : values) { + result ^= String.valueOf(value).hashCode(); + } + return result; + } + + /** + * Gives the substring of the given text before the given separator. + *

    + * If the text does not contain the given separator then the given text is + * returned. + * + * @param text d + * @param separator d + * @return the substring of the given text before the given separator. + */ + public static String substringBefore(String text, char separator) { + if (isEmpty(text)) { + return text; + } + int sepPos = text.indexOf(separator); + if (sepPos < 0) { + return text; + } + return text.substring(0, sepPos); + } + + /** + * Gives the substring of the given text before the last occurrence of the + * given separator. + *

    + * If the text does not contain the given separator then the given text is + * returned. + * + * @param text d + * @param separator d + * @return the substring of the given text before the last occurrence of the given separator. + */ + public static String substringBeforeLast(String text, char separator) { + if (isEmpty(text)) { + return text; + } + int cPos = text.lastIndexOf(separator); + if (cPos < 0) { + return text; + } + return text.substring(0, cPos); + } + + /** + * Gives the substring of the given text after the last occurrence of the + * given separator. + *

    + * If the text does not contain the given separator then "" is returned. + * + * @param text d + * @param separator d + * @return the substring of the given text after the last occurrence of the given separator. + */ + public static String substringAfterLast(String text, char separator) { + if (isEmpty(text)) { + return text; + } + int cPos = text.lastIndexOf(separator); + if (cPos < 0) { + return ""; + } + return text.substring(cPos + 1); + } + + /** + * Gives the substring of the given text after the given separator. + *

    + * If the text does not contain the given separator then "" is returned. + * + * @param text the input text + * @param c the separator char + * @return the substring of the given text after the given separator. + */ + public static String substringAfter(String text, char c) { + if (isEmpty(text)) { + return text; + } + int cPos = text.indexOf(c); + if (cPos < 0) { + return ""; + } + return text.substring(cPos + 1); + } + + public static String formatHtml(String text) { + StringBuilder body = new StringBuilder(); + for (String s : text.split("\\r?\\n")) { + s = s.replaceAll("^\\s+|\\s+$", ""); + if (s.length() > 0) { + //段落为一张图片才认定为图片章节/漫画并启用多看单图优化,否则认定为普通文字夹杂着的图片文字。 + if (s.matches("(?i)^]+)/?>$")) { + body.append(s.replaceAll("(?i)^]+)/?>$", + "

    ")); + } else { + body.append("

    ").append(s).append("

    "); + } + } + } + return body.toString(); + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java new file mode 100644 index 000000000..ed299f591 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java @@ -0,0 +1,412 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package me.ag2s.epublib.util.commons.io; + + + +import android.os.Build; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import me.ag2s.epublib.util.IOUtil; + +import static me.ag2s.epublib.util.IOUtil.EOF; + + +/** + * This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes. + * + * This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the + * first byte in the stream. + * + * The {@link ByteOrderMark} implementation has the following pre-defined BOMs: + *
      + *
    • UTF-8 - {@link ByteOrderMark#UTF_8}
    • + *
    • UTF-16BE - {@link ByteOrderMark#UTF_16LE}
    • + *
    • UTF-16LE - {@link ByteOrderMark#UTF_16BE}
    • + *
    • UTF-32BE - {@link ByteOrderMark#UTF_32LE}
    • + *
    • UTF-32LE - {@link ByteOrderMark#UTF_32BE}
    • + *
    + * + * + *

    Example 1 - Detect and exclude a UTF-8 BOM

    + * + *
    + * BOMInputStream bomIn = new BOMInputStream(in);
    + * if (bomIn.hasBOM()) {
    + *     // has a UTF-8 BOM
    + * }
    + * 
    + * + *

    Example 2 - Detect a UTF-8 BOM (but don't exclude it)

    + * + *
    + * boolean include = true;
    + * BOMInputStream bomIn = new BOMInputStream(in, include);
    + * if (bomIn.hasBOM()) {
    + *     // has a UTF-8 BOM
    + * }
    + * 
    + * + *

    Example 3 - Detect Multiple BOMs

    + * + *
    + * BOMInputStream bomIn = new BOMInputStream(in,
    + *   ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,
    + *   ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE
    + *   );
    + * if (bomIn.hasBOM() == false) {
    + *     // No BOM found
    + * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {
    + *     // has a UTF-16LE BOM
    + * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {
    + *     // has a UTF-16BE BOM
    + * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {
    + *     // has a UTF-32LE BOM
    + * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {
    + *     // has a UTF-32BE BOM
    + * }
    + * 
    + * + * @see ByteOrderMark + * @see Wikipedia - Byte Order Mark + * @since 2.0 + */ +public class BOMInputStream extends ProxyInputStream { + private final boolean include; + /** + * BOMs are sorted from longest to shortest. + */ + private final List boms; + private ByteOrderMark byteOrderMark; + private int[] firstBytes; + private int fbLength; + private int fbIndex; + private int markFbIndex; + private boolean markedAtStart; + + /** + * Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM. + * + * @param delegate + * the InputStream to delegate to + */ + @SuppressWarnings("unused") + public BOMInputStream(final InputStream delegate) { + this(delegate, false, ByteOrderMark.UTF_8); + } + + /** + * Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it. + * + * @param delegate + * the InputStream to delegate to + * @param include + * true to include the UTF-8 BOM or false to exclude it + */ + @SuppressWarnings("unused") + public BOMInputStream(final InputStream delegate, final boolean include) { + this(delegate, include, ByteOrderMark.UTF_8); + } + + /** + * Constructs a new BOM InputStream that excludes the specified BOMs. + * + * @param delegate + * the InputStream to delegate to + * @param boms + * The BOMs to detect and exclude + */ + @SuppressWarnings("unused") + public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) { + this(delegate, false, boms); + } + + /** + * Compares ByteOrderMark objects in descending length order. + */ + private static final Comparator ByteOrderMarkLengthComparator = (bom1, bom2) -> { + final int len1 = bom1.length(); + final int len2 = bom2.length(); + return Integer.compare(len2, len1); + }; + + /** + * Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them. + * + * @param delegate + * the InputStream to delegate to + * @param include + * true to include the specified BOMs or false to exclude them + * @param boms + * The BOMs to detect and optionally exclude + */ + public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) { + super(delegate); + if (IOUtil.length(boms) == 0) { + throw new IllegalArgumentException("No BOMs specified"); + } + this.include = include; + final List list = Arrays.asList(boms); + // Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + list.sort(ByteOrderMarkLengthComparator); + } + this.boms = list; + + } + + /** + * Indicates whether the stream contains one of the specified BOMs. + * + * @return true if the stream has one of the specified BOMs, otherwise false if it does not + * @throws IOException + * if an error reading the first bytes of the stream occurs + */ + @SuppressWarnings("unused") + public boolean hasBOM() throws IOException { + return getBOM() != null; + } + + /** + * Indicates whether the stream contains the specified BOM. + * + * @param bom + * The BOM to check for + * @return true if the stream has the specified BOM, otherwise false if it does not + * @throws IllegalArgumentException + * if the BOM is not one the stream is configured to detect + * @throws IOException + * if an error reading the first bytes of the stream occurs + */ + @SuppressWarnings("unused") + public boolean hasBOM(final ByteOrderMark bom) throws IOException { + if (!boms.contains(bom)) { + throw new IllegalArgumentException("Stream not configure to detect " + bom); + } + getBOM(); + return byteOrderMark != null && byteOrderMark.equals(bom); + } + + /** + * Return the BOM (Byte Order Mark). + * + * @return The BOM or null if none + * @throws IOException + * if an error reading the first bytes of the stream occurs + */ + public ByteOrderMark getBOM() throws IOException { + if (firstBytes == null) { + fbLength = 0; + // BOMs are sorted from longest to shortest + final int maxBomSize = boms.get(0).length(); + firstBytes = new int[maxBomSize]; + // Read first maxBomSize bytes + for (int i = 0; i < firstBytes.length; i++) { + firstBytes[i] = in.read(); + fbLength++; + if (firstBytes[i] < 0) { + break; + } + } + // match BOM in firstBytes + byteOrderMark = find(); + if (byteOrderMark != null) { + if (!include) { + if (byteOrderMark.length() < firstBytes.length) { + fbIndex = byteOrderMark.length(); + } else { + fbLength = 0; + } + } + } + } + return byteOrderMark; + } + + /** + * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}. + * + * @return The BOM charset Name or null if no BOM found + * @throws IOException + * if an error reading the first bytes of the stream occurs + * + */ + public String getBOMCharsetName() throws IOException { + getBOM(); + return byteOrderMark == null ? null : byteOrderMark.getCharsetName(); + } + + /** + * This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte + * read() method, either returning a valid byte or -1 to indicate that the initial bytes have been + * processed already. + * + * @return the byte read (excluding BOM) or -1 if the end of stream + * @throws IOException + * if an I/O error occurs + */ + private int readFirstBytes() throws IOException { + getBOM(); + return fbIndex < fbLength ? firstBytes[fbIndex++] : EOF; + } + + /** + * Find a BOM with the specified bytes. + * + * @return The matched BOM or null if none matched + */ + private ByteOrderMark find() { + for (final ByteOrderMark bom : boms) { + if (matches(bom)) { + return bom; + } + } + return null; + } + + /** + * Check if the bytes match a BOM. + * + * @param bom + * The BOM + * @return true if the bytes match the bom, otherwise false + */ + private boolean matches(final ByteOrderMark bom) { + // if (bom.length() != fbLength) { + // return false; + // } + // firstBytes may be bigger than the BOM bytes + for (int i = 0; i < bom.length(); i++) { + if (bom.get(i) != firstBytes[i]) { + return false; + } + } + return true; + } + + // ---------------------------------------------------------------------------- + // Implementation of InputStream + // ---------------------------------------------------------------------------- + + /** + * Invokes the delegate's read() method, detecting and optionally skipping BOM. + * + * @return the byte read (excluding BOM) or -1 if the end of stream + * @throws IOException + * if an I/O error occurs + */ + @Override + public int read() throws IOException { + final int b = readFirstBytes(); + return b >= 0 ? b : in.read(); + } + + /** + * Invokes the delegate's read(byte[], int, int) method, detecting and optionally skipping BOM. + * + * @param buf + * the buffer to read the bytes into + * @param off + * The start offset + * @param len + * The number of bytes to read (excluding BOM) + * @return the number of bytes read or -1 if the end of stream + * @throws IOException + * if an I/O error occurs + */ + @Override + public int read(final byte[] buf, int off, int len) throws IOException { + int firstCount = 0; + int b = 0; + while (len > 0 && b >= 0) { + b = readFirstBytes(); + if (b >= 0) { + buf[off++] = (byte) (b & 0xFF); + len--; + firstCount++; + } + } + final int secondCount = in.read(buf, off, len); + return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : firstCount + secondCount; + } + + /** + * Invokes the delegate's read(byte[]) method, detecting and optionally skipping BOM. + * + * @param buf + * the buffer to read the bytes into + * @return the number of bytes read (excluding BOM) or -1 if the end of stream + * @throws IOException + * if an I/O error occurs + */ + @Override + public int read(final byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + /** + * Invokes the delegate's mark(int) method. + * + * @param readlimit + * read ahead limit + */ + @Override + public synchronized void mark(final int readlimit) { + markFbIndex = fbIndex; + markedAtStart = firstBytes == null; + in.mark(readlimit); + } + + /** + * Invokes the delegate's reset() method. + * + * @throws IOException + * if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + fbIndex = markFbIndex; + if (markedAtStart) { + firstBytes = null; + } + + in.reset(); + } + + /** + * Invokes the delegate's skip(long) method, detecting and optionally skipping BOM. + * + * @param n + * the number of bytes to skip + * @return the number of bytes to skipped or -1 if the end of stream + * @throws IOException + * if an I/O error occurs + */ + @Override + public long skip(final long n) throws IOException { + int skipped = 0; + while ((n > skipped) && (readFirstBytes() >= 0)) { + skipped++; + } + return in.skip(n - skipped) + skipped; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java new file mode 100644 index 000000000..3fdb6b8af --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java @@ -0,0 +1,194 @@ +package me.ag2s.epublib.util.commons.io; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.Serializable; +import java.util.Locale; + +/** + * Byte Order Mark (BOM) representation - see {@link BOMInputStream}. + * + * @see BOMInputStream + * @see Wikipedia: Byte Order Mark + * @see W3C: Autodetection of Character Encodings + * (Non-Normative) + * @since 2.0 + */ +public class ByteOrderMark implements Serializable { + + private static final long serialVersionUID = 1L; + + /** UTF-8 BOM */ + public static final ByteOrderMark UTF_8 = new ByteOrderMark("UTF-8", 0xEF, 0xBB, 0xBF); + + /** UTF-16BE BOM (Big-Endian) */ + public static final ByteOrderMark UTF_16BE = new ByteOrderMark("UTF-16BE", 0xFE, 0xFF); + + /** UTF-16LE BOM (Little-Endian) */ + public static final ByteOrderMark UTF_16LE = new ByteOrderMark("UTF-16LE", 0xFF, 0xFE); + + /** + * UTF-32BE BOM (Big-Endian) + * @since 2.2 + */ + public static final ByteOrderMark UTF_32BE = new ByteOrderMark("UTF-32BE", 0x00, 0x00, 0xFE, 0xFF); + + /** + * UTF-32LE BOM (Little-Endian) + * @since 2.2 + */ + public static final ByteOrderMark UTF_32LE = new ByteOrderMark("UTF-32LE", 0xFF, 0xFE, 0x00, 0x00); + + /** + * Unicode BOM character; external form depends on the encoding. + * @see Byte Order Mark (BOM) FAQ + * @since 2.5 + */ + @SuppressWarnings("unused") + public static final char UTF_BOM = '\uFEFF'; + + private final String charsetName; + private final int[] bytes; + + /** + * Construct a new BOM. + * + * @param charsetName The name of the charset the BOM represents + * @param bytes The BOM's bytes + * @throws IllegalArgumentException if the charsetName is null or + * zero length + * @throws IllegalArgumentException if the bytes are null or zero + * length + */ + public ByteOrderMark(final String charsetName, final int... bytes) { + if (charsetName == null || charsetName.isEmpty()) { + throw new IllegalArgumentException("No charsetName specified"); + } + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("No bytes specified"); + } + this.charsetName = charsetName; + this.bytes = new int[bytes.length]; + System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); + } + + /** + * Return the name of the {@link java.nio.charset.Charset} the BOM represents. + * + * @return the character set name + */ + public String getCharsetName() { + return charsetName; + } + + /** + * Return the length of the BOM's bytes. + * + * @return the length of the BOM's bytes + */ + public int length() { + return bytes.length; + } + + /** + * The byte at the specified position. + * + * @param pos The position + * @return The specified byte + */ + public int get(final int pos) { + return bytes[pos]; + } + + /** + * Return a copy of the BOM's bytes. + * + * @return a copy of the BOM's bytes + */ + public byte[] getBytes() { + final byte[] copy = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + copy[i] = (byte)bytes[i]; + } + return copy; + } + + /** + * Indicates if this BOM's bytes equals another. + * + * @param obj The object to compare to + * @return true if the bom's bytes are equal, otherwise + * false + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof ByteOrderMark)) { + return false; + } + final ByteOrderMark bom = (ByteOrderMark)obj; + if (bytes.length != bom.length()) { + return false; + } + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] != bom.get(i)) { + return false; + } + } + return true; + } + + /** + * Return the hashcode for this BOM. + * + * @return the hashcode for this BOM. + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int hashCode = getClass().hashCode(); + for (final int b : bytes) { + hashCode += b; + } + return hashCode; + } + + /** + * Provide a String representation of the BOM. + * + * @return the length of the BOM's bytes + */ + @Override + @SuppressWarnings("NullableProblems") + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getClass().getSimpleName()); + builder.append('['); + builder.append(charsetName); + builder.append(": "); + for (int i = 0; i < bytes.length; i++) { + if (i > 0) { + builder.append(","); + } + builder.append("0x"); + builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase(Locale.ROOT)); + } + builder.append(']'); + return builder.toString(); + } + +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java new file mode 100644 index 000000000..b4e024f71 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ag2s.epublib.util.commons.io; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Like {@link Consumer} but throws {@link IOException}. + * + * @param the type of the input to the operations. + * @since 2.7 + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @throws IOException if an I/O error occurs. + */ + void accept(T t) throws IOException; + + /** + * Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after} + * operation. If performing either operation throws an exception, it is relayed to the caller of the composed + * operation. If performing this operation throws an exception, the {@code after} operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} + * operation + * @throws NullPointerException if {@code after} is null + */ + @SuppressWarnings("unused") + default IOConsumer andThen(final IOConsumer after) { + Objects.requireNonNull(after); + return (final T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java new file mode 100644 index 000000000..3fe2415ab --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java @@ -0,0 +1,252 @@ +package me.ag2s.epublib.util.commons.io; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import me.ag2s.epublib.util.IOUtil; + +import static me.ag2s.epublib.util.IOUtil.EOF; + + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. + *

    + * It is an alternative base class to FilterInputStream + * to increase reusability, because FilterInputStream changes the + * methods being called, such as read(byte[]) to read(byte[], int, int). + *

    + *

    + * See the protected methods for ways in which a subclass can easily decorate + * a stream with custom pre-, post- or error processing functionality. + *

    + */ +public abstract class ProxyInputStream extends FilterInputStream { + + /** + * Constructs a new ProxyInputStream. + * + * @param proxy the InputStream to delegate to + */ + public ProxyInputStream(final InputStream proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'in' + } + + /** + * Invokes the delegate's read() method. + * + * @return the byte read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + try { + beforeRead(1); + final int b = in.read(); + afterRead(b != EOF ? 1 : EOF); + return b; + } catch (final IOException e) { + handleIOException(e); + return EOF; + } + } + + /** + * Invokes the delegate's read(byte[]) method. + * + * @param bts the buffer to read the bytes into + * @return the number of bytes read or EOF if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(final byte[] bts) throws IOException { + try { + beforeRead(IOUtil.length(bts)); + final int n = in.read(bts); + afterRead(n); + return n; + } catch (final IOException e) { + handleIOException(e); + return EOF; + } + } + + /** + * Invokes the delegate's read(byte[], int, int) method. + * + * @param bts the buffer to read the bytes into + * @param off The start offset + * @param len The number of bytes to read + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(final byte[] bts, final int off, final int len) throws IOException { + try { + beforeRead(len); + final int n = in.read(bts, off, len); + afterRead(n); + return n; + } catch (final IOException e) { + handleIOException(e); + return EOF; + } + } + + /** + * Invokes the delegate's skip(long) method. + * + * @param ln the number of bytes to skip + * @return the actual number of bytes skipped + * @throws IOException if an I/O error occurs + */ + @Override + public long skip(final long ln) throws IOException { + try { + return in.skip(ln); + } catch (final IOException e) { + handleIOException(e); + return 0; + } + } + + /** + * Invokes the delegate's available() method. + * + * @return the number of available bytes + * @throws IOException if an I/O error occurs + */ + @Override + public int available() throws IOException { + try { + return super.available(); + } catch (final IOException e) { + handleIOException(e); + return 0; + } + } + + /** + * Invokes the delegate's close() method. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + IOUtil.close(in, this::handleIOException); + } + + /** + * Invokes the delegate's mark(int) method. + * + * @param readlimit read ahead limit + */ + @Override + public synchronized void mark(final int readlimit) { + in.mark(readlimit); + } + + /** + * Invokes the delegate's reset() method. + * + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void reset() throws IOException { + try { + in.reset(); + } catch (final IOException e) { + handleIOException(e); + } + } + + /** + * Invokes the delegate's markSupported() method. + * + * @return true if mark is supported, otherwise false + */ + @Override + public boolean markSupported() { + return in.markSupported(); + } + + /** + * Invoked by the read methods before the call is proxied. The number + * of bytes that the caller wanted to read (1 for the {@link #read()} + * method, buffer length for {@link #read(byte[])}, etc.) is given as + * an argument. + *

    + * Subclasses can override this method to add common pre-processing + * functionality without having to override all the read methods. + * The default implementation does nothing. + *

    + * Note this method is not called from {@link #skip(long)} or + * {@link #reset()}. You need to explicitly override those methods if + * you want to add pre-processing steps also to them. + * + * @param n number of bytes that the caller asked to be read + * @since 2.0 + */ + @SuppressWarnings("unused") + + protected void beforeRead(final int n) { + // no-op + } + + /** + * Invoked by the read methods after the proxied call has returned + * successfully. The number of bytes returned to the caller (or -1 if + * the end of stream was reached) is given as an argument. + *

    + * Subclasses can override this method to add common post-processing + * functionality without having to override all the read methods. + * The default implementation does nothing. + *

    + * Note this method is not called from {@link #skip(long)} or + * {@link #reset()}. You need to explicitly override those methods if + * you want to add post-processing steps also to them. + * + * @param n number of bytes read, or -1 if the end of stream was reached + * @since 2.0 + */ + @SuppressWarnings("unused") + protected void afterRead(final int n) { + // no-op + } + + /** + * Handle any IOExceptions thrown. + *

    + * This method provides a point to implement custom exception + * handling. The default behavior is to re-throw the exception. + * + * @param e The IOException thrown + * @throws IOException if an I/O error occurs + * @since 2.0 + */ + protected void handleIOException(final IOException e) throws IOException { + throw e; + } + +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java new file mode 100644 index 000000000..3499354be --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java @@ -0,0 +1,806 @@ +package me.ag2s.epublib.util.commons.io; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.text.MessageFormat; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import me.ag2s.epublib.util.IOUtil; + + +/** + * Character stream that handles all the necessary Voodoo to figure out the + * charset encoding of the XML document within the stream. + *

    + * IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader. + * This one IS a character stream. + *

    + *

    + * All this has to be done without consuming characters from the stream, if not + * the XML parser will not recognized the document as a valid XML. This is not + * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers + * right now, XmlStreamReader handles it and things work in all parsers). + *

    + *

    + * The XmlStreamReader class handles the charset encoding of XML documents in + * Files, raw streams and HTTP streams by offering a wide set of constructors. + *

    + *

    + * By default the charset encoding detection is lenient, the constructor with + * the lenient flag can be used for a script (following HTTP MIME and XML + * specifications). All this is nicely explained by Mark Pilgrim in his blog, + * Determining the character encoding of a feed. + *

    + *

    + * Originally developed for ROME under + * Apache License 2.0. + *

    + * + * //@seerr XmlStreamWriter + * @since 2.0 + */ +public class XmlStreamReader extends Reader { + private static final int BUFFER_SIZE = IOUtil.DEFAULT_BUFFER_SIZE; + + private static final String UTF_8 = "UTF-8"; + + private static final String US_ASCII = "US-ASCII"; + + private static final String UTF_16BE = "UTF-16BE"; + + private static final String UTF_16LE = "UTF-16LE"; + + private static final String UTF_32BE = "UTF-32BE"; + + private static final String UTF_32LE = "UTF-32LE"; + + private static final String UTF_16 = "UTF-16"; + + private static final String UTF_32 = "UTF-32"; + + private static final String EBCDIC = "CP1047"; + + private static final ByteOrderMark[] BOMS = new ByteOrderMark[] { + ByteOrderMark.UTF_8, + ByteOrderMark.UTF_16BE, + ByteOrderMark.UTF_16LE, + ByteOrderMark.UTF_32BE, + ByteOrderMark.UTF_32LE + }; + + // UTF_16LE and UTF_32LE have the same two starting BOM bytes. + private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] { + new ByteOrderMark(UTF_8, 0x3C, 0x3F, 0x78, 0x6D), + new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F), + new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00), + new ByteOrderMark(UTF_32BE, 0x00, 0x00, 0x00, 0x3C, + 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D), + new ByteOrderMark(UTF_32LE, 0x3C, 0x00, 0x00, 0x00, + 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00), + new ByteOrderMark(EBCDIC, 0x4C, 0x6F, 0xA7, 0x94) + }; + + private final Reader reader; + + private final String encoding; + + private final String defaultEncoding; + + /** + * Returns the default encoding to use if none is set in HTTP content-type, + * XML prolog and the rules based on content-type are not adequate. + *

    + * If it is NULL the content-type based rules are used. + * + * @return the default encoding to use. + */ + public String getDefaultEncoding() { + return defaultEncoding; + } + + /** + * Creates a Reader for a File. + *

    + * It looks for the UTF-8 BOM first, if none sniffs the XML prolog charset, + * if this is also missing defaults to UTF-8. + *

    + * It does a lenient charset encoding detection, check the constructor with + * the lenient parameter for details. + * + * @param file File to create a Reader from. + * @throws IOException thrown if there is a problem reading the file. + */ + @SuppressWarnings("unused") + public XmlStreamReader(final File file) throws IOException { + this(new FileInputStream(Objects.requireNonNull(file))); + } + + /** + * Creates a Reader for a raw InputStream. + *

    + * It follows the same logic used for files. + *

    + * It does a lenient charset encoding detection, check the constructor with + * the lenient parameter for details. + * + * @param inputStream InputStream to create a Reader from. + * @throws IOException thrown if there is a problem reading the stream. + */ + public XmlStreamReader(final InputStream inputStream) throws IOException { + this(inputStream, true); + } + + /** + * Creates a Reader for a raw InputStream. + *

    + * It follows the same logic used for files. + *

    + * If lenient detection is indicated and the detection above fails as per + * specifications it then attempts the following: + *

    + * If the content type was 'text/html' it replaces it with 'text/xml' and + * tries the detection again. + *

    + * Else if the XML prolog had a charset encoding that encoding is used. + *

    + * Else if the content type had a charset encoding that encoding is used. + *

    + * Else 'UTF-8' is used. + *

    + * If lenient detection is indicated an XmlStreamReaderException is never + * thrown. + * + * @param inputStream InputStream to create a Reader from. + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @throws IOException thrown if there is a problem reading the stream. + * @throws XmlStreamReaderException thrown if the charset encoding could not + * be determined according to the specs. + */ + public XmlStreamReader(final InputStream inputStream, final boolean lenient) throws IOException { + this(inputStream, lenient, null); + } + + /** + * Creates a Reader for a raw InputStream. + *

    + * It follows the same logic used for files. + *

    + * If lenient detection is indicated and the detection above fails as per + * specifications it then attempts the following: + *

    + * If the content type was 'text/html' it replaces it with 'text/xml' and + * tries the detection again. + *

    + * Else if the XML prolog had a charset encoding that encoding is used. + *

    + * Else if the content type had a charset encoding that encoding is used. + *

    + * Else 'UTF-8' is used. + *

    + * If lenient detection is indicated an XmlStreamReaderException is never + * thrown. + * + * @param inputStream InputStream to create a Reader from. + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @param defaultEncoding The default encoding + * @throws IOException thrown if there is a problem reading the stream. + * @throws XmlStreamReaderException thrown if the charset encoding could not + * be determined according to the specs. + */ + public XmlStreamReader(final InputStream inputStream, final boolean lenient, final String defaultEncoding) + throws IOException { + Objects.requireNonNull(inputStream, "inputStream"); + this.defaultEncoding = defaultEncoding; + final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); + final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); + this.encoding = doRawStream(bom, pis, lenient); + this.reader = new InputStreamReader(pis, encoding); + } + + /** + * Creates a Reader using the InputStream of a URL. + *

    + * If the URL is not of type HTTP and there is not 'content-type' header in + * the fetched data it uses the same logic used for Files. + *

    + * If the URL is a HTTP Url or there is a 'content-type' header in the + * fetched data it uses the same logic used for an InputStream with + * content-type. + *

    + * It does a lenient charset encoding detection, check the constructor with + * the lenient parameter for details. + * + * @param url URL to create a Reader from. + * @throws IOException thrown if there is a problem reading the stream of + * the URL. + */ + @SuppressWarnings("unused") + public XmlStreamReader(final URL url) throws IOException { + this(Objects.requireNonNull(url, "url").openConnection(), null); + } + + /** + * Creates a Reader using the InputStream of a URLConnection. + *

    + * If the URLConnection is not of type HttpURLConnection and there is not + * 'content-type' header in the fetched data it uses the same logic used for + * files. + *

    + * If the URLConnection is a HTTP Url or there is a 'content-type' header in + * the fetched data it uses the same logic used for an InputStream with + * content-type. + *

    + * It does a lenient charset encoding detection, check the constructor with + * the lenient parameter for details. + * + * @param conn URLConnection to create a Reader from. + * @param defaultEncoding The default encoding + * @throws IOException thrown if there is a problem reading the stream of + * the URLConnection. + */ + public XmlStreamReader(final URLConnection conn, final String defaultEncoding) throws IOException { + Objects.requireNonNull(conn, "conm"); + this.defaultEncoding = defaultEncoding; + final boolean lenient = true; + final String contentType = conn.getContentType(); + final InputStream inputStream = conn.getInputStream(); + final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); + final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); + if (conn instanceof HttpURLConnection || contentType != null) { + this.encoding = processHttpStream(bom, pis, contentType, lenient); + } else { + this.encoding = doRawStream(bom, pis, lenient); + } + this.reader = new InputStreamReader(pis, encoding); + } + + /** + * Creates a Reader using an InputStream and the associated content-type + * header. + *

    + * First it checks if the stream has BOM. If there is not BOM checks the + * content-type encoding. If there is not content-type encoding checks the + * XML prolog encoding. If there is not XML prolog encoding uses the default + * encoding mandated by the content-type MIME type. + *

    + * It does a lenient charset encoding detection, check the constructor with + * the lenient parameter for details. + * + * @param inputStream InputStream to create the reader from. + * @param httpContentType content-type header to use for the resolution of + * the charset encoding. + * @throws IOException thrown if there is a problem reading the file. + */ + public XmlStreamReader(final InputStream inputStream, final String httpContentType) + throws IOException { + this(inputStream, httpContentType, true); + } + + /** + * Creates a Reader using an InputStream and the associated content-type + * header. This constructor is lenient regarding the encoding detection. + *

    + * First it checks if the stream has BOM. If there is not BOM checks the + * content-type encoding. If there is not content-type encoding checks the + * XML prolog encoding. If there is not XML prolog encoding uses the default + * encoding mandated by the content-type MIME type. + *

    + * If lenient detection is indicated and the detection above fails as per + * specifications it then attempts the following: + *

    + * If the content type was 'text/html' it replaces it with 'text/xml' and + * tries the detection again. + *

    + * Else if the XML prolog had a charset encoding that encoding is used. + *

    + * Else if the content type had a charset encoding that encoding is used. + *

    + * Else 'UTF-8' is used. + *

    + * If lenient detection is indicated an XmlStreamReaderException is never + * thrown. + * + * @param inputStream InputStream to create the reader from. + * @param httpContentType content-type header to use for the resolution of + * the charset encoding. + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @param defaultEncoding The default encoding + * @throws IOException thrown if there is a problem reading the file. + * @throws XmlStreamReaderException thrown if the charset encoding could not + * be determined according to the specs. + */ + public XmlStreamReader(final InputStream inputStream, final String httpContentType, + final boolean lenient, final String defaultEncoding) throws IOException { + Objects.requireNonNull(inputStream, "inputStream"); + this.defaultEncoding = defaultEncoding; + final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS); + final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES); + this.encoding = processHttpStream(bom, pis, httpContentType, lenient); + this.reader = new InputStreamReader(pis, encoding); + } + + /** + * Creates a Reader using an InputStream and the associated content-type + * header. This constructor is lenient regarding the encoding detection. + *

    + * First it checks if the stream has BOM. If there is not BOM checks the + * content-type encoding. If there is not content-type encoding checks the + * XML prolog encoding. If there is not XML prolog encoding uses the default + * encoding mandated by the content-type MIME type. + *

    + * If lenient detection is indicated and the detection above fails as per + * specifications it then attempts the following: + *

    + * If the content type was 'text/html' it replaces it with 'text/xml' and + * tries the detection again. + *

    + * Else if the XML prolog had a charset encoding that encoding is used. + *

    + * Else if the content type had a charset encoding that encoding is used. + *

    + * Else 'UTF-8' is used. + *

    + * If lenient detection is indicated an XmlStreamReaderException is never + * thrown. + * + * @param inputStream InputStream to create the reader from. + * @param httpContentType content-type header to use for the resolution of + * the charset encoding. + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @throws IOException thrown if there is a problem reading the file. + * @throws XmlStreamReaderException thrown if the charset encoding could not + * be determined according to the specs. + */ + public XmlStreamReader(final InputStream inputStream, final String httpContentType, + final boolean lenient) throws IOException { + this(inputStream, httpContentType, lenient, null); + } + + /** + * Returns the charset encoding of the XmlStreamReader. + * + * @return charset encoding. + */ + public String getEncoding() { + return encoding; + } + + /** + * Invokes the underlying reader's read(char[], int, int) method. + * @param buf the buffer to read the characters into + * @param offset The start offset + * @param len The number of bytes to read + * @return the number of characters read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + @Override + public int read(final char[] buf, final int offset, final int len) throws IOException { + return reader.read(buf, offset, len); + } + + /** + * Closes the XmlStreamReader stream. + * + * @throws IOException thrown if there was a problem closing the stream. + */ + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * Process the raw stream. + * + * @param bom BOMInputStream to detect byte order marks + * @param pis BOMInputStream to guess XML encoding + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @return the encoding to be used + * @throws IOException thrown if there is a problem reading the stream. + */ + private String doRawStream(final BOMInputStream bom, final BOMInputStream pis, final boolean lenient) + throws IOException { + final String bomEnc = bom.getBOMCharsetName(); + final String xmlGuessEnc = pis.getBOMCharsetName(); + final String xmlEnc = getXmlProlog(pis, xmlGuessEnc); + try { + return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); + } catch (final XmlStreamReaderException ex) { + if (lenient) { + return doLenientDetection(null, ex); + } + throw ex; + } + } + + /** + * Process a HTTP stream. + * + * @param bom BOMInputStream to detect byte order marks + * @param pis BOMInputStream to guess XML encoding + * @param httpContentType The HTTP content type + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @return the encoding to be used + * @throws IOException thrown if there is a problem reading the stream. + */ + private String processHttpStream(final BOMInputStream bom, final BOMInputStream pis, final String httpContentType, + final boolean lenient) throws IOException { + final String bomEnc = bom.getBOMCharsetName(); + final String xmlGuessEnc = pis.getBOMCharsetName(); + final String xmlEnc = getXmlProlog(pis, xmlGuessEnc); + try { + return calculateHttpEncoding(httpContentType, bomEnc, xmlGuessEnc, xmlEnc, lenient); + } catch (final XmlStreamReaderException ex) { + if (lenient) { + return doLenientDetection(httpContentType, ex); + } + throw ex; + } + } + + /** + * Do lenient detection. + * + * @param httpContentType content-type header to use for the resolution of + * the charset encoding. + * @param ex The thrown exception + * @return the encoding + * @throws IOException thrown if there is a problem reading the stream. + */ + private String doLenientDetection(String httpContentType, + XmlStreamReaderException ex) throws IOException { + if (httpContentType != null && httpContentType.startsWith("text/html")) { + httpContentType = httpContentType.substring("text/html".length()); + httpContentType = "text/xml" + httpContentType; + try { + return calculateHttpEncoding(httpContentType, ex.getBomEncoding(), + ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true); + } catch (final XmlStreamReaderException ex2) { + ex = ex2; + } + } + String encoding = ex.getXmlEncoding(); + if (encoding == null) { + encoding = ex.getContentTypeEncoding(); + } + if (encoding == null) { + encoding = defaultEncoding == null ? UTF_8 : defaultEncoding; + } + return encoding; + } + + /** + * Calculate the raw encoding. + * + * @param bomEnc BOM encoding + * @param xmlGuessEnc XML Guess encoding + * @param xmlEnc XML encoding + * @return the raw encoding + * @throws IOException thrown if there is a problem reading the stream. + */ + String calculateRawEncoding(final String bomEnc, final String xmlGuessEnc, + final String xmlEnc) throws IOException { + + // BOM is Null + if (bomEnc == null) { + if (xmlGuessEnc == null || xmlEnc == null) { + return defaultEncoding == null ? UTF_8 : defaultEncoding; + } + if (xmlEnc.equals(UTF_16) && + (xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) { + return xmlGuessEnc; + } + return xmlEnc; + } + + // BOM is UTF-8 + if (bomEnc.equals(UTF_8)) { + if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + if (xmlEnc != null && !xmlEnc.equals(UTF_8)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + return bomEnc; + } + + // BOM is UTF-16BE or UTF-16LE + if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) { + if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + return bomEnc; + } + + // BOM is UTF-32BE or UTF-32LE + if (bomEnc.equals(UTF_32BE) || bomEnc.equals(UTF_32LE)) { + if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + if (xmlEnc != null && !xmlEnc.equals(UTF_32) && !xmlEnc.equals(bomEnc)) { + final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + return bomEnc; + } + + // BOM is something else + final String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc); + } + + + /** + * Calculate the HTTP encoding. + * + * @param httpContentType The HTTP content type + * @param bomEnc BOM encoding + * @param xmlGuessEnc XML Guess encoding + * @param xmlEnc XML encoding + * @param lenient indicates if the charset encoding detection should be + * relaxed. + * @return the HTTP encoding + * @throws IOException thrown if there is a problem reading the stream. + */ + String calculateHttpEncoding(final String httpContentType, + final String bomEnc, final String xmlGuessEnc, final String xmlEnc, + final boolean lenient) throws IOException { + + // Lenient and has XML encoding + if (lenient && xmlEnc != null) { + return xmlEnc; + } + + // Determine mime/encoding content types from HTTP Content Type + final String cTMime = getContentTypeMime(httpContentType); + final String cTEnc = getContentTypeEncoding(httpContentType); + final boolean appXml = isAppXml(cTMime); + final boolean textXml = isTextXml(cTMime); + + // Mime type NOT "application/xml" or "text/xml" + if (!appXml && !textXml) { + final String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + } + + // No content type encoding + if (cTEnc == null) { + if (appXml) { + return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc); + } + return defaultEncoding == null ? US_ASCII : defaultEncoding; + } + + // UTF-16BE or UTF-16LE content type encoding + if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) { + if (bomEnc != null) { + final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + } + return cTEnc; + } + + // UTF-16 content type encoding + if (cTEnc.equals(UTF_16)) { + if (bomEnc != null && bomEnc.startsWith(UTF_16)) { + return bomEnc; + } + final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + } + + // UTF-32BE or UTF-132E content type encoding + if (cTEnc.equals(UTF_32BE) || cTEnc.equals(UTF_32LE)) { + if (bomEnc != null) { + final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + } + return cTEnc; + } + + // UTF-32 content type encoding + if (cTEnc.equals(UTF_32)) { + if (bomEnc != null && bomEnc.startsWith(UTF_32)) { + return bomEnc; + } + final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc); + } + + return cTEnc; + } + + /** + * Returns MIME type or NULL if httpContentType is NULL. + * + * @param httpContentType the HTTP content type + * @return The mime content type + */ + static String getContentTypeMime(final String httpContentType) { + String mime = null; + if (httpContentType != null) { + final int i = httpContentType.indexOf(";"); + if (i >= 0) { + mime = httpContentType.substring(0, i); + } else { + mime = httpContentType; + } + mime = mime.trim(); + } + return mime; + } + + private static final Pattern CHARSET_PATTERN = Pattern + .compile("charset=[\"']?([.[^; \"']]*)[\"']?"); + + /** + * Returns charset parameter value, NULL if not present, NULL if + * httpContentType is NULL. + * + * @param httpContentType the HTTP content type + * @return The content type encoding (upcased) + */ + static String getContentTypeEncoding(final String httpContentType) { + String encoding = null; + if (httpContentType != null) { + final int i = httpContentType.indexOf(";"); + if (i > -1) { + final String postMime = httpContentType.substring(i + 1); + final Matcher m = CHARSET_PATTERN.matcher(postMime); + encoding = m.find() ? m.group(1) : null; + encoding = encoding != null ? encoding.toUpperCase(Locale.ROOT) : null; + } + } + return encoding; + } + + /** + * Pattern capturing the encoding of the "xml" processing instruction. + */ + public static final Pattern ENCODING_PATTERN = Pattern.compile( + "<\\?xml.*encoding[\\s]*=[\\s]*((?:\".[^\"]*\")|(?:'.[^']*'))", + Pattern.MULTILINE); + + /** + * Returns the encoding declared in the , NULL if none. + * + * @param inputStream InputStream to create the reader from. + * @param guessedEnc guessed encoding + * @return the encoding declared in the + * @throws IOException thrown if there is a problem reading the stream. + */ + private static String getXmlProlog(final InputStream inputStream, final String guessedEnc) + throws IOException { + String encoding = null; + if (guessedEnc != null) { + final byte[] bytes = new byte[BUFFER_SIZE]; + inputStream.mark(BUFFER_SIZE); + int offset = 0; + int max = BUFFER_SIZE; + int c = inputStream.read(bytes, offset, max); + int firstGT = -1; + String xmlProlog = ""; // avoid possible NPE warning (cannot happen; this just silences the warning) + while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) { + offset += c; + max -= c; + c = inputStream.read(bytes, offset, max); + xmlProlog = new String(bytes, 0, offset, guessedEnc); + firstGT = xmlProlog.indexOf('>'); + } + if (firstGT == -1) { + if (c == -1) { + throw new IOException("Unexpected end of XML stream"); + } + throw new IOException( + "XML prolog or ROOT element not found on first " + + offset + " bytes"); + } + final int bytesRead = offset; + if (bytesRead > 0) { + inputStream.reset(); + final BufferedReader bReader = new BufferedReader(new StringReader( + xmlProlog.substring(0, firstGT + 1))); + final StringBuffer prolog = new StringBuffer(); + String line; + while ((line = bReader.readLine()) != null) { + prolog.append(line); + } + final Matcher m = ENCODING_PATTERN.matcher(prolog); + if (m.find()) { + encoding = Objects.requireNonNull(m.group(1)).toUpperCase(Locale.ROOT); + encoding = encoding.substring(1, encoding.length() - 1); + } + } + } + return encoding; + } + + /** + * Indicates if the MIME type belongs to the APPLICATION XML family. + * + * @param mime The mime type + * @return true if the mime type belongs to the APPLICATION XML family, + * otherwise false + */ + static boolean isAppXml(final String mime) { + return mime != null && + (mime.equals("application/xml") || + mime.equals("application/xml-dtd") || + mime.equals("application/xml-external-parsed-entity") || + mime.startsWith("application/") && mime.endsWith("+xml")); + } + + /** + * Indicates if the MIME type belongs to the TEXT XML family. + * + * @param mime The mime type + * @return true if the mime type belongs to the TEXT XML family, + * otherwise false + */ + static boolean isTextXml(final String mime) { + return mime != null && + (mime.equals("text/xml") || + mime.equals("text/xml-external-parsed-entity") || + mime.startsWith("text/") && mime.endsWith("+xml")); + } + + private static final String RAW_EX_1 = + "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch"; + + private static final String RAW_EX_2 = + "Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM"; + + private static final String HTTP_EX_1 = + "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL"; + + private static final String HTTP_EX_2 = + "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch"; + + private static final String HTTP_EX_3 = + "Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME"; + +} \ No newline at end of file diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java new file mode 100644 index 000000000..a903279d4 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java @@ -0,0 +1,139 @@ +package me.ag2s.epublib.util.commons.io; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException; + +/** + * The XmlStreamReaderException is thrown by the XmlStreamReader constructors if + * the charset encoding can not be determined according to the XML 1.0 + * specification and RFC 3023. + *

    + * The exception returns the unconsumed InputStream to allow the application to + * do an alternate processing with the stream. Note that the original + * InputStream given to the XmlStreamReader cannot be used as that one has been + * already read. + *

    + * + * @since 2.0 + */ +public class XmlStreamReaderException extends IOException { + + private static final long serialVersionUID = 1L; + + private final String bomEncoding; + + private final String xmlGuessEncoding; + + private final String xmlEncoding; + + private final String contentTypeMime; + + private final String contentTypeEncoding; + + /** + * Creates an exception instance if the charset encoding could not be + * determined. + *

    + * Instances of this exception are thrown by the XmlStreamReader. + *

    + * + * @param msg message describing the reason for the exception. + * @param bomEnc BOM encoding. + * @param xmlGuessEnc XML guess encoding. + * @param xmlEnc XML prolog encoding. + */ + public XmlStreamReaderException(final String msg, final String bomEnc, + final String xmlGuessEnc, final String xmlEnc) { + this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc); + } + + /** + * Creates an exception instance if the charset encoding could not be + * determined. + *

    + * Instances of this exception are thrown by the XmlStreamReader. + *

    + * + * @param msg message describing the reason for the exception. + * @param ctMime MIME type in the content-type. + * @param ctEnc encoding in the content-type. + * @param bomEnc BOM encoding. + * @param xmlGuessEnc XML guess encoding. + * @param xmlEnc XML prolog encoding. + */ + public XmlStreamReaderException(final String msg, final String ctMime, final String ctEnc, + final String bomEnc, final String xmlGuessEnc, final String xmlEnc) { + super(msg); + contentTypeMime = ctMime; + contentTypeEncoding = ctEnc; + bomEncoding = bomEnc; + xmlGuessEncoding = xmlGuessEnc; + xmlEncoding = xmlEnc; + } + + /** + * Returns the BOM encoding found in the InputStream. + * + * @return the BOM encoding, null if none. + */ + public String getBomEncoding() { + return bomEncoding; + } + + /** + * Returns the encoding guess based on the first bytes of the InputStream. + * + * @return the encoding guess, null if it couldn't be guessed. + */ + public String getXmlGuessEncoding() { + return xmlGuessEncoding; + } + + /** + * Returns the encoding found in the XML prolog of the InputStream. + * + * @return the encoding of the XML prolog, null if none. + */ + public String getXmlEncoding() { + return xmlEncoding; + } + + /** + * Returns the MIME type in the content-type used to attempt determining the + * encoding. + * + * @return the MIME type in the content-type, null if there was not + * content-type or the encoding detection did not involve HTTP. + */ + public String getContentTypeMime() { + return contentTypeMime; + } + + /** + * Returns the encoding in the content-type used to attempt determining the + * encoding. + * + * @return the encoding in the content-type, null if there was not + * content-type, no encoding in it or the encoding detection did not + * involve HTTP. + */ + public String getContentTypeEncoding() { + return contentTypeEncoding; + } +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/domain/UmdBook.java b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdBook.java new file mode 100644 index 000000000..14f1f895d --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdBook.java @@ -0,0 +1,81 @@ +package me.ag2s.umdlib.domain; + +import java.io.IOException; +import java.io.OutputStream; + +import me.ag2s.umdlib.tool.WrapOutputStream; + +public class UmdBook { + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + private int num; + + + /** Header Part of UMD book */ + private UmdHeader header = new UmdHeader(); + /** + * Detail chapters Part of UMD book + * (include Titles & Contents of each chapter) + */ + private UmdChapters chapters = new UmdChapters(); + + /** Cover Part of UMD book (for example, and JPEG file) */ + private UmdCover cover = new UmdCover(); + + /** End Part of UMD book */ + private UmdEnd end = new UmdEnd(); + + /** + * Build the UMD file. + * @param os + * @throws IOException + */ + public void buildUmd(OutputStream os) throws IOException { + WrapOutputStream wos = new WrapOutputStream(os); + + header.buildHeader(wos); + chapters.buildChapters(wos); + cover.buildCover(wos); + end.buildEnd(wos); + } + + public UmdHeader getHeader() { + return header; + } + + public void setHeader(UmdHeader header) { + this.header = header; + } + + public UmdChapters getChapters() { + return chapters; + } + + public void setChapters(UmdChapters chapters) { + this.chapters = chapters; + } + + public UmdCover getCover() { + return cover; + } + + public void setCover(UmdCover cover) { + this.cover = cover; + } + + public UmdEnd getEnd() { + return end; + } + + public void setEnd(UmdEnd end) { + this.end = end; + } + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/domain/UmdChapters.java b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdChapters.java new file mode 100644 index 000000000..6508e3eb8 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdChapters.java @@ -0,0 +1,207 @@ +package me.ag2s.umdlib.domain; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.DeflaterOutputStream; + +import me.ag2s.umdlib.tool.UmdUtils; +import me.ag2s.umdlib.tool.WrapOutputStream; + +/** + * It includes all titles and contents of each chapter in the UMD file. + * And the content has been compressed by zlib. + * + * @author Ray Liang (liangguanhui@qq.com) + * 2009-12-20 + */ +public class UmdChapters { + + private static final int DEFAULT_CHUNK_INIT_SIZE = 32768; + private int TotalContentLen; + + public List getTitles() { + return titles; + } + + private List titles = new ArrayList<>(); + public List contentLengths = new ArrayList<>(); + public ByteArrayOutputStream contents = new ByteArrayOutputStream(); + + public void addTitle(String s){ + titles.add(UmdUtils.stringToUnicodeBytes(s)); + } + public void addTitle(byte[] s){ + titles.add(s); + } + public void addContentLength(Integer integer){ + contentLengths.add(integer); + } + public int getContentLength(int index){ + return contentLengths.get(index); + } + + public byte[] getContent(int index) { + int st=contentLengths.get(index); + byte[] b=contents.toByteArray(); + int end=index+1 chunkRbList = new ArrayList(); + + while(startPos < allContents.length) { + left = allContents.length - startPos; + len = DEFAULT_CHUNK_INIT_SIZE < left ? DEFAULT_CHUNK_INIT_SIZE : left; + + bos.reset(); + DeflaterOutputStream zos = new DeflaterOutputStream(bos); + zos.write(allContents, startPos, len); + zos.close(); + byte[] chunk = bos.toByteArray(); + + byte[] rb = UmdUtils.genRandomBytes(4); + wos.writeByte('$'); + wos.writeBytes(rb); // 4 random + chunkRbList.add(rb); + wos.writeInt(chunk.length + 9); + wos.write(chunk); + + // end of each chunk + wos.writeBytes('#', 0xF1, 0, 0, 0x15); + wos.write(zero16); + + startPos += len; + chunkCnt++; + } + + // end of all chunks + wos.writeBytes('#', 0x81, 0, 0x01, 0x09); + wos.writeBytes(0, 0, 0, 0); //random numbers + wos.write('$'); + wos.writeBytes(0, 0, 0, 0); //random numbers + wos.writeInt(chunkCnt * 4 + 9); + for (int i = chunkCnt - 1; i >= 0; i--) { + // random. They are as the same as random numbers in the begin of each chunk + // use desc order to output these random + wos.writeBytes(chunkRbList.get(i)); + } + } + + public void addChapter(String title, String content) { + titles.add(UmdUtils.stringToUnicodeBytes(title)); + byte[] b = UmdUtils.stringToUnicodeBytes(content); + contentLengths.add(b.length); + try { + contents.write(b); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void addFile(File f, String title) throws IOException { + byte[] temp = UmdUtils.readFile(f); + String s = new String(temp); + addChapter(title, s); + } + + public void addFile(File f) throws IOException { + String s = f.getName(); + int idx = s.lastIndexOf('.'); + if (idx >= 0) { + s = s.substring(0, idx); + } + addFile(f, s); + } + + public void clearChapters() { + titles.clear(); + contentLengths.clear(); + contents.reset(); + } + + public int getTotalContentLen() { + return TotalContentLen; + } + + public void setTotalContentLen(int totalContentLen) { + TotalContentLen = totalContentLen; + } +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/domain/UmdCover.java b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdCover.java new file mode 100644 index 000000000..b3c155300 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdCover.java @@ -0,0 +1,96 @@ +package me.ag2s.umdlib.domain; + + +import java.io.File; +import java.io.IOException; + +import me.ag2s.umdlib.tool.UmdUtils; +import me.ag2s.umdlib.tool.WrapOutputStream; + + +/** + * This is the cover part of the UMD file. + *

    + * NOTICE: if the "coverData" is empty, it will be skipped when building UMD file. + *

    + * There are 3 ways to load the image data: + *
      + *
    1. new constructor function of UmdCover.
    2. + *
    3. use UmdCover.load function.
    4. + *
    5. use UmdCover.initDefaultCover, it will generate a simple image with text.
    6. + *
    + * @author Ray Liang (liangguanhui@qq.com) + * 2009-12-20 + */ +public class UmdCover { + + private static int DEFAULT_COVER_WIDTH = 120; + private static int DEFAULT_COVER_HEIGHT = 160; + + private byte[] coverData; + + public UmdCover() { + } + + public UmdCover(byte[] coverData) { + this.coverData = coverData; + } + + public void load(File f) throws IOException { + this.coverData = UmdUtils.readFile(f); + } + + public void load(String fileName) throws IOException { + load(new File(fileName)); + } + + public void initDefaultCover(String title) throws IOException { +// BufferedImage img = new BufferedImage(DEFAULT_COVER_WIDTH, DEFAULT_COVER_HEIGHT, BufferedImage.TYPE_INT_RGB); +// Graphics g = img.getGraphics(); +// g.setColor(Color.BLACK); +// g.fillRect(0, 0, img.getWidth(), img.getHeight()); +// g.setColor(Color.WHITE); +// g.setFont(new Font("����", Font.PLAIN, 12)); +// +// FontMetrics fm = g.getFontMetrics(); +// int ascent = fm.getAscent(); +// int descent = fm.getDescent(); +// int strWidth = fm.stringWidth(title); +// int x = (img.getWidth() - strWidth) / 2; +// int y = (img.getHeight() - ascent - descent) / 2; +// g.drawString(title, x, y); +// g.dispose(); +// +// ByteArrayOutputStream baos = new ByteArrayOutputStream(); +// +// JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos); +// JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img); +// param.setQuality(0.5f, false); +// encoder.setJPEGEncodeParam(param); +// encoder.encode(img); +// +// coverData = baos.toByteArray(); + } + + public void buildCover(WrapOutputStream wos) throws IOException { + if (coverData == null || coverData.length == 0) { + return; + } + wos.writeBytes('#', 0x82, 0, 0x01, 0x0A, 0x01); + byte[] rb = UmdUtils.genRandomBytes(4); + wos.writeBytes(rb); //random numbers + wos.write('$'); + wos.writeBytes(rb); //random numbers + wos.writeInt(coverData.length + 9); + wos.write(coverData); + } + + public byte[] getCoverData() { + return coverData; + } + + public void setCoverData(byte[] coverData) { + this.coverData = coverData; + } + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/domain/UmdEnd.java b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdEnd.java new file mode 100644 index 000000000..129712f49 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdEnd.java @@ -0,0 +1,20 @@ +package me.ag2s.umdlib.domain; + +import java.io.IOException; + +import me.ag2s.umdlib.tool.WrapOutputStream; + +/** + * End part of UMD book, nothing to be special + * + * @author Ray Liang (liangguanhui@qq.com) + * 2009-12-20 + */ +public class UmdEnd { + + public void buildEnd(WrapOutputStream wos) throws IOException { + wos.writeBytes('#', 0x0C, 0, 0x01, 0x09); + wos.writeInt(wos.getWritten() + 4); + } + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/domain/UmdHeader.java b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdHeader.java new file mode 100644 index 000000000..389ea388f --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/domain/UmdHeader.java @@ -0,0 +1,162 @@ +package me.ag2s.umdlib.domain; + + +import java.io.IOException; + +import me.ag2s.umdlib.tool.UmdUtils; +import me.ag2s.umdlib.tool.WrapOutputStream; + +/** + * Header of UMD file. + * It includes a lot of properties of header. + * All the properties are String type. + * + * @author Ray Liang (liangguanhui@qq.com) + * 2009-12-20 + */ +public class UmdHeader { + public byte getUmdType() { + return umdType; + } + + public void setUmdType(byte umdType) { + this.umdType = umdType; + } + + private byte umdType; + private String title; + + private String author; + + private String year; + + private String month; + + private String day; + + private String bookType; + + private String bookMan; + + private String shopKeeper; + private final static byte B_type_umd = (byte) 0x01; + private final static byte B_type_title = (byte) 0x02; + private final static byte B_type_author = (byte) 0x03; + private final static byte B_type_year = (byte) 0x04; + private final static byte B_type_month = (byte) 0x05; + private final static byte B_type_day = (byte) 0x06; + private final static byte B_type_bookType = (byte) 0x07; + private final static byte B_type_bookMan = (byte) 0x08; + private final static byte B_type_shopKeeper = (byte) 0x09; + + public void buildHeader(WrapOutputStream wos) throws IOException { + wos.writeBytes(0x89, 0x9b, 0x9a, 0xde); // UMD file type flags + wos.writeByte('#'); + wos.writeBytes(0x01, 0x00, 0x00, 0x08); // Unknown + wos.writeByte(0x01); //0x01 is text type; while 0x02 is Image type. + wos.writeBytes(UmdUtils.genRandomBytes(2)); //random number + + // start properties output + buildType(wos, B_type_title, getTitle()); + buildType(wos, B_type_author, getAuthor()); + buildType(wos, B_type_year, getYear()); + buildType(wos, B_type_month, getMonth()); + buildType(wos, B_type_day, getDay()); + buildType(wos, B_type_bookType, getBookType()); + buildType(wos, B_type_bookMan, getBookMan()); + buildType(wos, B_type_shopKeeper, getShopKeeper()); + } + + public void buildType(WrapOutputStream wos, byte type, String content) throws IOException { + if (content == null || content.length() == 0) { + return; + } + + wos.writeBytes('#', type, 0, 0); + + byte[] temp = UmdUtils.stringToUnicodeBytes(content); + wos.writeByte(temp.length + 5); + wos.write(temp); + } + + + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getBookMan() { + return bookMan; + } + + public void setBookMan(String bookMan) { + this.bookMan = bookMan; + } + + public String getShopKeeper() { + return shopKeeper; + } + + public void setShopKeeper(String shopKeeper) { + this.shopKeeper = shopKeeper; + } + + public String getYear() { + return year; + } + + public void setYear(String year) { + this.year = year; + } + + public String getMonth() { + return month; + } + + public void setMonth(String month) { + this.month = month; + } + + public String getDay() { + return day; + } + + public void setDay(String day) { + this.day = day; + } + + public String getBookType() { + return bookType; + } + + public void setBookType(String bookType) { + this.bookType = bookType; + } + + @Override + public String toString() { + return "UmdHeader{" + + "umdType=" + umdType + + ", title='" + title + '\'' + + ", author='" + author + '\'' + + ", year='" + year + '\'' + + ", month='" + month + '\'' + + ", day='" + day + '\'' + + ", bookType='" + bookType + '\'' + + ", bookMan='" + bookMan + '\'' + + ", shopKeeper='" + shopKeeper + '\'' + + '}'; + } +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/tool/StreamReader.java b/epublib/src/main/java/me/ag2s/umdlib/tool/StreamReader.java new file mode 100644 index 000000000..663e73f6b --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/tool/StreamReader.java @@ -0,0 +1,124 @@ +package me.ag2s.umdlib.tool; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class StreamReader { + private InputStream is; + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + private long offset; + private long size; + + private void incCount(int value) { + int temp = (int) (offset + value); + if (temp < 0) { + temp = Integer.MAX_VALUE; + } + offset = temp; + } + public StreamReader(InputStream inputStream) throws IOException { + this.is=inputStream; + //this.size=inputStream.getChannel().size(); + } + + public short readUint8() throws IOException { + byte[] b=new byte[1]; + is.read(b); + incCount(1); + return (short) ((b[0] & 0xFF)); + + } + + public byte readByte() throws IOException { + byte[] b=new byte[1]; + is.read(b); + incCount(1); + return b[0]; + } + public byte[] readBytes(int len) throws IOException { + if (len<1){ + System.out.println(len); + throw new IllegalArgumentException("Length must > 0: " + len); + } + byte[] b=new byte[len]; + is.read(b); + incCount(len); + return b; + } + public String readHex(int len) throws IOException { + if (len<1){ + System.out.println(len); + throw new IllegalArgumentException("Length must > 0: " + len); + } + byte[] b=new byte[len]; + is.read(b); + incCount(len); + return UmdUtils.toHex(b); + } + + public short readShort() throws IOException { + byte[] b=new byte[2]; + is.read(b); + incCount(2); + short x = (short) (((b[0] & 0xFF) << 8) | ((b[1] & 0xFF) << 0)); + return x; + } + public short readShortLe() throws IOException { + byte[] b=new byte[2]; + is.read(b); + incCount(2); + short x = (short) (((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0)); + return x; + } + public int readInt() throws IOException { + byte[] b=new byte[4]; + is.read(b); + incCount(4); + int x = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) | + ((b[2] & 0xFF) << 8) | ((b[3] & 0xFF) << 0); + return x; + } + public int readIntLe() throws IOException { + byte[] b=new byte[4]; + is.read(b); + incCount(4); + int x = ((b[3] & 0xFF) << 24) | ((b[2] & 0xFF) << 16) | + ((b[1] & 0xFF) << 8) | ((b[0] & 0xFF) << 0); + return x; + } + public void skip(int len) throws IOException { + readBytes(len); + } + + + public byte[] read(byte[] b) throws IOException { + is.read(b); + incCount(b.length); + return b; + } + + public byte[] read(byte[] b, int off, int len) throws IOException { + is.read(b, off, len); + incCount(len); + return b; + } + + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/tool/UmdUtils.java b/epublib/src/main/java/me/ag2s/umdlib/tool/UmdUtils.java new file mode 100644 index 000000000..2fa804d58 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/tool/UmdUtils.java @@ -0,0 +1,159 @@ + +package me.ag2s.umdlib.tool; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Random; +import java.util.zip.InflaterInputStream; + + +public class UmdUtils { + + private static final int EOF = -1; + private static final int BUFFER_SIZE = 8 * 1024; + + + /** + * 将字符串编码成Unicode形式的byte[] + * @param s 要编码的字符串 + * @return 编码好的byte[] + */ + public static byte[] stringToUnicodeBytes(String s) { + if (s == null) { + throw new NullPointerException(); + } + + int len = s.length(); + byte[] ret = new byte[len * 2]; + int a, b, c; + for (int i = 0; i < len; i++) { + c = s.charAt(i); + a = c >> 8; + b = c & 0xFF; + if (a < 0) { + a += 0xFF; + } + if (b < 0) { + b += 0xFF; + } + ret[i * 2] = (byte) b; + ret[i * 2 + 1] = (byte) a; + } + return ret; + } + + /** + * 将编码成Unicode形式的byte[]解码成原始字符串 + * @param bytes 编码成Unicode形式的byte[] + * @return 原始字符串 + */ + public static String unicodeBytesToString(byte[] bytes){ + char[] s=new char[bytes.length/2]; + StringBuilder sb=new StringBuilder(); + int a,b,c; + for(int i=0;i= 0) { + baos.write(ch); + } + baos.flush(); + return baos.toByteArray(); + } finally { + fis.close(); + } + } + + private static Random random = new Random(); + + public static byte[] genRandomBytes(int len) { + if (len <= 0) { + throw new IllegalArgumentException("Length must > 0: " + len); + } + byte[] ret = new byte[len]; + for (int i = 0; i < ret.length; i++) { + ret[i] = (byte) random.nextInt(256); + } + return ret; + } + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java b/epublib/src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java new file mode 100644 index 000000000..80ec1982e --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java @@ -0,0 +1,91 @@ +package me.ag2s.umdlib.tool; + +import java.io.IOException; +import java.io.OutputStream; + +public class WrapOutputStream extends OutputStream { + + private OutputStream os; + private int written; + + public WrapOutputStream(OutputStream os) { + this.os = os; + } + + private void incCount(int value) { + int temp = written + value; + if (temp < 0) { + temp = Integer.MAX_VALUE; + } + written = temp; + } + + // it is different from the writeInt of DataOutputStream + public void writeInt(int v) throws IOException { + os.write((v >>> 0) & 0xFF); + os.write((v >>> 8) & 0xFF); + os.write((v >>> 16) & 0xFF); + os.write((v >>> 24) & 0xFF); + incCount(4); + } + + public void writeByte(byte b) throws IOException { + write(b); + } + + public void writeByte(int n) throws IOException { + write(n); + } + + public void writeBytes(byte ... bytes) throws IOException { + write(bytes); + } + + public void writeBytes(int ... vals) throws IOException { + for (int v : vals) { + write(v); + } + } + + public void write(byte[] b, int off, int len) throws IOException { + os.write(b, off, len); + incCount(len); + } + + public void write(byte[] b) throws IOException { + os.write(b); + incCount(b.length); + } + + public void write(int b) throws IOException { + os.write(b); + incCount(1); + } + + ///////////////////////////////////////////////// + + public void close() throws IOException { + os.close(); + } + + public void flush() throws IOException { + os.flush(); + } + + public boolean equals(Object obj) { + return os.equals(obj); + } + + public int hashCode() { + return os.hashCode(); + } + + public String toString() { + return os.toString(); + } + + public int getWritten() { + return written; + } + +} diff --git a/epublib/src/main/java/me/ag2s/umdlib/umd/UmdReader.java b/epublib/src/main/java/me/ag2s/umdlib/umd/UmdReader.java new file mode 100644 index 000000000..804973905 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/umdlib/umd/UmdReader.java @@ -0,0 +1,222 @@ +package me.ag2s.umdlib.umd; + +import java.io.IOException; +import java.io.InputStream; + + +import me.ag2s.umdlib.domain.UmdBook; +import me.ag2s.umdlib.domain.UmdCover; +import me.ag2s.umdlib.domain.UmdHeader; +import me.ag2s.umdlib.tool.StreamReader; +import me.ag2s.umdlib.tool.UmdUtils; + +/** + * UMD格式的电子书解析 + * 格式规范参考: + * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5d.html + * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5l.html + * + */ + +public class UmdReader { + UmdBook book; + InputStream inputStream; + int _AdditionalCheckNumber; + int _TotalContentLen; + boolean end = false; + + + public synchronized UmdBook read(InputStream inputStream) throws Exception { + + book = new UmdBook(); + this.inputStream=inputStream; + StreamReader reader = new StreamReader(inputStream); + UmdHeader umdHeader = new UmdHeader(); + book.setHeader(umdHeader); + if (reader.readIntLe() != 0xde9a9b89) { + throw new IOException("Wrong header"); + } + short num1 = -1; + byte ch = reader.readByte(); + while (ch == 35) { + //int num2=reader.readByte(); + short segType = reader.readShortLe(); + byte segFlag = reader.readByte(); + short len = (short) (reader.readUint8() - 5); + + System.out.println("块标识:" + segType); + //short length1 = reader.readByte(); + ReadSection(segType, segFlag, len, reader, umdHeader); + + if ((int) segType == 241 || (int) segType == 10) { + segType = num1; + } + for (ch = reader.readByte(); ch == 36; ch = reader.readByte()) { + //int num3 = reader.readByte(); + System.out.println(ch); + int additionalCheckNumber = reader.readIntLe(); + int length2 = (reader.readIntLe() - 9); + ReadAdditionalSection(segType, additionalCheckNumber, length2, reader); + } + num1 = segType; + + } + System.out.println(book.getHeader().toString()); + return book; + + } + + private void ReadAdditionalSection(short segType, int additionalCheckNumber, int length, StreamReader reader) throws Exception { + switch (segType) { + case 14: + //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)))); + break; + case 15: + //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)))); + break; + case 129: + reader.readBytes(length); + break; + case 130: + //byte[] covers = reader.readBytes(length); + book.setCover(new UmdCover(reader.readBytes(length))); + //this._Book.Cover = BitmapImage.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))); + break; + case 131: + System.out.println(length / 4); + book.setNum(length / 4); + for (int i = 0; i < length / 4; ++i) { + book.getChapters().addContentLength(reader.readIntLe()); + } + break; + case 132: + //System.out.println(length/4); + System.out.println(_AdditionalCheckNumber); + System.out.println(additionalCheckNumber); + if (this._AdditionalCheckNumber != additionalCheckNumber) { + System.out.println(length); + book.getChapters().contents.write(UmdUtils.decompress(reader.readBytes(length))); + book.getChapters().contents.flush(); + break; + } else { + for (int i = 0; i < book.getNum(); i++) { + short len = reader.readUint8(); + byte[] title = reader.readBytes(len); + //System.out.println(UmdUtils.unicodeBytesToString(title)); + book.getChapters().addTitle(title); + } + } + + + break; + default: + /*Console.WriteLine("未知内容"); + Console.WriteLine("Seg Type = " + (object) segType); + Console.WriteLine("Seg Len = " + (object) length); + Console.WriteLine("content = " + (object) reader.ReadBytes((int) length));*/ + break; + } + } + + public void ReadSection(short segType, byte segFlag, short length, StreamReader reader, UmdHeader header) throws IOException { + switch (segType) { + case 1://umd文件头 DCTS_CMD_ID_VERSION + header.setUmdType(reader.readByte()); + reader.readBytes(2);//Random 2 + System.out.println("UMD文件类型:" + header.getUmdType()); + break; + case 2://文件标题 DCTS_CMD_ID_TITLE + header.setTitle(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("文件标题:" + header.getTitle()); + break; + case 3://作者 + header.setAuthor(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("作者:" + header.getAuthor()); + break; + case 4://年 + header.setYear(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("年:" + header.getYear()); + break; + case 5://月 + header.setMonth(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("月:" + header.getMonth()); + break; + case 6://日 + header.setDay(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("日:" + header.getDay()); + break; + case 7://小说类型 + header.setBookType(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("小说类型:" + header.getBookType()); + break; + case 8://出版商 + header.setBookMan(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("出版商:" + header.getBookMan()); + break; + case 9:// 零售商 + header.setShopKeeper(UmdUtils.unicodeBytesToString(reader.readBytes(length))); + System.out.println("零售商:" + header.getShopKeeper()); + break; + case 10://CONTENT ID + System.out.println("CONTENT ID:" + reader.readHex(length)); + break; + case 11: + //内容长度 DCTS_CMD_ID_FILE_LENGTH + _TotalContentLen = reader.readIntLe(); + book.getChapters().setTotalContentLen(_TotalContentLen); + System.out.println("内容长度:" + _TotalContentLen); + break; + case 12://UMD文件结束 + end = true; + int num2 = reader.readIntLe(); + System.out.println("整个文件长度" + num2); + break; + case 13: + break; + case 14: + int num3 = (int) reader.readByte(); + break; + case 15: + reader.readBytes(length); + break; + case 129://正文 + case 131://章节偏移 + _AdditionalCheckNumber = reader.readIntLe(); + System.out.println("章节偏移:" + _AdditionalCheckNumber); + break; + case 132://章节标题,正文 + _AdditionalCheckNumber = reader.readIntLe(); + System.out.println("章节标题,正文:" + _AdditionalCheckNumber); + break; + case 130://封面(jpg) + int num4 = (int) reader.readByte(); + _AdditionalCheckNumber = reader.readIntLe(); + break; + case 135://页面偏移(Page Offset) + reader.readUint8();//fontSize 一字节 字体大小 + reader.readUint8();//screenWidth 屏幕宽度 + reader.readBytes(4);//BlockRandom 指向一个页面偏移数据块 + break; + case 240://CDS KEY + break; + case 241://许可证(LICENCE KEY) + //System.out.println("整个文件长度" + length); + System.out.println("许可证(LICENCE KEY):" + reader.readHex(16)); + break; + default: + if (length > 0) { + byte[] numArray = reader.readBytes(length); + } + + + } + } + + + @Override + public String toString() { + return "UmdReader{" + + "book=" + book + + '}'; + } +} diff --git a/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent b/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent new file mode 100644 index 000000000..f7b58d257 --- /dev/null +++ b/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent @@ -0,0 +1,1135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd b/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd new file mode 100644 index 000000000..34cc2b10e --- /dev/null +++ b/epublib/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd @@ -0,0 +1,390 @@ + + + + + + + + + +%OEBEntities; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd b/epublib/src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd new file mode 100644 index 000000000..b889c41a5 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod new file mode 100644 index 000000000..a44bb3fa9 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + +]]> + +]]> + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + +]]> + + + + + + +]]> +]]> + + + + + + + +]]> + + + + + + + + +]]> + + + + +]]> +]]> + + + + + + +]]> +]]> + + + + + + + + + + +]]> + + + + + +]]> + + + + + +]]> +]]> + + + + + +]]> + + + + + +]]> + + + + + +]]> +]]> +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod new file mode 100644 index 000000000..4a4fa6caa --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod new file mode 100644 index 000000000..104e57002 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod @@ -0,0 +1,142 @@ + + + + + + + + + +]]> + + + + +]]> + + + + +]]> + + + + + + + + +]]> + + + + + + + + + + + +]]> + + +]]> + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod new file mode 100644 index 000000000..dca21ca07 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod @@ -0,0 +1,53 @@ + + + + + + + + + + + + +]]> + + + +]]> + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod new file mode 100644 index 000000000..fcd67bf6b --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod @@ -0,0 +1,47 @@ + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod new file mode 100644 index 000000000..0eeb16419 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod @@ -0,0 +1,164 @@ + + + + + + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + +]]> + + + +]]> + + + + +]]> + + + +]]> + + + + +]]> + + + +]]> + + + + +]]> + + + +]]> + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod new file mode 100644 index 000000000..30968bb7a --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod @@ -0,0 +1,40 @@ + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod new file mode 100644 index 000000000..ab37c73c0 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod @@ -0,0 +1,57 @@ + + + + + + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod new file mode 100644 index 000000000..b1faf15cc --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod @@ -0,0 +1,39 @@ + + + + + + + +%xhtml-lat1; + + +%xhtml-symbol; + + +%xhtml-special; + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod new file mode 100644 index 000000000..5977f038b --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod @@ -0,0 +1,114 @@ + + + + + + + + + + +]]> + + + + + + +]]> + + + + + + + + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod new file mode 100644 index 000000000..a2ea3ae85 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1 b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1 new file mode 100644 index 000000000..a2ea3ae85 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1 @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod new file mode 100644 index 000000000..2d3d43f1c --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod @@ -0,0 +1,66 @@ + + + + + + + + + + + + +]]> + + + +]]> + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod new file mode 100644 index 000000000..ad8a798cf --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod @@ -0,0 +1,135 @@ + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod new file mode 100644 index 000000000..98b0b926a --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod new file mode 100644 index 000000000..f37976a6c --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod @@ -0,0 +1,97 @@ + + + + + + + + +%xhtml-arch.mod;]]> + + + +%xhtml-notations.mod;]]> + + + +%xhtml-datatypes.mod;]]> + + + +%xhtml-xlink.mod; + + + +%xhtml-qname.mod;]]> + + + +%xhtml-events.mod;]]> + + + +%xhtml-attribs.mod;]]> + + + +%xhtml-model.redecl; + + + +%xhtml-model.mod;]]> + + + +%xhtml-charent.mod;]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod new file mode 100644 index 000000000..85d8348fb --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod @@ -0,0 +1,54 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod new file mode 100644 index 000000000..7eea4f9a2 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod @@ -0,0 +1,51 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod new file mode 100644 index 000000000..ebada109f --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod @@ -0,0 +1,203 @@ + + + + + + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod new file mode 100644 index 000000000..3e41322c4 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod @@ -0,0 +1,138 @@ + + + + + + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod new file mode 100644 index 000000000..4d6bd01a6 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod @@ -0,0 +1,62 @@ + + + + + + + + + + + + + +]]> + + + +]]> + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod new file mode 100644 index 000000000..6d526cd17 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent new file mode 100644 index 000000000..ffee223eb --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod new file mode 100644 index 000000000..4a15f1dd8 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod @@ -0,0 +1,59 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod new file mode 100644 index 000000000..72bdb25c5 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod new file mode 100644 index 000000000..d2f6d2c65 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod @@ -0,0 +1,47 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod new file mode 100644 index 000000000..2da12d023 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod new file mode 100644 index 000000000..bee7aeb02 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod @@ -0,0 +1,60 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod new file mode 100644 index 000000000..4ba079160 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod @@ -0,0 +1,48 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod new file mode 100644 index 000000000..42a0d6dfe --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod @@ -0,0 +1,38 @@ + + + + + + + + +%xhtml-inlpres.mod;]]> + + + +%xhtml-blkpres.mod;]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod new file mode 100644 index 000000000..35c180a68 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + +%xhtml-qname-extra.mod; + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + +]]> + + + + +%xhtml-qname.redecl; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod new file mode 100644 index 000000000..0152ab02f --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod @@ -0,0 +1,67 @@ + + + + + + + + + + + + +]]> + + + +]]> + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent new file mode 100644 index 000000000..ca358b2fe --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod new file mode 100644 index 000000000..45da878fd --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod new file mode 100644 index 000000000..c826f0f07 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + +]]> + + + +]]> + + + + + + + +]]> + + + + + + +]]> + + + + + + + +]]> + + + +]]> + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod new file mode 100644 index 000000000..dc85a9e6e --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod @@ -0,0 +1,48 @@ + + + + + + + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent new file mode 100644 index 000000000..63c2abfa6 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1 b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1 new file mode 100644 index 000000000..63c2abfa6 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1 @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod new file mode 100644 index 000000000..540b7346e --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + + + + + + + +]]> + + + +]]> + + + + + + +]]> + + + +]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod new file mode 100644 index 000000000..a461e1e14 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod @@ -0,0 +1,52 @@ + + + + + + + + +%xhtml-inlstruct.mod;]]> + + + +%xhtml-inlphras.mod;]]> + + + +%xhtml-blkstruct.mod;]]> + + + +%xhtml-blkphras.mod;]]> + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod new file mode 100644 index 000000000..eb834f3d3 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent new file mode 100644 index 000000000..ffee223eb --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent new file mode 100644 index 000000000..ca358b2fe --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent new file mode 100644 index 000000000..63c2abfa6 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd new file mode 100644 index 000000000..2927b9ece --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd @@ -0,0 +1,978 @@ + + + + + +%HTMLlat1; + + +%HTMLsymbol; + + +%HTMLspecial; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd new file mode 100644 index 000000000..628f27ac5 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd @@ -0,0 +1,1201 @@ + + + + + +%HTMLlat1; + + +%HTMLsymbol; + + +%HTMLspecial; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd new file mode 100644 index 000000000..2a999b5b3 --- /dev/null +++ b/epublib/src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + +%xhtml-inlstyle.mod;]]> + + + + + + + +%xhtml-framework.mod;]]> + + + + +]]> + + + + +%xhtml-text.mod;]]> + + + + +%xhtml-hypertext.mod;]]> + + + + +%xhtml-list.mod;]]> + + + + + + +%xhtml-edit.mod;]]> + + + + +%xhtml-bdo.mod;]]> + + + + + + +%xhtml-ruby.mod;]]> + + + + +%xhtml-pres.mod;]]> + + + + +%xhtml-link.mod;]]> + + + + +%xhtml-meta.mod;]]> + + + + +%xhtml-base.mod;]]> + + + + +%xhtml-script.mod;]]> + + + + +%xhtml-style.mod;]]> + + + + +%xhtml-image.mod;]]> + + + + +%xhtml-csismap.mod;]]> + + + + +%xhtml-ssismap.mod;]]> + + + + +%xhtml-param.mod;]]> + + + + +%xhtml-object.mod;]]> + + + + +%xhtml-table.mod;]]> + + + + +%xhtml-form.mod;]]> + + + + +%xhtml-legacy.mod;]]> + + + + +%xhtml-struct.mod;]]> + + + diff --git a/epublib/src/main/resources/log4j.properties b/epublib/src/main/resources/log4j.properties new file mode 100644 index 000000000..bdfcdfe7f --- /dev/null +++ b/epublib/src/main/resources/log4j.properties @@ -0,0 +1,55 @@ +#------------------------------------------------------------------------------ +# +# The following properties set the logging levels and log appender. The +# log4j.rootCategory variable defines the default log level and one or more +# appenders. For the console, use 'S'. For the daily rolling file, use 'R'. +# For an HTML formatted log, use 'H'. +# +# To override the default (rootCategory) log level, define a property of the +# form (see below for available values): +# +# log4j.logger. = +# +# Available logger names: +# TODO +# +# Possible Log Levels: +# FATAL, ERROR, WARN, INFO, DEBUG +# +#------------------------------------------------------------------------------ +log4j.rootCategory=INFO, S + +#------------------------------------------------------------------------------ +# +# The following properties configure the console (stdout) appender. +# See http://logging.apache.org/log4j/docs/api/index.html for details. +# +#------------------------------------------------------------------------------ +log4j.appender.S = org.apache.log4j.ConsoleAppender +log4j.appender.S.layout = org.apache.log4j.PatternLayout +log4j.appender.S.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [%p] %l %m%n + +#------------------------------------------------------------------------------ +# +# The following properties configure the Daily Rolling File appender. +# See http://logging.apache.org/log4j/docs/api/index.html for details. +# +#------------------------------------------------------------------------------ +log4j.appender.R = org.apache.log4j.DailyRollingFileAppender +log4j.appender.R.File = logs/epublib.log +log4j.appender.R.Append = true +log4j.appender.R.DatePattern = '.'yyy-MM-dd +log4j.appender.R.layout = org.apache.log4j.PatternLayout +log4j.appender.R.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} %c{1} [%p] %m%n + +#------------------------------------------------------------------------------ +# +# The following properties configure the Rolling File appender in HTML. +# See http://logging.apache.org/log4j/docs/api/index.html for details. +# +#------------------------------------------------------------------------------ +log4j.appender.H = org.apache.log4j.RollingFileAppender +log4j.appender.H.File = logs/epublib_log.html +log4j.appender.H.MaxFileSize = 100KB +log4j.appender.H.Append = false +log4j.appender.H.layout = org.apache.log4j.HTMLLayout \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1e56316f..4a88e2259 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat May 30 10:00:31 CST 2020 +#Fri May 07 15:24:46 CST 2021 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index e7b4def49..6e5b9d8ee 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':app',':epublib'