Several improvements in UI (better colors for night mode, added icons).

Better handling of startup (login activity would reset if put in the background).
Allow use of schemeless hostname for login.
Destroy main activity and clear cache on logout.
Change of endpoint for favorites retrieval for one with much better performance.
This commit is contained in:
Antoine POPINEAU 2019-10-23 20:21:18 +02:00
parent 78468167ca
commit e84455390b
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
22 changed files with 200 additions and 97 deletions

View File

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.apognu.otter"> package="com.github.apognu.otter">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> <permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<application <application
android:name="com.github.apognu.otter.Otter" android:name="com.github.apognu.otter.Otter"
@ -14,31 +13,39 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<!-- <meta-data <!-- <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> --> android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/> -->
<activity android:name="com.github.apognu.otter.activities.LoginActivity" android:noHistory="true" android:launchMode="singleInstance"> <activity
android:name="com.github.apognu.otter.activities.SplashActivity"
android:launchMode="singleInstance"
android:noHistory="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.github.apognu.otter.activities.MainActivity"/> <activity
<activity android:name="com.github.apognu.otter.activities.SearchActivity" android:launchMode="singleTop"/> android:name="com.github.apognu.otter.activities.LoginActivity"
<activity android:name="com.github.apognu.otter.activities.SettingsActivity"/> android:launchMode="singleInstance" />
<activity android:name="com.github.apognu.otter.activities.LicencesActivity"/> <activity android:name="com.github.apognu.otter.activities.MainActivity" />
<activity
android:name="com.github.apognu.otter.activities.SearchActivity"
android:launchMode="singleTop" />
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
<service android:name="com.github.apognu.otter.playback.PlayerService"/> <service android:name="com.github.apognu.otter.playback.PlayerService" />
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver"/> <receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
</application> </application>

View File

