parent
b983ca07c6
commit
0fe7b59989
@ -0,0 +1,31 @@ |
|||||||
|
package xyz.fycz.myreader.entity; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author fengyue |
||||||
|
* @date 2021/6/12 20:55 |
||||||
|
*/ |
||||||
|
public class Quotation { |
||||||
|
private String hitokoto; |
||||||
|
private String from; |
||||||
|
|
||||||
|
public String getHitokoto() { |
||||||
|
return hitokoto; |
||||||
|
} |
||||||
|
|
||||||
|
public void setHitokoto(String hitokoto) { |
||||||
|
this.hitokoto = hitokoto; |
||||||
|
} |
||||||
|
|
||||||
|
public String getFrom() { |
||||||
|
return from; |
||||||
|
} |
||||||
|
|
||||||
|
public void setFrom(String from) { |
||||||
|
this.from = from; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return hitokoto + " --- " + from; |
||||||
|
} |
||||||
|
} |
@ -1,56 +0,0 @@ |
|||||||
package xyz.fycz.myreader.widget.page; |
|
||||||
|
|
||||||
|
|
||||||
public class BookChapterBean { |
|
||||||
|
|
||||||
//章节所属的小说(网络)
|
|
||||||
public String bookId; |
|
||||||
//章节名
|
|
||||||
public String title; |
|
||||||
|
|
||||||
//章节内容在文章中的起始位置(本地)
|
|
||||||
public long start; |
|
||||||
//章节内容在文章中的终止位置(本地)
|
|
||||||
public long end; |
|
||||||
|
|
||||||
public String getBookId() { |
|
||||||
return bookId; |
|
||||||
} |
|
||||||
|
|
||||||
public void setBookId(String id) { |
|
||||||
this.bookId = id; |
|
||||||
} |
|
||||||
|
|
||||||
public String getTitle() { |
|
||||||
return title; |
|
||||||
} |
|
||||||
|
|
||||||
public void setTitle(String title) { |
|
||||||
this.title = title; |
|
||||||
} |
|
||||||
|
|
||||||
public long getStart() { |
|
||||||
return start; |
|
||||||
} |
|
||||||
|
|
||||||
public void setStart(long start) { |
|
||||||
this.start = start; |
|
||||||
} |
|
||||||
|
|
||||||
public long getEnd() { |
|
||||||
return end; |
|
||||||
} |
|
||||||
|
|
||||||
public void setEnd(long end) { |
|
||||||
this.end = end; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public String toString() { |
|
||||||
return "BookChapterBean{" + |
|
||||||
"title='" + title + '\'' + |
|
||||||
", start=" + start + |
|
||||||
", end=" + end + |
|
||||||
'}'; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,328 @@ |
|||||||
|
package xyz.fycz.myreader.widget.page; |
||||||
|
|
||||||
|
import android.graphics.Bitmap; |
||||||
|
import android.graphics.BitmapFactory; |
||||||
|
import android.graphics.Canvas; |
||||||
|
import android.text.TextUtils; |
||||||
|
import android.util.Log; |
||||||
|
|
||||||
|
|
||||||
|
import net.sf.jazzlib.ZipFile; |
||||||
|
|
||||||
|
import org.jsoup.Jsoup; |
||||||
|
import org.jsoup.nodes.Document; |
||||||
|
import org.jsoup.nodes.Element; |
||||||
|
import org.jsoup.nodes.TextNode; |
||||||
|
import org.jsoup.select.Elements; |
||||||
|
|
||||||
|
import java.io.BufferedReader; |
||||||
|
import java.io.ByteArrayInputStream; |
||||||
|
import java.io.File; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStreamReader; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import io.reactivex.Observable; |
||||||
|
import io.reactivex.ObservableOnSubscribe; |
||||||
|
import io.reactivex.Observer; |
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers; |
||||||
|
import io.reactivex.disposables.Disposable; |
||||||
|
import io.reactivex.schedulers.Schedulers; |
||||||
|
import nl.siegmann.epublib.domain.Book; |
||||||
|
import nl.siegmann.epublib.domain.MediaType; |
||||||
|
import nl.siegmann.epublib.domain.Metadata; |
||||||
|
import nl.siegmann.epublib.domain.Resource; |
||||||
|
import nl.siegmann.epublib.domain.SpineReference; |
||||||
|
import nl.siegmann.epublib.domain.TOCReference; |
||||||
|
import nl.siegmann.epublib.epub.EpubReader; |
||||||
|
import nl.siegmann.epublib.service.MediatypeService; |
||||||
|
import xyz.fycz.myreader.base.observer.MyObserver; |
||||||
|
import xyz.fycz.myreader.common.APPCONST; |
||||||
|
import xyz.fycz.myreader.entity.Setting; |
||||||
|
import xyz.fycz.myreader.greendao.entity.Chapter; |
||||||
|
import xyz.fycz.myreader.greendao.service.ChapterService; |
||||||
|
import xyz.fycz.myreader.util.help.StringHelper; |
||||||
|
import xyz.fycz.myreader.util.utils.FileUtils; |
||||||
|
import xyz.fycz.myreader.util.utils.StringUtils; |
||||||
|
|
||||||
|
public class EpubPageLoader extends PageLoader { |
||||||
|
|
||||||
|
//编码类型
|
||||||
|
private Charset mCharset; |
||||||
|
|
||||||
|
private Book epubBook; |
||||||
|
|
||||||
|
EpubPageLoader(PageView pageView, xyz.fycz.myreader.greendao.entity.Book bookShelfBean, Setting setting) { |
||||||
|
super(pageView, bookShelfBean, setting); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void refreshChapterList() { |
||||||
|
if (mCollBook == null) return; |
||||||
|
|
||||||
|
Observable.create((ObservableOnSubscribe<xyz.fycz.myreader.greendao.entity.Book>) e -> { |
||||||
|
File bookFile = new File(mCollBook.getChapterUrl()); |
||||||
|
epubBook = readBook(bookFile); |
||||||
|
|
||||||
|
if (epubBook == null) { |
||||||
|
e.onError(new Exception("文件解析失败")); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (TextUtils.isEmpty(mCollBook.getInfoUrl())) { |
||||||
|
mCollBook.setInfoUrl("UTF-8"); |
||||||
|
} |
||||||
|
mCharset = Charset.forName(mCollBook.getInfoUrl()); |
||||||
|
|
||||||
|
if (TextUtils.isEmpty(mCollBook.getImgUrl())){ |
||||||
|
saveCover(); |
||||||
|
} |
||||||
|
e.onNext(mCollBook); |
||||||
|
e.onComplete(); |
||||||
|
}).subscribeOn(Schedulers.single()) |
||||||
|
.flatMap(this::checkChapterList) |
||||||
|
.observeOn(AndroidSchedulers.mainThread()) |
||||||
|
.subscribe(new MyObserver<xyz.fycz.myreader.greendao.entity.Book>() { |
||||||
|
@Override |
||||||
|
public void onSubscribe(Disposable d) { |
||||||
|
mChapterDis = d; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onNext(xyz.fycz.myreader.greendao.entity.Book bookShelfBean) { |
||||||
|
isChapterListPrepare = true; |
||||||
|
// 加载并显示当前章节
|
||||||
|
openChapter(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onError(Throwable e) { |
||||||
|
chapterError(e.getMessage()); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static Book readBook(File file) { |
||||||
|
try { |
||||||
|
EpubReader epubReader = new EpubReader(); |
||||||
|
MediaType[] lazyTypes = { |
||||||
|
MediatypeService.CSS, |
||||||
|
MediatypeService.GIF, |
||||||
|
MediatypeService.JPG, |
||||||
|
MediatypeService.PNG, |
||||||
|
MediatypeService.MP3, |
||||||
|
MediatypeService.MP4}; |
||||||
|
ZipFile zipFile = new ZipFile(file); |
||||||
|
return epubReader.readEpubLazy(zipFile, "utf-8", Arrays.asList(lazyTypes)); |
||||||
|
} catch (Exception e) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void saveCover() throws IOException { |
||||||
|
byte[] data = epubBook.getCoverImage().getData(); |
||||||
|
FileUtils.writeFile(data, APPCONST.COVER_DIR, mCollBook.getId() + ".jpg"); |
||||||
|
mCollBook.setImgUrl(APPCONST.COVER_DIR + mCollBook.getId() + ".jpg"); |
||||||
|
} |
||||||
|
|
||||||
|
/*private void extractScaledCoverImage() { |
||||||
|
try { |
||||||
|
byte[] data = epubBook.getCoverImage().getData(); |
||||||
|
Bitmap rawCover = BitmapFactory.decodeByteArray(data, 0, data.length); |
||||||
|
int width = rawCover.getWidth(); |
||||||
|
int height = rawCover.getHeight(); |
||||||
|
if (width <= mVisibleWidth && width >= 0.8 * mVisibleWidth) { |
||||||
|
cover = rawCover; |
||||||
|
return; |
||||||
|
} |
||||||
|
height = Math.round(mVisibleWidth * 1.0f * height / width); |
||||||
|
cover = Bitmap.createScaledBitmap(rawCover, mVisibleWidth, height, true); |
||||||
|
} catch (Exception ignored) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void drawCover(Canvas canvas, float top) { |
||||||
|
if (cover == null) { |
||||||
|
extractScaledCoverImage(); |
||||||
|
} |
||||||
|
if (cover == null) |
||||||
|
return; |
||||||
|
int left = (mDisplayWidth - cover.getWidth()) / 2; |
||||||
|
canvas.drawBitmap(cover, left, top, mTextPaint); |
||||||
|
}*/ |
||||||
|
|
||||||
|
private List<Chapter> loadChapters() { |
||||||
|
Metadata metadata = epubBook.getMetadata(); |
||||||
|
mCollBook.setName(metadata.getFirstTitle()); |
||||||
|
if (metadata.getAuthors().size() > 0) { |
||||||
|
String author = metadata.getAuthors().get(0).toString().replaceAll("^, |, $", ""); |
||||||
|
mCollBook.setAuthor(author); |
||||||
|
} |
||||||
|
if (metadata.getDescriptions().size() > 0) { |
||||||
|
mCollBook.setDesc(Jsoup.parse(metadata.getDescriptions().get(0)).text()); |
||||||
|
} |
||||||
|
mChapterList = new ArrayList<>(); |
||||||
|
List<TOCReference> refs = epubBook.getTableOfContents().getTocReferences(); |
||||||
|
if (refs == null || refs.isEmpty()) { |
||||||
|
List<SpineReference> spineReferences = epubBook.getSpine().getSpineReferences(); |
||||||
|
for (int i = 0, size = spineReferences.size(); i < size; i++) { |
||||||
|
Resource resource = spineReferences.get(i).getResource(); |
||||||
|
String title = resource.getTitle(); |
||||||
|
if (TextUtils.isEmpty(title)) { |
||||||
|
try { |
||||||
|
Document doc = Jsoup.parse(new String(resource.getData(), mCharset)); |
||||||
|
Elements elements = doc.getElementsByTag("title"); |
||||||
|
if (elements.size() > 0) { |
||||||
|
title = elements.get(0).text(); |
||||||
|
} |
||||||
|
} catch (IOException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
Chapter bean = new Chapter(); |
||||||
|
bean.setNumber(i); |
||||||
|
bean.setBookId(mCollBook.getId()); |
||||||
|
bean.setUrl(resource.getHref()); |
||||||
|
if (i == 0 && title.isEmpty()) { |
||||||
|
bean.setTitle("封面"); |
||||||
|
} else { |
||||||
|
bean.setTitle(title); |
||||||
|
} |
||||||
|
mChapterList.add(bean); |
||||||
|
} |
||||||
|
} else { |
||||||
|
parseMenu(refs, 0); |
||||||
|
for (int i = 0; i < mChapterList.size(); i++) { |
||||||
|
mChapterList.get(i).setNumber(i); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return mChapterList; |
||||||
|
} |
||||||
|
|
||||||
|
private void parseMenu(List<TOCReference> refs, int level) { |
||||||
|
if (refs == null) return; |
||||||
|
for (TOCReference ref : refs) { |
||||||
|
if (ref.getResource() != null) { |
||||||
|
Chapter chapter = new Chapter(); |
||||||
|
chapter.setBookId(mCollBook.getId()); |
||||||
|
chapter.setTitle(ref.getTitle()); |
||||||
|
chapter.setUrl(ref.getCompleteHref()); |
||||||
|
chapter.setId(StringHelper.getStringRandom(15)); |
||||||
|
mChapterList.add(chapter); |
||||||
|
} |
||||||
|
if (ref.getChildren() != null && !ref.getChildren().isEmpty()) { |
||||||
|
parseMenu(ref.getChildren(), level + 1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected String getChapterContent(Chapter chapter) throws Exception { |
||||||
|
Resource resource = epubBook.getResources().getByHref(chapter.getUrl()); |
||||||
|
StringBuilder content = new StringBuilder(); |
||||||
|
Document doc = Jsoup.parse(new String(resource.getData(), mCharset)); |
||||||
|
Elements elements = doc.getAllElements(); |
||||||
|
for (Element element : elements) { |
||||||
|
List<TextNode> contentEs = element.textNodes(); |
||||||
|
for (int i = 0; i < contentEs.size(); i++) { |
||||||
|
String text = contentEs.get(i).text().trim(); |
||||||
|
text = StringUtils.formatHtml(text); |
||||||
|
if (elements.size() > 1) { |
||||||
|
if (text.length() > 0) { |
||||||
|
if (content.length() > 0) { |
||||||
|
content.append("\r\n"); |
||||||
|
} |
||||||
|
content.append("\u3000\u3000").append(text); |
||||||
|
} |
||||||
|
} else { |
||||||
|
content.append(text); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return content.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
private Observable<xyz.fycz.myreader.greendao.entity.Book> checkChapterList(xyz.fycz.myreader.greendao.entity.Book collBook) { |
||||||
|
if (mCollBook.getChapterTotalNum() != 0) { |
||||||
|
mChapterList = ChapterService.getInstance().findBookAllChapterByBookId(mCollBook.getId()); |
||||||
|
mPageChangeListener.onCategoryFinish(mChapterList); |
||||||
|
return Observable.just(collBook); |
||||||
|
} else { |
||||||
|
return Observable.create((ObservableOnSubscribe<List<Chapter>>) e -> { |
||||||
|
List<Chapter> chapterList = loadChapters(); |
||||||
|
if (!chapterList.isEmpty()) { |
||||||
|
e.onNext(chapterList); |
||||||
|
} else { |
||||||
|
e.onError(new IllegalAccessException("epubBook sub-chapter failed!")); |
||||||
|
} |
||||||
|
e.onComplete(); |
||||||
|
}) |
||||||
|
.flatMap(chapterList -> { |
||||||
|
mPageChangeListener.onCategoryFinish(chapterList); |
||||||
|
return Observable.just(collBook); |
||||||
|
}) |
||||||
|
.doOnNext(bookShelfBean -> { |
||||||
|
// 存储章节到数据库
|
||||||
|
ChapterService.getInstance().addChapters(mChapterList); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected BufferedReader getChapterReader(Chapter chapter) throws Exception { |
||||||
|
Log.d("getChapterReader", chapter.getTitle()); |
||||||
|
byte[] content = getChapterContent(chapter).getBytes(); |
||||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(content); |
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(bais)); |
||||||
|
return br; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean hasChapterData(Chapter chapter) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/*public void updateChapter() { |
||||||
|
mPageView.getActivity().toast("目录更新中"); |
||||||
|
Observable.create((ObservableOnSubscribe<BookShelfBean>) e -> { |
||||||
|
if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) { |
||||||
|
book.getBookInfoBean().setCharset("UTF-8"); |
||||||
|
} |
||||||
|
mCharset = Charset.forName(book.getBookInfoBean().getCharset()); |
||||||
|
//清除原目录
|
||||||
|
BookshelfHelp.delChapterList(book.getNoteUrl()); |
||||||
|
callback.getChapterList().clear(); |
||||||
|
e.onNext(book); |
||||||
|
e.onComplete(); |
||||||
|
}).flatMap(this::checkChapterList) |
||||||
|
.compose(RxUtils::toSimpleSingle) |
||||||
|
.subscribe(new Observer<BookShelfBean>() { |
||||||
|
@Override |
||||||
|
public void onSubscribe(Disposable d) { |
||||||
|
compositeDisposable.add(d); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onNext(BookShelfBean bookShelfBean) { |
||||||
|
mPageView.getActivity().toast("更新完成"); |
||||||
|
isChapterListPrepare = true; |
||||||
|
// 加载并显示当前章节
|
||||||
|
skipToChapter(bookShelfBean.getDurChapter(), bookShelfBean.getDurChapterPage()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onError(Throwable e) { |
||||||
|
durDhapterError(e.getMessage()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onComplete() { |
||||||
|
|
||||||
|
} |
||||||
|
}); |
||||||
|
}*/ |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="1024" |
||||||
|
android:viewportHeight="1024"> |
||||||
|
<path |
||||||
|
android:pathData="M512,512m-512,0a512,512 0,1 0,1024 0,512 512,0 1,0 -1024,0Z" |
||||||
|
android:fillColor="#FFFFFF"/> |
||||||
|
<path |
||||||
|
android:pathData="M685.6,306.4L350.4,306.4L499.2,176c11.2,-9.6 27.2,-9.6 38.4,0l148,130.4zM417.6,280.8h200L520,195.2c-1.6,-0.8 -3.2,-0.8 -4,0L417.6,280.8z" |
||||||
|
android:fillColor="#333333"/> |
||||||
|
<path |
||||||
|
android:pathData="M208,288h624c17.6,0 32,14.4 32,32v464c0,17.6 -14.4,32 -32,32H208c-17.6,0 -32,-14.4 -32,-32V320c0,-17.6 14.4,-32 32,-32z" |
||||||
|
android:fillColor="#81B7EF"/> |
||||||
|
<path |
||||||
|
android:pathData="M832,827.2L208,827.2c-24,0 -43.2,-19.2 -43.2,-43.2L164.8,320c0,-24 19.2,-43.2 43.2,-43.2h624c24,0 43.2,19.2 43.2,43.2v464c0,24 -19.2,43.2 -43.2,43.2zM208,299.2c-11.2,0 -20.8,8.8 -20.8,20.8v464c0,11.2 8.8,20.8 20.8,20.8h624c11.2,0 20.8,-8.8 20.8,-20.8L852.8,320c0,-11.2 -8.8,-20.8 -20.8,-20.8L208,299.2z" |
||||||
|
android:fillColor="#333333"/> |
||||||
|
<path |
||||||
|
android:pathData="M744,452.8H536c-7.2,0 -12.8,-5.6 -12.8,-12.8s5.6,-12.8 12.8,-12.8h208c7.2,0 12.8,5.6 12.8,12.8s-5.6,12.8 -12.8,12.8zM744,512.8H536c-7.2,0 -12.8,-5.6 -12.8,-12.8s5.6,-12.8 12.8,-12.8h208c7.2,0 12.8,5.6 12.8,12.8s-5.6,12.8 -12.8,12.8zM664,576.8H536c-7.2,0 -12.8,-5.6 -12.8,-12.8s5.6,-12.8 12.8,-12.8h128c7.2,0 12.8,5.6 12.8,12.8s-5.6,12.8 -12.8,12.8zM288,752h96c17.6,0 32,14.4 32,32s-14.4,32 -32,32H288c-17.6,0 -32,-14.4 -32,-32s14.4,-32 32,-32z" |
||||||
|
android:fillColor="#FFFFFF"/> |
||||||
|
<path |
||||||
|
android:pathData="M384,827.2L288,827.2c-24,0 -43.2,-19.2 -43.2,-43.2s19.2,-43.2 43.2,-43.2h96c24,0 43.2,19.2 43.2,43.2s-19.2,43.2 -43.2,43.2zM288,763.2c-11.2,0 -20.8,8.8 -20.8,20.8s8.8,20.8 20.8,20.8h96c11.2,0 20.8,-8.8 20.8,-20.8s-8.8,-20.8 -20.8,-20.8L288,763.2z" |
||||||
|
android:fillColor="#333333"/> |
||||||
|
<path |
||||||
|
android:pathData="M520,216m-24,0a24,24 0,1 0,48 0,24 24,0 1,0 -48,0Z" |
||||||
|
android:fillColor="#333333"/> |
||||||
|
</vector> |
Loading…
Reference in new issue