Skip to content

Commit

Permalink
Merge pull request #97 from shockbytes/pages-books-over-time
Browse files Browse the repository at this point in the history
Pages books over time
  • Loading branch information
shockbytes authored Oct 15, 2020
2 parents 67707ae + a2ae12d commit c1f56d6
Show file tree
Hide file tree
Showing 93 changed files with 2,338 additions and 505 deletions.
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,54 @@ Download the client secret file from the Google Cloud Console.
This file is necessary in order to retrieve the JWT request token of
the user during the login process.

## Versions
## Backlog
The backlog is currently empty.

### Backlog
Empty...
## Outlook & planned features

### Version 4.2
- [ ] Wishlist for books that are not purchased yet
### Versions 5.x
- [ ] Use Firebase Data for book suggestions
- [ ] Let users suggest favorite books to others
- [ ] Improved search database (Google Books API) lookup query
- [ ] Shockbytes Backup
- [ ] Simplify book management
- [ ] Remove local backup
- [ ] Add online Shockbytes backup as only way to backup data
- [ ] Move Goodreads CSV import from BETA to RELEASE state
- [ ] Switch between online and offline storage (user has full control over the data)
- [ ] Add web client support
- [ ] Paged request when user clicks on "not my book" in book download view

### Version 4.1
- [ ] Use Firebase Data for book suggestions
- [ ] Wishlist for books that are not purchased yet

### Version 4.0 - CAMPING WITH FIREBASE
- [ ] Add online sync capability
- [ ] Migrate from local to remote storage

### Version 3.16
- [ ] Login with Firebase
- [ ] Add Onboarding + Login

### Version 3.15
- [ ] Statistics pages over time / month + Goal per month
- [ ] Reset page statistics per book
- [ ] Hide page statistics in details page
## Current development