@ -1,8 +1,8 @@
package com.github.apognu.otter.activities package com.github.apognu.otter.activities
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.LoginDialog import com.github.apognu.otter.fragments.LoginDialog
@ -19,33 +19,29 @@ import kotlinx.coroutines.launch
data class FwCredentials(val token: String) data class FwCredentials(val token: String)
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE).apply {
when (contains("access_token")) {
true -> Intent(this@LoginActivity, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
false -> setContentView(R.layout.activity_login)
}
}
login?.setOnClickListener { login?.setOnClickListener {
val hostname = hostname.text.toString().trim() var hostname = hostname.text.toString().trim()
val username = username.text.toString() val username = username.text.toString()
val password = password.text.toString() val password = password.text.toString()
try { try {
if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname)) if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
val url = Uri.parse(hostname) Uri.parse(hostname).apply {
if (scheme == "http") {
throw Exception(getString(R.string.login_error_hostname_https))
}
if (url.scheme != "https") { if (scheme == null) hostname = "https://${hostname}"
throw Exception(getString(R.string.login_error_hostname_https))
} }
} catch (e: Exception) { } catch (e: Exception) {
val message = val message =

View File

@ -19,6 +19,7 @@ import com.github.apognu.otter.fragments.BrowseFragment
import com.github.apognu.otter.fragments.QueueFragment import com.github.apognu.otter.fragments.QueueFragment
import com.github.apognu.otter.playback.MediaControlsManager import com.github.apognu.otter.playback.MediaControlsManager
import com.github.apognu.otter.playback.PlayerService import com.github.apognu.otter.playback.PlayerService
import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.repositories.Repository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
@ -32,7 +33,12 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
enum class ResultCode(val code: Int) {
LOGOUT(1001)
}
private val favoriteRepository = FavoritesRepository(this) private val favoriteRepository = FavoritesRepository(this)
private val favoriteCheckRepository = FavoritedRepository(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -127,12 +133,25 @@ class MainActivity : AppCompatActivity() {
R.id.nav_queue -> launchDialog(QueueFragment()) R.id.nav_queue -> launchDialog(QueueFragment())
R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java)) R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java))
R.id.settings -> startActivity(Intent(this, SettingsActivity::class.java)) R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0)
} }
return true return true
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == ResultCode.LOGOUT.code) {
Intent(this, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(this)
finish()
}
}
}
private fun launchFragment(fragment: Fragment) { private fun launchFragment(fragment: Fragment) {
supportFragmentManager.fragments.lastOrNull()?.also { oldFragment -> supportFragmentManager.fragments.lastOrNull()?.also { oldFragment ->
oldFragment.enterTransition = null oldFragment.enterTransition = null
@ -235,11 +254,10 @@ class MainActivity : AppCompatActivity() {
.centerCrop() .centerCrop()
.into(now_playing_details_cover) .into(now_playing_details_cover)
favoriteRepository.fetch().untilNetwork(IO) { favorites -> favoriteCheckRepository.fetch().untilNetwork(IO) { favorites ->
GlobalScope.launch(Main) { GlobalScope.launch(Main) {
val favorites = favorites.map { it.track.id }
track.favorite = favorites.contains(track.id) track.favorite = favorites.contains(track.id)
when (track.favorite) { when (track.favorite) {
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite)) true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground)) false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))

View File

@ -56,12 +56,10 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
.setPositiveButton(android.R.string.yes) { _, _ -> .setPositiveButton(android.R.string.yes) { _, _ ->
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear() PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
Intent(context, LoginActivity::class.java).apply { context.cacheDir.deleteRecursively()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(this) activity?.setResult(MainActivity.ResultCode.LOGOUT.code)
activity?.finish() activity?.finish()
}
} }
.setNegativeButton(android.R.string.no, null) .setNegativeButton(android.R.string.no, null)
.show() .show()

View File

@ -0,0 +1,29 @@
package com.github.apognu.otter.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.utils.AppContext
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE).apply {
when (contains("access_token")) {
true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
}
}
}
}

View File

@ -18,7 +18,7 @@ import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.* import kotlinx.android.synthetic.main.row_track.view.*
import java.util.* import java.util.*
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : FunkwhaleAdapter<Favorite, FavoritesAdapter.ViewHolder>() { class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : FunkwhaleAdapter<Track, FavoritesAdapter.ViewHolder>() {
interface OnFavoriteListener { interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean) fun onToggleFavorite(id: Int, state: Boolean)
} }
@ -28,7 +28,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
override fun getItemCount() = data.size override fun getItemCount() = data.size
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return data[position].track.id.toLong() return data[position].id.toLong()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -44,14 +44,14 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
val favorite = data[position] val favorite = data[position]
Picasso.get() Picasso.get()
.maybeLoad(maybeNormalizeUrl(favorite.track.album.cover.original)) .maybeLoad(maybeNormalizeUrl(favorite.album.cover.original))
.fit() .fit()
.placeholder(R.drawable.cover) .placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0)) .transform(RoundedCornersTransformation(16, 0))
.into(holder.cover) .into(holder.cover)
holder.title.text = favorite.track.title holder.title.text = favorite.title
holder.artist.text = favorite.track.artist.name holder.artist.text = favorite.artist.name
Build.VERSION_CODES.P.onApi( Build.VERSION_CODES.P.onApi(
{ {
@ -64,19 +64,19 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
}) })
if (favorite.track == currentTrack || favorite.track.current) { if (favorite == currentTrack || favorite.current) {
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD) holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD) holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
} }
context?.let { context?.let {
when (favorite.track.favorite) { when (favorite.favorite) {
true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite)) true -> holder.favorite.setColorFilter(context.getColor(R.color.colorFavorite))
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected)) false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
} }
holder.favorite.setOnClickListener { holder.favorite.setOnClickListener {
favoriteListener.onToggleFavorite(favorite.track.id, !favorite.track.favorite) favoriteListener.onToggleFavorite(favorite.id, !favorite.favorite)
data.remove(favorite) data.remove(favorite)
notifyItemRemoved(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition)
@ -90,9 +90,9 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
setOnMenuItemClickListener { setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(favorite.track))) R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(favorite)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(favorite.track)) R.id.track_play_next -> CommandBus.send(Command.PlayNext(favorite))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(favorite.track)) R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(favorite))
} }
true true
@ -132,7 +132,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
true -> CommandBus.send(Command.PlayTrack(layoutPosition)) true -> CommandBus.send(Command.PlayTrack(layoutPosition))
false -> { false -> {
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply { data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
CommandBus.send(Command.ReplaceQueue(this.map { it.track })) CommandBus.send(Command.ReplaceQueue(this))
context.toast("All tracks were added to your queue") context.toast("All tracks were added to your queue")
} }

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FavoritesFragment : FunkwhaleFragment<Favorite, FavoritesAdapter>() { class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites override val recycler: RecyclerView get() = favorites
@ -22,7 +22,6 @@ class FavoritesFragment : FunkwhaleFragment<Favorite, FavoritesAdapter>() {
adapter = FavoritesAdapter(context, FavoriteListener()) adapter = FavoritesAdapter(context, FavoriteListener())
repository = FavoritesRepository(context) repository = FavoritesRepository(context)
favoritesRepository = FavoritesRepository(context)
watchEventBus() watchEventBus()
} }
@ -38,7 +37,7 @@ class FavoritesFragment : FunkwhaleFragment<Favorite, FavoritesAdapter>() {
} }
play.setOnClickListener { play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled().map { it.track })) CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
} }
} }

