6 changed files with 283 additions and 104 deletions
@ -0,0 +1,119 @@
|
||||
/** |
||||
* Teapod |
||||
* |
||||
* Copyright 2020-2022 <seil0@mosad.xyz> |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation; either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program; if not, write to the Free Software |
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |
||||
* MA 02110-1301, USA. |
||||
* |
||||
*/ |
||||
|
||||
package org.mosad.teapod.ui.activity.main.viewmodel |
||||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope |
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.viewModelScope |
||||
import kotlinx.coroutines.async |
||||
import kotlinx.coroutines.flow.* |
||||
import kotlinx.coroutines.launch |
||||
import org.mosad.teapod.parser.crunchyroll.* |
||||
import kotlin.random.Random |
||||
|
||||
class HomeViewModel : ViewModel() { |
||||
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading) |
||||
|
||||
sealed class UiState { |
||||
object Loading : UiState() |
||||
data class Normal( |
||||
val upNextItems: List<ContinueWatchingItem>, |
||||
val watchlistItems: List<Item>, |
||||
val recentlyAddedItems: List<Item>, |
||||
val topTenItems: List<Item>, |
||||
val highlightItem: Item, |
||||
val highlightIsWatchlist:Boolean |
||||
) : UiState() |
||||
data class Error(val message: String?) : UiState() |
||||
} |
||||
|
||||
init { |
||||
load() |
||||
} |
||||
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) { |
||||
scope.launch { uiState.collect { collector(it) } } |
||||
} |
||||
|
||||
fun load() { |
||||
viewModelScope.launch { |
||||
uiState.emit(UiState.Loading) |
||||
try { |
||||
// run the loading in parallel to speed up the process |
||||
|
||||
val upNextJob = viewModelScope.async { Crunchyroll.upNextAccount().items } |
||||
val watchlistJob = viewModelScope.async { Crunchyroll.watchlist(50).items } |
||||
val recentlyAddedJob = viewModelScope.async { |
||||
Crunchyroll.browse(sortBy = SortBy.NEWLY_ADDED, n = 50).items |
||||
} |
||||
val topTenJob = viewModelScope.async { |
||||
Crunchyroll.browse(sortBy = SortBy.POPULARITY, n = 10).items |
||||
} |
||||
|
||||
val recentlyAddedItems = recentlyAddedJob.await() |
||||
// FIXME crashes on newTitles.items.size == 0 |
||||
val highlightItem = recentlyAddedItems[Random.nextInt(recentlyAddedItems.size)] |
||||
val highlightItemIsWatchlist = Crunchyroll.isWatchlist(highlightItem.id) |
||||
|
||||
uiState.emit(UiState.Normal( |
||||
upNextJob.await(), watchlistJob.await(), recentlyAddedJob.await(), |
||||
topTenJob.await(), highlightItem, highlightItemIsWatchlist |
||||
)) |
||||
} catch (e: Exception) { |
||||
uiState.emit(UiState.Error(e.message)) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Toggle the watchlist state of the highlight media. |
||||
*/ |
||||
fun toggleHighlightWatchlist() { |
||||
viewModelScope.launch { |
||||
uiState.update { currentUiState -> |
||||
if (currentUiState is UiState.Normal) { |
||||
if (currentUiState.highlightIsWatchlist) { |
||||
Crunchyroll.deleteWatchlist(currentUiState.highlightItem.id) |
||||
} else { |
||||
Crunchyroll.postWatchlist(currentUiState.highlightItem.id) |
||||
} |
||||
|
||||
// update the watchlist after a item has been added/removed |
||||
val watchlistItems = Crunchyroll.watchlist(50).items |
||||
|
||||
currentUiState.copy( |
||||
watchlistItems = watchlistItems, |
||||
highlightIsWatchlist = !currentUiState.highlightIsWatchlist) |
||||
} else { |
||||
currentUiState |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,55 @@
|
||||
package org.mosad.teapod.util.adapter |
||||
|
||||
import android.view.LayoutInflater |
||||
import android.view.ViewGroup |
||||
import androidx.recyclerview.widget.DiffUtil |
||||
import androidx.recyclerview.widget.ListAdapter |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import com.bumptech.glide.Glide |
||||
import org.mosad.teapod.databinding.ItemMediaBinding |
||||
import org.mosad.teapod.util.ItemMedia |
||||
|
||||
class MediaItemListAdapter(private val onClickListener: OnClickListener) : ListAdapter<ItemMedia, MediaItemListAdapter.MediaViewHolder>(DiffCallback) { |
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { |
||||
return MediaViewHolder( |
||||
ItemMediaBinding.inflate( |
||||
LayoutInflater.from(parent.context), |
||||
parent, |
||||
false |
||||
) |
||||
) |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { |
||||
val item = getItem(position) |
||||
holder.binding.root.setOnClickListener { |
||||
onClickListener.onClick(item) |
||||
} |
||||
holder.bind(item) |
||||
} |
||||
|
||||
inner class MediaViewHolder(val binding: ItemMediaBinding) : |
||||
RecyclerView.ViewHolder(binding.root) { |
||||
|
||||
fun bind(item: ItemMedia) { |
||||
binding.textTitle.text = item.title |
||||
// can we use the view instead of context here? |
||||
Glide.with(binding.root.context).load(item.posterUrl).into(binding.imagePoster) |
||||
} |
||||
} |
||||
|
||||
companion object DiffCallback : DiffUtil.ItemCallback<ItemMedia>() { |
||||
override fun areItemsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean { |
||||
return oldItem.id == newItem.id |
||||
} |
||||
|
||||
override fun areContentsTheSame(oldItem: ItemMedia, newItem: ItemMedia): Boolean { |
||||
return oldItem == newItem |
||||
} |
||||
} |
||||
|
||||
class OnClickListener(val clickListener: (item: ItemMedia) -> Unit) { |
||||
fun onClick(item: ItemMedia) = clickListener(item) |
||||
} |
||||
} |
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
|
Loading…
Reference in new issue