Skip to content
Permalink
Browse files

Start showing next episode to watch in show details

  • Loading branch information...
chrisbanes committed Aug 5, 2019
1 parent 3ef489e commit 3be043dd465e6dd6d3a458c3cae6eb0d2a990d82
@@ -69,6 +69,8 @@
<item quantity="other">Runtime: %1$d minutes</item>
</plurals>

<string name="details_next_episode_to_watch">Next episode to watch</string>

<string name="follow_show_add">Follow show</string>
<string name="follow_show_remove">Un-follow show</string>

@@ -20,6 +20,7 @@ import androidx.room.Dao
import androidx.room.Query
import app.tivi.data.entities.Episode
import io.reactivex.Flowable
import app.tivi.data.resultentities.EpisodeWithSeason

@Dao
abstract class EpisodesDao : EntityDao<Episode> {
@@ -52,4 +53,28 @@ abstract class EpisodesDao : EntityDao<Episode> {
" INNER JOIN episodes AS eps ON eps.season_id = s.id" +
" WHERE eps.id = :episodeId")
abstract suspend fun showIdForEpisodeId(episodeId: Long): Long

@Query("""
SELECT eps.*, MAX((100 * s.number) + eps.number) AS computed_abs_number
FROM shows
INNER JOIN seasons AS s ON shows.id = s.show_id
INNER JOIN episodes AS eps ON eps.season_id = s.id
INNER JOIN episode_watch_entries AS ew ON ew.episode_id = eps.id
WHERE s.number != 0
AND s.ignored = 0
AND shows.id = :showId
""")
abstract fun latestWatchedEpisodeForShowId(showId: Long): Flowable<EpisodeWithSeason>

@Query("""
SELECT eps.*, MIN((1000 * s.number) + eps.number) AS computed_abs_number
FROM shows
INNER JOIN seasons AS s ON shows.id = s.show_id
INNER JOIN episodes AS eps ON eps.season_id = s.id
WHERE s.number != 0
AND s.ignored = 0
AND shows.id = :showId
AND ((1000 * s.number) + eps.number) > ((1000 * :seasonNumber) + :episodeNumber)
""")
abstract fun nextEpisodeForShowAfter(showId: Long, seasonNumber: Int, episodeNumber: Int): Flowable<EpisodeWithSeason>
}
@@ -56,6 +56,8 @@ class SeasonsEpisodesRepository @Inject constructor(

fun observeEpisodeWatches(episodeId: Long) = episodeWatchStore.observeEpisodeWatches(episodeId)

fun observeNextEpisodeToWatch(showId: Long) = seasonsEpisodesStore.observeShowNextEpisodeToWatch(showId)

suspend fun needShowSeasonsUpdate(showId: Long, expiry: Instant = instantInPast(days = 7)): Boolean {
return seasonsLastRequestStore.isRequestBefore(showId, expiry)
}
@@ -22,6 +22,7 @@ import app.tivi.data.daos.EpisodesDao
import app.tivi.data.daos.SeasonsDao
import app.tivi.data.entities.Episode
import app.tivi.data.entities.Season
import app.tivi.data.resultentities.EpisodeWithSeason
import app.tivi.data.resultentities.SeasonWithEpisodesAndWatches
import app.tivi.data.syncers.syncerForEntity
import app.tivi.util.Logger
@@ -58,6 +59,12 @@ class SeasonsEpisodesStore @Inject constructor(
return seasonsDao.seasonsWithEpisodesForShowId(showId).asFlow()
}

fun observeShowNextEpisodeToWatch(showId: Long): Flow<EpisodeWithSeason> {
return episodesDao.latestWatchedEpisodeForShowId(showId).flatMap {
episodesDao.nextEpisodeForShowAfter(showId, it.season!!.number!!, it.episode!!.number!!)
}.asFlow()
}

/**
* Gets the ID for the season with the given trakt Id. If the trakt Id does not exist in the
* database, it is inserted and the generated ID is returned.
@@ -0,0 +1,42 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package app.tivi.data.resultentities

import androidx.room.Embedded
import androidx.room.Relation
import app.tivi.data.entities.Episode
import app.tivi.data.entities.Season
import java.util.Objects

class EpisodeWithSeason {
@Embedded
var episode: Episode? = null

@Relation(parentColumn = "season_id", entityColumn = "id")
var _seasons: List<Season> = emptyList()

val season: Season?
get() = _seasons.getOrNull(0)

override fun equals(other: Any?): Boolean = when {
other === this -> true
other is EpisodeWithSeason -> episode == other.episode && _seasons == other._seasons
else -> false
}

override fun hashCode(): Int = Objects.hash(episode, _seasons)
}
@@ -0,0 +1,45 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package app.tivi.data.resultentities

import androidx.room.Embedded
import androidx.room.Relation
import app.tivi.data.entities.Season
import app.tivi.data.entities.TiviShow
import java.util.Objects

class SeasonWithShow {
@Embedded
lateinit var season: Season

@Relation(parentColumn = "show_id", entityColumn = "id")
var _shows: List<TiviShow> = emptyList()

val show: TiviShow
get() {
assert(_shows.size == 1)
return _shows[0]
}

override fun equals(other: Any?): Boolean = when {
other === this -> true
other is SeasonWithShow -> season == other.season && _shows == other._shows
else -> false
}

override fun hashCode(): Int = Objects.hash(season, _shows)
}
@@ -0,0 +1,36 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package app.tivi.interactors

import app.tivi.data.repositories.episodes.SeasonsEpisodesRepository
import app.tivi.data.resultentities.EpisodeWithSeason
import app.tivi.util.AppCoroutineDispatchers
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class ObserveShowNextEpisodeToWatch @Inject constructor(
private val repository: SeasonsEpisodesRepository,
private val dispatchers: AppCoroutineDispatchers
) : SubjectInteractor<ObserveShowNextEpisodeToWatch.Params, EpisodeWithSeason>() {
override val dispatcher = dispatchers.io

override fun createObservable(params: Params): Flow<EpisodeWithSeason> {
return repository.observeNextEpisodeToWatch(params.showId)
}

data class Params(val showId: Long)
}
@@ -23,10 +23,10 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class ObserveFollowedShowSeasonData @Inject constructor(
class ObserveShowSeasonData @Inject constructor(
private val dispatchers: AppCoroutineDispatchers,
private val seasonsEpisodesRepository: SeasonsEpisodesRepository
) : SubjectInteractor<ObserveFollowedShowSeasonData.Params, List<SeasonWithEpisodesAndWatches>>() {
) : SubjectInteractor<ObserveShowSeasonData.Params, List<SeasonWithEpisodesAndWatches>>() {
override val dispatcher: CoroutineDispatcher = dispatchers.io

override fun createObservable(params: Params): Flow<List<SeasonWithEpisodesAndWatches>> {
@@ -18,12 +18,12 @@ package app.tivi.interactors

import app.tivi.data.repositories.episodes.SeasonsEpisodesRepository
import app.tivi.data.repositories.followedshows.FollowedShowsRepository
import app.tivi.interactors.UpdateFollowedShowSeasonData.Params
import app.tivi.interactors.UpdateShowSeasonData.Params
import app.tivi.util.AppCoroutineDispatchers
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Inject

class UpdateFollowedShowSeasonData @Inject constructor(
class UpdateShowSeasonData @Inject constructor(
dispatchers: AppCoroutineDispatchers,
private val seasonsEpisodesRepository: SeasonsEpisodesRepository,
private val followedShowsRepository: FollowedShowsRepository
@@ -20,7 +20,7 @@ import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import app.tivi.interactors.UpdateFollowedShowSeasonData
import app.tivi.interactors.UpdateShowSeasonData
import app.tivi.tasks.inject.ChildWorkerFactory
import app.tivi.util.Logger
import com.squareup.inject.assisted.Assisted
@@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
class SyncShowWatchedProgress @AssistedInject constructor(
@Assisted params: WorkerParameters,
@Assisted context: Context,
private val updateShowSeasonsAndWatchedProgress: UpdateFollowedShowSeasonData,
private val updateShowSeasonData: UpdateShowSeasonData,
private val logger: Logger
) : CoroutineWorker(context, params) {
companion object {
@@ -43,11 +43,11 @@ class SyncShowWatchedProgress @AssistedInject constructor(
}

override suspend fun doWork(): Result {
withContext(updateShowSeasonsAndWatchedProgress.dispatcher) {
withContext(updateShowSeasonData.dispatcher) {
val showId = inputData.getLong(PARAM_SHOW_ID, -1)
logger.d("$TAG worker running for show id: $showId")

updateShowSeasonsAndWatchedProgress(UpdateFollowedShowSeasonData.Params(showId, true))
updateShowSeasonData(UpdateShowSeasonData.Params(showId, true))
}

return Result.success()
@@ -65,6 +65,23 @@ class ShowDetailsEpoxyController @Inject constructor(
override fun buildModels(viewState: ShowDetailsViewState) {
buildShowModels(viewState.show)

val episodeWithSeason = viewState.nextEpisodeToWatch()
if (episodeWithSeason != null) {
detailsHeader {
id("next_episode_header")
title(R.string.details_next_episode_to_watch)
spanSizeOverride(TotalSpanOverride)
}
detailsNextEpisodeToWatch {
id("next_episode_header_${episodeWithSeason.hashCode()}")
spanSizeOverride(TotalSpanOverride)
season(episodeWithSeason.season)
episode(episodeWithSeason.episode)
textCreator(textCreator)
clickListener { view -> callbacks?.onEpisodeClicked(episodeWithSeason.episode!!, view) }
}
}

buildRelatedShowsModels(viewState.relatedShows, viewState.tmdbImageUrlProvider)

buildSeasonsModels(viewState.seasons, viewState.expandedSeasonIds)
@@ -18,6 +18,7 @@ package app.tivi.showdetails.details

import androidx.lifecycle.viewModelScope
import app.tivi.SharedElementHelper
import app.tivi.TiviMvRxViewModel
import app.tivi.data.entities.ActionDate
import app.tivi.data.entities.Episode
import app.tivi.data.entities.Season
@@ -28,19 +29,19 @@ import app.tivi.interactors.ChangeSeasonWatchedStatus.Action
import app.tivi.interactors.ChangeSeasonWatchedStatus.Params
import app.tivi.interactors.ChangeShowFollowStatus
import app.tivi.interactors.ChangeShowFollowStatus.Action.TOGGLE
import app.tivi.interactors.ObserveFollowedShowSeasonData
import app.tivi.interactors.ObserveRelatedShows
import app.tivi.interactors.ObserveShowDetails
import app.tivi.interactors.ObserveShowFollowStatus
import app.tivi.interactors.UpdateFollowedShowSeasonData
import app.tivi.interactors.ObserveShowNextEpisodeToWatch
import app.tivi.interactors.ObserveShowSeasonData
import app.tivi.interactors.UpdateRelatedShows
import app.tivi.interactors.UpdateShowDetails
import app.tivi.interactors.UpdateShowSeasonData
import app.tivi.interactors.execute
import app.tivi.interactors.launchInteractor
import app.tivi.interactors.launchObserve
import app.tivi.showdetails.ShowDetailsNavigator
import app.tivi.tmdb.TmdbManager
import app.tivi.TiviMvRxViewModel
import app.tivi.interactors.launchObserve
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@@ -56,10 +57,11 @@ class ShowDetailsFragmentViewModel @AssistedInject constructor(
observeShowDetails: ObserveShowDetails,
private val updateRelatedShows: UpdateRelatedShows,
observeRelatedShows: ObserveRelatedShows,
private val updateShowSeasons: UpdateFollowedShowSeasonData,
observeShowSeasons: ObserveFollowedShowSeasonData,
private val updateShowSeasons: UpdateShowSeasonData,
observeShowSeasons: ObserveShowSeasonData,
private val changeSeasonWatchedStatus: ChangeSeasonWatchedStatus,
observeShowFollowStatus: ObserveShowFollowStatus,
observeNextEpisodeToWatch: ObserveShowNextEpisodeToWatch,
tmdbManager: TmdbManager,
private val changeShowFollowStatus: ChangeShowFollowStatus,
private val changeSeasonFollowStatus: ChangeSeasonFollowStatus
@@ -91,6 +93,11 @@ class ShowDetailsFragmentViewModel @AssistedInject constructor(
}
}

viewModelScope.launchObserve(observeNextEpisodeToWatch) {
it.distinctUntilChanged()
.execute { copy(nextEpisodeToWatch = it) }
}

viewModelScope.launch {
tmdbManager.imageProviderFlow
.execute { copy(tmdbImageUrlProvider = it) }
@@ -110,7 +117,9 @@ class ShowDetailsFragmentViewModel @AssistedInject constructor(
viewModelScope.launchInteractor(observeRelatedShows,
ObserveRelatedShows.Params(it.showId))
viewModelScope.launchInteractor(observeShowSeasons,
ObserveFollowedShowSeasonData.Params(it.showId))
ObserveShowSeasonData.Params(it.showId))
viewModelScope.launchInteractor(observeNextEpisodeToWatch,
ObserveShowNextEpisodeToWatch.Params(it.showId))
}

refresh(false)
@@ -122,13 +131,13 @@ class ShowDetailsFragmentViewModel @AssistedInject constructor(
viewModelScope.launchInteractor(updateRelatedShows,
UpdateRelatedShows.Params(it.showId, fromUserInteraction))
viewModelScope.launchInteractor(updateShowSeasons,
UpdateFollowedShowSeasonData.Params(it.showId, fromUserInteraction))
UpdateShowSeasonData.Params(it.showId, fromUserInteraction))
}

fun onToggleMyShowsButtonClicked() = withState {
viewModelScope.launch {
changeShowFollowStatus.execute(ChangeShowFollowStatus.Params(it.showId, TOGGLE))
updateShowSeasons.execute(UpdateFollowedShowSeasonData.Params(it.showId, false))
updateShowSeasons.execute(UpdateShowSeasonData.Params(it.showId, false))
}
}

@@ -18,6 +18,7 @@ package app.tivi.showdetails.details

import app.tivi.data.entities.ShowTmdbImage
import app.tivi.data.entities.TiviShow
import app.tivi.data.resultentities.EpisodeWithSeason
import app.tivi.data.resultentities.RelatedShowEntryWithShow
import app.tivi.data.resultentities.SeasonWithEpisodesAndWatches
import app.tivi.tmdb.TmdbImageUrlProvider
@@ -32,6 +33,7 @@ data class ShowDetailsViewState(
val posterImage: ShowTmdbImage? = null,
val backdropImage: ShowTmdbImage? = null,
val relatedShows: Async<List<RelatedShowEntryWithShow>> = Uninitialized,
val nextEpisodeToWatch: Async<EpisodeWithSeason> = Uninitialized,
val seasons: Async<List<SeasonWithEpisodesAndWatches>> = Uninitialized,
val expandedSeasonIds: Set<Long> = emptySet(),
val tmdbImageUrlProvider: Async<TmdbImageUrlProvider> = Uninitialized

0 comments on commit 3be043d

Please sign in to comment.
You can’t perform that action at this time.