### Version 3.16
- [ ] Send csv export via Mail
- [ ] Move actions into Book item (https://github.com/florent37/ExpansionPanel)
- [ ] Add Onboarding + optional Login
- [ ] Experimental remote storage Firestore implementation (for test account)
- [ ] Fix bug with local book covers
- [ ] In MultiBareBoneBookView (not showing up)
- [ ] In the label overview (too big)

## Changelog

### Version 3.15
* Statistics pages/books over time / month + Goal per month
* Fix issue where MarkerView draws out of ChartView bounds
* Change toolbar behavior in Statistics screen
* Make books per month zoomable
* Fix problems when setting books per month reading goal offset
* Fix issues with pages per month reading goal update
* Reset page statistics per book
* Hide page statistics in details page

### Version 3.14 - SUMMER CLEANUP
* Move sort into settings
Expand Down
5 changes: 2 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId "at.shockbytes.dante"
minSdkVersion 21
targetSdkVersion 30
versionCode 36
versionName "3.14"
versionCode 37
versionName "3.15"

multiDexEnabled true
vectorDrawables.useSupportLibrary = true
Expand Down Expand Up @@ -79,7 +79,6 @@ dependencies {
implementation "androidx.cardview:cardview:${rootProject.androidXVersion}"
implementation "androidx.appcompat:appcompat:$androidXAppCompatVersion"
implementation "androidx.core:core:$androidXCoreVersion"
implementation "androidx.preference:preference:$androidXPreferenceVersion"
implementation 'androidx.fragment:fragment-ktx:1.2.5'

implementation 'androidx.recyclerview:recyclerview:1.1.0'
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/at/shockbytes/dante/DanteApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DanteApp : MultiDexApplication(), CoreComponentProvider {

private val coreComponent: CoreComponent by lazy {
DaggerCoreComponent.builder()
.coreModule(CoreModule())
.coreModule(CoreModule(this))
.networkModule(NetworkModule())
.build()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package at.shockbytes.dante.backup.provider.shockbytes

import androidx.fragment.app.FragmentActivity
import at.shockbytes.dante.BuildConfig
import at.shockbytes.dante.backup.model.BackupMetadata
import at.shockbytes.dante.backup.model.BackupMetadataState
import at.shockbytes.dante.backup.model.BackupStorageProvider
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/java/at/shockbytes/dante/flagging/FeatureFlag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ package at.shockbytes.dante.flagging

enum class FeatureFlag(val key: String, val displayName: String, val defaultValue: Boolean) {

BOOK_SUGGESTIONS("book_suggestions", "Suggestions", false),
PAGE_RECORD_STATISTICS("page_record_statistics", "Page records stats", false);
BOOK_SUGGESTIONS("book_suggestions", "Suggestions", false);

companion object {

fun activeFlags(): List<FeatureFlag> {
return listOf(BOOK_SUGGESTIONS, PAGE_RECORD_STATISTICS)
return listOf(BOOK_SUGGESTIONS)
}
}
}
29 changes: 25 additions & 4 deletions app/src/main/java/at/shockbytes/dante/injection/AppComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,29 @@ import at.shockbytes.dante.ui.activity.LoginActivity
import at.shockbytes.dante.ui.activity.MainActivity
import at.shockbytes.dante.ui.activity.NotesActivity
import at.shockbytes.dante.ui.activity.SearchActivity
import at.shockbytes.dante.ui.activity.TimeLineActivity
import at.shockbytes.dante.ui.fragment.*
import at.shockbytes.dante.ui.fragment.AnnouncementFragment
import at.shockbytes.dante.ui.fragment.BackupBackupFragment
import at.shockbytes.dante.ui.fragment.BackupFragment
import at.shockbytes.dante.ui.fragment.BackupRestoreFragment
import at.shockbytes.dante.ui.fragment.BookDetailFragment
import at.shockbytes.dante.ui.fragment.FeatureFlagConfigFragment
import at.shockbytes.dante.ui.fragment.ImportBooksStorageFragment
import at.shockbytes.dante.ui.fragment.LabelCategoryBottomSheetFragment
import at.shockbytes.dante.ui.fragment.LabelPickerBottomSheetFragment
import at.shockbytes.dante.ui.fragment.LauncherIconPickerFragment
import at.shockbytes.dante.ui.fragment.LoginFragment
import at.shockbytes.dante.ui.fragment.MainBookFragment
import at.shockbytes.dante.ui.fragment.ManualAddFragment
import at.shockbytes.dante.ui.fragment.MenuFragment
import at.shockbytes.dante.ui.fragment.OnlineStorageFragment
import at.shockbytes.dante.ui.fragment.PageRecordsDetailFragment
import at.shockbytes.dante.ui.fragment.PickRandomBookFragment
import at.shockbytes.dante.ui.fragment.RateFragment
import at.shockbytes.dante.ui.fragment.SearchFragment
import at.shockbytes.dante.ui.fragment.SettingsFragment
import at.shockbytes.dante.ui.fragment.StatisticsFragment
import at.shockbytes.dante.ui.fragment.SuggestionsFragment
import at.shockbytes.dante.ui.fragment.TimeLineFragment
import at.shockbytes.dante.ui.fragment.dialog.GoogleSignInDialogFragment
import at.shockbytes.dante.ui.fragment.dialog.GoogleWelcomeScreenDialogFragment
import at.shockbytes.dante.ui.fragment.dialog.SortStrategyDialogFragment
Expand Down Expand Up @@ -102,7 +123,7 @@ interface AppComponent {

fun inject(fragment: ImportBooksStorageFragment)

fun inject(activity: TimeLineActivity)

fun inject(fragment: PickRandomBookFragment)

fun inject(fragment: PageRecordsDetailFragment)
}
4 changes: 2 additions & 2 deletions app/src/main/java/at/shockbytes/dante/injection/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ class AppModule(private val app: Application) {

@Provides
fun provideDanteSettings(
sharedPreferences: SharedPreferences,
schedulers: SchedulerFacade
sharedPreferences: SharedPreferences,
schedulers: SchedulerFacade
): DanteSettings {
return DanteSettings(app.applicationContext, sharedPreferences, schedulers)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import at.shockbytes.dante.ui.viewmodel.LoginViewModel
import at.shockbytes.dante.ui.viewmodel.MainViewModel
import at.shockbytes.dante.ui.viewmodel.ManualAddViewModel
import at.shockbytes.dante.ui.viewmodel.OnlineStorageViewModel
import at.shockbytes.dante.ui.viewmodel.PageRecordsDetailViewModel
import at.shockbytes.dante.ui.viewmodel.SearchViewModel
import at.shockbytes.dante.ui.viewmodel.StatisticsViewModel
import at.shockbytes.dante.ui.viewmodel.TimelineViewModel
Expand Down Expand Up @@ -127,4 +128,9 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(LauncherIconPickerViewModel::class)
internal abstract fun launcherIconPickerViewModel(viewModel: LauncherIconPickerViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(PageRecordsDetailViewModel::class)
internal abstract fun pageRecordsDetailViewModel(viewModel: PageRecordsDetailViewModel): ViewModel
}
98 changes: 81 additions & 17 deletions app/src/main/java/at/shockbytes/dante/stats/BookStatsBuilder.kt
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
package at.shockbytes.dante.stats

import android.graphics.Color
import at.shockbytes.dante.R
import at.shockbytes.dante.core.bareBone
import at.shockbytes.dante.core.book.*
import at.shockbytes.dante.flagging.FeatureFlag
import at.shockbytes.dante.flagging.FeatureFlagging
import at.shockbytes.dante.core.book.BareBoneBook
import at.shockbytes.dante.core.book.BookEntity
import at.shockbytes.dante.core.book.BookState
import at.shockbytes.dante.core.book.Languages
import at.shockbytes.dante.core.book.PageRecord
import at.shockbytes.dante.core.book.ReadingGoal
import at.shockbytes.dante.ui.adapter.stats.model.LabelStatsItem
import at.shockbytes.dante.ui.custom.bookspages.BooksAndPageRecordDataPoint
import at.shockbytes.util.AppUtils
import org.joda.time.DateTime
import org.joda.time.Duration
import org.joda.time.Months
import org.joda.time.format.DateTimeFormat

class BookStatsBuilder(private val featureFlagging: FeatureFlagging) {
object BookStatsBuilder {

fun createFrom(
books: List<BookEntity>,
pageRecords: List<PageRecord>
fun build(
books: List<BookEntity>,
pageRecords: List<PageRecord>,
pagesPerMonthGoal: ReadingGoal.PagesPerMonthReadingGoal,
booksPerMonthGoal: ReadingGoal.BooksPerMonthReadingGoal
): List<BookStatsViewItem> {
return mutableListOf(
createBooksAndPagesItem(books),
createPagesOverTimeItem(pageRecords, pagesPerMonthGoal),
createBooksOverTimeItem(books, booksPerMonthGoal),
createReadingDurationItem(books),
createFavoriteItem(books),
createLanguageItem(books),
createLabelItem(books),
createOthersItem(books)
).apply {
if (featureFlagging[FeatureFlag.PAGE_RECORD_STATISTICS]) {
this.add(1, createPagesOverTimeItem(pageRecords))
}
}
)
}

private fun createBooksAndPagesItem(books: List<BookEntity>): BookStatsViewItem {
Expand Down Expand Up @@ -63,11 +69,65 @@ class BookStatsBuilder(private val featureFlagging: FeatureFlagging) {
)
}

private fun createPagesOverTimeItem(pageRecords: List<PageRecord>): BookStatsViewItem {
// TODO Implement this method later...
return BookStatsViewItem.PagesOverTime.Empty
private fun createPagesOverTimeItem(
pageRecords: List<PageRecord>,
pagesPerMonthGoal: ReadingGoal.PagesPerMonthReadingGoal
): BookStatsViewItem.BooksAndPagesOverTime {

if (pageRecords.isEmpty()) {
return BookStatsViewItem.BooksAndPagesOverTime.Empty(R.string.statistics_header_pages_over_time)
}
val format = DateTimeFormat.forPattern("MMM yy")

return pageRecords
.groupBy { record ->
val dt = record.dateTime
MonthYear(dt.monthOfYear, dt.year)
}
.toSortedMap()
.map { (monthYear, records) ->

val pages = records
.sumBy { it.diffPages }
// There can be negative values, hard bounce them at 0
// Example: User logs 100 pages in July but deletes 20 pages in August
// which leads to a value of -20. This should not happen!
.coerceAtLeast(0)

BooksAndPageRecordDataPoint(pages, formattedDate = format.print(monthYear.dateTime))
}
.let { pageRecordDataPoints ->
BookStatsViewItem.BooksAndPagesOverTime.Present.Pages(pageRecordDataPoints, pagesPerMonthGoal)
}
}

private fun createBooksOverTimeItem(
books: List<BookEntity>,
booksPerMonthGoal: ReadingGoal.BooksPerMonthReadingGoal
): BookStatsViewItem.BooksAndPagesOverTime {

if (books.isEmpty()) {
return BookStatsViewItem.BooksAndPagesOverTime.Empty(R.string.statistics_header_books_over_time)
}
val format = DateTimeFormat.forPattern("MMM yy")

return books
.filter { it.state == BookState.READ }
.groupBy { book ->
val dt = DateTime(book.endDate)
MonthYear(dt.monthOfYear, dt.year)
}
.toSortedMap()
.map { (monthYear, booksPerMonth) ->
BooksAndPageRecordDataPoint(
value = booksPerMonth.count(),
formattedDate = format.print(monthYear.dateTime)
)
}
.let { pageRecordDataPoints ->
BookStatsViewItem.BooksAndPagesOverTime.Present.Books(pageRecordDataPoints, booksPerMonthGoal)
}
}

private fun createReadingDurationItem(books: List<BookEntity>): BookStatsViewItem {

Expand Down Expand Up @@ -108,7 +168,6 @@ class BookStatsBuilder(private val featureFlagging: FeatureFlagging) {

private fun favoriteAuthor(books: List<BookEntity>): FavoriteAuthor? {
return books
.asSequence()
.groupBy { book ->
book.author
}
Expand Down Expand Up @@ -150,8 +209,13 @@ class BookStatsBuilder(private val featureFlagging: FeatureFlagging) {
val labels = books.asSequence()
.map { it.labels }
.flatten()
.groupBy { LabelStatsItem(it.title, Color.parseColor(it.hexColor)) }
.groupBy { Pair(it.title, it.hexColor) }
.mapValues { it.value.size }
.map { (labelPair, size) ->
val (title, hexColor) = labelPair
LabelStatsItem(title, Color.parseColor(hexColor), size)
}
.sortedBy { it.size }

return if (labels.isEmpty()) {
BookStatsViewItem.LabelStats.Empty
Expand Down
24 changes: 18 additions & 6 deletions app/src/main/java/at/shockbytes/dante/stats/BookStatsViewItem.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package at.shockbytes.dante.stats

import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import at.shockbytes.dante.R
import at.shockbytes.dante.core.book.BareBoneBook
import at.shockbytes.dante.core.book.Languages
import at.shockbytes.dante.core.book.ReadingGoal
import at.shockbytes.dante.ui.adapter.stats.model.LabelStatsItem
import at.shockbytes.dante.ui.custom.bookspages.BooksAndPageRecordDataPoint

sealed class BookStatsViewItem {

Expand All @@ -20,13 +23,24 @@ sealed class BookStatsViewItem {
data class Present(val booksAndPages: BooksPagesInfo) : BooksAndPages()
}

sealed class PagesOverTime : BookStatsViewItem() {
sealed class BooksAndPagesOverTime : BookStatsViewItem() {

override val layoutId: Int = R.layout.item_stats_pages_over_time

object Empty : PagesOverTime()
data class Empty(@StringRes val headerRes: Int) : BooksAndPagesOverTime()

data class Present(val pagesPerMonths: List<PagesPerMonth>) : PagesOverTime()
sealed class Present : BooksAndPagesOverTime() {

data class Pages(
val pagesPerMonths: List<BooksAndPageRecordDataPoint>,
val readingGoal: ReadingGoal.PagesPerMonthReadingGoal
) : Present()

data class Books(
val booksPerMonths: List<BooksAndPageRecordDataPoint>,
val readingGoal: ReadingGoal.BooksPerMonthReadingGoal
) : Present()
}
}

sealed class ReadingDuration : BookStatsViewItem() {
Expand Down Expand Up @@ -70,9 +84,7 @@ sealed class BookStatsViewItem {

object Empty : LabelStats()

data class Present(
val labels: Map<LabelStatsItem, Int>
) : LabelStats()
data class Present(val labels: List<LabelStatsItem>) : LabelStats()
}

sealed class Others : BookStatsViewItem() {
Expand Down
Loading

0 comments on commit c1f56d6

Please sign in to comment.