View File

@ -12,16 +12,16 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.BufferedReader import java.io.BufferedReader
class FavoritesRepository(override val context: Context?) : Repository<Favorite, FavoritesCache>() { class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
override val cacheId = "favorites" override val cacheId = "favorites.v2"
override val upstream = HttpUpstream<Favorite, FunkwhaleResponse<Favorite>>(HttpUpstream.Behavior.AtOnce, "/api/v1/favorites/tracks?playable=true", object : TypeToken<FavoritesResponse>() {}.type) override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks?favorites=true&playable=true", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Favorite>) = FavoritesCache(data) override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritesCache::class.java).deserialize(reader) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
override fun onDataFetched(data: List<Favorite>) = data.map { override fun onDataFetched(data: List<Track>) = data.map {
it.apply { it.apply {
it.track.favorite = true it.favorite = true
} }
} }
@ -53,3 +53,11 @@ class FavoritesRepository(override val context: Context?) : Repository<Favorite,
} }
} }
} }
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
override val cacheId = "favorited"
override val upstream = HttpUpstream<Int, FunkwhaleResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
override fun cache(data: List<Int>) = FavoritedCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
}

View File

@ -34,7 +34,7 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(private val behavior: Beha
} }
override fun fetch(data: List<D>): Channel<Repository.Response<D>>? { override fun fetch(data: List<D>): Channel<Repository.Response<D>>? {
if (behavior == Behavior.Single && data.isNotEmpty()) return null if (behavior == Behavior.Single && data.isNotEmpty()) return null
val page = ceil(data.size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1 val page = ceil(data.size / AppContext.PAGE_SIZE.toDouble()).toInt() + 1

View File

@ -1,7 +1,10 @@
package com.github.apognu.otter.repositories package com.github.apognu.otter.repositories
import android.content.Context import android.content.Context
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.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -15,17 +18,10 @@ class PlaylistTracksRepository(override val context: Context?, playlistId: Int)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
override fun onDataFetched(data: List<PlaylistTrack>): List<PlaylistTrack> = runBlocking { override fun onDataFetched(data: List<PlaylistTrack>): List<PlaylistTrack> = runBlocking {
val favorites = FavoritesRepository(context).fetch(Origin.Network.origin).receive().data val favorites = FavoritedRepository(context).fetch(Origin.Network.origin).receive().data
log(favorites.toString())
data.map { track -> data.map { track ->
val favorite = favorites.find { it.track.id == track.track.id } track.track.favorite = favorites.contains(track.track.id)
if (favorite != null) {
track.track.favorite = true
}
track track
} }
} }

View File

@ -18,15 +18,10 @@ class SearchRepository(override val context: Context?, query: String) : Reposito
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking { override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val favorites = FavoritesRepository(context).fetch(Origin.Network.origin).receive().data val favorites = FavoritedRepository(context).fetch(Origin.Network.origin).receive().data
data.map { track -> data.map { track ->
val favorite = favorites.find { it.track.id == track.id } track.favorite = favorites.contains(track.id)
if (favorite != null) {
track.favorite = true
}
track track
} }
} }

View File

@ -18,15 +18,10 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader) override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking { override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val favorites = FavoritesRepository(context).fetch(Origin.Network.origin).receive().data val favorites = FavoritedRepository(context).fetch(Origin.Network.origin).receive().data
data.map { track -> data.map { track ->
val favorite = favorites.find { it.track.id == track.id } track.favorite = favorites.contains(track.id)
if (favorite != null) {
track.favorite = true
}
track track
} }
} }

View File

