新增WebDav

pull/5/head
fengyuecanzhu 4 years ago
parent 031c3ccc98
commit ac07c5d670
  1. BIN
      .idea/caches/build_file_checksums.ser
  2. 4
      README.md
  3. 8
      app/build.gradle
  4. 1
      app/src/main/AndroidManifest.xml
  5. 8
      app/src/main/assets/updatelog.fy
  6. 15
      app/src/main/java/xyz/fycz/myreader/application/MyApplication.java
  7. 23
      app/src/main/java/xyz/fycz/myreader/base/observer/MyObserver.java
  8. 17
      app/src/main/java/xyz/fycz/myreader/base/observer/MySingleObserver.java
  9. 3
      app/src/main/java/xyz/fycz/myreader/common/APPCONST.java
  10. 11
      app/src/main/java/xyz/fycz/myreader/common/Common.java
  11. 4
      app/src/main/java/xyz/fycz/myreader/greendao/service/BookService.java
  12. 47
      app/src/main/java/xyz/fycz/myreader/model/SearchEngine.java
  13. 134
      app/src/main/java/xyz/fycz/myreader/model/backup/UserService.java
  14. 178
      app/src/main/java/xyz/fycz/myreader/model/storage/Backup.kt
  15. 239
      app/src/main/java/xyz/fycz/myreader/model/storage/BackupRestoreUi.kt
  16. 48
      app/src/main/java/xyz/fycz/myreader/model/storage/Preferences.kt
  17. 137
      app/src/main/java/xyz/fycz/myreader/model/storage/Restore.kt
  18. 121
      app/src/main/java/xyz/fycz/myreader/model/storage/WebDavHelp.kt
  19. 3
      app/src/main/java/xyz/fycz/myreader/ui/activity/AboutActivity.java
  20. 11
      app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java
  21. 8
      app/src/main/java/xyz/fycz/myreader/ui/activity/BookstoreActivity.java
  22. 4
      app/src/main/java/xyz/fycz/myreader/ui/activity/MainActivity.java
  23. 29
      app/src/main/java/xyz/fycz/myreader/ui/activity/MoreSettingActivity.java
  24. 2
      app/src/main/java/xyz/fycz/myreader/ui/activity/ReadActivity.java
  25. 8
      app/src/main/java/xyz/fycz/myreader/ui/activity/SearchBookActivity.java
  26. 131
      app/src/main/java/xyz/fycz/myreader/ui/activity/WebDavSettingActivity.java
  27. 3
      app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseAdapter.java
  28. 8
      app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDetailedAdapter.java
  29. 8
      app/src/main/java/xyz/fycz/myreader/ui/adapter/BookcaseDragAdapter.java
  30. 9
      app/src/main/java/xyz/fycz/myreader/ui/adapter/SearchBookAdapter.java
  31. 11
      app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/BookStoreBookHolder.java
  32. 70
      app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/SearchBookHolder.java
  33. 76
      app/src/main/java/xyz/fycz/myreader/ui/dialog/MyAlertDialog.java
  34. 113
      app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java
  35. 53
      app/src/main/java/xyz/fycz/myreader/ui/presenter/BookcasePresenter.java
  36. 6
      app/src/main/java/xyz/fycz/myreader/ui/presenter/ReadPresenter.java
  37. 49
      app/src/main/java/xyz/fycz/myreader/util/HttpUtil.java
  38. 40
      app/src/main/java/xyz/fycz/myreader/util/SharedPreUtils.java
  39. 6
      app/src/main/java/xyz/fycz/myreader/util/StringHelper.java
  40. 387
      app/src/main/java/xyz/fycz/myreader/util/utils/DocumentUtil.java
  41. 601
      app/src/main/java/xyz/fycz/myreader/util/utils/FileUtils.java
  42. 43
      app/src/main/java/xyz/fycz/myreader/util/utils/GsonExtensions.kt
  43. 50
      app/src/main/java/xyz/fycz/myreader/util/utils/ImageLoader.kt
  44. 1
      app/src/main/java/xyz/fycz/myreader/util/webdav/README.md
  45. 250
      app/src/main/java/xyz/fycz/myreader/util/webdav/WebDav.kt
  46. 16
      app/src/main/java/xyz/fycz/myreader/util/webdav/http/Handler.kt
  47. 9
      app/src/main/java/xyz/fycz/myreader/util/webdav/http/HttpAuth.kt
  48. 56
      app/src/main/java/xyz/fycz/myreader/webapi/CommonApi.java
  49. 3
      app/src/main/java/xyz/fycz/myreader/webapi/LanZousApi.java
  50. 10
      app/src/main/java/xyz/fycz/myreader/webapi/crawler/ReadCrawlerUtil.java
  51. 3
      app/src/main/java/xyz/fycz/myreader/webapi/crawler/find/QiDianMobileRank.java
  52. 158
      app/src/main/java/xyz/fycz/myreader/widget/CoverImageView.kt
  53. 416
      app/src/main/java/xyz/fycz/myreader/widget/page/LocalPageLoader.java
  54. 22
      app/src/main/res/layout/activity_more_setting.xml
  55. 3
      app/src/main/res/layout/activity_search_book.xml
  56. 105
      app/src/main/res/layout/activity_webdav_setting.xml
  57. 3
      app/src/main/res/layout/edit_dialog.xml
  58. 5
      app/src/main/res/layout/gridview_book_detailed_item.xml
  59. 12
      app/src/main/res/layout/gridview_book_item.xml
  60. 15
      app/src/main/res/layout/layout_book_detail_header.xml
  61. 6
      app/src/main/res/layout/listview_book_store_book_item.xml
  62. 30
      app/src/main/res/layout/listview_search_book_item.xml
  63. 20
      app/src/main/res/values/strings.xml
  64. 4
      app/version_code.properties
  65. 8
      build.gradle

