From eeb5cc68482aa6f26d9e98a63283e1f874b24f60 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 29 Oct 2020 20:27:25 +0800 Subject: [PATCH] Add reading history, show last time you read chapter in detail page of book you read --- lib/api/content.dart | 20 ++++- lib/database.dart | 124 ++++++++++++++++++++++++-- lib/main.dart | 16 +++- lib/pages/fictiondetail.dart | 59 +++++++++++-- lib/pages/fictionreader.dart | 164 +++++++++++++++++++++++++++++------ lib/views/drawer.dart | 8 +- pubspec.lock | 108 +++++++++++++++++++++-- pubspec.yaml | 10 ++- 8 files changed, 446 insertions(+), 63 deletions(-) diff --git a/lib/api/content.dart b/lib/api/content.dart index 4971735..234a506 100644 --- a/lib/api/content.dart +++ b/lib/api/content.dart @@ -1,9 +1,14 @@ -import 'package:http/http.dart' as Http; import 'package:html/parser.dart' show parse; +import 'package:http/http.dart' as Http; class FictionContent { + final String fictionTitle; + final String chapterTitle; + final String fictionID; + final String chapterID; final List lines; - FictionContent(this.lines); + FictionContent(this.fictionTitle, this.chapterTitle, this.fictionID, + this.chapterID, this.lines); static Future create(String novelID, String chapterID) async { try { // https://cn.ttkan.co/novel/user/page_direct?novel_id=jiansong-danshuiluyu&page=760 @@ -11,12 +16,21 @@ class FictionContent { "https://cn.ttkan.co/novel/user/page_direct?novel_id=$novelID&page=$chapterID"; final content = await Http.get(url); final dom = parse(content.body); + final fictionTitle = dom + .querySelectorAll( + "#__layout > div > div > div.frame_body > div.breadcrumb_nav.target > div > a")[2] + .text; + final chapterTitle = dom + .querySelector( + "#__layout > div > div > div.frame_body > div.title > h1") + .text; final lines = dom .querySelector(".content") .querySelectorAll("p") .map((e) => e.text) .toList(); - return FictionContent(lines); + return FictionContent( + fictionTitle, chapterTitle, novelID, chapterID, lines); } catch (_) { rethrow; } diff --git a/lib/database.dart b/lib/database.dart index c296771..f24005b 100644 --- a/lib/database.dart +++ b/lib/database.dart @@ -1,7 +1,11 @@ +import 'package:fiction_reader/api/content.dart'; import 'package:fiction_reader/api/search.dart'; import 'package:sqflite/sqflite.dart' as Sqflite; class Database { + final _bookmarksTable = "fiction_bookmark"; + final _fictionCacheTable = "fiction_cache"; + final _historyTable = "fiction_history"; final Sqflite.Database _database; Database(this._database); @@ -10,24 +14,46 @@ class Database { final database = await Sqflite.openDatabase("data.db"); final result = Database(database); result._createBookmarkTable(); + result._createFictionCacheTable(); + result._createFictionHistoryTable(); return result; } void _createBookmarkTable() { if (_database.isOpen) { - _database.execute(""" -CREATE TABLE IF NOT EXISTS bookmarks( + _database.execute(''' +CREATE TABLE IF NOT EXISTS $_bookmarksTable( title TEXT NOT NULL, author TEXT NOT NULL, description TEXT NOT NULL , book_id TEXT NOT NULL PRIMARY KEY -)"""); +)'''); } } + void _createFictionCacheTable() { + if (_database.isOpen) _database.execute(""" +CREATE TABLE IF NOT EXISTS $_fictionCacheTable( + fiction_id TEXT NOT NULL PRIMARY KEY, + chapter_id TEXT NOT NULL, + fiction_title TEXT NOT NULL, + chapter_title TEXT NOT NULL, + content TEXT +) +"""); + } + + void _createFictionHistoryTable() { + if (_database.isOpen) _database.execute(""" +CREATE TABLE IF NOT EXISTS $_historyTable ( + fiction_id TEXT NOT NULL PRIMARY KEY, + last_read TEXT +)"""); + } + Future addBookmark(Novel novel) { return _database.insert( - "bookmarks", + _bookmarksTable, { "title": novel.title, "author": novel.author, @@ -38,11 +64,95 @@ CREATE TABLE IF NOT EXISTS bookmarks( } Future removeBookmark(Novel novel) { - return _database - .execute("delete from bookmarks where book_id=?", [novel.novelID]); + return _database.execute( + "delete from $_bookmarksTable where book_id=?", [novel.novelID]); } Future>> listBookmarks() async { - return _database.query("bookmarks"); + return _database.query(_bookmarksTable); + } + + Future cacheExists(String fictionId, String chapterId) async { + return (await _database.query( + _fictionCacheTable, + where: "fiction_id=? and chapter_id=?", + whereArgs: [fictionId, chapterId], + )) + .length > + 0; + } + + Future readFromCache( + String fictionId, String chapterId) async { + final result = (await _database.query(_fictionCacheTable, + where: "fiction_id=? and chapter_id=?", + whereArgs: [fictionId, chapterId])) + .first; + return FictionContent( + result["fiction_title"], + result["chapter_title"], + result["fiction_id"], + result["chapter_id"], + result["content"].split("\n")); + } + + Future cacheIfNotCached(FictionContent fictionContent) async { + // fiction_id TEXT NOT NULL PRIMARY KEY, + // chapter_id TEXT NOT NULL, + // fiction_title TEXT NOT NULL, + // chapter_title TEXT NOT NULL, + // content TEXT + if (!await cacheExists( + fictionContent.fictionID, fictionContent.chapterID)) { + return _database.insert(_fictionCacheTable, { + "fiction_id": fictionContent.fictionID, + "chapter_id": fictionContent.chapterID, + "fiction_title": fictionContent.fictionTitle, + "chapter_title": fictionContent.chapterTitle, + "content": fictionContent.lines.join("\n"), + }); + } + return Future.value(0); + } + + Future historyExists(String fictionId) async { + return (await _database.query( + _historyTable, + where: "fiction_id=?", + whereArgs: [fictionId], + )) + .length > + 0; + } + + remember(String fictionId, String lastRead) async { + if (!await historyExists(fictionId)) { + _database.insert(_historyTable, { + "fiction_id": fictionId, + "last_read": lastRead, + }); + } else { + _database.update( + _historyTable, + { + "last_read": lastRead, + }, + where: "fiction_id=?", + whereArgs: [fictionId], + ); + } + } + + Future tellMe(String fictionId) async { + final result = await _database.query( + _historyTable, + where: "fiction_id=?", + whereArgs: [fictionId], + ); + if (result.length > 0) { + return result.first["last_read"] as String; + } else { + return null; + } } } diff --git a/lib/main.dart b/lib/main.dart index a3482ba..c9bcd21 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,15 +27,23 @@ class _MyAppState extends State { void jumpToDetail(String id) { setState(() { - pages.add(MaterialPage( - child: - Provider.value(value: jumpToReader, child: FictionDetail(id)))); + pages.add( + MaterialPage( + child: Provider.value( + value: jumpToReader, + child: _databaseProvider( + child: FictionDetail(id), + ), + ), + ), + ); }); } void jumpToReader(String id, Chapter chapter) { setState(() { - pages.add(MaterialPage(child: FictionReaderPage(id, chapter))); + pages.add(MaterialPage( + child: _databaseProvider(child: FictionReaderPage(id, chapter)))); }); } diff --git a/lib/pages/fictiondetail.dart b/lib/pages/fictiondetail.dart index 87d9e44..a3a3e4a 100644 --- a/lib/pages/fictiondetail.dart +++ b/lib/pages/fictiondetail.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../api/detail.dart' as DetailAPI; +import '../database.dart'; class FictionDetail extends StatelessWidget { final String _id; @@ -89,15 +90,55 @@ class FictionDetail extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - data.title, - style: - TextStyle(fontSize: 20, color: Colors.blueGrey[700]), - ), - Text( - data.author, - style: - TextStyle(fontSize: 15, color: Colors.blueGrey[500]), + Consumer2( + builder: (_, database, jumpToDetail, __) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + data.title, + style: TextStyle( + fontSize: 20, color: Colors.blueGrey[700]), + ), + Text( + data.author, + style: TextStyle( + fontSize: 15, color: Colors.blueGrey[500]), + ), + ], + ), + if (database != null) + FutureBuilder( + future: database.historyExists(_id), + builder: (_, snapshot) { + if (!snapshot.hasData) { + return Container(); + } + if (snapshot.data) { + return FlatButton( + child: Text( + "上次阅读", + style: TextStyle(fontSize: 20), + ), + onPressed: () { + database.tellMe(_id).then((value) => + jumpToDetail( + _id, + data.chapters + .where((element) => + element.id == value) + .first)); + }, + ); + } + return Container(); + }, + ) + ], + ), ), Text( data.status, diff --git a/lib/pages/fictionreader.dart b/lib/pages/fictionreader.dart index aba3b70..a5ddb16 100644 --- a/lib/pages/fictionreader.dart +++ b/lib/pages/fictionreader.dart @@ -1,44 +1,156 @@ +import 'dart:async'; + import 'package:fiction_reader/api/content.dart'; import 'package:fiction_reader/api/detail.dart'; +import 'package:fiction_reader/database.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; class FictionReaderPage extends StatefulWidget { final String _novelID; final Chapter _chapter; + FictionReaderPage(this._novelID, this._chapter); + @override _FictionReaderPageState createState() => _FictionReaderPageState(); } class _FictionReaderPageState extends State { + final StreamController> _fictionStreamController = + StreamController(); + PageController _pageViewController; + Database _database; + + @override + void initState() { + super.initState(); + final pageIndex = int.parse(widget._chapter.id); + _pageViewController = PageController(initialPage: pageIndex); + } + @override Widget build(BuildContext context) { - return FutureBuilder( - future: FictionContent.create(widget._novelID, widget._chapter.id), - builder: (_, snapshot) => Scaffold( - appBar: AppBar( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(widget._chapter.title), - if (snapshot.connectionState == ConnectionState.waiting) - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ) - ], - ), - ), - body: SafeArea( - child: Container( - child: ListView( - children: [ - if (snapshot.connectionState == ConnectionState.done) - ...snapshot.data.lines.map((it) => Text(it)) - ], - ), - ), - ), - ), + return Consumer( + builder: (_, database, __) { + if (_database == null) { + final fictionId = widget._novelID; + final chapterId = widget._chapter.id; + if (database != null) { + _database = database; + database.cacheExists(fictionId, chapterId).then((value) { + if (value) { + _fictionStreamController + .add(database.readFromCache(fictionId, chapterId)); + } else { + _fictionStreamController.add( + FictionContent.create(widget._novelID, widget._chapter.id)); + } + _database.remember(fictionId, chapterId); + }); + } + } + return StreamBuilder>( + stream: _fictionStreamController.stream, + builder: (_, fictionStreamSnapshot) { + if (!fictionStreamSnapshot.hasData) + return Scaffold( + appBar: AppBar( + title: Text("Loading..."), + ), + ); + return FutureBuilder( + future: fictionStreamSnapshot.data, + builder: (context, snapshot) { + if (database != null && snapshot.hasData) { + database.cacheIfNotCached(snapshot.data); + } + return Scaffold( + appBar: AppBar( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(snapshot.hasData + ? snapshot.data.fictionTitle + : "Loading..."), + if (snapshot.hasData) + Text( + snapshot.data.chapterTitle, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.blueGrey[100]), + ), + ], + ), + ), + if (snapshot.connectionState == ConnectionState.waiting) + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ) + ], + ), + ), + body: SafeArea( + child: Container( + decoration: BoxDecoration(color: Colors.green[100]), + child: PageView.builder( + controller: _pageViewController, + onPageChanged: (page) { + _changePage(page); + }, + itemBuilder: (_, index) => SingleChildScrollView( + child: (snapshot.connectionState == + ConnectionState.done) + ? SafeArea( + maintainBottomViewPadding: true, + minimum: EdgeInsets.only(bottom: 70), + child: Text( + snapshot.data.lines + .where((String element) => + element.trim().length > 0) + .map((String e) => + "\t\t\t\t\t\t${e.trim()}") + .join("\n"), + style: + GoogleFonts.maShanZheng(fontSize: 35), + ), + ) + : Container(), + ), + ), + ), + ), + ); + }, + ); + }, + ); + }, ); } + + void _changePage(int page) { + final fictionId = widget._novelID; + final chapterId = page.toString(); + _database.cacheExists(fictionId, chapterId).then((value) { + if (value) { + _fictionStreamController + .add(_database.readFromCache(fictionId, chapterId)); + print("$fictionId:$chapterId loaded from cache."); + } else { + _fictionStreamController + .add(FictionContent.create(fictionId, chapterId)); + print("$fictionId:$chapterId loaded from the internet."); + } + }); + _database.remember(fictionId, chapterId); + } } diff --git a/lib/views/drawer.dart b/lib/views/drawer.dart index 3353b69..473a938 100644 --- a/lib/views/drawer.dart +++ b/lib/views/drawer.dart @@ -1,4 +1,5 @@ import "package:flutter/material.dart"; +import 'package:google_fonts/google_fonts.dart'; enum PageIndex { Bookshelf, @@ -14,7 +15,7 @@ Drawer mainDrawer( DrawerHeader( child: Text( "Fiction reader", - style: TextStyle(fontSize: 40, fontFamily: "Courgette"), + style: GoogleFonts.courgette(fontSize: 40), ), decoration: BoxDecoration(color: Colors.blue), ), @@ -54,7 +55,6 @@ Widget _myListTile( onchange(PageIndex.Search); else onchange(PageIndex.Bookshelf); - if (state.currentState.hasDrawer && state.currentState.isDrawerOpen) { Navigator.of(state.currentContext).pop(); } @@ -64,9 +64,7 @@ Widget _myListTile( child: Center( child: Text( text, - style: TextStyle( - fontSize: 30, - ), + style: GoogleFonts.sanchez(fontSize: 30), ), ), ), diff --git a/pubspec.lock b/pubspec.lock index d1e8207..5bece6c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "11.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.40.1" + version: "0.40.4" args: dependency: transitive description: @@ -106,6 +106,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.4" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" flutter: dependency: "direct main" description: flutter @@ -118,6 +132,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" html: dependency: "direct main" description: @@ -146,6 +167,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.4" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.1" io: dependency: transitive description: @@ -230,13 +258,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.22" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+4" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+1" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.9.2" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" pool: dependency: transitive description: @@ -244,6 +321,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" provider: dependency: "direct main" description: @@ -424,6 +508,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.3" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" yaml: dependency: transitive description: @@ -432,5 +530,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.10.0-110 <=2.11.0-218.0.dev" - flutter: ">=1.16.0 <2.0.0" + dart: ">=2.10.0-110 <=2.11.0-242.0.dev" + flutter: ">=1.17.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index beead4e..cee6de6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,8 @@ dependencies: sqflite: 1.3.1+2 + google_fonts: ^1.1.1 + dev_dependencies: test: ^1.15.4 @@ -37,10 +39,10 @@ flutter: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - fonts: - - family: Courgette - fonts: - - asset: assets/fonts/Courgette-Regular.ttf + # fonts: + # - family: Courgette + # fonts: + # - asset: assets/fonts/Courgette-Regular.ttf # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf