Add architecture document
Add a document describing the high-level auxio architecture.
This commit is contained in:
parent
cef4cb68da
commit
eb5292d083
12 changed files with 154 additions and 58 deletions
1
.github/CONTRIBUTING.md
vendored
1
.github/CONTRIBUTING.md
vendored
|
@ -31,6 +31,7 @@ If you do make a request, provide the following:
|
||||||
- Why do you think it will benefit everyone's usage of the app?
|
- Why do you think it will benefit everyone's usage of the app?
|
||||||
|
|
||||||
If you have the knowledge, you can also implement the feature yourself and create a [Pull Request](https://github.com/OxygenCobalt/Auxio/pulls), but its recommended that **you create an issue beforehand to give me a heads up.**
|
If you have the knowledge, you can also implement the feature yourself and create a [Pull Request](https://github.com/OxygenCobalt/Auxio/pulls), but its recommended that **you create an issue beforehand to give me a heads up.**
|
||||||
|
Its also recommended that you read about [Auxio's Architecture](../info/ARCHITECTURE.md) so that your change does not harm the codebase.
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,6 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
|
||||||
// SettingsManager is lazy-initted to prevent it from being used before its initialized.
|
|
||||||
private val settingsManager: SettingsManager by lazy {
|
|
||||||
SettingsManager.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a bitmap for a song. onDone will be called when the bitmap is loaded.
|
* Get a bitmap for a song. onDone will be called when the bitmap is loaded.
|
||||||
* **Do not use this on the UI elements, instead use the Binding Adapters.**
|
* **Do not use this on the UI elements, instead use the Binding Adapters.**
|
||||||
|
@ -28,7 +23,7 @@ private val settingsManager: SettingsManager by lazy {
|
||||||
* @param onDone What to do with the bitmap when the loading is finished. Bitmap will be null if loading failed/shouldn't occur.
|
* @param onDone What to do with the bitmap when the loading is finished. Bitmap will be null if loading failed/shouldn't occur.
|
||||||
*/
|
*/
|
||||||
fun getBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
fun getBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||||
if (!settingsManager.showCovers) {
|
if (!SettingsManager.getInstance().showCovers) {
|
||||||
onDone(null)
|
onDone(null)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -49,12 +44,12 @@ fun getBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("coverArt")
|
@BindingAdapter("coverArt")
|
||||||
fun ImageView.bindCoverArt(song: Song) {
|
fun ImageView.bindCoverArt(song: Song) {
|
||||||
if (!settingsManager.showCovers) {
|
if (!SettingsManager.getInstance().showCovers) {
|
||||||
setImageResource(R.drawable.ic_song)
|
setImageResource(R.drawable.ic_song)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = getDefaultRequest()
|
val request = newRequest()
|
||||||
.doCoverSetup(context, song)
|
.doCoverSetup(context, song)
|
||||||
.error(R.drawable.ic_song)
|
.error(R.drawable.ic_song)
|
||||||
.build()
|
.build()
|
||||||
|
@ -67,12 +62,12 @@ fun ImageView.bindCoverArt(song: Song) {
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("coverArt")
|
@BindingAdapter("coverArt")
|
||||||
fun ImageView.bindCoverArt(album: Album) {
|
fun ImageView.bindCoverArt(album: Album) {
|
||||||
if (!settingsManager.showCovers) {
|
if (!SettingsManager.getInstance().showCovers) {
|
||||||
setImageResource(R.drawable.ic_album)
|
setImageResource(R.drawable.ic_album)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = getDefaultRequest()
|
val request = newRequest()
|
||||||
.doCoverSetup(context, album)
|
.doCoverSetup(context, album)
|
||||||
.error(R.drawable.ic_album)
|
.error(R.drawable.ic_album)
|
||||||
.build()
|
.build()
|
||||||
|
@ -85,10 +80,11 @@ fun ImageView.bindCoverArt(album: Album) {
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("artistImage")
|
@BindingAdapter("artistImage")
|
||||||
fun ImageView.bindArtistImage(artist: Artist) {
|
fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
if (!settingsManager.showCovers) {
|
if (!SettingsManager.getInstance().showCovers) {
|
||||||
setImageResource(R.drawable.ic_artist)
|
setImageResource(R.drawable.ic_artist)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val request: ImageRequest
|
val request: ImageRequest
|
||||||
|
|
||||||
// If there is more than one album, then create a mosaic of them.
|
// If there is more than one album, then create a mosaic of them.
|
||||||
|
@ -101,7 +97,7 @@ fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
|
|
||||||
val fetcher = MosaicFetcher(context)
|
val fetcher = MosaicFetcher(context)
|
||||||
|
|
||||||
request = getDefaultRequest()
|
request = newRequest()
|
||||||
.data(uris)
|
.data(uris)
|
||||||
.fetcher(fetcher)
|
.fetcher(fetcher)
|
||||||
.error(R.drawable.ic_artist)
|
.error(R.drawable.ic_artist)
|
||||||
|
@ -110,7 +106,7 @@ fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
// Otherwise, just get the first cover and use that
|
// Otherwise, just get the first cover and use that
|
||||||
// If the artist doesn't have any albums [Which happens], then don't even bother with that.
|
// If the artist doesn't have any albums [Which happens], then don't even bother with that.
|
||||||
if (artist.albums.isNotEmpty()) {
|
if (artist.albums.isNotEmpty()) {
|
||||||
request = getDefaultRequest()
|
request = newRequest()
|
||||||
.doCoverSetup(context, artist.albums[0])
|
.doCoverSetup(context, artist.albums[0])
|
||||||
.error(R.drawable.ic_artist)
|
.error(R.drawable.ic_artist)
|
||||||
.build()
|
.build()
|
||||||
|
@ -129,7 +125,7 @@ fun ImageView.bindArtistImage(artist: Artist) {
|
||||||
*/
|
*/
|
||||||
@BindingAdapter("genreImage")
|
@BindingAdapter("genreImage")
|
||||||
fun ImageView.bindGenreImage(genre: Genre) {
|
fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
if (!settingsManager.showCovers) {
|
if (!SettingsManager.getInstance().showCovers) {
|
||||||
setImageResource(R.drawable.ic_genre)
|
setImageResource(R.drawable.ic_genre)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -137,21 +133,20 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
val request: ImageRequest
|
val request: ImageRequest
|
||||||
val genreCovers = mutableListOf<Uri>()
|
val genreCovers = mutableListOf<Uri>()
|
||||||
|
|
||||||
genre.songs.groupBy { it.album }.forEach {
|
// Group the genre's songs by their album's cover and add them
|
||||||
genreCovers.add(it.key.coverUri)
|
genre.songs.groupBy { it.album.coverUri }.forEach { genreCovers.add(it.key) }
|
||||||
}
|
|
||||||
|
|
||||||
if (genreCovers.size >= 4) {
|
if (genreCovers.size >= 4) {
|
||||||
val fetcher = MosaicFetcher(context)
|
val fetcher = MosaicFetcher(context)
|
||||||
|
|
||||||
request = getDefaultRequest()
|
request = newRequest()
|
||||||
.data(genreCovers.slice(0..3))
|
.data(genreCovers.slice(0..3))
|
||||||
.fetcher(fetcher)
|
.fetcher(fetcher)
|
||||||
.error(R.drawable.ic_genre)
|
.error(R.drawable.ic_genre)
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
if (genreCovers.isNotEmpty()) {
|
if (genreCovers.isNotEmpty()) {
|
||||||
request = getDefaultRequest()
|
request = newRequest()
|
||||||
.doCoverSetup(context, genre.songs[0])
|
.doCoverSetup(context, genre.songs[0])
|
||||||
.error(R.drawable.ic_genre)
|
.error(R.drawable.ic_genre)
|
||||||
.build()
|
.build()
|
||||||
|
@ -170,7 +165,7 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
* @return The same builder that this is applied to
|
* @return The same builder that this is applied to
|
||||||
*/
|
*/
|
||||||
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Album): ImageRequest.Builder {
|
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Album): ImageRequest.Builder {
|
||||||
if (settingsManager.useQualityCovers) {
|
if (SettingsManager.getInstance().useQualityCovers) {
|
||||||
fetcher(QualityCoverFetcher(context))
|
fetcher(QualityCoverFetcher(context))
|
||||||
data(data.songs[0])
|
data(data.songs[0])
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,7 +180,7 @@ private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Album): Im
|
||||||
* @return The same builder that this is applied to
|
* @return The same builder that this is applied to
|
||||||
*/
|
*/
|
||||||
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Song): ImageRequest.Builder {
|
private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Song): ImageRequest.Builder {
|
||||||
if (settingsManager.useQualityCovers) {
|
if (SettingsManager.getInstance().useQualityCovers) {
|
||||||
fetcher(QualityCoverFetcher(context))
|
fetcher(QualityCoverFetcher(context))
|
||||||
data(data)
|
data(data)
|
||||||
} else {
|
} else {
|
||||||
|
@ -199,6 +194,6 @@ private fun ImageRequest.Builder.doCoverSetup(context: Context, data: Song): Ima
|
||||||
* Get the base request used by the above functions
|
* Get the base request used by the above functions
|
||||||
* @return The base request
|
* @return The base request
|
||||||
*/
|
*/
|
||||||
private fun ImageView.getDefaultRequest(): ImageRequest.Builder {
|
private fun ImageView.newRequest(): ImageRequest.Builder {
|
||||||
return ImageRequest.Builder(context).target(this)
|
return ImageRequest.Builder(context).target(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,12 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
|
db.apply {
|
||||||
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
|
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
|
||||||
|
execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
|
||||||
|
|
||||||
onCreate(db)
|
onCreate(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
@ -19,6 +17,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.recycler.DiffCallback
|
import org.oxycblt.auxio.recycler.DiffCallback
|
||||||
import org.oxycblt.auxio.recycler.viewholders.BaseHolder
|
import org.oxycblt.auxio.recycler.viewholders.BaseHolder
|
||||||
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
|
import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single adapter for both the Next Queue and the User Queue.
|
* The single adapter for both the Next Queue and the User Queue.
|
||||||
|
@ -52,11 +51,11 @@ class QueueAdapter(
|
||||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
USER_QUEUE_HEADER_ITEM_TYPE -> UserQueueHeaderViewHolder(
|
USER_QUEUE_HEADER_ITEM_TYPE -> UserQueueHeaderViewHolder(
|
||||||
parent.context, ItemActionHeaderBinding.inflate(LayoutInflater.from(parent.context))
|
ItemActionHeaderBinding.inflate(parent.context.inflater)
|
||||||
)
|
)
|
||||||
|
|
||||||
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
|
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
|
||||||
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
|
ItemQueueSongBinding.inflate(parent.context.inflater)
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> error("Someone messed with the ViewHolder item types.")
|
else -> error("Someone messed with the ViewHolder item types.")
|
||||||
|
@ -159,11 +158,11 @@ class QueueAdapter(
|
||||||
* ViewHolder for the **user queue header**. Has the clear queue button.
|
* ViewHolder for the **user queue header**. Has the clear queue button.
|
||||||
*/
|
*/
|
||||||
inner class UserQueueHeaderViewHolder(
|
inner class UserQueueHeaderViewHolder(
|
||||||
context: Context, private val binding: ItemActionHeaderBinding
|
private val binding: ItemActionHeaderBinding
|
||||||
) : BaseHolder<Header>(binding) {
|
) : BaseHolder<Header>(binding) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.headerButton.contentDescription = context.getString(
|
binding.headerButton.contentDescription = binding.headerButton.context.getString(
|
||||||
R.string.description_clear_user_queue
|
R.string.description_clear_user_queue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -352,15 +352,15 @@ class PlaybackStateManager private constructor() {
|
||||||
* @param to The destination index.
|
* @param to The destination index.
|
||||||
*/
|
*/
|
||||||
fun moveQueueItems(from: Int, to: Int): Boolean {
|
fun moveQueueItems(from: Int, to: Int): Boolean {
|
||||||
try {
|
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
|
||||||
val item = mQueue.removeAt(from)
|
|
||||||
mQueue.add(to, item)
|
|
||||||
} catch (exception: IndexOutOfBoundsException) {
|
|
||||||
logE("Indices were out of bounds, did not move queue item")
|
logE("Indices were out of bounds, did not move queue item")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val item = mQueue.removeAt(from)
|
||||||
|
mQueue.add(to, item)
|
||||||
|
|
||||||
forceQueueUpdate()
|
forceQueueUpdate()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -411,15 +411,15 @@ class PlaybackStateManager private constructor() {
|
||||||
* @param to The destination index.
|
* @param to The destination index.
|
||||||
*/
|
*/
|
||||||
fun moveUserQueueItems(from: Int, to: Int) {
|
fun moveUserQueueItems(from: Int, to: Int) {
|
||||||
try {
|
if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) {
|
||||||
val item = mUserQueue.removeAt(from)
|
|
||||||
mUserQueue.add(to, item)
|
|
||||||
} catch (exception: IndexOutOfBoundsException) {
|
|
||||||
logE("Indices were out of bounds, did not move queue item")
|
logE("Indices were out of bounds, did not move queue item")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val item = mUserQueue.removeAt(from)
|
||||||
|
mUserQueue.add(to, item)
|
||||||
|
|
||||||
forceUserQueueUpdate()
|
forceUserQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.content.res.ColorStateList
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -23,6 +22,7 @@ import com.reddit.indicatorfastscroll.FastScrollItemIndicator
|
||||||
import com.reddit.indicatorfastscroll.FastScrollerView
|
import com.reddit.indicatorfastscroll.FastScrollerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
import org.oxycblt.auxio.ui.toColor
|
import org.oxycblt.auxio.ui.toColor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,9 +56,7 @@ class NoLeakThumbView @JvmOverloads constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// --- VIEW SETUP ---
|
// --- VIEW SETUP ---
|
||||||
LayoutInflater.from(context).inflate(
|
context.inflater.inflate(R.layout.fast_scroller_thumb_view, this, true)
|
||||||
R.layout.fast_scroller_thumb_view, this, true
|
|
||||||
)
|
|
||||||
|
|
||||||
thumbView = findViewById(R.id.fast_scroller_thumb)
|
thumbView = findViewById(R.id.fast_scroller_thumb)
|
||||||
textView = thumbView.findViewById(R.id.fast_scroller_thumb_text)
|
textView = thumbView.findViewById(R.id.fast_scroller_thumb_text)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package org.oxycblt.auxio.recycler.viewholders
|
package org.oxycblt.auxio.recycler.viewholders
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import org.oxycblt.auxio.databinding.ItemAlbumBinding
|
import org.oxycblt.auxio.databinding.ItemAlbumBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemArtistBinding
|
import org.oxycblt.auxio.databinding.ItemArtistBinding
|
||||||
|
@ -13,6 +12,7 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A table of all ViewHolder codes. Please add to these so that all viewholder codes are unique.
|
* A table of all ViewHolder codes. Please add to these so that all viewholder codes are unique.
|
||||||
|
@ -59,7 +59,7 @@ class SongViewHolder private constructor(
|
||||||
doOnLongClick: (view: View, data: Song) -> Unit
|
doOnLongClick: (view: View, data: Song) -> Unit
|
||||||
): SongViewHolder {
|
): SongViewHolder {
|
||||||
return SongViewHolder(
|
return SongViewHolder(
|
||||||
ItemSongBinding.inflate(LayoutInflater.from(context)),
|
ItemSongBinding.inflate(context.inflater),
|
||||||
doOnClick, doOnLongClick
|
doOnClick, doOnLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ class AlbumViewHolder private constructor(
|
||||||
doOnLongClick: (view: View, data: Album) -> Unit
|
doOnLongClick: (view: View, data: Album) -> Unit
|
||||||
): AlbumViewHolder {
|
): AlbumViewHolder {
|
||||||
return AlbumViewHolder(
|
return AlbumViewHolder(
|
||||||
ItemAlbumBinding.inflate(LayoutInflater.from(context)),
|
ItemAlbumBinding.inflate(context.inflater),
|
||||||
doOnClick, doOnLongClick
|
doOnClick, doOnLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ class ArtistViewHolder private constructor(
|
||||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
doOnLongClick: (view: View, data: Artist) -> Unit
|
||||||
): ArtistViewHolder {
|
): ArtistViewHolder {
|
||||||
return ArtistViewHolder(
|
return ArtistViewHolder(
|
||||||
ItemArtistBinding.inflate(LayoutInflater.from(context)),
|
ItemArtistBinding.inflate(context.inflater),
|
||||||
doOnClick, doOnLongClick
|
doOnClick, doOnLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ class GenreViewHolder private constructor(
|
||||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
doOnLongClick: (view: View, data: Genre) -> Unit
|
||||||
): GenreViewHolder {
|
): GenreViewHolder {
|
||||||
return GenreViewHolder(
|
return GenreViewHolder(
|
||||||
ItemGenreBinding.inflate(LayoutInflater.from(context)),
|
ItemGenreBinding.inflate(context.inflater),
|
||||||
doOnClick, doOnLongClick
|
doOnClick, doOnLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ class HeaderViewHolder(private val binding: ItemHeaderBinding) : BaseHolder<Head
|
||||||
*/
|
*/
|
||||||
fun from(context: Context): HeaderViewHolder {
|
fun from(context: Context): HeaderViewHolder {
|
||||||
return HeaderViewHolder(
|
return HeaderViewHolder(
|
||||||
ItemHeaderBinding.inflate(LayoutInflater.from(context))
|
ItemHeaderBinding.inflate(context.inflater)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package org.oxycblt.auxio.settings.ui
|
package org.oxycblt.auxio.settings.ui
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemAccentBinding
|
import org.oxycblt.auxio.databinding.ItemAccentBinding
|
||||||
import org.oxycblt.auxio.ui.ACCENTS
|
import org.oxycblt.auxio.ui.ACCENTS
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.inflater
|
||||||
import org.oxycblt.auxio.ui.toStateList
|
import org.oxycblt.auxio.ui.toStateList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +20,7 @@ class AccentAdapter(
|
||||||
override fun getItemCount(): Int = ACCENTS.size
|
override fun getItemCount(): Int = ACCENTS.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
return ViewHolder(ItemAccentBinding.inflate(LayoutInflater.from(parent.context)))
|
return ViewHolder(ItemAccentBinding.inflate(parent.context.inflater))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
|
|
@ -45,11 +45,7 @@ val ACCENTS = arrayOf(
|
||||||
* @property theme The theme resource for this accent
|
* @property theme The theme resource for this accent
|
||||||
* @property name The name of this accent
|
* @property name The name of this accent
|
||||||
*/
|
*/
|
||||||
data class Accent(
|
data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name: Int) {
|
||||||
@ColorRes val color: Int,
|
|
||||||
@StyleRes val theme: Int,
|
|
||||||
@StringRes val name: Int
|
|
||||||
) {
|
|
||||||
/**
|
/**
|
||||||
* Get a [ColorStateList] of the accent
|
* Get a [ColorStateList] of the accent
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -78,7 +78,6 @@ fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int): String {
|
||||||
*/
|
*/
|
||||||
val Context.inflater: LayoutInflater get() = LayoutInflater.from(this)
|
val Context.inflater: LayoutInflater get() = LayoutInflater.from(this)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [Toast] from a [String]
|
* Create a [Toast] from a [String]
|
||||||
* @param context [Context] required to create the toast
|
* @param context [Context] required to create the toast
|
||||||
|
|
|
@ -59,7 +59,7 @@ class MemberBinder<T : ViewDataBinding>(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise create the binding and return that.
|
// Otherwise create the binding and return that.
|
||||||
return inflate(LayoutInflater.from(thisRef.requireContext())).also {
|
return inflate(thisRef.requireContext().inflater).also {
|
||||||
fragmentBinding = it
|
fragmentBinding = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
106
info/ARCHITECTURE.md
Normal file
106
info/ARCHITECTURE.md
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document is designed to provide a simple overview of Auxio's architecture and some guides on how to add to the codebase in an elegant manner. It will be updated as aspects about Auxio change.
|
||||||
|
|
||||||
|
#### Package structure overview
|
||||||
|
|
||||||
|
```
|
||||||
|
org.oxycblt.auxio # Main UI's and logging utilities
|
||||||
|
├──.coil # Fetchers and utilities for Coil, contains binding adapters than be used in the user interface.
|
||||||
|
├──.database # Databases and their items for Auxio
|
||||||
|
├──.detail # UIs for more album/artist/genre details
|
||||||
|
│ └──.adapters # RecyclerView adapters for the detail UIs, which display the header information and items
|
||||||
|
├──.library # Library UI
|
||||||
|
├──.loading # Loading UI
|
||||||
|
├──.music # Music storage and loading
|
||||||
|
│ └──.processing # Systems for music loading and organization
|
||||||
|
├──.playback # Playback UI and systems
|
||||||
|
│ ├──.queue # Queue user interface
|
||||||
|
│ └──.state # Backend/Modes for the playback state
|
||||||
|
├──.recycler # Shared RecyclerView utilities and modes
|
||||||
|
│ └──.viewholders # Shared ViewHolders and ViewHolder utilities
|
||||||
|
├──.search # Search UI
|
||||||
|
├──.settings # Settings UI and systems
|
||||||
|
│ └──.ui # Contains UI's related to the settings view, such as the about screen
|
||||||
|
├──.songs # Songs UI
|
||||||
|
├──.ui # Shared user interface utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Primary code structure
|
||||||
|
|
||||||
|
Auxio's codebase is mostly centered around 4 different types of code.
|
||||||
|
|
||||||
|
- UIs: Fragments, RecyclerView items, and Activities are part of this class. All of them should have little data logic in them and should primarily focus on displaying information in their UIs.
|
||||||
|
- ViewModels: These usually contain data and values that a UI can display, along with doing data processing. The data often takes the form of `MutableLiveData` or `LiveData`, which can be observed.
|
||||||
|
- Shared Objects: These are the fundamental building blocks of Auxio, and exist at the process level. These are usually retrieved using `getInstance` or a similar function. Shared Objects should be avoided in UIs, as their volatility can cause problems. Its better to use a ViewModel and their exposed data instead.
|
||||||
|
- Utilities: These are largely found in the `.ui`, `.music`, and `.coil` packages, taking the form of standalone or extension functions that can be used anywhere.
|
||||||
|
|
||||||
|
Ideally, UIs should only be talking to ViewModels, ViewModels should only be talking to the Shared Objects, and Shared Objects should only be talking to other shared objects. All objects can use the utility functions.
|
||||||
|
|
||||||
|
#### UI Structure
|
||||||
|
|
||||||
|
Auxio only has one activity, that being `MainActivity`. When adding a new UI, it should be added as a `Fragment` or a `RecyclerView` item depending on the situation.
|
||||||
|
|
||||||
|
Databinding should *always* be used instead of `findViewById`. `by memberBinding` is used if the binding needs to be a member variable in order to avoid memory leaks.
|
||||||
|
|
||||||
|
Usually, fragment creation is done in `onCreateView`, and organized into three parts:
|
||||||
|
|
||||||
|
- Create variables [Bindings, Adapters, etc]
|
||||||
|
- Set up the UI
|
||||||
|
- Set up LiveData observers
|
||||||
|
|
||||||
|
When creating a ViewHolder for a `RecyclerView`, one should use `BaseHolder` to standardize the binding process and automate some code shared across all ViewHolders.
|
||||||
|
|
||||||
|
#### Binding Adapters
|
||||||
|
|
||||||
|
Data is often bound using Binding Adapters, which are XML attributes assigned in layout files that can automatically display data, usually written as `app:bindingAdapterName="@{data}"`. Its recommended to use these instead of duplicating code manually. These can be found in `.coil` and `.music`.
|
||||||
|
|
||||||
|
#### Playback system
|
||||||
|
|
||||||
|
Auxio's playback system is somewhat unorthodox, as it avoids a lot of the built-in android code in favor of a more understandable and controllable system. Its structured around a couple of objects, the connections being highlighted in this diagram.
|
||||||
|
|
||||||
|
```
|
||||||
|
Playback UI Queue UIs PlaybackService
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
PlaybackViewModel─────┘ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
PlaybackStateManager───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. If you want to add something to the playback system, this is likely where you should add it.
|
||||||
|
|
||||||
|
This object should ***NEVER*** be used in a UI, as it does not sanitize input and can cause major problems if a Volatile UI interacts with it. It's callback system is also prone to memory leaks if not cleared when done. `PlaybackViewModel` can be used instead, as it exposes stable data and abstracted functions that UI's can use to interact with the playback state.
|
||||||
|
|
||||||
|
`PlaybackService`'s job is to use the playback state to manage the ExoPlayer instance and also modify the state depending on system external events, such as when a button is pressed on a headset. It should **never** be bound to, mostly because there is no need given that `PlaybackViewModel` exposes the same data in a much safer fashion.
|
||||||
|
|
||||||
|
#### Using Music Data
|
||||||
|
|
||||||
|
All music objects inherit `BaseModel`, which guarantees that all music has both an ID and a name.
|
||||||
|
|
||||||
|
- Songs are the most basic element, with them having a reference to their album and genre.
|
||||||
|
- Albums contain a list of their songs and their parent artist.
|
||||||
|
- Artists contain a list of songs, a list of albums, and their most prominent genre.
|
||||||
|
- Genres contain a list of songs, its preferred to use `displayName` with genres as that will convert the any numbered names into non-numbered names.
|
||||||
|
|
||||||
|
`BaseModel` can be used as an argument type to specify that any music type, while `Parent` can be used as an argument type to only specify music objects that have child items, such as albums or artists.
|
||||||
|
|
||||||
|
#### Using Settings
|
||||||
|
|
||||||
|
Access to settings should preferably be done with `SettingsManager` as it can be accessed everywhere without a context.
|
||||||
|
|
||||||
|
#### Using Coil
|
||||||
|
|
||||||
|
[Coil](https://github.com/coil-kt/coil) is the image loader used by Auxio. All image loading is done through these four functions/binding adapters:
|
||||||
|
|
||||||
|
- `app:coverArt`: Binding Adapter that will load the cover art for a song or album
|
||||||
|
- `app:artistImage`: Binding Adapter that will load the artist image
|
||||||
|
- `app:genreImage`: Binding Adapter that will load the genre image
|
||||||
|
- `getBitmap`: Function that will take a song and return a bitmap, this should not be used in anything UI related, that is what the binding adapters above are for.
|
||||||
|
|
||||||
|
This should be enough to cover most use cases in Auxio.
|
||||||
|
|
||||||
|
#### Logging
|
||||||
|
|
||||||
|
Its recommended to use `logD` and `logE` for logging debug messages and errors. Both will automatically use the names of the objects that call it, and logging messages done with `logD` wont show in release builds.
|
Loading…
Reference in a new issue