@ -8,10 +8,12 @@
发现界面:排行榜、分类、书城
详细功能可查看图片或下载自行体验
详细功能可查看图片或下载自行体验或自行编译
demo下载:https://fycz.lanzoui.com/iBofFh42pxg
如有问题请加QQ群:1085028304
![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/1.png)
![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/2.png)
![Image](https://github.com/fengyuecanzhu/FYReader/tree/master/img/3.png)

@ -1,5 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao'//greendao插件dependencies
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def releaseTime() {
return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+08:00"))
@ -78,7 +80,13 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
api 'androidx.core:core-ktx:1.3.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//anko
def anko_version = '0.10.8'
implementation "org.jetbrains.anko:anko-sdk27:$anko_version"
implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version"
implementation 'com.jakewharton:butterknife:10.0.0'

@ -122,6 +122,7 @@
<activity android:name=".ui.activity.MoreSettingActivity"/>
<activity android:name=".ui.activity.BookstoreActivity"/>
<activity android:name=".ui.activity.WebDavSettingActivity"/>
<receiver android:name=".util.notification.NotificationClickReceiver"/>
<receiver android:name=".ui.presenter.BookcasePresenter$cancelDownloadReceiver"/>

@ -1,5 +1,11 @@
2020.10.03
风月读书v1.20.100315
1、优化搜索书籍高频刷新的问题
2、优化书籍默认封面显示
3、修复换源对话框书源重复的问题
2020.10.02
风月读书v1.20.100120
风月读书v1.20.100220
1、新增书源:搜小说网、全小说网、奇奇小说
2、新增停止搜索按钮
3、优化搜索书籍显示

@ -40,6 +40,7 @@ import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.base.BaseActivity;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.common.URLCONST;
@ -95,7 +96,7 @@ public class MyApplication extends Application {
}
public boolean isNightFS() {
return SharedPreUtils.getInstance().getBoolean("isNightFS", false);
return SharedPreUtils.getInstance().getBoolean(getString(R.string.isNightFS), false);
}
/**
@ -103,7 +104,7 @@ public class MyApplication extends Application {
* @param isNightMode
*/
public void setNightTheme(boolean isNightMode) {
SharedPreUtils.getInstance().putBoolean("isNightFS", false);
SharedPreUtils.getInstance().putBoolean(getmContext().getString(R.string.isNightFS), false);
Setting setting = SysManager.getSetting();
setting.setDayStyle(!isNightMode);
SysManager.saveSetting(setting);
@ -181,6 +182,10 @@ public class MyApplication extends Application {
handler.post(runnable);
}
public static Handler getHandler(){
return handler;
}
public static MyApplication getApplication() {
return application;
}
@ -313,11 +318,11 @@ public class MyApplication extends Application {
downloadLink = contents[2].substring(contents[2].indexOf(":") + 1).trim();
updateContent = contents[3].substring(contents[3].indexOf(":") + 1);
SharedPreUtils spu = SharedPreUtils.getInstance();
spu.putString("lanzousKeyStart", contents[4].substring(contents[4].indexOf(":") + 1));
spu.putString(getmContext().getString(R.string.lanzousKeyStart), contents[4].substring(contents[4].indexOf(":") + 1));
if (!StringHelper.isEmpty(downloadLink)) {
spu.putString("downloadLink", downloadLink);
spu.putString(getmContext().getString(R.string.downloadLink), downloadLink);
} else {
spu.putString("downloadLink", URLCONST.APP_DIR_UR);
spu.putString(getmContext().getString(R.string.downloadLink), URLCONST.APP_DIR_UR);
}
String[] updateContents = updateContent.split("/");
for (String string : updateContents) {

@ -0,0 +1,23 @@
//Copyright (c) 2017. 章钦豪. All rights reserved.
package xyz.fycz.myreader.base.observer;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
public abstract class MyObserver<T> implements Observer<T> {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
}

@ -0,0 +1,17 @@
package xyz.fycz.myreader.base.observer;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.Disposable;
public abstract class MySingleObserver<T> implements SingleObserver<T> {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onError(Throwable e) {
}
}

@ -84,4 +84,7 @@ public class APPCONST {
public static final String FORMAT_FILE_DATE = "yyyy-MM-dd";
public final static String channelIdDownload = "channel_download";
public static final String DEFAULT_WEB_DAV_URL = "https://dav.jianguoyun.com/dav/";
}

@ -1,11 +0,0 @@
package xyz.fycz.myreader.common;
public interface Common {
int SUCCESS = 1;
int INSUCCESS = 0;
int LOGINEXIT = 2;
int MESSAGE_HINT = 3;
}

@ -6,6 +6,8 @@ import android.text.TextUtils;
import net.ricecode.similarity.JaroWinklerStrategy;
import net.ricecode.similarity.StringSimilarityService;
import net.ricecode.similarity.StringSimilarityServiceImpl;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.greendao.entity.Chapter;
@ -101,7 +103,7 @@ public class BookService extends BaseService {
book.setSortCode(0);
book.setGroupSort(0);
book.setGroupId(SharedPreUtils.getInstance().getString("curBookGroupId", ""));
book.setGroupId(SharedPreUtils.getInstance().getString(MyApplication.getmContext().getString(R.string.curBookGroupId), ""));
if (StringHelper.isEmpty(book.getId())) {
book.setId(StringHelper.getStringRandom(25));
}

@ -7,13 +7,19 @@ import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.entity.SearchBookBean;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.entity.Chapter;
import xyz.fycz.myreader.greendao.service.BookMarkService;
import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap;
import xyz.fycz.myreader.util.SharedPreUtils;
import xyz.fycz.myreader.util.ToastUtils;
import xyz.fycz.myreader.webapi.CommonApi;
import xyz.fycz.myreader.webapi.crawler.base.BookInfoCrawler;
import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler;
import java.io.UnsupportedEncodingException;
@ -44,7 +50,7 @@ public class SearchEngine {
private OnSearchListener searchListener;
public SearchEngine() {
threadsNum = SharedPreUtils.getInstance().getInt("threadNum", 8);
threadsNum = SharedPreUtils.getInstance().getInt(MyApplication.getmContext().getString(R.string.threadNum), 8);
}
public void setOnSearchListener(OnSearchListener searchListener) {
@ -237,6 +243,33 @@ public class SearchEngine {
}
public synchronized void getBookInfo(Book book, BookInfoCrawler bic, OnGetBookInfoListener listener){
CommonApi.getBookInfo(book, bic)
.subscribeOn(scheduler)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Book>() {
@Override
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
}
@Override
public void onNext(@io.reactivex.annotations.NonNull Book book) {
listener.loadFinish(true);
}
@Override
public void onError(@io.reactivex.annotations.NonNull Throwable e) {
listener.loadFinish(false);
}
@Override
public void onComplete() {
}
});
}
/************************************************************************/
public interface OnSearchListener {
@ -250,4 +283,16 @@ public class SearchEngine {
void searchBookError(Throwable throwable);
}
public interface OnGetBookInfoListener{
void loadFinish(Boolean isSuccess);
}
public interface OnGetBookChaptersListener{
void loadFinish(List<Chapter> chapters, Boolean isSuccess);
}
public interface OnGetChapterContentListener{
void loadFinish(String content, Boolean isSuccess);
}
}

@ -1,6 +1,9 @@
package xyz.fycz.myreader.model.backup;
import io.reactivex.annotations.NonNull;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.model.storage.Backup;
import xyz.fycz.myreader.model.storage.Restore;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.common.URLCONST;
@ -190,68 +193,80 @@ public class UserService {
* 网络备份
* @return
*/
public static boolean webBackup(){
public static void webBackup(ResultCallback rc){
Map<String,String> userInfo = readConfig();
if (userInfo == null){
return false;
}
BackupAndRestore bar = new BackupAndRestore();
bar.backup("webBackup");
File inputFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup");
if (!inputFile.exists()) {
return false;
rc.onFinish(false, 0);
}
File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip");
FileInputStream fis = null;
HttpURLConnection conn = null;
try {
//压缩文件
ZipUtils.zipFile(inputFile, zipFile);
fis = new FileInputStream(zipFile);
URL url = new URL(URLCONST.APP_WEB_URL + "bak?username=" + userInfo.get("userName") +
makeSignalParam());
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-type", "multipart/form-data");
conn.setDoInput(true);
conn.setDoOutput(true);
OutputStream out = conn.getOutputStream();
byte[] bytes = new byte[1024];
int len = -1;
while ((len = fis.read(bytes)) != -1){
out.write(bytes, 0, len);
Backup.INSTANCE.backup(MyApplication.getmContext(), APPCONST.FILE_DIR + "webBackup/", new Backup.CallBack() {
@Override
public void backupSuccess() {
MyApplication.getApplication().newThread(() ->{
File inputFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup");
if (!inputFile.exists()) {
rc.onFinish(false, 0);
}
File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip");
FileInputStream fis = null;
HttpURLConnection conn = null;
try {
//压缩文件
ZipUtils.zipFile(inputFile, zipFile);
fis = new FileInputStream(zipFile);
URL url = new URL(URLCONST.APP_WEB_URL + "bak?username=" + userInfo.get("userName") +
makeSignalParam());
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-type", "multipart/form-data");
conn.setDoInput(true);
conn.setDoOutput(true);
OutputStream out = conn.getOutputStream();
byte[] bytes = new byte[1024];
int len = -1;
while ((len = fis.read(bytes)) != -1){
out.write(bytes, 0, len);
}
out.flush();
zipFile.delete();
BufferedReader bw = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder sb = new StringBuilder();
String line = bw.readLine();
while (line != null) {
sb.append(line);
line = bw.readLine();
}
String[] info = sb.toString().split(":");
int code = Integer.parseInt(info[0].trim());
rc.onFinish(code == 104, 0);
} catch (Exception e) {
e.printStackTrace();
rc.onError(e);
} finally {
IOUtils.close(fis);
if (conn != null) {
conn.disconnect();
}
}
});
}
out.flush();
zipFile.delete();
BufferedReader bw = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder sb = new StringBuilder();
String line = bw.readLine();
while (line != null) {
sb.append(line);
line = bw.readLine();
}
String[] info = sb.toString().split(":");
int code = Integer.parseInt(info[0].trim());
return code == 104;
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
IOUtils.close(fis);
if (conn != null) {
conn.disconnect();
@Override
public void backupError(@NonNull String msg) {
ToastUtils.showError(msg);
rc.onFinish(false, 0);
}
}
}, false);
}
/**
* 网络恢复
* @return
*/
public static boolean webRestore(){
public static void webRestore(ResultCallback rc){
Map<String,String> userInfo = readConfig();
if (userInfo == null){
return false;
rc.onFinish(false, 0);
}
FileOutputStream fos = null;
File zipFile = FileUtils.getFile(APPCONST.FILE_DIR + "webBackup.zip");
@ -274,16 +289,25 @@ public class UserService {
fos.flush();
if (zipFile.length() == 0){
zipFile.delete();
return false;
rc.onFinish(false, 0);
}
ZipUtils.unzipFile(zipFile.getAbsolutePath(), APPCONST.FILE_DIR);
BackupAndRestore bar = new BackupAndRestore();
bar.restore("webBackup");
zipFile.delete();
return true;
Restore.INSTANCE.restore(APPCONST.FILE_DIR + "webBackup/", new Restore.CallBack() {
@Override
public void restoreSuccess() {
zipFile.delete();
rc.onFinish(true, 0);
}
@Override
public void restoreError(@NonNull String msg) {
ToastUtils.showError(msg);
rc.onFinish(false, 0);
}
});
} catch (Exception e) {
e.printStackTrace();
return false;
rc.onError(e);
}finally {
IOUtils.close(fos);
if (conn != null) {

@ -0,0 +1,178 @@
package xyz.fycz.myreader.model.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.reactivex.Single
import io.reactivex.SingleOnSubscribe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import xyz.fycz.myreader.application.MyApplication
import xyz.fycz.myreader.application.SysManager
import xyz.fycz.myreader.base.observer.MySingleObserver
import xyz.fycz.myreader.common.APPCONST
import xyz.fycz.myreader.greendao.GreenDaoManager
import xyz.fycz.myreader.greendao.service.BookMarkService
import xyz.fycz.myreader.greendao.service.BookService
import xyz.fycz.myreader.greendao.service.SearchHistoryService
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.utils.DocumentUtil
import xyz.fycz.myreader.util.utils.FileUtils
import xyz.fycz.myreader.util.utils.GSON
import java.io.File
import java.util.concurrent.TimeUnit
object Backup {
val backupPath = MyApplication.getApplication().filesDir.absolutePath + File.separator + "backup"
val defaultPath by lazy {
APPCONST.BACKUP_FILE_DIR
}
val backupFileNames by lazy {
arrayOf(
"myBooks.json",
"mySearchHistory.json",
"myBookMark.json",
"myBookGroup.json",
"setting.json",
"config.xml"
)
}
fun autoBack() {
val lastBackup = SharedPreUtils.getInstance().getLong("lastBackup", 0)
if (System.currentTimeMillis() - lastBackup < TimeUnit.DAYS.toMillis(1)) {
return
}
val path = SharedPreUtils.getInstance().getString("backupPath", defaultPath)
if (path == null) {
backup(MyApplication.getmContext(), defaultPath, null, true)
} else {
backup(MyApplication.getmContext(), path, null, true)
}
}
fun backup(context: Context, path: String, callBack: CallBack?, isAuto: Boolean = false) {
SharedPreUtils.getInstance().putLong("lastBackup", System.currentTimeMillis())
Single.create(SingleOnSubscribe<Boolean> { e ->
BookService.getInstance().allBooks.let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.getFile(backupPath + File.separator + "myBooks.json").writeText(json)
}
}
SearchHistoryService.getInstance().findAllSearchHistory().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.getFile(backupPath + File.separator + "mySearchHistory.json")
.writeText(json)
}
}
GreenDaoManager.getInstance().session.bookMarkDao.queryBuilder().list().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.getFile(backupPath + File.separator + "myBookMark.json")
.writeText(json)
}
}
GreenDaoManager.getInstance().session.bookGroupDao.queryBuilder().list().let {
if (it.isNotEmpty()) {
val json = GSON.toJson(it)
FileUtils.getFile(backupPath + File.separator + "myBookGroup.json")
.writeText(json)
}
}
val json = GSON.toJson(SysManager.getSetting())
FileUtils.getFile(backupPath + File.separator + "setting.json")
.writeText(json)
Preferences.getSharedPreferences(context, backupPath, "config")?.let { sp ->
val edit = sp.edit()
SharedPreUtils.getInstance().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)
} else {
copyBackup(path, isAuto)
}
e.onSuccess(true)
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MySingleObserver<Boolean>() {
override fun onSuccess(t: Boolean) {
callBack?.backupSuccess()
}
override fun onError(e: Throwable) {
e.printStackTrace()
callBack?.backupError(e.localizedMessage ?: "ERROR")
}
})
}
@Throws(Exception::class)
private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) {
synchronized(this) {
DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->
for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
if (isAuto) {
treeDoc.findFile("auto")?.findFile(fileName)?.delete()
var autoDoc = treeDoc.findFile("auto")
if (autoDoc == null) {
autoDoc = treeDoc.createDirectory("auto")
}
autoDoc?.createFile("", fileName)?.let {
DocumentUtil.writeBytes(context, file.readBytes(), it)
}
} else {
treeDoc.findFile(fileName)?.delete()
treeDoc.createFile("", fileName)?.let {
DocumentUtil.writeBytes(context, file.readBytes(), it)
}
}
}
}
}
}
}
@Throws(java.lang.Exception::class)
private fun copyBackup(path: String, isAuto: Boolean) {
synchronized(this) {
for (fileName in backupFileNames) {
if (isAuto) {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(FileUtils.getFile(path + File.separator + "auto" + File.separator + fileName), true)
}
} else {
val file = File(backupPath + File.separator + fileName)
if (file.exists()) {
file.copyTo(FileUtils.getFile(path + File.separator + fileName), true)
}
}
}
}
}
interface CallBack {
fun backupSuccess()
fun backupError(msg: String)
}
}

@ -0,0 +1,239 @@
package xyz.fycz.myreader.model.storage
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.net.Uri
import android.text.TextUtils
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import io.reactivex.Single
import io.reactivex.SingleEmitter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.jetbrains.anko.alert
import org.jetbrains.anko.toast
import xyz.fycz.myreader.base.observer.MySingleObserver
import xyz.fycz.myreader.common.APPCONST
import xyz.fycz.myreader.model.storage.WebDavHelp.getWebDavFileNames
import xyz.fycz.myreader.model.storage.WebDavHelp.showRestoreDialog
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.ToastUtils
import java.util.*
object BackupRestoreUi : Backup.CallBack, Restore.CallBack {
private const val backupSelectRequestCode = 22
private const val restoreSelectRequestCode = 33
private fun getBackupPath(): String? {
return SharedPreUtils.getInstance().getString("backupPath", APPCONST.BACKUP_FILE_DIR)
}
private fun setBackupPath(path: String?) {
if (path.isNullOrEmpty()) {
SharedPreUtils.getInstance().remove("backupPath")
} else {
SharedPreUtils.getInstance().putString("backupPath", path)
}
}
override fun backupSuccess() {
ToastUtils.showSuccess("备份成功")
}
override fun backupError(msg: String) {
ToastUtils.showError(msg)
}
override fun restoreSuccess() {
ToastUtils.showSuccess("恢复成功")
}
override fun restoreError(msg: String) {
ToastUtils.showError(msg)
}
fun backup(activity: Activity) {
val backupPath = getBackupPath()
if (backupPath.isNullOrEmpty()) {
// selectBackupFolder(activity)
ToastUtils.showError("backupPath.isNullOrEmpty")
} else {
if (backupPath.isContentPath()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(activity, uri)
if (doc?.canWrite() == true) {
Backup.backup(activity, backupPath, this)
} else {
// selectBackupFolder(activity)
ToastUtils.showError("doc?.canWrite() != true")
}
} else {
// backupUsePermission(activity)
ToastUtils.showError("backupPath.isNotContentPath")
}
}
}
/*private fun backupUsePermission(activity: Activity, path: String = Backup.defaultPath) {
PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.get_storage_per)
.onGranted {
setBackupPath(path)
Backup.backup(activity, path, this)
}
.request()
}
fun selectBackupFolder(activity: Activity) {
activity.alert {
titleResource = R.string.select_folder
items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) {
0 -> {
setBackupPath(Backup.defaultPath)
backupUsePermission(activity)
}
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
activity.startActivityForResult(intent, backupSelectRequestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
activity.toast(e.localizedMessage ?: "ERROR")
}
}
2 -> {
PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.get_storage_per)
.onGranted {
selectBackupFolderApp(activity, false)
}
.request()
}
}
}
}.show()
}
private fun selectBackupFolderApp(activity: Activity, isRestore: Boolean) {
val picker = FilePicker(activity, FilePicker.DIRECTORY)
picker.setBackgroundColor(ContextCompat.getColor(activity, R.color.background))
picker.setTopBackgroundColor(ContextCompat.getColor(activity, R.color.background))
picker.setItemHeight(30)
picker.setOnFilePickListener { currentPath: String ->
setBackupPath(currentPath)
if (isRestore) {
Restore.restore(currentPath, this)
} else {
Backup.backup(activity, currentPath, this)
}
}
picker.show()
}*/
fun restore(activity: Activity) {
Single.create { emitter: SingleEmitter<ArrayList<String>?> ->
emitter.onSuccess(getWebDavFileNames())
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MySingleObserver<ArrayList<String>?>() {
override fun onSuccess(strings: ArrayList<String>) {
if (!showRestoreDialog(activity, strings, this@BackupRestoreUi)) {
val path = getBackupPath()
if (TextUtils.isEmpty(path)) {
//selectRestoreFolder(activity)
ToastUtils.showError("TextUtils.isEmpty(path)")
} else {
if (path.isContentPath()) {
val uri = Uri.parse(path)
val doc = DocumentFile.fromTreeUri(activity, uri)
if (doc?.canWrite() == true) {
Restore.restore(activity, Uri.parse(path), this@BackupRestoreUi)
} else {
// selectRestoreFolder(activity)
ToastUtils.showError("doc?.canWrite() != true")
}
} else {
// restoreUsePermission(activity)
ToastUtils.showError("path.isNotContentPath")
}
}
}
}
})
}
/*private fun restoreUsePermission(activity: Activity, path: String = Backup.defaultPath) {
PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.get_storage_per)
.onGranted {
setBackupPath(path)
Restore.restore(path, this)
}
.request()
}
private fun selectRestoreFolder(activity: Activity) {
activity.alert {
titleResource = R.string.select_folder
items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) {
0 -> restoreUsePermission(activity)
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
activity.startActivityForResult(intent, restoreSelectRequestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
activity.toast(e.localizedMessage ?: "ERROR")
}
}
2 -> {
PermissionsCompat.Builder(activity)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.get_storage_per)
.onGranted {
selectBackupFolderApp(activity, true)
}
.request()
}
}
}
}.show()
}*/
/*fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
backupSelectRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
MApplication.getInstance().contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
setBackupPath(uri.toString())
Backup.backup(MApplication.getInstance(), uri.toString(), this)
}
}
restoreSelectRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
MApplication.getInstance().contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
setBackupPath(uri.toString())
Restore.restore(MApplication.getInstance(), uri, this)
}
}
}
}*/
}
fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true

@ -0,0 +1,48 @@
package xyz.fycz.myreader.model.storage
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.SharedPreferences
import java.io.File
object Preferences {
/**
* 用反射生成 SharedPreferences
* @param context
* @param dir
* @param fileName 文件名,不需要 '.xml' 后缀
* @return
*/
fun getSharedPreferences(
context: Context,
dir: String,
fileName: String
): SharedPreferences? {
try {
// 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象
val fieldMBase = ContextWrapper::class.java.getDeclaredField("mBase")
fieldMBase.isAccessible = true
// 获取 mBase变量
val objMBase = fieldMBase.get(context)
// 获取 ContextImpl.mPreferencesDir变量,该变量保存了数据文件的保存路径
val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir")
fieldMPreferencesDir.isAccessible = true
// 创建自定义路径
// String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Android";
val file = File(dir)
// 修改mPreferencesDir变量的值
fieldMPreferencesDir.set(objMBase, file)
// 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml
return context.getSharedPreferences(fileName, Activity.MODE_PRIVATE)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
return null
}
}

@ -0,0 +1,137 @@
package xyz.fycz.myreader.model.storage
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.reactivex.Single
import io.reactivex.SingleOnSubscribe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import xyz.fycz.myreader.application.MyApplication
import xyz.fycz.myreader.application.SysManager
import xyz.fycz.myreader.base.observer.MySingleObserver
import xyz.fycz.myreader.entity.Setting
import xyz.fycz.myreader.greendao.GreenDaoManager
import xyz.fycz.myreader.greendao.entity.Book
import xyz.fycz.myreader.greendao.entity.BookGroup
import xyz.fycz.myreader.greendao.entity.BookMark
import xyz.fycz.myreader.greendao.entity.SearchHistory
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.utils.*
import java.io.File
object Restore {
fun restore(context: Context, uri: Uri, callBack: CallBack?) {
Single.create(SingleOnSubscribe<Boolean> { e ->
DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->
for (fileName in Backup.backupFileNames) {
if (doc.name == fileName) {
DocumentUtil.readBytes(context, doc.uri)?.let {
FileUtils.getFile(Backup.backupPath + File.separator + fileName)
.writeBytes(it)
}
}
}
}
e.onSuccess(true)
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MySingleObserver<Boolean>() {
override fun onSuccess(t: Boolean) {
restore(Backup.backupPath, callBack)
}
override fun onError(e: Throwable) {
e.printStackTrace()
callBack?.restoreError(e.localizedMessage ?: "ERROR")
}
})
}
fun restore(path: String, callBack: CallBack?) {
Single.create(SingleOnSubscribe<Boolean> { e ->
try {
val file = FileUtils.getFile(path + File.separator + "myBooks.json")
val json = file.readText()
GSON.fromJsonArray<Book>(json)?.forEach { bookshelf ->
/*if (bookshelf.noteUrl != null) {
DbHelper.getDaoSession().bookShelfBeanDao.insertOrReplace(bookshelf)
}
if (bookshelf.bookInfoBean.noteUrl != null) {
DbHelper.getDaoSession().bookInfoBeanDao.insertOrReplace(bookshelf.bookInfoBean)
}*/
GreenDaoManager.getInstance().session.bookDao.insertOrReplace(bookshelf)
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileUtils.getFile(path + File.separator + "mySearchHistory.json")
val json = file.readText()
GSON.fromJsonArray<SearchHistory>(json)?.let {
GreenDaoManager.getInstance().session.searchHistoryDao.insertOrReplaceInTx(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileUtils.getFile(path + File.separator + "myBookMark.json")
val json = file.readText()
GSON.fromJsonArray<BookMark>(json)?.let {
GreenDaoManager.getInstance().session.bookMarkDao.insertOrReplaceInTx(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileUtils.getFile(path + File.separator + "myBookGroup.json")
val json = file.readText()
GSON.fromJsonArray<BookGroup>(json)?.let {
GreenDaoManager.getInstance().session.bookGroupDao.insertOrReplaceInTx(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
try {
val file = FileUtils.getFile(path + File.separator + "setting.json")
val json = file.readText()
SysManager.saveSetting(GSON.fromJsonObject<Setting>(json))
} catch (e: Exception) {
e.printStackTrace()
}
Preferences.getSharedPreferences(MyApplication.getmContext(), path, "config")?.all?.map {
val edit = SharedPreUtils.getInstance()
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.putInt("versionCode", MyApplication.getVersionCode())
}
e.onSuccess(true)
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MySingleObserver<Boolean>() {
override fun onSuccess(t: Boolean) {
MyApplication.getApplication().initNightTheme()
callBack?.restoreSuccess()
}
override fun onError(e: Throwable) {
e.printStackTrace()
callBack?.restoreError(e.localizedMessage ?: "ERROR")
}
})
}
interface CallBack {
fun restoreSuccess()
fun restoreError(msg: String)
}
}

@ -0,0 +1,121 @@
package xyz.fycz.myreader.model.storage
import android.content.Context
import android.os.Handler
import android.os.Looper
import io.reactivex.Single
import io.reactivex.SingleOnSubscribe
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.jetbrains.anko.selector
import xyz.fycz.myreader.base.observer.MySingleObserver
import xyz.fycz.myreader.common.APPCONST
import xyz.fycz.myreader.util.SharedPreUtils
import xyz.fycz.myreader.util.ToastUtils
import xyz.fycz.myreader.util.ZipUtils
import xyz.fycz.myreader.util.utils.FileUtils
import xyz.fycz.myreader.util.webdav.WebDav
import xyz.fycz.myreader.util.webdav.http.HttpAuth
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.min
object WebDavHelp {
private val zipFilePath = FileUtils.getCachePath() + "/backup" + ".zip"
private val unzipFilesPath by lazy {
FileUtils.getCachePath()
}
private fun getWebDavUrl(): String {
var url = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL)
if (url.isNullOrEmpty()) {
url = APPCONST.DEFAULT_WEB_DAV_URL
}
if (!url.endsWith("/")) url += "/"
return url
}
private fun initWebDav(): Boolean {
val account = SharedPreUtils.getInstance().getString("webdavAccount", "")
val password = SharedPreUtils.getInstance().getString("webdavPassword", "")
if (!account.isNullOrBlank() && !password.isNullOrBlank()) {
HttpAuth.auth = HttpAuth.Auth(account, password)
return true
}
return false
}
fun getWebDavFileNames(): ArrayList<String> {
val url = getWebDavUrl()
val names = arrayListOf<String>()
try {
if (initWebDav()) {
var files = WebDav(url + "FYReader/").listFiles()
files = files.reversed()
for (index: Int in 0 until min(10, files.size)) {
files[index].displayName?.let {
names.add(it)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return names
}
fun showRestoreDialog(context: Context, names: ArrayList<String>, callBack: Restore.CallBack?): Boolean {
return if (names.isNotEmpty()) {
context.selector(title = "选择恢复文件", items = names) { _, index ->
if (index in 0 until names.size) {
restoreWebDav(names[index], callBack)
}
}
true
} else {
false
}
}
private fun restoreWebDav(name: String, callBack: Restore.CallBack?) {
Single.create(SingleOnSubscribe<Boolean> { e ->
getWebDavUrl().let {
val file = WebDav(it + "FYReader/" + name)
file.downloadTo(zipFilePath, true)
@Suppress("BlockingMethodInNonBlockingContext")
ZipUtils.unzipFile(zipFilePath, unzipFilesPath)
}
e.onSuccess(true)
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MySingleObserver<Boolean>() {
override fun onSuccess(t: Boolean) {
Restore.restore(unzipFilesPath, callBack)
}
})
}
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)) {
WebDav(getWebDavUrl() + "FYReader").makeAsDir()
val putUrl = getWebDavUrl() + "FYReader/backup" +
SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis())) + ".zip"
WebDav(putUrl).upload(zipFilePath)
}
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
ToastUtils.showError("WebDav\n${e.localizedMessage}")
}
}
}
}

@ -12,6 +12,7 @@ import butterknife.BindView;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.base.BaseActivity2;
import xyz.fycz.myreader.common.URLCONST;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.util.ShareUtils;
import xyz.fycz.myreader.util.SharedPreUtils;
@ -71,7 +72,7 @@ public class AboutActivity extends BaseActivity2 {
ToastUtils.showSuccess("邮箱复制成功!");
});
vmShare.setOnClickListener(v -> ShareUtils.share(this, getString(R.string.share_text) +
SharedPreUtils.getInstance().getString("downloadLink")));
SharedPreUtils.getInstance().getString(getString(R.string.downloadLink, URLCONST.LAN_ZOUS_URL))));
vmUpdate.setOnClickListener(v -> MyApplication.checkVersionByServer(this, true, null));
vmUpdateLog.setOnClickListener(v -> DialogCreator.createAssetTipDialog(this, "更新日志", "updatelog.fy"));
vmQQ.setOnClickListener(v -> {

@ -42,6 +42,7 @@ import xyz.fycz.myreader.ui.adapter.DetailCatalogAdapter;
import xyz.fycz.myreader.util.StringHelper;
import xyz.fycz.myreader.util.utils.NetworkUtils;
import xyz.fycz.myreader.webapi.CommonApi;
import xyz.fycz.myreader.widget.CoverImageView;
import java.util.ArrayList;
@ -51,7 +52,7 @@ import java.util.ArrayList;
*/
public class BookDetailedActivity extends BaseActivity2 {
@BindView(R.id.book_detail_iv_cover)
ImageView mIvCover;
CoverImageView mIvCover;
/* @BindView(R.id.book_detail_iv_blur_cover)
ImageView mIvBlurCover;*/
@BindView(R.id.book_detail_tv_author)
@ -323,13 +324,7 @@ public class BookDetailedActivity extends BaseActivity2 {
mTvDesc.setText("\t\t\t\t" + mBook.getDesc());
mTvType.setText(mBook.getType());
if (!MyApplication.isDestroy(this)) {
Glide.with(this)
.load(mBook.getImgUrl())
.error(R.mipmap.no_image)
.placeholder(R.mipmap.no_image)
//设置圆角
.apply(RequestOptions.bitmapTransform(new RoundedCorners(8)))
.into(mIvCover);
mIvCover.load(mBook.getImgUrl(), mBook.getName(), mBook.getAuthor());
/*Glide.with(this)
.load(mBook.getImgUrl())
.transition(DrawableTransitionOptions.withCrossFade(1500))

@ -204,11 +204,11 @@ public class BookstoreActivity extends BaseActivity2 {
getData();
if (findCrawler.hasImg()) {
SharedPreUtils spu = SharedPreUtils.getInstance();
boolean isReadTopTip = spu.getBoolean("isReadTopTip", false);
boolean isReadTopTip = spu.getBoolean(getString(R.string.isReadTopTip), false);
if (!isReadTopTip) {
DialogCreator.createCommonDialog(this, "提示", getResources().getString(R.string.top_sort_tip, title),
true, "知道了", "不再提示", null,
(dialog, which) -> spu.putBoolean("isReadTopTip", true));
(dialog, which) -> spu.putBoolean(getString(R.string.isReadTopTip), true));
}
}
}
@ -219,11 +219,11 @@ public class BookstoreActivity extends BaseActivity2 {
private void getData() {
if (findCrawler instanceof QiDianMobileRank) {
SharedPreUtils spu = SharedPreUtils.getInstance();
if (spu.getString("qdCookie", "").equals("")) {
if (spu.getString(getString(R.string.qdCookie), "").equals("")) {
((QiDianMobileRank) findCrawler).initCookie(this, new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
spu.putString("qdCookie", (String) o);
spu.putString(getString(R.string.qdCookie), (String) o);
mBookTypes = ((QiDianMobileRank) findCrawler).getRankTypes();
curType = mBookTypes.get(0);
mHandler.sendMessage(mHandler.obtainMessage(1));

@ -76,7 +76,7 @@ public class MainActivity extends BaseActivity2 {
@Override
protected void initData(Bundle savedInstanceState) {
super.initData(savedInstanceState);
groupName = SharedPreUtils.getInstance().getString("curBookGroupName", "");
groupName = SharedPreUtils.getInstance().getString(getString(R.string.curBookGroupName), "");
titles = new String[]{"书架", "发现", "我的"};
mBookcaseFragment = new BookcaseFragment();
mFindFragment = new FindFragment();
@ -235,7 +235,7 @@ public class MainActivity extends BaseActivity2 {
case R.id.action_change_group: case R.id.action_group_man:
if (!mBookcaseFragment.getmBookcasePresenter().hasOnGroupChangeListener()) {
mBookcaseFragment.getmBookcasePresenter().addOnGroupChangeListener(() -> {
groupName = SharedPreUtils.getInstance().getString("curBookGroupName", "所有书籍");
groupName = SharedPreUtils.getInstance().getString(getString(R.string.curBookGroupName), "所有书籍");
getSupportActionBar().setSubtitle(groupName);
});
}

@ -3,6 +3,8 @@ package xyz.fycz.myreader.ui.activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.*;
@ -11,11 +13,19 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import butterknife.BindView;
import com.google.android.material.textfield.TextInputLayout;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.base.BaseActivity2;
import xyz.fycz.myreader.base.observer.MySingleObserver;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.model.storage.BackupRestoreUi;
import xyz.fycz.myreader.model.storage.WebDavHelp;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.dialog.MultiChoiceDialog;
import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
@ -23,6 +33,7 @@ import xyz.fycz.myreader.entity.Setting;
import xyz.fycz.myreader.enums.BookSource;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.service.BookService;
import xyz.fycz.myreader.util.StringHelper;
import xyz.fycz.myreader.util.utils.FileUtils;
import xyz.fycz.myreader.util.SharedPreUtils;
import xyz.fycz.myreader.util.ToastUtils;
@ -41,6 +52,8 @@ import static xyz.fycz.myreader.common.APPCONST.BOOK_CACHE_PATH;
*/
public class MoreSettingActivity extends BaseActivity2 {
@BindView(R.id.more_setting_ll_webdav)
LinearLayout mLlWebdav;
@BindView(R.id.more_setting_rl_volume)
RelativeLayout mRlVolume;
@BindView(R.id.more_setting_sc_volume)
@ -83,6 +96,8 @@ public class MoreSettingActivity extends BaseActivity2 {
RelativeLayout mRlBookstore;
@BindView(R.id.more_setting_sc_bookstore)
SwitchCompat mScBookstore;*/
private Setting mSetting;
private boolean isVolumeTurnPage;
private int resetScreenTime;
@ -125,7 +140,7 @@ public class MoreSettingActivity extends BaseActivity2 {
catheCap = mSetting.getCatheGap();
autoRefresh = mSetting.isRefreshWhenStart();
openBookStore = mSetting.isOpenBookStore();
threadNum = SharedPreUtils.getInstance().getInt("threadNum", 8);
threadNum = SharedPreUtils.getInstance().getInt(getString(R.string.threadNum), 8);
}
@Override
@ -147,6 +162,7 @@ public class MoreSettingActivity extends BaseActivity2 {
mTvThreadNum.setText(getString(R.string.cur_thread_num, threadNum));
}
private void initSwitchStatus() {
mScVolume.setChecked(isVolumeTurnPage);
mScMatchChapter.setChecked(isMatchChapter);
@ -157,6 +173,8 @@ public class MoreSettingActivity extends BaseActivity2 {
@Override
protected void initClick() {
super.initClick();
mLlWebdav.setOnClickListener(v -> startActivity(WebDavSettingActivity.class));
mRlVolume.setOnClickListener(
(v) -> {
if (isVolumeTurnPage) {
@ -257,7 +275,7 @@ public class MoreSettingActivity extends BaseActivity2 {
}
}
if (sb.lastIndexOf(",") >= 0) sb.deleteCharAt(sb.lastIndexOf(","));
spu.putString("searchSource", sb.toString());
spu.putString(getString(R.string.searchSource), sb.toString());
}, null, new DialogCreator.OnMultiDialogListener() {
@Override
public void onItemClick(DialogInterface dialog, int which, boolean isChecked) {
@ -287,7 +305,7 @@ public class MoreSettingActivity extends BaseActivity2 {
.setView(view)
.setPositiveButton("确定", (dialog, which) -> {
threadNum = threadPick.getValue();
SharedPreUtils.getInstance().putInt("threadNum", threadNum);
SharedPreUtils.getInstance().putInt(getString(R.string.threadNum), threadNum);
mTvThreadNum.setText(getString(R.string.cur_thread_num, threadNum));
}).setNegativeButton("取消", null)
.show();
@ -371,7 +389,7 @@ public class MoreSettingActivity extends BaseActivity2 {
String eCatheFileSize;
if (eCatheFile.exists() && eCatheFile.isDirectory()) {
eCatheFileSize = FileUtils.getFileSize(FileUtils.getDirSize(eCatheFile));
}else {
} else {
eCatheFileSize = "0";
}
CharSequence[] cathes = {"章节缓存:" + eCatheFileSize, "图片缓存:" + catheFileSize};
@ -397,6 +415,7 @@ public class MoreSettingActivity extends BaseActivity2 {
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -533,4 +552,6 @@ public class MoreSettingActivity extends BaseActivity2 {
mBooksName[i] = book.getName();
}
}
}

@ -20,6 +20,7 @@ import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.SysManager;
import xyz.fycz.myreader.base.BaseActivity;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.model.storage.Backup;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.presenter.ReadPresenter;
import xyz.fycz.myreader.widget.page.PageView;
@ -121,6 +122,7 @@ public class ReadActivity extends BaseActivity {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
Backup.INSTANCE.autoBack();
super.onBackPressed();
}

@ -119,9 +119,9 @@ public class SearchBookActivity extends BaseActivity2 {
if (srlSearchBookList != null) {
srlSearchBookList.finishRefresh();
}
if (curThreadCount == 0 && !isStopSearch) {
/*if (curThreadCount == 0 && !isStopSearch) {
rpb.setIsAutoLoading(false);
}
}*/
break;
case 3:
fabSearchStop.setVisibility(View.GONE);
@ -158,7 +158,7 @@ public class SearchBookActivity extends BaseActivity2 {
if (rpb != null) {
rpb.setIsAutoLoading(false);
}
mHandler.sendEmptyMessage(3);
fabSearchStop.setVisibility(View.GONE);
}
@Override
@ -357,7 +357,7 @@ public class SearchBookActivity extends BaseActivity2 {
} else {
isStopSearch = false;
mSearchBookAdapter = new SearchBookAdapter(mBooks);
mSearchBookAdapter = new SearchBookAdapter(mBooks, searchEngine);
rvSearchBooksList.setAdapter(mSearchBookAdapter);
//进入书籍详情页

@ -0,0 +1,131 @@
package xyz.fycz.myreader.ui.activity;
import android.os.Bundle;
import android.text.InputType;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.widget.Toolbar;
import butterknife.BindView;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.base.BaseActivity2;
import xyz.fycz.myreader.base.observer.MySingleObserver;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.model.storage.BackupRestoreUi;
import xyz.fycz.myreader.model.storage.WebDavHelp;
import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
import xyz.fycz.myreader.util.SharedPreUtils;
import xyz.fycz.myreader.util.StringHelper;
import xyz.fycz.myreader.util.ToastUtils;
import java.util.ArrayList;
/**
* @author fengyue
* @date 2020/10/4 20:44
*/
public class WebDavSettingActivity extends BaseActivity2 {
@BindView(R.id.webdav_setting_webdav_url)
LinearLayout llWebdavUrl;
@BindView(R.id.tv_webdav_url)
TextView tvWebdavUrl;
@BindView(R.id.webdav_setting_webdav_account)
LinearLayout llWebdavAccount;
@BindView(R.id.tv_webdav_account)
TextView tvWebdavAccount;
@BindView(R.id.webdav_setting_webdav_password)
LinearLayout llWebdavPassword;
@BindView(R.id.tv_webdav_password)
TextView tvWebdavPassword;
@BindView(R.id.webdav_setting_webdav_restore)
LinearLayout llWebdavRestore;
private String webdavUrl;
private String webdavAccount;
private String webdavPassword;
@Override
protected int getContentId() {
return R.layout.activity_webdav_setting;
}
@Override
protected void setUpToolbar(Toolbar toolbar) {
super.setUpToolbar(toolbar);
setStatusBarColor(R.color.colorPrimary, true);
getSupportActionBar().setTitle(getString(R.string.webdav_setting));
}
@Override
protected void initData(Bundle savedInstanceState) {
super.initData(savedInstanceState);
webdavUrl = SharedPreUtils.getInstance().getString("webdavUrl", APPCONST.DEFAULT_WEB_DAV_URL);
webdavAccount = SharedPreUtils.getInstance().getString("webdavAccount", "");
webdavPassword = SharedPreUtils.getInstance().getString("webdavPassword", "");
}
@Override
protected void initWidget() {
super.initWidget();
tvWebdavUrl.setText(webdavUrl);
tvWebdavAccount.setText(StringHelper.isEmpty(webdavAccount) ? "请输入WebDav账号" : webdavAccount);
tvWebdavPassword.setText(StringHelper.isEmpty(webdavPassword) ? "请输入WebDav授权密码" : "************");
}
@Override
protected void initClick() {
super.initClick();
final String[] webdavTexts = new String[3];
llWebdavUrl.setOnClickListener(v -> {
MyAlertDialog.createInputDia(this, getString(R.string.webdav_url),
"", webdavUrl.equals(APPCONST.DEFAULT_WEB_DAV_URL) ?
"" : webdavUrl, true, 100,
text -> webdavTexts[0] = text,
(dialog, which) -> {
webdavUrl = webdavTexts[0];
tvWebdavUrl.setText(webdavUrl);
SharedPreUtils.getInstance().putString("webdavUrl", webdavUrl);
dialog.dismiss();
});
});
llWebdavAccount.setOnClickListener(v -> {
MyAlertDialog.createInputDia(this, getString(R.string.webdav_account),
"", webdavAccount, true, 100,
text -> webdavTexts[1] = text,
(dialog, which) -> {
webdavAccount = webdavTexts[1];
tvWebdavAccount.setText(webdavAccount);
SharedPreUtils.getInstance().putString("webdavAccount", webdavAccount);
dialog.dismiss();
});
});
llWebdavPassword.setOnClickListener(v -> {
MyAlertDialog.createInputDia(this, getString(R.string.webdav_password),
"", webdavPassword, InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD,
true, 100,
text -> webdavTexts[2] = text,
(dialog, which) -> {
webdavPassword = webdavTexts[2];
tvWebdavPassword.setText("************");
SharedPreUtils.getInstance().putString("webdavPassword", webdavPassword);
dialog.dismiss();
});
});
llWebdavRestore.setOnClickListener(v -> {
Single.create((SingleOnSubscribe<ArrayList<String>>) emitter -> {
emitter.onSuccess(WebDavHelp.INSTANCE.getWebDavFileNames());
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new MySingleObserver<ArrayList<String>>() {
@Override
public void onSuccess(ArrayList<String> strings) {
if (!WebDavHelp.INSTANCE.showRestoreDialog(WebDavSettingActivity.this, strings, BackupRestoreUi.INSTANCE)) {
ToastUtils.showWarring("没有备份");
}
}
});
});
}
}

@ -17,6 +17,7 @@ import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
import xyz.fycz.myreader.widget.CoverImageView;
import xyz.fycz.myreader.widget.custom.DragAdapter;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.greendao.entity.Chapter;
@ -311,7 +312,7 @@ public abstract class BookcaseAdapter extends DragAdapter {
static class ViewHolder {
CheckBox cbBookChecked;
ImageView ivBookImg;
CoverImageView ivBookImg;
TextView tvBookName;
BadgeView tvNoReadNum;
ProgressBar pbLoading;

@ -70,13 +70,7 @@ public class BookcaseDetailedAdapter extends BookcaseAdapter {
if (StringHelper.isEmpty(book.getImgUrl())) {
book.setImgUrl("");
}
Glide.with(mContext)
.load(book.getImgUrl())
.error(R.mipmap.no_image)
.placeholder(R.mipmap.no_image)
//设置圆角
.apply(RequestOptions.bitmapTransform(new RoundedCorners(8)))
.into(viewHolder.ivBookImg);
viewHolder.ivBookImg.load(book.getImgUrl(), book.getName(),book.getAuthor());
viewHolder.tvBookName.setText(book.getName());

@ -67,13 +67,7 @@ public class BookcaseDragAdapter extends BookcaseAdapter {
book.setImgUrl("");
}
Glide.with(mContext)
.load(book.getImgUrl())
.error(R.mipmap.no_image)
.placeholder(R.mipmap.no_image)
//设置圆角
.apply(RequestOptions.bitmapTransform(new RoundedCorners(8)))
.into(viewHolder.ivBookImg);
viewHolder.ivBookImg.load(book.getImgUrl(), book.getName(),book.getAuthor());
viewHolder.tvBookName.setText(book.getName());

@ -7,6 +7,7 @@ import xyz.fycz.myreader.base.adapter.BaseListAdapter;
import xyz.fycz.myreader.base.adapter.IViewHolder;
import xyz.fycz.myreader.entity.SearchBookBean;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.model.SearchEngine;
import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap;
import xyz.fycz.myreader.ui.adapter.holder.SearchBookHolder;
@ -19,6 +20,12 @@ import java.util.List;
*/
public class SearchBookAdapter extends BaseListAdapter<SearchBookBean> {
private ConcurrentMultiValueMap<SearchBookBean, Book> mBooks;
private SearchEngine searchEngine;
public SearchBookAdapter(ConcurrentMultiValueMap<SearchBookBean, Book> mBooks, SearchEngine searchEngine) {
this.mBooks = mBooks;
this.searchEngine = searchEngine;
}
public SearchBookAdapter(ConcurrentMultiValueMap<SearchBookBean, Book> mBooks) {
this.mBooks = mBooks;
@ -26,7 +33,7 @@ public class SearchBookAdapter extends BaseListAdapter<SearchBookBean> {
@Override
protected IViewHolder<SearchBookBean> createViewHolder(int viewType) {
return new SearchBookHolder(mBooks);
return new SearchBookHolder(mBooks, searchEngine);
}
public synchronized void addAll(List<SearchBookBean> newDataS, String keyWord) {

@ -16,6 +16,7 @@ import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.enums.BookSource;
import xyz.fycz.myreader.greendao.entity.Book;
import xyz.fycz.myreader.ui.activity.BookDetailedActivity;
import xyz.fycz.myreader.widget.CoverImageView;
/**
* @author fengyue
@ -23,7 +24,7 @@ import xyz.fycz.myreader.ui.activity.BookDetailedActivity;
*/
public class BookStoreBookHolder extends ViewHolderImpl<Book> {
private ImageView tvBookImg;
private CoverImageView tvBookImg;
private TextView tvBookName;
private TextView tvBookAuthor;
private TextView tvBookTime;
@ -63,13 +64,7 @@ public class BookStoreBookHolder extends ViewHolderImpl<Book> {
if (hasImg){
tvBookImg.setVisibility(View.VISIBLE);
if (!MyApplication.isDestroy(mActivity)) {
Glide.with(getContext())
.load(data.getImgUrl())
.error(R.mipmap.no_image)
.placeholder(R.mipmap.no_image)
//设置圆角
.apply(RequestOptions.bitmapTransform(new RoundedCorners(8)))
.into(tvBookImg);
tvBookImg.load(data.getImgUrl(), data.getName(), data.getAuthor());
}
}
if (data.getSource() != null) {

@ -1,5 +1,6 @@
package xyz.fycz.myreader.ui.adapter.holder;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Handler;
import android.util.Log;
@ -11,6 +12,7 @@ import com.bumptech.glide.request.RequestOptions;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.base.adapter.ViewHolderImpl;
import xyz.fycz.myreader.model.SearchEngine;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.entity.SearchBookBean;
import xyz.fycz.myreader.enums.BookSource;
@ -21,6 +23,7 @@ import xyz.fycz.myreader.webapi.CommonApi;
import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil;
import xyz.fycz.myreader.webapi.crawler.base.BookInfoCrawler;
import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler;
import xyz.fycz.myreader.widget.CoverImageView;
import java.util.List;
@ -30,22 +33,25 @@ import java.util.List;
*/
public class SearchBookHolder extends ViewHolderImpl<SearchBookBean> {
private ConcurrentMultiValueMap<SearchBookBean, Book> mBooks;
private SearchEngine searchEngine;
private Handler mHandle = new Handler(message -> {
switch (message.what) {
case 1:
Book book = (Book) message.obj;
initOtherInfo(book);
int pos = message.arg1;
initOtherInfo(book, pos);
break;
}
return false;
});
public SearchBookHolder(ConcurrentMultiValueMap<SearchBookBean, Book> mBooks) {
public SearchBookHolder(ConcurrentMultiValueMap<SearchBookBean, Book> mBooks, SearchEngine searchEngine) {
this.mBooks = mBooks;
this.searchEngine = searchEngine;
}
ImageView ivBookImg;
CoverImageView ivBookImg;
TextView tvBookName;
TextView tvDesc;
TextView tvAuthor;
@ -69,6 +75,7 @@ public class SearchBookHolder extends ViewHolderImpl<SearchBookBean> {
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(SearchBookBean data, int pos) {
List<Book> aBooks = mBooks.getValues(data);
@ -77,56 +84,61 @@ public class SearchBookHolder extends ViewHolderImpl<SearchBookBean> {
if (StringHelper.isEmpty(book.getImgUrl())){
book.setImgUrl("");
}
if (!StringHelper.isEmpty(book.getDesc())) {
tvDesc.setText("简介:" + book.getDesc());
}
if (!StringHelper.isEmpty(book.getType())) {
tvType.setText(book.getType());
}
if (!StringHelper.isEmpty(book.getNewestChapterTitle())) {
tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle()));
}
if (!StringHelper.isEmpty(book.getAuthor())) {
tvAuthor.setText(book.getAuthor());
}
tvBookName.setText(book.getName());
tvAuthor.setText(book.getAuthor());
tvSource.setText(getContext().getString(R.string.source_title_num, BookSource.fromString(book.getSource()).text, bookCount));
ReadCrawler rc = ReadCrawlerUtil.getReadCrawler(book.getSource());
if (rc instanceof BookInfoCrawler){
if (tvBookName.getTag() == null || !(Boolean) tvBookName.getTag()) {
tvBookName.setTag(true);
} else {
initOtherInfo(book);
initOtherInfo(book, pos);
return;
}
Log.i(book.getName(), "initOtherInfo");
BookInfoCrawler bic = (BookInfoCrawler) rc;
CommonApi.getBookInfo(book, bic, new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
searchEngine.getBookInfo(book, bic, isSuccess -> {
if (isSuccess)
mHandle.sendMessage(mHandle.obtainMessage(1, pos, 0, book));
}
@Override
public void onError(Exception e) {
else
tvBookName.setTag(false);
}
});
}else {
initOtherInfo(book);
initOtherInfo(book, pos);
}
}
private void initOtherInfo(Book book){
private void initOtherInfo(Book book, int pos){
//简介
if (book.getDesc() == null) {
tvDesc.setText("");
}else {
if (StringHelper.isEmpty(tvDesc.getText().toString())) {
tvDesc.setText("简介:" + book.getDesc());
}
tvType.setText(book.getType());
tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle()));
tvAuthor.setText(book.getAuthor());
if (StringHelper.isEmpty(tvType.getText().toString())) {
tvType.setText(book.getType());
}
if (StringHelper.isEmpty(tvNewestChapter.getText().toString())) {
tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle()));
}
if (StringHelper.isEmpty(tvAuthor.getText().toString())) {
tvAuthor.setText(book.getAuthor());
}
//图片
if (!MyApplication.isDestroy((Activity) getContext())) {
Glide.with(getContext())
.load(book.getImgUrl())
// .override(DipPxUtil.dip2px(getContext(), 80), DipPxUtil.dip2px(getContext(), 150))
.error(R.mipmap.no_image)
.placeholder(R.mipmap.no_image)
//设置圆角
.apply(RequestOptions.bitmapTransform(new RoundedCorners(8)))
.into(ivBookImg);
ivBookImg.load(book.getImgUrl(), book.getName(), book.getAuthor());
}
}
}

@ -1,15 +1,89 @@
package xyz.fycz.myreader.ui.dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.textfield.TextInputLayout;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.util.StringHelper;
/**
* @author fengyue
* @date 2020/9/20 9:48
*/
public class MyAlertDialog {
public static AlertDialog.Builder build(Context context){
public static AlertDialog.Builder build(Context context) {
return new AlertDialog.Builder(context, R.style.alertDialogTheme);
}
public static AlertDialog createInputDia(Context context, String title, String hint, String initText,
Integer inputType, boolean cancelable, int maxLen, onInputChangeListener oic,
DialogInterface.OnClickListener posListener) {
View view = LayoutInflater.from(context).inflate(R.layout.edit_dialog, null);
TextInputLayout textInputLayout = view.findViewById(R.id.text_input_lay);
textInputLayout.setCounterMaxLength(maxLen);
EditText editText = textInputLayout.getEditText();
editText.setHint(hint);
if (inputType != null) editText.setInputType(inputType);
if (!StringHelper.isEmpty(initText)) editText.setText(initText);
editText.requestFocus();
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
MyApplication.getHandler().postDelayed(() -> imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED), 220);
AlertDialog inputDia = build(context)
.setTitle(title)
.setView(view)
.setCancelable(cancelable)
.setPositiveButton("确认", (dialog, which) -> {
posListener.onClick(dialog, which);
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
})
.setNegativeButton("取消", (dialog, which) -> {
imm.toggleSoftInput(0, InputMethodManager.SHOW_FORCED);
})
.show();
Button posBtn = inputDia.getButton(AlertDialog.BUTTON_POSITIVE);
posBtn.setEnabled(false);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String text = editText.getText().toString();
if (editText.getText().length() > 0 && editText.getText().length() <= maxLen && !text.equals(initText)) {
posBtn.setEnabled(true);
} else {
posBtn.setEnabled(false);
}
oic.onChange(text);
}
});
return inputDia;
}
public static AlertDialog createInputDia(Context context, String title, String hint, String initText,
boolean cancelable, int maxLen, onInputChangeListener oic,
DialogInterface.OnClickListener posListener) {
return createInputDia(context, title, hint, initText, InputType.TYPE_CLASS_TEXT, cancelable, maxLen, oic, posListener);
}
public interface onInputChangeListener{
void onChange(String text);
}
}

@ -18,6 +18,9 @@ import xyz.fycz.myreader.model.backup.BackupAndRestore;
import xyz.fycz.myreader.model.backup.UserService;
import xyz.fycz.myreader.base.BaseFragment;
import xyz.fycz.myreader.common.APPCONST;
import xyz.fycz.myreader.model.storage.Backup;
import xyz.fycz.myreader.model.storage.Restore;
import xyz.fycz.myreader.ui.activity.MainActivity;
import xyz.fycz.myreader.ui.dialog.DialogCreator;
import xyz.fycz.myreader.ui.dialog.MyAlertDialog;
import xyz.fycz.myreader.entity.Setting;
@ -29,6 +32,7 @@ import xyz.fycz.myreader.ui.activity.MoreSettingActivity;
import xyz.fycz.myreader.util.SharedPreUtils;
import xyz.fycz.myreader.util.ToastUtils;
import xyz.fycz.myreader.util.utils.NetworkUtils;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import java.io.File;
import java.text.SimpleDateFormat;
@ -150,15 +154,15 @@ public class MineFragment extends BaseFragment {
AlertDialog bookDialog = MyAlertDialog.build(getContext())
.setTitle(getContext().getResources().getString(R.string.menu_bookcase_backup))
.setItems(backupMenu, (dialog, which) -> {
switch (which) {
case 0:
mHandler.sendMessage(mHandler.obtainMessage(2));
break;
case 1:
mHandler.sendMessage(mHandler.obtainMessage(3));
break;
}
})
switch (which) {
case 0:
mHandler.sendMessage(mHandler.obtainMessage(2));
break;
case 1:
mHandler.sendMessage(mHandler.obtainMessage(3));
break;
}
})
.setNegativeButton(null, null)
.setPositiveButton(null, null)
.create();
@ -224,15 +228,15 @@ public class MineFragment extends BaseFragment {
themeMode = which;
switch (which) {
case 0:
SharedPreUtils.getInstance().putBoolean("isNightFS", true);
SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), true);
break;
case 1:
SharedPreUtils.getInstance().putBoolean("isNightFS", false);
SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), false);
mSetting.setDayStyle(true);
SysManager.saveSetting(mSetting);
break;
case 2:
SharedPreUtils.getInstance().putBoolean("isNightFS", false);
SharedPreUtils.getInstance().putBoolean(getString(R.string.isNightFS), false);
mSetting.setDayStyle(false);
SysManager.saveSetting(mSetting);
break;
@ -255,7 +259,7 @@ public class MineFragment extends BaseFragment {
mRlFeedback.setOnClickListener(v -> {
DialogCreator.createCommonDialog(getContext(), "问题反馈", "请加入QQ群(1085028304)反馈问题!", true,
"加入QQ群", "取消", (dialog, which) -> {
if (!MyApplication.joinQQGroup(getContext(),"8PIOnHFuH6A38hgxvD_Rp2Bu-Ke1ToBn")){
if (!MyApplication.joinQQGroup(getContext(), "8PIOnHFuH6A38hgxvD_Rp2Bu-Ke1ToBn")) {
ClipboardManager mClipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
//数据
ClipData mClipData = ClipData.newPlainText("Label", "1085028304");
@ -285,11 +289,22 @@ public class MineFragment extends BaseFragment {
DialogCreator.createCommonDialog(getContext(), "确认备份吗?", "新备份会替换原有备份!", true,
(dialogInterface, i) -> {
dialogInterface.dismiss();
if (mBackupAndRestore.backup("localBackup")) {
/*if (mBackupAndRestore.backup("localBackup")) {
DialogCreator.createTipDialog(getContext(), "备份成功,备份文件路径:" + APPCONST.BACKUP_FILE_DIR);
} else {
DialogCreator.createTipDialog(getContext(), "未给予储存权限,备份失败!");
}
}*/
Backup.INSTANCE.backup(MyApplication.getmContext(), APPCONST.BACKUP_FILE_DIR, new Backup.CallBack() {
@Override
public void backupSuccess() {
DialogCreator.createTipDialog(getContext(), "备份成功,备份文件路径:" + APPCONST.BACKUP_FILE_DIR);
}
@Override
public void backupError(@io.reactivex.annotations.NonNull String msg) {
DialogCreator.createTipDialog(getContext(), "未给予储存权限,备份失败!");
}
}, false);
}, (dialogInterface, i) -> dialogInterface.dismiss());
}
@ -300,7 +315,7 @@ public class MineFragment extends BaseFragment {
DialogCreator.createCommonDialog(getContext(), "确认恢复吗?", "恢复书架会覆盖原有书架!", true,
(dialogInterface, i) -> {
dialogInterface.dismiss();
if (mBackupAndRestore.restore("localBackup")) {
/*if (mBackupAndRestore.restore("localBackup")) {
mHandler.sendMessage(mHandler.obtainMessage(7));
// DialogCreator.createTipDialog(mMainActivity,
// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");
@ -308,7 +323,22 @@ public class MineFragment extends BaseFragment {
ToastUtils.showSuccess("书架恢复成功!");
} else {
DialogCreator.createTipDialog(getContext(), "未找到备份文件或未给予储存权限,恢复失败!");
}
}*/
Restore.INSTANCE.restore(APPCONST.BACKUP_FILE_DIR, new Restore.CallBack() {
@Override
public void restoreSuccess() {
mHandler.sendMessage(mHandler.obtainMessage(7));
// DialogCreator.createTipDialog(mMainActivity,
// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");
mSetting = SysManager.getSetting();
ToastUtils.showSuccess("书架恢复成功!");
}
@Override
public void restoreError(@io.reactivex.annotations.NonNull String msg) {
DialogCreator.createTipDialog(getContext(), "未找到备份文件或未给予储存权限,恢复失败!");
}
});
}, (dialogInterface, i) -> dialogInterface.dismiss());
}
@ -333,17 +363,27 @@ public class MineFragment extends BaseFragment {
SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd");
String nowTimeStr = sdf.format(nowTime);
SharedPreUtils spb = SharedPreUtils.getInstance();
String synTime = spb.getString("synTime");
String synTime = spb.getString(getString(R.string.synTime));
if (!nowTimeStr.equals(synTime) || !isAutoSyn) {
MyApplication.getApplication().newThread(() -> {
if (UserService.webBackup()) {
spb.putString("synTime", nowTimeStr);
if (!isAutoSyn) {
DialogCreator.createTipDialog(getContext(), "成功将书架同步至网络!");
UserService.webBackup(new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
if ((boolean) o) {
spb.putString(getString(R.string.synTime), nowTimeStr);
if (!isAutoSyn) {
DialogCreator.createTipDialog(getContext(), "成功将书架同步至网络!");
}
} else {
if (!isAutoSyn) {
DialogCreator.createTipDialog(getContext(), "同步失败,请重试!");
}
}
} else {
}
@Override
public void onError(Exception e) {
if (!isAutoSyn) {
DialogCreator.createTipDialog(getContext(), "同步失败,请重试!");
DialogCreator.createTipDialog(getContext(), "同步失败,请重试!\n" + e.getLocalizedMessage());
}
}
});
@ -362,7 +402,7 @@ public class MineFragment extends BaseFragment {
(dialogInterface, i) -> {
dialogInterface.dismiss();
MyApplication.getApplication().newThread(() -> {
if (UserService.webRestore()) {
/*if (UserService.webRestore()) {
mHandler.sendMessage(mHandler.obtainMessage(7));
// DialogCreator.createTipDialog(mMainActivity,
// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");、
@ -370,7 +410,26 @@ public class MineFragment extends BaseFragment {
ToastUtils.showSuccess("成功将书架从网络同步至本地!");
} else {
DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!");
}
}*/
UserService.webRestore(new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
if ((boolean) o) {
mHandler.sendMessage(mHandler.obtainMessage(7));
// DialogCreator.createTipDialog(mMainActivity,
// "恢复成功!\n注意:本功能属于实验功能,书架恢复后,书籍初次加载时可能加载失败,返回重新加载即可!");、
mSetting = SysManager.getSetting();
ToastUtils.showSuccess("成功将书架从网络同步至本地!");
} else {
DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!");
}
}
@Override
public void onError(Exception e) {
DialogCreator.createTipDialog(getContext(), "未找到同步文件,同步失败!\n" + e.getLocalizedMessage());
}
});
});
}, (dialogInterface, i) -> dialogInterface.dismiss());
}

@ -336,7 +336,7 @@ public class BookcasePresenter implements BasePresenter {
//初始化书籍
private void initBook() {
mBooks.clear();
String curBookGroupId = SharedPreUtils.getInstance().getString("curBookGroupId", "");
String curBookGroupId = SharedPreUtils.getInstance().getString(mMainActivity.getString(R.string.curBookGroupId), "");
isGroup = !"".equals(curBookGroupId);
if (mBookcaseAdapter != null) {
mBookcaseAdapter.setGroup(isGroup);
@ -481,12 +481,12 @@ public class BookcasePresenter implements BasePresenter {
mMainActivity.startActivity(fileSystemIntent);
break;
case R.id.action_download_all:
if (!SharedPreUtils.getInstance().getBoolean("isReadDownloadAllTip")) {
if (!SharedPreUtils.getInstance().getBoolean(mMainActivity.getString(R.string.isReadDownloadAllTip), false)) {
DialogCreator.createCommonDialog(mMainActivity, "一键缓存",
mMainActivity.getString(R.string.all_cathe_tip), true,
(dialog, which) -> {
downloadAll(true);
SharedPreUtils.getInstance().putBoolean("isReadDownloadAllTip", true);
SharedPreUtils.getInstance().putBoolean(mMainActivity.getString(R.string.isReadDownloadAllTip), true);
}, null);
} else {
downloadAll(true);
@ -515,8 +515,8 @@ public class BookcasePresenter implements BasePresenter {
curBookGroupId = mBookGroups.get(menuItem.getOrder() - 1).getId();
curBookGroupName = mBookGroups.get(menuItem.getOrder() - 1).getName();
}
SharedPreUtils.getInstance().putString("curBookGroupId", curBookGroupId);
SharedPreUtils.getInstance().putString("curBookGroupName", curBookGroupName);
SharedPreUtils.getInstance().putString(mMainActivity.getString(R.string.curBookGroupId), curBookGroupId);
SharedPreUtils.getInstance().putString(mMainActivity.getString(R.string.curBookGroupName), curBookGroupName);
ogcl.onChange();
init();
return true;
@ -587,6 +587,7 @@ public class BookcasePresenter implements BasePresenter {
private void showAddOrRenameGroupDia(boolean isRename, boolean isAddGroup, int groupNum){
View view = LayoutInflater.from(mMainActivity).inflate(R.layout.edit_dialog, null);
TextInputLayout textInputLayout = view.findViewById(R.id.text_input_lay);
textInputLayout.setCounterMaxLength(10);
EditText editText = textInputLayout.getEditText();
editText.setHint("请输入分组名");
BookGroup bookGroup = !isRename ? new BookGroup() : mBookGroups.get(groupNum);
@ -622,8 +623,8 @@ public class BookcasePresenter implements BasePresenter {
}else {
mBookGroupService.updateEntity(bookGroup);
SharedPreUtils spu = SharedPreUtils.getInstance();
if (spu.getString("curBookGroupName", "").equals(oldName)){
spu.putString("curBookGroupName", newGroupName.toString());
if (spu.getString(mMainActivity.getString(R.string.curBookGroupName), "").equals(oldName)){
spu.putString(mMainActivity.getString(R.string.curBookGroupName), newGroupName.toString());
ogcl.onChange();
}
}
@ -677,9 +678,9 @@ public class BookcasePresenter implements BasePresenter {
sb.deleteCharAt(sb.lastIndexOf("、"));
}
SharedPreUtils spu = SharedPreUtils.getInstance();
if (mBookGroupService.getGroupById(spu.getString("curBookGroupId", "")) == null){
spu.putString("curBookGroupId", "");
spu.putString("curBookGroupName", "");
if (mBookGroupService.getGroupById(spu.getString(mMainActivity.getString(R.string.curBookGroupId), "")) == null){
spu.putString(mMainActivity.getString(R.string.curBookGroupId), "");
spu.putString(mMainActivity.getString(R.string.curBookGroupName), "");
ogcl.onChange();
init();
}
@ -959,20 +960,30 @@ public class BookcasePresenter implements BasePresenter {
SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd");
String nowTimeStr = sdf.format(nowTime);
SharedPreUtils spb = SharedPreUtils.getInstance();
String synTime = spb.getString("synTime");
String synTime = spb.getString(mMainActivity.getString(R.string.synTime));
if (!nowTimeStr.equals(synTime) || !isAutoSyn) {
MyApplication.getApplication().newThread(() -> {
if (UserService.webBackup()) {
spb.putString("synTime", nowTimeStr);
if (!isAutoSyn) {
DialogCreator.createTipDialog(mMainActivity, "成功将书架同步至网络!");
UserService.webBackup(new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
if ((boolean) o){
spb.putString(mMainActivity.getString(R.string.synTime), nowTimeStr);
if (!isAutoSyn) {
DialogCreator.createTipDialog(mMainActivity, "成功将书架同步至网络!");
}
}else {
if (!isAutoSyn) {
DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!");
}
}
}
} else {
if (!isAutoSyn) {
DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!");
@Override
public void onError(Exception e) {
if (!isAutoSyn) {
DialogCreator.createTipDialog(mMainActivity, "同步失败,请重试!\n" + e.getLocalizedMessage());
}
}
}
});
});
}
}

@ -216,7 +216,7 @@ public class ReadPresenter implements BasePresenter {
@Override
public void start() {
if (SharedPreUtils.getInstance().getBoolean("isNightFS", false)) {
if (SharedPreUtils.getInstance().getBoolean(mReadActivity.getString(R.string.isNightFS), false)) {
mSetting.setDayStyle(!ColorUtil.isColorLight(mReadActivity.getColor(R.color.textPrimary)));
}
@ -256,7 +256,7 @@ public class ReadPresenter implements BasePresenter {
//当书籍Collected且书籍id不为空的时候保存上次阅读信息
if (isCollected && !StringHelper.isEmpty(mBook.getId())) {
//保存上次阅读信息
SharedPreUtils.getInstance().putString("lastRead", mBook.getId());
SharedPreUtils.getInstance().putString(mReadActivity.getString(R.string.lastRead), mBook.getId());
}
@ -325,7 +325,7 @@ public class ReadPresenter implements BasePresenter {
mBook = (Book) mReadActivity.getIntent().getSerializableExtra(APPCONST.BOOK);
//mBook为空,说明是从快捷方式启动
if (mBook == null) {
String bookId = SharedPreUtils.getInstance().getString("lastRead", "");
String bookId = SharedPreUtils.getInstance().getString(mReadActivity.getString(R.string.lastRead), "");
if ("".equals(bookId)) {//没有上次阅读信息
ToastUtils.showWarring("当前没有阅读任何书籍,无法加载上次阅读书籍!");
mReadActivity.finish();

@ -1,11 +1,13 @@
package xyz.fycz.myreader.util;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.util.Base64;
import android.util.Log;
import com.google.gson.Gson;
import okhttp3.*;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.application.TrustAllCerts;
import xyz.fycz.myreader.webapi.callback.HttpCallback;
@ -53,14 +55,51 @@ public class HttpUtil {
return ssfFactory;
}
public static X509TrustManager createTrustAllManager() {
X509TrustManager tm = null;
try {
tm = new X509TrustManager() {
@SuppressLint("TrustAllX509TrustManager")
public void checkClientTrusted(X509Certificate[] chain, String authType) {
//do nothing,接受任意客户端证书
}
@SuppressLint("TrustAllX509TrustManager")
public void checkServerTrusted(X509Certificate[] chain, String authType) {
//do nothing,接受任意服务端证书
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
} catch (Exception ignored) {
}
return tm;
}
private static Interceptor getHeaderInterceptor() {
return chain -> {
Request request = chain.request()
.newBuilder()
.addHeader("Keep-Alive", "300")
.addHeader("Connection", "Keep-Alive")
.addHeader("Cache-Control", "no-cache")
.build();
return chain.proceed(request);
};
}
public static synchronized OkHttpClient getOkHttpClient() {
if (mClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS);
builder.sslSocketFactory(createSSLSocketFactory());
builder.hostnameVerifier((hostname, session) -> true);
.writeTimeout(15, TimeUnit.SECONDS)
.sslSocketFactory(createSSLSocketFactory(), createTrustAllManager())
.hostnameVerifier((hostname, session) -> true)
.protocols(Collections.singletonList(Protocol.HTTP_1_1))
.addInterceptor(getHeaderInterceptor());
mClient = builder
.build();
}
@ -271,10 +310,10 @@ public class HttpUtil {
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4168.3 Safari/537.36");
if (address.contains("qidian.com")) {
SharedPreUtils spu = SharedPreUtils.getInstance();
String cookie = spu.getString("qdCookie", "");
String cookie = spu.getString(MyApplication.getmContext().getString(R.string.qdCookie), "");
if (cookie.equals("")) {
requestBuilder.addHeader("cookie", "_csrfToken=eXRDlZxmRDLvFAmdgzqvwWAASrxxp2WkVlH4ZM7e; newstatisticUUID=1595991935_2026387981");
}else {
} else {
requestBuilder.addHeader("cookie", cookie);
}
}

@ -5,6 +5,8 @@ import android.content.SharedPreferences;
import xyz.fycz.myreader.application.MyApplication;
import java.util.Map;
/**
* SharedPreferences工具类
*/
@ -38,17 +40,27 @@ public class SharedPreUtils {
public void putString(String key,String value){
sharedWritable.putString(key,value);
sharedWritable.commit();
sharedWritable.apply();
}
public void putInt(String key,int value){
sharedWritable.putInt(key, value);
sharedWritable.commit();
sharedWritable.apply();
}
public void putBoolean(String key,boolean value){
sharedWritable.putBoolean(key, value);
sharedWritable.commit();
sharedWritable.apply();
}
public void putFloat(String key,float value){
sharedWritable.putFloat(key, value);
sharedWritable.apply();
}
public void putLong(String key, long value){
sharedWritable.putLong(key, value);
sharedWritable.apply();
}
public String getString(String key){
@ -74,4 +86,26 @@ public class SharedPreUtils {
public boolean getBoolean(String key,boolean def){
return sharedReadable.getBoolean(key, false);
}
public float getFloat(String key){
return getFloat(key, 0);
}
public float getFloat(String key, float def){
return sharedReadable.getFloat(key, def);
}
public long getLong(String key){
return getLong(key, 0);
}
public long getLong(String key, long def){
return sharedReadable.getLong(key, def);
}
public void remove(String key){
sharedWritable.remove(key).apply();
}
public Map<String, ?> getAll(){
return sharedReadable.getAll();
}
}

@ -1,5 +1,7 @@
package xyz.fycz.myreader.util;
import xyz.fycz.myreader.util.utils.StringUtils;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Random;
@ -118,9 +120,9 @@ public class StringHelper {
public static boolean isEmpty(String str){
if (str != null){
str = str.replace(" ","");
str = StringUtils.deleteWhitespace(str);
}
return str == null || str.equals("");
return str == null || str.equals("") || str.equals("null");
}
/**

@ -0,0 +1,387 @@
package xyz.fycz.myreader.util.utils;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.regex.Pattern;
/**
* Created by PureDark on 2016/9/24.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class DocumentUtil {
private static Pattern FilePattern = Pattern.compile("[\\\\/:*?\"<>|]");
public static boolean isFileExist(Context context, String fileName, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return isFileExist(context, fileName, rootUri, subDirs);
}
public static boolean isFileExist(Context context, String fileName, Uri rootUri, String... subDirs) {
DocumentFile root;
if ("content".equals(rootUri.getScheme()))
root = DocumentFile.fromTreeUri(context, rootUri);
else
root = DocumentFile.fromFile(new File(rootUri.getPath()));
return isFileExist(fileName, root, subDirs);
}
public static boolean isFileExist(String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return false;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
if (file != null && file.exists())
return true;
return false;
}
public static DocumentFile createDirIfNotExist(Context context, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return createDirIfNotExist(context, rootUri, subDirs);
}
public static DocumentFile createDirIfNotExist(Context context, Uri rootUri, String... subDirs) {
DocumentFile root;
if ("content".equals(rootUri.getScheme()))
root = DocumentFile.fromTreeUri(context, rootUri);
else
root = DocumentFile.fromFile(new File(rootUri.getPath()));
return createDirIfNotExist(root, subDirs);
}
public static DocumentFile createDirIfNotExist(@NonNull DocumentFile root, String... subDirs) {
DocumentFile parent = root;
try {
for (String subDir1 : subDirs) {
String subDirName = filenameFilter(Uri.decode(subDir1));
DocumentFile subDir = parent.findFile(subDirName);
if (subDir == null) {
subDir = parent.createDirectory(subDirName);
}
parent = subDir;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return parent;
}
public static DocumentFile createFileIfNotExist(Context context, String fileName, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return createFileIfNotExist(context, "", fileName, rootUri, subDirs);
}
public static DocumentFile createFileIfNotExist(Context context, String fileName, Uri rootUri, String... subDirs) {
return createFileIfNotExist(context, "", fileName, rootUri, subDirs);
}
public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return createFileIfNotExist(context, mimeType, fileName, rootUri, subDirs);
}
public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, Uri rootUri, String... subDirs) {
DocumentFile parent = createDirIfNotExist(context, rootUri, subDirs);
if (parent == null)
return null;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
if (file == null) {
file = parent.createFile(mimeType, fileName);
}
return file;
}
public static boolean deleteFile(Context context, String fileName, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return deleteFile(context, fileName, rootUri, subDirs);
}
public static boolean deleteFile(Context context, String fileName, Uri rootUri, String... subDirs) {
DocumentFile root;
if ("content".equals(rootUri.getScheme()))
root = DocumentFile.fromTreeUri(context, rootUri);
else
root = DocumentFile.fromFile(new File(rootUri.getPath()));
return deleteFile(fileName, root, subDirs);
}
public static boolean deleteFile(String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return false;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
return file != null && file.exists() && file.delete();
}
public static boolean writeBytes(Context context, byte[] data, String fileName, String rootPath, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootPath, subDirs);
if (parent == null)
return false;
DocumentFile file = parent.findFile(fileName);
return writeBytes(context, data, file.getUri());
}
public static boolean writeBytes(Context context, byte[] data, String fileName, Uri rootUri, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootUri, subDirs);
if (parent == null)
return false;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
return writeBytes(context, data, file.getUri());
}
public static boolean writeBytes(Context context, byte[] data, String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return false;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
return writeBytes(context, data, file.getUri());
}
public static boolean writeBytes(Context context, byte[] data, DocumentFile file) {
return writeBytes(context, data, file.getUri());
}
public static boolean writeBytes(Context context, byte[] data, Uri fileUri) {
try {
OutputStream out = context.getContentResolver().openOutputStream(fileUri, "wt"); //Write file need open with truncate mode, the mode truncate file upon opening (to zero bytes)
out.write(data);
out.close();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public static boolean writeFromInputStream(Context context, InputStream inStream, DocumentFile file) {
return writeFromInputStream(context, inStream, file.getUri());
}
public static boolean writeFromInputStream(Context context, InputStream inStream, Uri fileUri) {
try {
OutputStream out = context.getContentResolver().openOutputStream(fileUri);
int byteread;
byte[] buffer = new byte[1024];
while ((byteread = inStream.read(buffer)) > 0) {
out.write(buffer, 0, byteread);
}
inStream.close();
out.close();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public static byte[] readBytes(Context context, String fileName, String rootPath, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootPath, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return readBytes(context, file.getUri());
}
public static byte[] readBytes(Context context, String fileName, Uri rootUri, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootUri, subDirs);
if (parent == null)
return null;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return readBytes(context, file.getUri());
}
public static byte[] readBytes(Context context, String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return null;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return readBytes(context, file.getUri());
}
public static byte[] readBytes(Context context, DocumentFile file) {
if (file == null)
return null;
return readBytes(context, file.getUri());
}
public static byte[] readBytes(Context context, Uri fileUri) {
try {
InputStream fis = context.getContentResolver().openInputStream(fileUri);
int len = fis.available();
byte[] buffer = new byte[len];
fis.read(buffer);
fis.close();
return buffer;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static DocumentFile getDirDocument(Context context, String rootPath, String... subDirs) {
Uri rootUri;
if (rootPath.startsWith("content"))
rootUri = Uri.parse(rootPath);
else
rootUri = Uri.parse(Uri.decode(rootPath));
return getDirDocument(context, rootUri, subDirs);
}
public static DocumentFile getDirDocument(Context context, Uri rootUri, String... subDirs) {
DocumentFile root;
if ("content".equals(rootUri.getScheme()))
root = DocumentFile.fromTreeUri(context, rootUri);
else
root = DocumentFile.fromFile(new File(rootUri.getPath()));
return getDirDocument(root, subDirs);
}
public static DocumentFile getDirDocument(DocumentFile root, String... subDirs) {
DocumentFile parent = root;
for (int i = 0; i < subDirs.length; i++) {
String subDirName = Uri.decode(subDirs[i]);
DocumentFile subDir = parent.findFile(subDirName);
if (subDir != null)
parent = subDir;
else
return null;
}
return parent;
}
public static OutputStream getFileOutputSteam(Context context, String fileName, String rootPath, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootPath, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileOutputSteam(context, file.getUri());
}
public static OutputStream getFileOutputSteam(Context context, String fileName, Uri rootUri, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootUri, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileOutputSteam(context, file.getUri());
}
public static OutputStream getFileOutputSteam(Context context, String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileOutputSteam(context, file.getUri());
}
public static OutputStream getFileOutputSteam(Context context, DocumentFile file) {
return getFileOutputSteam(context, file.getUri());
}
public static OutputStream getFileOutputSteam(Context context, Uri fileUri) {
try {
OutputStream out = context.getContentResolver().openOutputStream(fileUri);
return out;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static InputStream getFileInputSteam(Context context, String fileName, String rootPath, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootPath, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileInputSteam(context, file.getUri());
}
public static InputStream getFileInputSteam(Context context, String fileName, Uri rootUri, String... subDirs) {
DocumentFile parent = getDirDocument(context, rootUri, subDirs);
if (parent == null)
return null;
fileName = filenameFilter(Uri.decode(fileName));
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileInputSteam(context, file.getUri());
}
public static InputStream getFileInputSteam(Context context, String fileName, DocumentFile root, String... subDirs) {
DocumentFile parent = getDirDocument(root, subDirs);
if (parent == null)
return null;
DocumentFile file = parent.findFile(fileName);
if (file == null)
return null;
return getFileInputSteam(context, file.getUri());
}
public static InputStream getFileInputSteam(Context context, DocumentFile file) {
return getFileInputSteam(context, file.getUri());
}
public static InputStream getFileInputSteam(Context context, Uri fileUri) {
try {
InputStream in = context.getContentResolver().openInputStream(fileUri);
return in;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String filenameFilter(String str) {
return str == null ? null : FilePattern.matcher(str).replaceAll("_");
}
}

@ -1,21 +1,28 @@
package xyz.fycz.myreader.util.utils;
import android.app.Application;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
import info.monitorenter.cpdetector.io.*;
import io.reactivex.Single;
import io.reactivex.SingleEmitter;
import io.reactivex.SingleOnSubscribe;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.util.StringHelper;
import java.io.*;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
/**
* Created by newbiechen on 17-5-11.
*/
public class FileUtils {
//采用自己的格式去设置文件,防止文件被系统文件查询到
@ -234,4 +241,592 @@ public class FileUtils {
return charsetName;
}
/**
* 写文本文件 在Android系统中文件保存在 /data/data/PACKAGE_NAME/files/ 目录下
*
* @param context
*/
public static void write(Context context, String fileName, String content) {
if (content == null)
content = "";
try {
FileOutputStream fos = context.openFileOutput(fileName,
Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 读取文本文件
*
* @param context
* @param fileName
* @return
*/
public static String read(Context context, String fileName) {
try {
FileInputStream in = context.openFileInput(fileName);
return readInStream(in);
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
private static String readInStream(FileInputStream inStream) {
try {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[512];
int length = -1;
while ((length = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, length);
}
outStream.close();
inStream.close();
return outStream.toString();
} catch (IOException e) {
Log.i("FileTest", e.getMessage());
}
return null;
}
/**
* 向手机写图片
*
* @param buffer
* @param folder
* @param fileName
* @return
*/
public static boolean writeFile(byte[] buffer, String folder,
String fileName) {
boolean writeSucc = false;
File fileDir = new File(folder);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
File file = new File(folder,fileName);
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
out.write(buffer);
writeSucc = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return writeSucc;
}
public static boolean writeFile(byte[] buffer, File file) {
boolean writeSucc = false;
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
out.write(buffer);
writeSucc = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return writeSucc;
}
/**
* 根据文件绝对路径获取文件名
*
* @param filePath
* @return
*/
public static String getFileName(String filePath) {
if (StringHelper.isEmpty(filePath))
return "";
//return filePath.substring(filePath.lastIndexOf("?") + 1);
return filePath.substring(filePath.lastIndexOf("/") + 1);
}
/**
* 根据文件的绝对路径获取文件名但不包含扩展名
*
* @param filePath
* @return
*/
public static String getFileNameNoFormat(String filePath) {
if (StringHelper.isEmpty(filePath)) {
return "";
}
int point = filePath.lastIndexOf('.');
return filePath.substring(filePath.lastIndexOf(File.separator) + 1,
point);
}
/**
* 获取文件扩展名
*
* @param fileName
* @return
*/
public static String getFileFormat(String fileName) {
if (StringHelper.isEmpty(fileName))
return "";
int point = fileName.lastIndexOf('.');
return fileName.substring(point + 1);
}
/**
* 获取目录文件个数
*
* @return
*/
public long getFileList(File dir) {
long count = 0;
File[] files = dir.listFiles();
count = files.length;
for (File file : files) {
if (file.isDirectory()) {
count = count + getFileList(file);// 递归
count--;
}
}
return count;
}
public static byte[] toBytes(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int ch;
while ((ch = in.read()) != -1) {
out.write(ch);
}
byte buffer[] = out.toByteArray();
out.close();
return buffer;
}
/**
* 检查文件是否存在
*
* @param name
* @return
*/
public static boolean checkFileExists(String name) {
boolean status;
if (!name.equals("")) {
File path = Environment.getExternalStorageDirectory();
File newPath = new File(path.toString() + name);
status = newPath.exists();
} else {
status = false;
}
return status;
}
/**
* 返回项目的files目录
* @return
*/
public static String getPath(){
File path = Environment.getExternalStorageDirectory();
return path.toString();
}
/**
* 检查路径是否存在
*
* @param path
* @return
*/
public static boolean checkFilePathExists(String path) {
return new File(path).exists();
}
/**
* 计算SD卡的剩余空间
*
* @return 返回-1说明没有安装sd卡
*/
public static long getFreeDiskSpace() {
String status = Environment.getExternalStorageState();
long freeSpace = 0;
if (status.equals(Environment.MEDIA_MOUNTED)) {
try {
File path = Environment.getExternalStorageDirectory();
StatFs stat = new StatFs(path.getPath());
long blockSize = stat.getBlockSize();
long availableBlocks = stat.getAvailableBlocks();
freeSpace = availableBlocks * blockSize / 1024;
} catch (Exception e) {
e.printStackTrace();
}
} else {
return -1;
}
return (freeSpace);
}
/**
* 新建目录
*
* @param directoryName
* @return
*/
public static boolean createDirectory(String directoryName) {
boolean status;
if (!directoryName.equals("")) {
File path = Environment.getExternalStorageDirectory();
File newPath = new File(path.toString() + directoryName);
status = newPath.mkdir();
status = true;
} else
status = false;
return status;
}
/**
* 检查是否安装SD卡
*
* @return
*/
public static boolean checkSaveLocationExists() {
String sDCardStatus = Environment.getExternalStorageState();
boolean status;
status = sDCardStatus.equals(Environment.MEDIA_MOUNTED);
return status;
}
/**
* 删除目录(包括目录里的所有文件)
*
* @param fileName
* @return
*/
public static boolean deleteDirectory(String fileName) {
boolean status;
SecurityManager checker = new SecurityManager();
if (!fileName.equals("")) {
File path = Environment.getExternalStorageDirectory();
File newPath = new File(path.toString() + fileName);
checker.checkDelete(newPath.toString());
if (newPath.isDirectory()) {
String[] listfile = newPath.list();
// delete all files within the specified directory and then
// delete the directory
try {
for (int i = 0; i < listfile.length; i++) {
File deletedFile = new File(newPath.toString() + "/"
+ listfile[i].toString());
deletedFile.delete();
}
newPath.delete();
Log.i("DirManager delDirectory", fileName);
status = true;
} catch (Exception e) {
e.printStackTrace();
status = false;
}
} else
status = false;
} else
status = false;
return status;
}
/**
* 删除空目录
*
* 返回 0代表成功 ,1 代表没有删除权限, 2代表不是空目录,3 代表未知错误
*
* @return
*/
public static int deleteBlankPath(String path) {
File f = new File(path);
if (!f.canWrite()) {
return 1;
}
if (f.list() != null && f.list().length > 0) {
return 2;
}
if (f.delete()) {
return 0;
}
return 3;
}
/**
* 重命名
*
* @param oldName
* @param newName
* @return
*/
public static boolean reNamePath(String oldName, String newName) {
File f = new File(oldName);
return f.renameTo(new File(newName));
}
/**
* 删除文件
*
* @param filePath
*/
public static boolean deleteFileWithPath(String filePath) {
SecurityManager checker = new SecurityManager();
File f = new File(filePath);
checker.checkDelete(filePath);
if (f.isFile()) {
Log.i("DirManager delFile", filePath);
f.delete();
return true;
}
return false;
}
/**
* 获取SD卡的根目录末尾带\
*
* @return
*/
public static String getSDRoot() {
return Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator;
}
/**
* 列出root目录下所有子目录
*
* @return 绝对路径
*/
public static List<String> listPath(String root) {
List<String> allDir = new ArrayList<String>();
SecurityManager checker = new SecurityManager();
File path = new File(root);
checker.checkRead(root);
if (path.isDirectory()) {
for (File f : path.listFiles()) {
if (f.isDirectory()) {
allDir.add(f.getAbsolutePath());
}
}
}
return allDir;
}
public enum PathStatus {
SUCCESS, EXITS, ERROR
}
/**
* 创建目录
*
*/
public static xyz.fycz.myreader.util.FileUtils.PathStatus createPath(String newPath) {
File path = new File(newPath);
if (path.exists()) {
return xyz.fycz.myreader.util.FileUtils.PathStatus.EXITS;
}
if (path.mkdir()) {
return xyz.fycz.myreader.util.FileUtils.PathStatus.SUCCESS;
} else {
return xyz.fycz.myreader.util.FileUtils.PathStatus.ERROR;
}
}
/**
* 截取路径名
*
* @return
*/
public static String getPathName(String absolutePath) {
int start = absolutePath.lastIndexOf(File.separator) + 1;
int end = absolutePath.length();
return absolutePath.substring(start, end);
}
public static String getFilePathFromContentUri(Context context, Uri uri) {
String photoPath = "";
if(context == null || uri == null) {
return photoPath;
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
String docId = DocumentsContract.getDocumentId(uri);
if(isExternalStorageDocument(uri)) {
String[] split = docId.split(":");
if(split.length >= 2) {
String type = split[0];
if("primary".equalsIgnoreCase(type)) {
photoPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
}
}
else if(isDownloadsDocument(uri)) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
photoPath = getDataColumn(context, contentUri, null, null);
}
else if(isMediaDocument(uri)) {
String[] split = docId.split(":");
if(split.length >= 2) {
String type = split[0];
Uri contentUris = null;
if("image".equals(type)) {
contentUris = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
else if("video".equals(type)) {
contentUris = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}
else if("audio".equals(type)) {
contentUris = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
String selection = MediaStore.Images.Media._ID + "=?";
String[] selectionArgs = new String[] { split[1] };
photoPath = getDataColumn(context, contentUris, selection, selectionArgs);
}
}
}
else if("file".equalsIgnoreCase(uri.getScheme())) {
photoPath = uri.getPath();
}
else {
photoPath = getDataColumn(context, uri, null, null);
}
return photoPath;
}
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
Cursor cursor = null;
String column = MediaStore.Images.Media.DATA;
String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null && !cursor.isClosed())
cursor.close();
}
return null;
}
public static byte[] File2byte(String filePath)
{
byte[] buffer = null;
try
{
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1)
{
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
return buffer;
}
public static void byte2File(byte[] buf, String filePath, String fileName)
{
BufferedOutputStream bos = null;
FileOutputStream fos = null;
File file = null;
try
{
File dir = new File(filePath);
if (!dir.exists() && dir.isDirectory())
{
dir.mkdirs();
}
file = new File(filePath + File.separator + fileName);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
bos.write(buf);
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
if (bos != null)
{
try
{
bos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
if (fos != null)
{
try
{
fos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
}

@ -0,0 +1,43 @@
package xyz.fycz.myreader.util.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import org.jetbrains.anko.attempt
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
val GSON: Gson by lazy {
GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
}
inline fun <reified T> genericType() = object : TypeToken<T>() {}.type
@Throws(JsonSyntaxException::class)
inline fun <reified T> Gson.fromJsonObject(json: String?): T? {//可转成任意类型
return attempt {
val result: T? = fromJson(json, genericType<T>())
result
}.value
}
@Throws(JsonSyntaxException::class)
inline fun <reified T> Gson.fromJsonArray(json: String?): List<T>? {
return attempt {
val result: List<T>? = fromJson(json, ParameterizedTypeImpl(T::class.java))
result
}.value
}
class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType {
override fun getRawType(): Type = List::class.java
override fun getOwnerType(): Type? = null
override fun getActualTypeArguments(): Array<Type> = arrayOf(clazz)
}

@ -0,0 +1,50 @@
package xyz.fycz.myreader.util.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import java.io.File
object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> {
return when {
path.isNullOrEmpty() -> Glide.with(context).load(path)
path.startsWith("http", true) -> Glide.with(context).load(path)
else -> try {
Glide.with(context).load(File(path))
} catch (e: Exception) {
Glide.with(context).load(path)
}
}
}
fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> {
return Glide.with(context).load(resId)
}
fun load(context: Context, file: File?): RequestBuilder<Drawable> {
return Glide.with(context).load(file)
}
fun load(context: Context, uri: Uri?): RequestBuilder<Drawable> {
return Glide.with(context).load(uri)
}
fun load(context: Context, drawable: Drawable?): RequestBuilder<Drawable> {
return Glide.with(context).load(drawable)
}
fun load(context: Context, bitmap: Bitmap?): RequestBuilder<Drawable> {
return Glide.with(context).load(bitmap)
}
fun load(context: Context, bytes: ByteArray?): RequestBuilder<Drawable> {
return Glide.with(context).load(bytes)
}
}

@ -0,0 +1,250 @@
package xyz.fycz.myreader.util.webdav
import xyz.fycz.myreader.util.webdav.http.Handler
import xyz.fycz.myreader.util.webdav.http.HttpAuth
import okhttp3.*
import org.jsoup.Jsoup
import xyz.fycz.myreader.util.HttpUtil
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.*
class WebDav @Throws(MalformedURLException::class)
constructor(urlStr: String) {
companion object {
// 指定返回哪些属性
private const val DIR =
"""<?xml version="1.0"?>
<a:propfind xmlns:a="DAV:">
<a:prop>
<a:displayname/>
<a:resourcetype/>
<a:getcontentlength/>
<a:creationdate/>
<a:getlastmodified/>
%s
</a:prop>
</a:propfind>"""
}
private val url: URL = URL(null, urlStr, Handler)
private val httpUrl: String? by lazy {
val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://")
try {
return@lazy URLEncoder.encode(raw, "UTF-8")
.replace("\\+".toRegex(), "%20")
.replace("%3A".toRegex(), ":")
.replace("%2F".toRegex(), "/")
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
return@lazy null
}
}
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
/**
* 填充文件信息实例化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
}
/**
* 列出当前路径下的文件
*
* @param propsList 指定列出文件的哪些属性
* @return 文件列表
*/
@Throws(IOException::class)
@JvmOverloads
fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {
propFindResponse(propsList)?.let { response ->
if (response.isSuccessful) {
response.body()?.let { body ->
return parseDir(body.string())
}
}
}
return ArrayList()
}
@Throws(IOException::class)
private fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? {
val requestProps = StringBuilder()
for (p in propsList) {
requestProps.append("<a:").append(p).append("/>\n")
}
val requestPropsStr: String
requestPropsStr = 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 HttpUtil.getOkHttpClient().newCall(request.build()).execute()
}
return null
}
private fun parseDir(s: String): List<WebDav> {
val list = ArrayList<WebDav>()
val document = Jsoup.parse(s)
val elements = document.getElementsByTag("d:response")
httpUrl?.let { url ->
val baseUrl = if (url.endsWith("/")) url else "$url/"
for (element in elements) {
val href = element.getElementsByTag("d:href")[0].text()
if (!href.endsWith("/")) {
val fileName = href.substring(href.lastIndexOf("/") + 1)
val webDavFile: WebDav
try {
webDavFile = WebDav(baseUrl + fileName)
webDavFile.displayName = fileName
webDavFile.urlName = href
list.add(webDavFile)
} catch (e: MalformedURLException) {
e.printStackTrace()
}
}
}
}
return list
}
/**
* 根据自己的URL在远程处创建对应的文件夹
*
* @return 是否创建成功
*/
@Throws(IOException::class)
fun makeAsDir(): Boolean {
httpUrl?.let { url ->
val request = Request.Builder()
.url(url)
.method("MKCOL", null)
return execRequest(request)
}
return false
}
/**
* 下载到本地
*
* @param savedPath 本地的完整路径包括最后的文件名
* @param replaceExisting 是否替换本地的同名文件
* @return 下载是否成功
*/
fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean {
if (File(savedPath).exists()) {
if (!replaceExisting) return false
}
val inputS = getInputStream() ?: return false
File(savedPath).writeBytes(inputS.readBytes())
return true
}
/**
* 上传文件
*/
@Throws(IOException::class)
@JvmOverloads
fun upload(localPath: String, contentType: String? = null): Boolean {
val file = File(localPath)
if (!file.exists()) return false
val mediaType = if (contentType == null) null else MediaType.parse(contentType)
// 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息
val fileBody = RequestBody.create(mediaType, file)
httpUrl?.let {
val request = Request.Builder()
.url(it)
.put(fileBody)
return execRequest(request)
}
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)
)
}
val response = HttpUtil.getOkHttpClient().newCall(requestBuilder.build()).execute()
return response.isSuccessful
}
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 HttpUtil.getOkHttpClient().newCall(request.build()).execute().body()?.byteStream()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
}
return null
}
}

@ -0,0 +1,16 @@
package xyz.fycz.myreader.util.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
}
}

@ -0,0 +1,9 @@
package xyz.fycz.myreader.util.webdav.http
object HttpAuth {
var auth: Auth? = null
class Auth internal constructor(val user: String, val pass: String)
}

@ -1,10 +1,14 @@
package xyz.fycz.myreader.webapi;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.annotations.NonNull;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import xyz.fycz.myreader.common.URLCONST;
import xyz.fycz.myreader.entity.SearchBookBean;
import xyz.fycz.myreader.greendao.entity.Chapter;
import xyz.fycz.myreader.model.mulvalmap.ConcurrentMultiValueMap;
import xyz.fycz.myreader.util.StringHelper;
import xyz.fycz.myreader.util.utils.OkHttpUtils;
@ -16,6 +20,7 @@ import xyz.fycz.myreader.webapi.crawler.read.FYReadCrawler;
import xyz.fycz.myreader.webapi.crawler.read.TianLaiReadCrawler;
import java.io.IOException;
import java.util.List;
public class CommonApi extends BaseApi {
@ -41,6 +46,27 @@ public class CommonApi extends BaseApi {
});
}
/**
* 获取章节列表
*
* @param url
*/
public static Observable<List<Chapter>> getBookChapters(String url, final ReadCrawler rc) {
String charset = rc.getCharset();
return Observable.create(emitter -> {
try {
emitter.onNext(rc.getChaptersFromHtml(OkHttpUtils.getHtml(url, charset)));
} catch (Exception e) {
e.printStackTrace();
emitter.onError(e);
}
emitter.onComplete();
});
}
/**
* 获取章节正文
*
@ -75,6 +101,24 @@ public class CommonApi extends BaseApi {
});
}
/**
* 获取章节正文
*
* @param url
*/
public static Observable<String> getChapterContent(String url, final ReadCrawler rc) {
String charset = rc.getCharset();
return Observable.create(emitter -> {
try {
emitter.onNext(rc.getContentFormHtml(OkHttpUtils.getHtml(url, charset)));
} catch (Exception e) {
e.printStackTrace();
emitter.onError(e);
}
emitter.onComplete();
});
}
/**
* 搜索小说
@ -130,11 +174,11 @@ public class CommonApi extends BaseApi {
}else {
emitter.onNext(rc.getBooksFromSearchHtml(OkHttpUtils.getHtml(makeSearchUrl(rc.getSearchLink(), key), finalCharset)));
}
emitter.onComplete();
} catch (IOException e) {
} catch (Exception e) {
e.printStackTrace();
emitter.onError(e);
}
emitter.onComplete();
});
}
@ -155,7 +199,12 @@ public class CommonApi extends BaseApi {
url = book.getInfoUrl();
}
return Observable.create(emitter -> {
emitter.onNext(bic.getBookInfo(OkHttpUtils.getHtml(url, bic.getCharset()), book));
try {
emitter.onNext(bic.getBookInfo(OkHttpUtils.getHtml(url, bic.getCharset()), book));
} catch (Exception e) {
e.printStackTrace();
emitter.onError(e);
}
emitter.onComplete();
});
}
@ -180,7 +229,6 @@ public class CommonApi extends BaseApi {
@Override
public void onError(Exception e) {
callback.onError(e);
}
});
}

@ -2,6 +2,7 @@ package xyz.fycz.myreader.webapi;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.common.URLCONST;
@ -93,7 +94,7 @@ public class LanZousApi {
SharedPreUtils spu = SharedPreUtils.getInstance();
String lanzousKeyStart = "var pposturl = '";
try {
lanzousKeyStart = spu.getString("lanzousKeyStart");
lanzousKeyStart = spu.getString(MyApplication.getmContext().getString(R.string.lanzousKeyStart));
}catch (Exception e){
e.printStackTrace();
}

@ -1,6 +1,8 @@
package xyz.fycz.myreader.webapi.crawler;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.enums.BookSource;
import xyz.fycz.myreader.util.SharedPreUtils;
import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler;
@ -20,7 +22,7 @@ public class ReadCrawlerUtil {
public static ArrayList<ReadCrawler> getReadCrawlers() {
SharedPreUtils spu = SharedPreUtils.getInstance();
String searchSource = spu.getString("searchSource", null);
String searchSource = spu.getString(MyApplication.getmContext().getString(R.string.searchSource), null);
ArrayList<ReadCrawler> readCrawlers = new ArrayList<>();
if (searchSource == null) {
StringBuilder sb = new StringBuilder();
@ -33,7 +35,7 @@ public class ReadCrawlerUtil {
}
sb.deleteCharAt(sb.lastIndexOf(","));
searchSource = sb.toString();
spu.putString("searchSource", searchSource);
spu.putString(MyApplication.getmContext().getString(R.string.searchSource), searchSource);
} else if (!"".equals(searchSource)){
String[] sources = searchSource.split(",");
for (String source : sources) {
@ -45,7 +47,7 @@ public class ReadCrawlerUtil {
public static HashMap<CharSequence, Boolean> getDisableSources() {
SharedPreUtils spu = SharedPreUtils.getInstance();
String searchSource = spu.getString("searchSource", null);
String searchSource = spu.getString(MyApplication.getmContext().getString(R.string.searchSource), null);
HashMap<CharSequence, Boolean> mSources = new HashMap<>();
if (searchSource == null) {
for (BookSource bookSource : BookSource.values()) {
@ -79,7 +81,7 @@ public class ReadCrawlerUtil {
sb.append(",");
}
sb.deleteCharAt(sb.lastIndexOf(","));
SharedPreUtils.getInstance().putString("searchSource", sb.toString());
SharedPreUtils.getInstance().putString(MyApplication.getmContext().getString(R.string.searchSource), sb.toString());
}
public static ReadCrawler getReadCrawler(String bookSource) {

@ -4,6 +4,7 @@ import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import xyz.fycz.myreader.R;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.entity.bookstore.BookType;
@ -136,7 +137,7 @@ public class QiDianMobileRank extends FindCrawler {
}
url = url.replace("{sex}", !isFemale ? sex[0] : sex[1]);
SharedPreUtils spu = SharedPreUtils.getInstance();
String cookie = spu.getString("qdCookie", "");
String cookie = spu.getString(MyApplication.getmContext().getString(R.string.qdCookie), "");
if (!cookie.equals("")) {
url = url.replace("{cookie}", StringHelper.getSubString(cookie, "_csrfToken=", ";"));
} else {

@ -0,0 +1,158 @@
package xyz.fycz.myreader.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.text.TextPaint
import android.util.AttributeSet
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 xyz.fycz.myreader.R
import xyz.fycz.myreader.util.utils.ImageLoader
class CoverImageView : androidx.appcompat.widget.AppCompatImageView {
internal var width: Float = 0.toFloat()
internal var height: Float = 0.toFloat()
private var nameHeight = 0f
private var authorHeight = 0f
private val namePaint = TextPaint()
private val authorPaint = TextPaint()
private var name: String? = null
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
)
init {
namePaint.typeface = Typeface.DEFAULT_BOLD
namePaint.isAntiAlias = true
namePaint.textAlign = Paint.Align.CENTER
namePaint.textSkewX = -0.2f
authorPaint.typeface = Typeface.DEFAULT
authorPaint.isAntiAlias = true
authorPaint.textAlign = Paint.Align.CENTER
authorPaint.textSkewX = -0.1f
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val measuredHeight = measuredWidth * 7 / 5
super.onMeasure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
width = getWidth().toFloat()
height = getHeight().toFloat()
namePaint.textSize = width / 6
namePaint.strokeWidth = namePaint.textSize / 10
authorPaint.textSize = width / 9
authorPaint.strokeWidth = authorPaint.textSize / 10
nameHeight = height / 2
authorHeight = nameHeight + authorPaint.fontSpacing
}
override fun onDraw(canvas: Canvas) {
if (width >= 10 && height > 10) {
@SuppressLint("DrawAllocation")
val path = Path()
//四个圆角
path.moveTo(10f, 0f)
path.lineTo(width - 10, 0f)
path.quadTo(width, 0f, width, 10f)
path.lineTo(width, height - 10)
path.quadTo(width, height, width - 10, height)
path.lineTo(10f, height)
path.quadTo(0f, height, 0f, height - 10)
path.lineTo(0f, 10f)
path.quadTo(0f, 0f, 10f, 0f)
canvas.clipPath(path)
}
super.onDraw(canvas)
if (!loadFailed) return
name?.let {
namePaint.color = Color.WHITE
namePaint.style = Paint.Style.STROKE
canvas.drawText(it, width / 2, nameHeight, namePaint)
namePaint.color = Color.RED
namePaint.style = Paint.Style.FILL
canvas.drawText(it, width / 2, nameHeight, namePaint)
}
author?.let {
authorPaint.color = Color.WHITE
authorPaint.style = Paint.Style.STROKE
canvas.drawText(it, width / 2, authorHeight, authorPaint)
authorPaint.color = Color.RED
authorPaint.style = Paint.Style.FILL
canvas.drawText(it, width / 2, authorHeight, authorPaint)
}
}
fun setHeight(height: Int) {
val width = height * 5 / 7
minimumWidth = width
}
private fun setText(name: String?, author: String?) {
this.name =
when {
name == null -> null
name.length > 5 -> name.substring(0, 4) + ""
else -> name
}
this.author =
when {
author == null -> null
author.length > 8 -> author.substring(0, 7) + ""
else -> author
}
}
fun load(path: String?, name: String?, author: String?) {
setText(name, author)
ImageLoader.load(context, path)//Glide自动识别http://和file://
.placeholder(R.mipmap.default_cover)
.error(R.mipmap.default_cover)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
loadFailed = true
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
loadFailed = false
return false
}
})
.centerCrop()
.into(this)
}
}

@ -2,7 +2,10 @@ package xyz.fycz.myreader.widget.page;
import android.util.Log;
import io.reactivex.*;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import xyz.fycz.myreader.application.MyApplication;
import xyz.fycz.myreader.webapi.callback.ResultCallback;
import xyz.fycz.myreader.common.APPCONST;
@ -82,178 +85,197 @@ public class LocalPageLoader extends PageLoader {
* 1. 序章的添加
* 2. 章节存在的书本的虚拟分章效果
*/
public void loadChapters(final ResultCallback resultCallback) {
MyApplication.getApplication().newThread(() -> {
mBookFile = new File(mCollBook.getChapterUrl());
//获取文件编码
mCharset = FileUtils.getFileEncode(mBookFile.getAbsolutePath());
List<TxtChapter> chapters = new ArrayList<>();
RandomAccessFile bookStream = null;
boolean hasChapter = false;
try {
//获取文件流
bookStream = new RandomAccessFile(mBookFile, "r");
//寻找匹配文章标题的正则表达式,判断是否存在章节名
hasChapter = checkChapterType(bookStream);
//加载章节
byte[] buffer = new byte[BUFFER_SIZE];
//获取到的块起始点,在文件中的位置
long curOffset = 0;
//block的个数
int blockPos = 0;
//读取的长度
int length;
//获取文件中的数据到buffer,直到没有数据为止
while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) {
++blockPos;
//如果存在Chapter
if (hasChapter) {
//将数据转换成String
String blockContent = new String(buffer, 0, length, mCharset);
//当前Block下使过的String的指针
int seekPos = 0;
//进行正则匹配
Matcher matcher = mChapterPattern.matcher(blockContent);
//如果存在相应章节
while (matcher.find()) {
//获取匹配到的字符在字符串中的起始位置
int chapterStart = matcher.start();
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) {
//获取当前章节的内容
String chapterContent = blockContent.substring(seekPos, chapterStart);
//设置指针偏移
seekPos += chapterContent.length();
//如果当前对整个文件的偏移位置为0的话,那么就是序章
if (curOffset == 0) {
//创建序章
TxtChapter preChapter = new TxtChapter();
preChapter.title = "序章";
preChapter.start = 0;
preChapter.end = chapterContent.getBytes(mCharset).length; //获取String的byte值,作为最终值
//如果序章大小大于30才添加进去
if (preChapter.end - preChapter.start > 30) {
chapters.add(preChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = preChapter.end;
chapters.add(curChapter);
}
//否则就block分割之后,上一个章节的剩余内容
else {
//获取上一章节
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
//将当前段落添加上一章去
lastChapter.end += chapterContent.getBytes(mCharset).length;
//如果章节内容太小,则移除
if (lastChapter.end - lastChapter.start < 30) {
chapters.remove(lastChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = lastChapter.end;
chapters.add(curChapter);
}
} else {
//是否存在章节
if (chapters.size() != 0) {
//获取章节内容
String chapterContent = blockContent.substring(seekPos, matcher.start());
seekPos += chapterContent.length();
//获取上一章节
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset).length;
//如果章节内容太小,则移除
if (lastChapter.end - lastChapter.start < 30) {
chapters.remove(lastChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = lastChapter.end;
chapters.add(curChapter);
}
//如果章节不存在则创建章节
else {
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = 0;
chapters.add(curChapter);
}
public List<TxtChapter> loadChapters() throws IOException {
mBookFile = new File(mCollBook.getChapterUrl());
//获取文件编码
mCharset = FileUtils.getFileEncode(mBookFile.getAbsolutePath());
List<TxtChapter> chapters = new ArrayList<>();
RandomAccessFile bookStream = null;
boolean hasChapter = false;
//获取文件流
bookStream = new RandomAccessFile(mBookFile, "r");
//寻找匹配文章标题的正则表达式,判断是否存在章节名
hasChapter = checkChapterType(bookStream);
//加载章节
byte[] buffer = new byte[BUFFER_SIZE];
//获取到的块起始点,在文件中的位置
long curOffset = 0;
//block的个数
int blockPos = 0;
//读取的长度
int length;
//获取文件中的数据到buffer,直到没有数据为止
while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) {
++blockPos;
//如果存在Chapter
if (hasChapter) {
//将数据转换成String
String blockContent = new String(buffer, 0, length, mCharset);
//当前Block下使过的String的指针
int seekPos = 0;
//进行正则匹配
Matcher matcher = mChapterPattern.matcher(blockContent);
//如果存在相应章节
while (matcher.find()) {
//获取匹配到的字符在字符串中的起始位置
int chapterStart = matcher.start();
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容
//第一种情况一定是序章 第二种情况可能是上一个章节的内容
if (seekPos == 0 && chapterStart != 0) {
//获取当前章节的内容
String chapterContent = blockContent.substring(seekPos, chapterStart);
//设置指针偏移
seekPos += chapterContent.length();
//如果当前对整个文件的偏移位置为0的话,那么就是序章
if (curOffset == 0) {
//创建序章
TxtChapter preChapter = new TxtChapter();
preChapter.title = "序章";
preChapter.start = 0;
preChapter.end = chapterContent.getBytes(mCharset).length; //获取String的byte值,作为最终值
//如果序章大小大于30才添加进去
if (preChapter.end - preChapter.start > 30) {
chapters.add(preChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = preChapter.end;
chapters.add(curChapter);
}
//否则就block分割之后,上一个章节的剩余内容
else {
//获取上一章节
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
//将当前段落添加上一章去
lastChapter.end += chapterContent.getBytes(mCharset).length;
//如果章节内容太小,则移除
if (lastChapter.end - lastChapter.start < 30) {
chapters.remove(lastChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = lastChapter.end;
chapters.add(curChapter);
}
} else {
//是否存在章节
if (chapters.size() != 0) {
//获取章节内容
String chapterContent = blockContent.substring(seekPos, matcher.start());
seekPos += chapterContent.length();
//获取上一章节
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
lastChapter.end = lastChapter.start + chapterContent.getBytes(mCharset).length;
//如果章节内容太小,则移除
if (lastChapter.end - lastChapter.start < 30) {
chapters.remove(lastChapter);
}
//创建当前章节
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = lastChapter.end;
chapters.add(curChapter);
}
//如果章节不存在则创建章节
else {
TxtChapter curChapter = new TxtChapter();
curChapter.title = matcher.group();
curChapter.start = 0;
chapters.add(curChapter);
}
}
//进行本地虚拟分章
else {
//章节在buffer的偏移量
int chapterOffset = 0;
//当前剩余可分配的长度
int strLength = length;
//分章的位置
int chapterPos = 0;
while (strLength > 0) {
++chapterPos;
//是否长度超过一章
if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) {
//在buffer中一章的终止点
int end = length;
//寻找换行符作为终止点
for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) {
if (buffer[i] == Charset.BLANK) {
end = i;
break;
}
}
TxtChapter chapter = new TxtChapter();
chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
chapter.start = curOffset + chapterOffset + 1;
chapter.end = curOffset + end;
chapters.add(chapter);
//减去已经被分配的长度
strLength = strLength - (end - chapterOffset);
//设置偏移的位置
chapterOffset = end;
} else {
TxtChapter chapter = new TxtChapter();
chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
chapter.start = curOffset + chapterOffset + 1;
chapter.end = curOffset + length;
chapters.add(chapter);
strLength = 0;
}
}
//进行本地虚拟分章
else {
//章节在buffer的偏移量
int chapterOffset = 0;
//当前剩余可分配的长度
int strLength = length;
//分章的位置
int chapterPos = 0;
while (strLength > 0) {
++chapterPos;
//是否长度超过一章
if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) {
//在buffer中一章的终止点
int end = length;
//寻找换行符作为终止点
for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) {
if (buffer[i] == Charset.BLANK) {
end = i;
break;
}
}
TxtChapter chapter = new TxtChapter();
chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
chapter.start = curOffset + chapterOffset + 1;
chapter.end = curOffset + end;
chapters.add(chapter);
//减去已经被分配的长度
strLength = strLength - (end - chapterOffset);
//设置偏移的位置
chapterOffset = end;
} else {
TxtChapter chapter = new TxtChapter();
chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")";
chapter.start = curOffset + chapterOffset + 1;
chapter.end = curOffset + length;
chapters.add(chapter);
strLength = 0;
}
}
}
//block的偏移点
curOffset += length;
//block的偏移点
curOffset += length;
if (hasChapter) {
//设置上一章的结尾
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
lastChapter.end = curOffset;
}
if (hasChapter) {
//设置上一章的结尾
TxtChapter lastChapter = chapters.get(chapters.size() - 1);
lastChapter.end = curOffset;
}
//当添加的block太多的时候,执行GC
if (blockPos % 15 == 0) {
System.gc();
System.runFinalization();
}
}
//当添加的block太多的时候,执行GC
if (blockPos % 15 == 0) {
System.gc();
System.runFinalization();
}
}
IOUtils.close(bookStream);
System.gc();
System.runFinalization();
return chapters;
}
public void loadChapters(final ResultCallback resultCallback) {
// 通过RxJava异步处理分章事件
Single.create((SingleOnSubscribe<List<TxtChapter>>) e -> {
e.onSuccess(loadChapters());
}).compose(RxUtils::toSimpleSingle).subscribe(new SingleObserver<List<TxtChapter>>() {
@Override
public void onSubscribe(Disposable d) {
mChapterDisp = d;
}
@Override
public void onSuccess(List<TxtChapter> chapters) {
mChapterDisp = null;
isChapterListPrepare = true;
List<Chapter> mChapters = new ArrayList<>();
int i = 0;
for (TxtChapter txtChapter : chapters) {
@ -272,17 +294,14 @@ public class LocalPageLoader extends PageLoader {
if (resultCallback != null) {
resultCallback.onFinish(mChapters, 1);
}
} catch (IOException e) {
e.printStackTrace();
if (resultCallback != null) {
resultCallback.onError(e);
}
} finally {
IOUtils.close(bookStream);
System.gc();
System.runFinalization();
}
@Override
public void onError(Throwable e) {
resultCallback.onError((Exception) e);
chapterError();
Log.d(TAG, "file load error:" + e.toString());
}
});
}
@ -369,59 +388,6 @@ public class LocalPageLoader extends PageLoader {
return;
}
// 通过RxJava异步处理分章事件
Single.create((SingleOnSubscribe<Void>) e -> {
loadChapters(null);
e.onSuccess(new Void());
}).compose(RxUtils::toSimpleSingle).subscribe(new SingleObserver<Void>() {
@Override
public void onSubscribe(Disposable d) {
mChapterDisp = d;
}
@Override
public void onSuccess(Void value) {
mChapterDisp = null;
isChapterListPrepare = true;
// 提示目录加载完成
if (mPageChangeListener != null) {
mPageChangeListener.onCategoryFinish(mChapterList);
}
mCollBook.setChapterTotalNum(mChapterList.size());
// 加载并显示当前章节
openChapter();
}
@Override
public void onError(Throwable e) {
chapterError();
Log.d(TAG, "file load error:" + e.toString());
}
});
/*loadChapters(new ResultCallback() {
@Override
public void onFinish(Object o, int code) {
isChapterListPrepare = true;
// 提示目录加载完成
if (mPageChangeListener != null) {
mPageChangeListener.onCategoryFinish(mChapterList);
}
// 加载并显示当前章节
openChapter();
}
@Override
public void onError(Exception e) {
chapterError();
Log.d(TAG, "file load error:" + e.toString());
}
});*/
}
@Override

@ -16,6 +16,28 @@
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/more_setting_ll_webdav"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="8dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="vertical"
android:background="@drawable/selector_common_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_normal_size"
android:textColor="@color/textSecondary"
android:text="@string/webdav_setting"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textColor="@color/textAssist"
android:text="@string/webdav_setting_tip"/>
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"

@ -158,7 +158,8 @@
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/srl_search_book_list"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants">
<com.scwang.smartrefresh.header.MaterialHeader
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<include layout="@layout/toolbar"/>
<LinearLayout
android:id="@+id/webdav_setting_webdav_url"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="8dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="vertical"
android:background="@drawable/selector_common_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_normal_size"
android:textColor="@color/textPrimary"
android:text="@string/webdav_url"/>
<TextView
android:id="@+id/tv_webdav_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textColor="@color/textSecondary"
tools:text="https://dav.jianguoyun.com/dav/"/>
</LinearLayout>
<LinearLayout
android:id="@+id/webdav_setting_webdav_account"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="8dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="vertical"
android:background="@drawable/selector_common_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_normal_size"
android:textColor="@color/textPrimary"
android:text="@string/webdav_account"/>
<TextView
android:id="@+id/tv_webdav_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textColor="@color/textSecondary"
tools:text="输入你的WebDav账号"/>
</LinearLayout>
<LinearLayout
android:id="@+id/webdav_setting_webdav_password"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="8dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="vertical"
android:background="@drawable/selector_common_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_normal_size"
android:textColor="@color/textPrimary"
android:text="@string/webdav_password"/>
<TextView
android:id="@+id/tv_webdav_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textColor="@color/textSecondary"
tools:text="输入你的WebDav授权密码"/>
</LinearLayout>
<LinearLayout
android:id="@+id/webdav_setting_webdav_restore"
android:layout_width="match_parent"
android:layout_height="60dp"
android:paddingTop="8dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="vertical"
android:background="@drawable/selector_common_bg">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/text_normal_size"
android:textColor="@color/textPrimary"
android:text="@string/menu_backup_restore"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textColor="@color/textSecondary"
android:text="@string/webdav_restore_tip"/>
</LinearLayout>
</LinearLayout>

@ -6,8 +6,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
app:counterEnabled="true"
app:counterMaxLength="10">
app:counterEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"

@ -24,12 +24,13 @@
android:paddingEnd="2dp"
android:theme="@style/MyCheckBox"
android:visibility="gone"/>
<ImageView
<xyz.fycz.myreader.widget.CoverImageView
android:id="@+id/iv_book_img"
android:layout_width="64dp"
android:layout_height="88dp"
android:scaleType="fitXY"
app:srcCompat="@mipmap/default_cover"/>
app:srcCompat="@mipmap/default_cover" />
<LinearLayout android:id="@+id/ll_book_read"
android:layout_width="match_parent"

@ -13,12 +13,12 @@
android:orientation="vertical"
android:padding="10dp" >
<ImageView
android:id="@+id/iv_book_img"
android:layout_width="80dp"
android:layout_height="110dp"
android:scaleType="fitXY"
app:srcCompat="@mipmap/default_cover"/>
<xyz.fycz.myreader.widget.CoverImageView
android:id="@+id/iv_book_img"
android:layout_width="80dp"
android:layout_height="110dp"
android:scaleType="fitXY"
app:srcCompat="@mipmap/default_cover" />
<TextView
android:layout_marginTop="5dp"

@ -19,13 +19,14 @@
app:cardCornerRadius="0dp"
app:cardElevation="2dp">
<ImageView
android:id="@+id/book_detail_iv_cover"
android:layout_width="82dp"
android:layout_height="110dp"
android:scaleType="centerCrop"
android:background="@color/colorPrimary"
android:src="@mipmap/no_image"/>
<xyz.fycz.myreader.widget.CoverImageView
android:id="@+id/book_detail_iv_cover"
android:layout_width="82dp"
android:layout_height="110dp"
android:scaleType="centerCrop"
android:background="@color/colorPrimary"
app:srcCompat="@mipmap/default_cover" />
</androidx.cardview.widget.CardView>

@ -2,17 +2,17 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:background="@color/colorBackground"
android:padding="3dp">
<ImageView
<xyz.fycz.myreader.widget.CoverImageView
android:id="@+id/iv_book_img"
android:layout_width="42dp"
android:layout_height="60dp"
android:scaleType="fitXY"
android:src="@mipmap/no_image"
app:srcCompat="@mipmap/default_cover"
android:visibility="gone"/>
<LinearLayout

@ -1,23 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/colorBackground"
android:padding="5dp">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:background="@color/colorBackground"
android:padding="5dp">
<ImageView
<xyz.fycz.myreader.widget.CoverImageView
android:id="@+id/iv_book_img"
android:layout_width="64dp"
android:layout_height="88dp"
android:scaleType="fitXY"
app:srcCompat="@mipmap/no_image" />
app:srcCompat="@mipmap/default_cover" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:padding="3dp">
<LinearLayout
@ -34,7 +34,7 @@
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginTop="5dp"
android:text="bookname"
tools:text="bookname"
android:maxLength="12"
android:textSize="16sp"
android:textColor="@color/textPrimary"/>
@ -45,7 +45,7 @@
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:layout_marginTop="6dp"
android:text="author"
tools:text="author"
android:maxLines="1"
android:textSize="13sp"
android:textColor="@color/textSecondary"/>
@ -56,7 +56,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="1dp"
android:text="newest_chapter"
tools:text="newest_chapter"
android:maxLines="1"
android:textSize="13sp"
android:textColor="@color/textSecondary"/>
@ -65,7 +65,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2dp"
android:text="book desc"
tools:text="book desc"
android:ellipsize="end"
android:maxLines="1"
android:textSize="13sp"
@ -77,7 +77,7 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:text="type"
tools:text="type"
android:maxLines="1"
android:textSize="13sp"
android:textColor="@color/textSecondary"/>
@ -88,7 +88,7 @@
android:paddingTop="3dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:text="book source"
tools:text="book source"
android:maxLines="1"
android:textSize="13sp"
android:textColor="@color/textSecondary"/>

@ -156,6 +156,26 @@
<string name="newest_chapter">最新章节:%1$s</string>
<string name="source_title_num">书源:%1$s 共%2$d个源</string>
<!--SharedPre-->
<string name="lanzousKeyStart">lanzousKeyStart</string>
<string name="downloadLink">downloadLink</string>
<string name="qdCookie">qdCookie</string>
<string name="searchSource">searchSource</string>
<string name="synTime">synTime</string>
<string name="curBookGroupId">curBookGroupId</string>
<string name="curBookGroupName">curBookGroupName</string>
<string name="lastRead">lastRead</string>
<string name="threadNum">threadNum</string>
<string name="isNightFS">isNightFS</string>
<string name="isReadTopTip">isReadTopTip</string>
<string name="isReadDownloadAllTip">isReadDownloadAllTip</string>
<string name="webdav_url">WebDav 服务器地址</string>
<string name="webdav_account">WebDav 账号</string>
<string name="webdav_password">WebDav 密码</string>
<string name="webdav_restore_tip">从WebDav恢复</string>
<string name="webdav_setting">WebDav设置</string>
<string name="webdav_setting_tip">WebDav备份与恢复</string>
<string-array name="reset_screen_time">
<item>常亮</item>

@ -1,2 +1,2 @@
#Fri Oct 02 22:23:04 CST 2020
VERSION_CODE=150
#Sat Oct 03 16:52:44 CST 2020
VERSION_CODE=151

@ -1,15 +1,21 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
ext {
support_library_version = '28.0.0'
}
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
jcenter()
mavenCentral()
google()
maven { url 'http://s3.amazonaws.com/fabric-artifacts/public' }
maven { url 'https://plugins.gradle.org/m2/' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.2'
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

Loading…
Cancel
Save