diff --git a/app/src/main/java/com/github/apognu/otter/adapters/home/HomeMediaAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/home/HomeMediaAdapter.kt index d2081c2..091cb1f 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/home/HomeMediaAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/home/HomeMediaAdapter.kt @@ -6,21 +6,39 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.github.apognu.otter.R +import com.github.apognu.otter.fragments.HomeFragment +import com.github.apognu.otter.utils.Artist import com.github.apognu.otter.utils.mustNormalizeUrl import com.squareup.picasso.Picasso import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import kotlinx.android.synthetic.main.row_home_media.view.* -class HomeMediaAdapter(val context: Context?, val viewRes: Int = R.layout.row_home_media) : RecyclerView.Adapter() { - data class HomeMediaItem(val label: String, val cover: String?) +class HomeMediaAdapter( + private val context: Context?, + private val kind: ItemType, + private val viewRes: Int = R.layout.row_home_media, + private val listener: HomeFragment.OnHomeClickListener? = null +) : RecyclerView.Adapter() { + + enum class ItemType { + Tag, Artist, Album, Track + } + + data class HomeMediaItem( + val label: String, + val cover: String?, + val artist: Artist? = null + ) var data: List = listOf() override fun getItemCount() = data.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return LayoutInflater.from(context).inflate(viewRes, parent, false).run { - ViewHolder(this) + val view = LayoutInflater.from(context).inflate(viewRes, parent, false) + + return ViewHolder(view).also { + view.setOnClickListener(it) } } @@ -40,8 +58,14 @@ class HomeMediaAdapter(val context: Context?, val viewRes: Int = R.layout.row_ho } } - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener { val label = view.label val cover = view.cover + + override fun onClick(view: View?) { + when { + kind == ItemType.Artist -> listener?.onClick(artist = data[layoutPosition].artist) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/fragments/HomeFragment.kt b/app/src/main/java/com/github/apognu/otter/fragments/HomeFragment.kt index 6da9aec..1f0caa6 100644 --- a/app/src/main/java/com/github/apognu/otter/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/apognu/otter/fragments/HomeFragment.kt @@ -8,39 +8,75 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import com.github.apognu.otter.R import com.github.apognu.otter.adapters.home.HomeMediaAdapter +import com.github.apognu.otter.adapters.home.HomeMediaAdapter.ItemType import com.github.apognu.otter.repositories.Repository +import com.github.apognu.otter.repositories.home.RandomArtistsRepository import com.github.apognu.otter.repositories.home.RecentlyAddedRepository import com.github.apognu.otter.repositories.home.RecentlyListenedRepository import com.github.apognu.otter.repositories.home.TagsRepository -import com.github.apognu.otter.utils.untilNetwork +import com.github.apognu.otter.utils.* import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent +import com.google.gson.Gson import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import java.util.* class HomeFragment : Fragment() { + interface OnHomeClickListener { + fun onClick(artist: Artist? = null, album: Album? = null, track: Track? = null) + } + + val CACHE_DURATION = 15 * 60 * 1000 + + private var bus: Job? = null + private lateinit var tagsRepository: TagsRepository + private lateinit var randomArtistsRepository: RandomArtistsRepository private lateinit var recentlyAddedRepository: RecentlyAddedRepository private lateinit var recentlyListenedRepository: RecentlyListenedRepository private lateinit var tagsAdapter: HomeMediaAdapter + private lateinit var randomAdapter: HomeMediaAdapter private lateinit var recentlyAddedAdapter: HomeMediaAdapter private lateinit var recentlyListenedAdapter: HomeMediaAdapter - private lateinit var randomAdapter: HomeMediaAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) tagsRepository = TagsRepository(context) + randomArtistsRepository = RandomArtistsRepository(context) recentlyAddedRepository = RecentlyAddedRepository(context) recentlyListenedRepository = RecentlyListenedRepository(context) - tagsAdapter = HomeMediaAdapter(context, R.layout.row_tag) - recentlyAddedAdapter = HomeMediaAdapter(context) - recentlyListenedAdapter = HomeMediaAdapter(context) - randomAdapter = HomeMediaAdapter(context) + tagsAdapter = HomeMediaAdapter(context, ItemType.Tag, R.layout.row_tag) + randomAdapter = HomeMediaAdapter(context, ItemType.Artist, listener = ArtistClickListener()) + recentlyAddedAdapter = HomeMediaAdapter(context, ItemType.Track) + recentlyListenedAdapter = HomeMediaAdapter(context, ItemType.Track) + } + + override fun onResume() { + super.onResume() + + bus = GlobalScope.launch(IO) { + EventBus.get().collect { event -> + if (event is Event.ListingsChanged) { + refresh(true) + } + } + } + } + + override fun onPause() { + super.onPause() + + bus?.cancel() + bus = null } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -51,9 +87,11 @@ class HomeFragment : Fragment() { super.onViewCreated(view, savedInstanceState) tags.apply { + isNestedScrollingEnabled = false + adapter = tagsAdapter layoutManager = FlexboxLayoutManager(context).apply { - isNestedScrollingEnabled = false + justifyContent = JustifyContent.SPACE_BETWEEN } } @@ -75,34 +113,89 @@ class HomeFragment : Fragment() { refresh() } - private fun refresh() { - tagsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(IO) {data, _, _ -> + private fun originFor(repository: Repository<*, *>, force: Boolean = false): Repository.Origin { + if (force) return Repository.Origin.Network + + repository.cacheId?.let { cacheId -> + repository.cache(listOf())?.let { + Cache.get(context, "$cacheId-at")?.readLine()?.toLong()?.let { date -> + return if ((Date().time - date) < CACHE_DURATION) Repository.Origin.Cache + else Repository.Origin.Network + } + } + } + + return Repository.Origin.Network + } + + private fun cache(repository: Repository, data: List) { + repository.cacheId?.let { cacheId -> + repository.cache(data)?.let { cache -> + Cache.set( + context, + cacheId, + Gson().toJson(cache).toByteArray() + ) + + Cache.set(context, "$cacheId-at", Date().time.toString().toByteArray()) + } + } + } + + private fun refresh(force: Boolean = false) { + tagsRepository.fetch(originFor(tagsRepository, force).origin).untilNetwork(IO) { data, isCache, _ -> GlobalScope.launch(Main) { tagsAdapter.data = data.map { HomeMediaAdapter.HomeMediaItem(it.name, null) } tagsAdapter.notifyDataSetChanged() - tags_loader.visibility = View.GONE - tags.visibility = View.VISIBLE + tags_loader?.visibility = View.GONE + tags?.visibility = View.VISIBLE + + if (!isCache) cache(tagsRepository, data) } } - recentlyListenedRepository.fetch(Repository.Origin.Network.origin).untilNetwork(IO) { data, _, _ -> + randomArtistsRepository.fetch(originFor(randomArtistsRepository, force).origin).untilNetwork(IO) { data, isCache, _ -> + GlobalScope.launch(Main) { + randomAdapter.data = data.map { HomeMediaAdapter.HomeMediaItem(it.name, it.albums?.getOrNull(0)?.cover?.original, artist = it) } + randomAdapter.notifyDataSetChanged() + + random_loader?.visibility = View.GONE + random?.visibility = View.VISIBLE + + if (!isCache) cache(randomArtistsRepository, data) + } + } + + recentlyListenedRepository.fetch(originFor(recentlyListenedRepository, force).origin).untilNetwork(IO) { data, isCache, _ -> GlobalScope.launch(Main) { recentlyListenedAdapter.data = data.map { HomeMediaAdapter.HomeMediaItem(it.track.title, it.track.album.cover.original) } recentlyListenedAdapter.notifyDataSetChanged() - recently_listened_loader.visibility = View.GONE - recently_listened.visibility = View.VISIBLE + recently_listened_loader?.visibility = View.GONE + recently_listened?.visibility = View.VISIBLE + + if (!isCache) cache(recentlyListenedRepository, data) } } - recentlyAddedRepository.fetch(Repository.Origin.Network.origin).untilNetwork(IO) { data, _, _ -> + recentlyAddedRepository.fetch(originFor(recentlyAddedRepository, force).origin).untilNetwork(IO) { data, isCache, _ -> GlobalScope.launch(Main) { recentlyAddedAdapter.data = data.map { HomeMediaAdapter.HomeMediaItem(it.title, it.album.cover.original) } recentlyAddedAdapter.notifyDataSetChanged() - recently_added_loader.visibility = View.GONE - recently_added.visibility = View.VISIBLE + recently_added_loader?.visibility = View.GONE + recently_added?.visibility = View.VISIBLE + + if (!isCache) cache(recentlyAddedRepository, data) + } + } + } + + inner class ArtistClickListener : OnHomeClickListener { + override fun onClick(artist: Artist?, album: Album?, track: Track?) { + artist?.let { + ArtistsFragment.openAlbums(context, artist) } } } diff --git a/app/src/main/java/com/github/apognu/otter/repositories/home/RandomArtistsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/home/RandomArtistsRepository.kt new file mode 100644 index 0000000..a2bfa64 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/repositories/home/RandomArtistsRepository.kt @@ -0,0 +1,27 @@ +package com.github.apognu.otter.repositories.home + +import android.content.Context +import com.github.apognu.otter.repositories.HttpUpstream +import com.github.apognu.otter.repositories.Repository +import com.github.apognu.otter.utils.Artist +import com.github.apognu.otter.utils.ArtistsCache +import com.github.apognu.otter.utils.ArtistsResponse +import com.github.apognu.otter.utils.FunkwhaleResponse +import com.github.kittinunf.fuel.gson.gsonDeserializerOf +import com.google.gson.reflect.TypeToken +import java.io.BufferedReader + +class RandomArtistsRepository(override val context: Context?) : Repository() { + override val cacheId = "home-random-artists" + + override val upstream = + HttpUpstream>( + HttpUpstream.Behavior.Single, + "/api/v1/artists/?playable=true&ordering=random", + object : TypeToken() {}.type, + 10 + ) + + override fun cache(data: List) = ArtistsCache(data) + override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyAddedRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyAddedRepository.kt index 2f1e503..51ff87d 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyAddedRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyAddedRepository.kt @@ -3,8 +3,13 @@ package com.github.apognu.otter.repositories.home import android.content.Context import com.github.apognu.otter.repositories.HttpUpstream import com.github.apognu.otter.repositories.Repository -import com.github.apognu.otter.utils.* +import com.github.apognu.otter.utils.FunkwhaleResponse +import com.github.apognu.otter.utils.Track +import com.github.apognu.otter.utils.TracksCache +import com.github.apognu.otter.utils.TracksResponse +import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.google.gson.reflect.TypeToken +import java.io.BufferedReader class RecentlyAddedRepository(override val context: Context?) : Repository() { override val cacheId = "home-recently-added" @@ -16,4 +21,7 @@ class RecentlyAddedRepository(override val context: Context?) : Repository() {}.type, 10 ) + + override fun cache(data: List) = TracksCache(data) + override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader) } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyListenedRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyListenedRepository.kt index 32914ea..039c2e0 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyListenedRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/home/RecentlyListenedRepository.kt @@ -3,8 +3,13 @@ package com.github.apognu.otter.repositories.home import android.content.Context import com.github.apognu.otter.repositories.HttpUpstream import com.github.apognu.otter.repositories.Repository -import com.github.apognu.otter.utils.* +import com.github.apognu.otter.utils.FunkwhaleResponse +import com.github.apognu.otter.utils.PlaylistTrack +import com.github.apognu.otter.utils.PlaylistTracksCache +import com.github.apognu.otter.utils.PlaylistTracksResponse +import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.google.gson.reflect.TypeToken +import java.io.BufferedReader class RecentlyListenedRepository(override val context: Context?) : Repository() { override val cacheId = "home-recently-listened" @@ -16,4 +21,7 @@ class RecentlyListenedRepository(override val context: Context?) : Repository() {}.type, 10 ) + + override fun cache(data: List) = PlaylistTracksCache(data) + override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader) } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/repositories/home/TagsRepository.kt b/app/src/main/java/com/github/apognu/otter/repositories/home/TagsRepository.kt index 9c47c61..4a9ae47 100644 --- a/app/src/main/java/com/github/apognu/otter/repositories/home/TagsRepository.kt +++ b/app/src/main/java/com/github/apognu/otter/repositories/home/TagsRepository.kt @@ -3,11 +3,16 @@ package com.github.apognu.otter.repositories.home import android.content.Context import com.github.apognu.otter.repositories.HttpUpstream import com.github.apognu.otter.repositories.Repository -import com.github.apognu.otter.utils.* +import com.github.apognu.otter.utils.FunkwhaleResponse +import com.github.apognu.otter.utils.Tag +import com.github.apognu.otter.utils.TagsCache +import com.github.apognu.otter.utils.TagsResponse +import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.google.gson.reflect.TypeToken +import java.io.BufferedReader class TagsRepository(override val context: Context?) : Repository() { - override val cacheId = "tags" + override val cacheId = "home-tags" override val upstream = HttpUpstream>( @@ -15,4 +20,9 @@ class TagsRepository(override val context: Context?) : Repository() {}.type ) + + override fun onDataFetched(data: List) = data.shuffled().take(10) + + override fun cache(data: List) = TagsCache(data) + override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TagsCache::class.java).deserialize(reader) } \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/utils/Data.kt b/app/src/main/java/com/github/apognu/otter/utils/Data.kt index 1eb5883..1be0478 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Data.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Data.kt @@ -76,6 +76,7 @@ object Cache { fun set(context: Context?, key: String, value: ByteArray) = context?.let { with(File(it.cacheDir, key(key))) { + "$key (${key(key)} : $value" writeBytes(value) } } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 0449601..381d3a0 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -17,7 +17,7 @@ android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:layout_marginBottom="16dp" - android:text="Tags" /> + android:text="Some tags" /> + android:background="@drawable/ripple" + android:orientation="vertical" + android:padding="8dp"> + android:paddingVertical="6dp"> + android:textSize="16sp" /> \ No newline at end of file