@ -8,7 +8,7 @@ class AlbumsCache(data: List<Album>) : CacheItem<Album>(data)
class TracksCache(data: List<Track>) : CacheItem<Track>(data) class TracksCache(data: List<Track>) : CacheItem<Track>(data)
class PlaylistsCache(data: List<Playlist>) : CacheItem<Playlist>(data) class PlaylistsCache(data: List<Playlist>) : CacheItem<Playlist>(data)
class PlaylistTracksCache(data: List<PlaylistTrack>) : CacheItem<PlaylistTrack>(data) class PlaylistTracksCache(data: List<PlaylistTrack>) : CacheItem<PlaylistTrack>(data)
class FavoritesCache(data: List<Favorite>) : CacheItem<Favorite>(data) class FavoritedCache(data: List<Int>) : CacheItem<Int>(data)
class QueueCache(data: List<Track>) : CacheItem<Track>(data) class QueueCache(data: List<Track>) : CacheItem<Track>(data)
abstract class FunkwhaleResponse<D : Any> { abstract class FunkwhaleResponse<D : Any> {
@ -18,6 +18,10 @@ abstract class FunkwhaleResponse<D : Any> {
abstract fun getData(): List<D> abstract fun getData(): List<D>
} }
data class UserResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
override fun getData() = results
}
data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() { data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
override fun getData() = results override fun getData() = results
} }
@ -30,8 +34,8 @@ data class TracksResponse(override val count: Int, override val next: String?, v
override fun getData() = results override fun getData() = results
} }
data class FavoritesResponse(override val count: Int, override val next: String?, val results: List<Favorite>) : FunkwhaleResponse<Favorite>() { data class FavoritedResponse(override val count: Int, override val next: String?, val results: List<Favorited>) : FunkwhaleResponse<Int>() {
override fun getData() = results override fun getData() = results.map { it.track }
} }
data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : FunkwhaleResponse<Playlist>() { data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : FunkwhaleResponse<Playlist>() {
@ -100,7 +104,7 @@ data class Track(
} }
} }
data class Favorite(val id: Int, val track: Track) data class Favorited(val track: Int)
data class Playlist( data class Playlist(
val id: Int, val id: Int,

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM12,18c-0.89,0 -1.74,-0.2 -2.5,-0.55C11.56,16.5 13,14.42 13,12s-1.44,-4.5 -3.5,-5.45C10.26,6.2 11.11,6 12,6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM11,15L9.5,15v-2h-2v2L6,15L6,9h1.5v2.5h2L9.5,9L11,9v6zM18,14c0,0.55 -0.45,1 -1,1h-0.75v1.5h-1.5L14.75,15L14,15c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h3c0.55,0 1,0.45 1,1v4zM14.5,13.5h2v-3h-2v3z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,2h-8L4.02,8 4,20c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM12,8h-2L10,4h2v4zM15,8h-2L13,4h2v4zM18,8h-2L16,4h2v4z"/>
</vector>

View File

@ -33,6 +33,7 @@
android:hint="@string/login_hostname" android:hint="@string/login_hostname"
android:textColorHint="@drawable/login_input" android:textColorHint="@drawable/login_input"
app:boxStrokeColor="@drawable/login_input" app:boxStrokeColor="@drawable/login_input"
app:errorTextAppearance="@style/AppTheme.ErrorStyle"
app:hintTextColor="@drawable/login_input"> app:hintTextColor="@drawable/login_input">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
@ -54,6 +55,7 @@
android:hint="@string/login_username" android:hint="@string/login_username"
android:textColorHint="@drawable/login_input" android:textColorHint="@drawable/login_input"
app:boxStrokeColor="@drawable/login_input" app:boxStrokeColor="@drawable/login_input"
app:errorTextAppearance="@style/AppTheme.ErrorStyle"
app:hintTextColor="@drawable/login_input"> app:hintTextColor="@drawable/login_input">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
@ -75,6 +77,7 @@
android:hint="@string/login_password" android:hint="@string/login_password"
android:textColorHint="@drawable/login_input" android:textColorHint="@drawable/login_input"
app:boxStrokeColor="@drawable/login_input" app:boxStrokeColor="@drawable/login_input"
app:errorTextAppearance="@style/AppTheme.ErrorStyle"
app:hintTextColor="@drawable/login_input" app:hintTextColor="@drawable/login_input"
app:passwordToggleEnabled="true"> app:passwordToggleEnabled="true">
@ -94,5 +97,5 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:backgroundTint="@color/colorAccent" android:backgroundTint="@color/colorAccent"
android:text="@string/login_submit" android:text="@string/login_submit"
android:textColor="@android:color/white" /> android:textColor="@color/whiteWhileLight" />
</LinearLayout> </LinearLayout>

View File

@ -3,12 +3,16 @@
<color name="surface">#121212</color> <color name="surface">#121212</color>
<color name="colorPrimary">#283f4e</color> <color name="colorPrimary">#283f4e</color>
<color name="colorAccent">#99440c</color> <color name="colorAccent">#f1b44f</color>
<color name="colorSelected">#525252</color> <color name="colorSelected">#525252</color>
<color name="colorFavorite">#eba999</color>
<color name="itemTitle">#caffffff</color> <color name="itemTitle">#caffffff</color>
<color name="controlForeground">#caffffff</color> <color name="controlForeground">#caffffff</color>
<color name="controlColor">#53bce7</color>
<color name="controlColor">#327eae</color> <color name="whiteWhileLight">#000000</color>
<color name="blackWhileLight">#ffffff</color>
</resources> </resources>

View File

@ -6,6 +6,7 @@
<color name="colorPrimary">#327eae</color> <color name="colorPrimary">#327eae</color>
<color name="colorPrimaryDark">#3d3e40</color> <color name="colorPrimaryDark">#3d3e40</color>
<color name="colorAccent">#d35400</color> <color name="colorAccent">#d35400</color>
<color name="colorError">#b94705</color>
<color name="colorSelected">#dadada</color> <color name="colorSelected">#dadada</color>
<color name="colorFavorite">#e17055</color> <color name="colorFavorite">#e17055</color>
@ -14,4 +15,7 @@
<color name="controlForeground">@color/colorPrimary</color> <color name="controlForeground">@color/colorPrimary</color>
<color name="controlColor">@color/colorPrimary</color> <color name="controlColor">@color/colorPrimary</color>
<color name="whiteWhileLight">#ffffff</color>
<color name="blackWhileLight">#000000</color>
</resources> </resources>

View File

@ -27,8 +27,9 @@
<style name="AppTheme.Title"> <style name="AppTheme.Title">
<item name="android:fontFamily">sans-serif-light</item> <item name="android:fontFamily">sans-serif-light</item>
<item name="android:textSize">28sp</item> <item name="android:textSize">24sp</item>
<item name="android:textColor">@color/itemTitle</item> <item name="android:textColor">@color/itemTitle</item>
<item name="android:textStyle">bold</item>
</style> </style>
<style name="AppTheme.ItemTitle"> <style name="AppTheme.ItemTitle">
@ -44,6 +45,7 @@
<style name="AppTheme.Preference" parent="PreferenceThemeOverlay"> <style name="AppTheme.Preference" parent="PreferenceThemeOverlay">
<item name="android:textColor">@color/itemTitle</item> <item name="android:textColor">@color/itemTitle</item>
<item name="android:tint">@color/blackWhileLight</item>
<item name="preferenceCategoryStyle">@style/AppTheme.PreferenceCategory</item> <item name="preferenceCategoryStyle">@style/AppTheme.PreferenceCategory</item>
</style> </style>
@ -74,4 +76,8 @@
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
</style> </style>
<style name="AppTheme.ErrorStyle" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/colorError</item>
</style>
</resources> </resources>

View File

@ -8,11 +8,13 @@
android:defaultValue="quality" android:defaultValue="quality"
android:entries="@array/media_qualities" android:entries="@array/media_qualities"
android:entryValues="@array/media_qualities_values" android:entryValues="@array/media_qualities_values"
android:icon="@drawable/quality"
android:key="media_quality" android:key="media_quality"
android:title="@string/settings_media_quality" /> android:title="@string/settings_media_quality" />
<SeekBarPreference <SeekBarPreference
android:defaultValue="1" android:defaultValue="1"
android:icon="@drawable/storage"
android:key="media_cache_size" android:key="media_cache_size"
android:max="5" android:max="5"
android:min="0" android:min="0"
@ -28,14 +30,17 @@
android:defaultValue="system" android:defaultValue="system"
android:entries="@array/night_mode" android:entries="@array/night_mode"
android:entryValues="@array/night_mode_values" android:entryValues="@array/night_mode_values"
android:icon="@drawable/brightness"
android:key="night_mode" android:key="night_mode"
android:title="@string/settings_night_mode" /> android:title="@string/settings_night_mode" />
<Preference <Preference
android:icon="@drawable/favorite"
android:key="oss_licences" android:key="oss_licences"
android:title="@string/title_oss_licences" /> android:title="@string/title_oss_licences" />
<Preference <Preference
android:icon="@drawable/logout"
android:key="logout" android:key="logout"
android:title="@string/settings_logout" /> android:title="@string/settings_logout" />