all: switch to spotless
Switch to the spotless linter with ktfmt used as a backend instead of ktlint. This switch was done for two reasons: 1. ktfmt is more thorough than ktlint 2. License headers can be added more effectively with spotless than the default Android Studio behavior. Dump all of the changes now so I don't have to deal with it over a long period of time. I don't care.
This commit is contained in:
parent
627ab97948
commit
33da09a08a
99 changed files with 2829 additions and 3094 deletions
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev [v2.2.3, v2.3.0, or v3.0.0]
|
## dev [v2.2.3, v2.3.0, or v3.0.0]
|
||||||
|
|
||||||
|
#### Dev/Meta
|
||||||
|
- Switched to spotless and ktfmt instead of ktlint
|
||||||
|
|
||||||
## v2.2.2
|
## v2.2.2
|
||||||
#### What's New
|
#### What's New
|
||||||
- New spanish translations and metadata [courtesy of n-berenice]
|
- New spanish translations and metadata [courtesy of n-berenice]
|
||||||
|
|
|
||||||
17
app/NOTICE
Normal file
17
app/NOTICE
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) $today.year Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
@ -2,6 +2,7 @@ apply plugin: "com.android.application"
|
||||||
apply plugin: "kotlin-android"
|
apply plugin: "kotlin-android"
|
||||||
apply plugin: "kotlin-kapt"
|
apply plugin: "kotlin-kapt"
|
||||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||||
|
apply plugin: "com.diffplug.spotless"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 32
|
||||||
|
|
@ -45,12 +46,8 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
|
||||||
ktlint
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
preDebugBuild.dependsOn ktlintFormat
|
preDebugBuild.dependsOn spotlessApply
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -104,24 +101,13 @@ dependencies {
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
implementation "com.google.android.material:material:1.6.0-alpha03"
|
implementation "com.google.android.material:material:1.6.0-alpha03"
|
||||||
|
|
||||||
// --- DEBUG ---
|
|
||||||
|
|
||||||
// Lint
|
|
||||||
ktlint "com.pinterest:ktlint:0.44.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task ktlint(type: JavaExec, group: "verification") {
|
spotless {
|
||||||
description = "Check Kotlin code style."
|
kotlin {
|
||||||
mainClass.set("com.pinterest.ktlint.Main")
|
target "src/**/*.kt"
|
||||||
classpath = configurations.ktlint
|
|
||||||
args "src/**/*.kt"
|
|
||||||
}
|
|
||||||
check.dependsOn ktlint
|
|
||||||
|
|
||||||
task ktlintFormat(type: JavaExec, group: "formatting") {
|
ktfmt('0.30').dropboxStyle()
|
||||||
description = "Fix Kotlin code style deviations."
|
licenseHeaderFile("NOTICE")
|
||||||
mainClass.set("com.pinterest.ktlint.Main")
|
}
|
||||||
classpath = configurations.ktlint
|
|
||||||
args "-F", "src/**/*.kt"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AuxioApp.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
|
@ -31,10 +30,12 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Plan for a general UI rework
|
* TODO: Plan for a general UI rework
|
||||||
|
* ```
|
||||||
* - Refactor fragment class
|
* - Refactor fragment class
|
||||||
* - Remove databinding and dedup layouts
|
* - Remove databinding and dedup layouts
|
||||||
* - Rework RecyclerView management and item dragging
|
* - Rework RecyclerView management and item dragging
|
||||||
* - Rework sealed classes to minimize whens and maximize overrides
|
* - Rework sealed classes to minimize whens and maximize overrides
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MainActivity.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
@ -39,10 +38,8 @@ import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The single [AppCompatActivity] for Auxio.
|
* The single [AppCompatActivity] for Auxio. TODO: Add a new view for crashes with a stack trace
|
||||||
* TODO: Add a new view for crashes with a stack trace
|
* TODO: Custom language support TODO: Rework menus [perhaps add multi-select]
|
||||||
* TODO: Custom language support
|
|
||||||
* TODO: Rework menus [perhaps add multi-select]
|
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private val playbackModel: PlaybackViewModel by viewModels()
|
private val playbackModel: PlaybackViewModel by viewModels()
|
||||||
|
|
@ -52,9 +49,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
setupTheme()
|
setupTheme()
|
||||||
|
|
||||||
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
|
val binding =
|
||||||
this, R.layout.activity_main
|
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
|
||||||
)
|
|
||||||
|
|
||||||
applyEdgeToEdgeWindow(binding)
|
applyEdgeToEdgeWindow(binding)
|
||||||
|
|
||||||
|
|
@ -82,9 +78,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
if (action == Intent.ACTION_VIEW && !isConsumed) {
|
||||||
// Mark the intent as used so this does not fire again
|
// Mark the intent as used so this does not fire again
|
||||||
intent.putExtra(KEY_INTENT_USED, true)
|
intent.putExtra(KEY_INTENT_USED, true)
|
||||||
intent.data?.let { fileUri ->
|
intent.data?.let { fileUri -> playbackModel.playWithUri(fileUri, this) }
|
||||||
playbackModel.playWithUri(fileUri, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,12 +123,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
WindowInsets.Builder()
|
WindowInsets.Builder()
|
||||||
.setInsets(
|
.setInsets(
|
||||||
WindowInsets.Type.systemBars(),
|
WindowInsets.Type.systemBars(),
|
||||||
insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
|
insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()))
|
||||||
)
|
|
||||||
.setInsets(
|
.setInsets(
|
||||||
WindowInsets.Type.systemGestures(),
|
WindowInsets.Type.systemGestures(),
|
||||||
insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures())
|
insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures()))
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
.applyLeftRightInsets(binding)
|
.applyLeftRightInsets(binding)
|
||||||
}
|
}
|
||||||
|
|
@ -144,12 +136,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
systemUiVisibility =
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
|
||||||
setOnApplyWindowInsetsListener { _, insets ->
|
setOnApplyWindowInsetsListener { _, insets -> insets.applyLeftRightInsets(binding) }
|
||||||
insets.applyLeftRightInsets(binding)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -157,10 +147,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun WindowInsets.applyLeftRightInsets(binding: ViewBinding): WindowInsets {
|
private fun WindowInsets.applyLeftRightInsets(binding: ViewBinding): WindowInsets {
|
||||||
val bars = systemBarInsetsCompat
|
val bars = systemBarInsetsCompat
|
||||||
|
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(left = bars.left, right = bars.right)
|
||||||
left = bars.left,
|
|
||||||
right = bars.right
|
|
||||||
)
|
|
||||||
|
|
||||||
return replaceSystemBarInsetsCompat(0, bars.top, 0, bars.bottom)
|
return replaceSystemBarInsetsCompat(0, bars.top, 0, bars.bottom)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MainFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
|
@ -39,10 +38,10 @@ import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
* the more high-level navigation features.
|
* high-level navigation features.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt TODO: Add a new view with a stack trace whenever the music loading process
|
||||||
* TODO: Add a new view with a stack trace whenever the music loading process fails.
|
* fails.
|
||||||
*/
|
*/
|
||||||
class MainFragment : Fragment() {
|
class MainFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
@ -58,22 +57,18 @@ class MainFragment : Fragment() {
|
||||||
val binding = FragmentMainBinding.inflate(inflater)
|
val binding = FragmentMainBinding.inflate(inflater)
|
||||||
|
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
val permLauncher = registerForActivityResult(
|
val permLauncher =
|
||||||
ActivityResultContracts.RequestPermission()
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
) {
|
musicModel.reloadMusic(requireContext())
|
||||||
musicModel.reloadMusic(requireContext())
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
requireActivity()
|
||||||
viewLifecycleOwner,
|
.onBackPressedDispatcher
|
||||||
Callback(binding).also {
|
.addCallback(viewLifecycleOwner, Callback(binding).also { callback = it })
|
||||||
callback = it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
// Auxio's layout completely breaks down when it's window is resized too small,
|
// Auxio's layout completely breaks down when it's window is resized too small,
|
||||||
|
|
@ -110,15 +105,15 @@ class MainFragment : Fragment() {
|
||||||
is MusicStore.Response.Err -> {
|
is MusicStore.Response.Err -> {
|
||||||
logW("Received Error")
|
logW("Received Error")
|
||||||
|
|
||||||
val errorRes = when (response.kind) {
|
val errorRes =
|
||||||
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
when (response.kind) {
|
||||||
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
||||||
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
||||||
}
|
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
||||||
|
}
|
||||||
|
|
||||||
val snackbar = Snackbar.make(
|
val snackbar =
|
||||||
binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE
|
Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE)
|
||||||
)
|
|
||||||
|
|
||||||
when (response.kind) {
|
when (response.kind) {
|
||||||
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
||||||
|
|
@ -126,7 +121,6 @@ class MainFragment : Fragment() {
|
||||||
musicModel.reloadMusic(requireContext())
|
musicModel.reloadMusic(requireContext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicStore.ErrorKind.NO_PERMS -> {
|
MusicStore.ErrorKind.NO_PERMS -> {
|
||||||
snackbar.setAction(R.string.lbl_grant) {
|
snackbar.setAction(R.string.lbl_grant) {
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
|
@ -164,7 +158,8 @@ class MainFragment : Fragment() {
|
||||||
if (!binding.playbackLayout.collapse()) {
|
if (!binding.playbackLayout.collapse()) {
|
||||||
val navController = binding.exploreNavHost.findNavController()
|
val navController = binding.exploreNavHost.findNavController()
|
||||||
|
|
||||||
if (navController.currentDestination?.id == navController.graph.startDestinationId) {
|
if (navController.currentDestination?.id ==
|
||||||
|
navController.graph.startDestinationId) {
|
||||||
isEnabled = false
|
isEnabled = false
|
||||||
requireActivity().onBackPressed()
|
requireActivity().onBackPressed()
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Accent.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,103 +14,112 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.accent
|
package org.oxycblt.auxio.accent
|
||||||
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
val ACCENT_COUNT: Int get() = ACCENT_NAMES.size
|
val ACCENT_COUNT: Int
|
||||||
|
get() = ACCENT_NAMES.size
|
||||||
|
|
||||||
private val ACCENT_NAMES = arrayOf(
|
private val ACCENT_NAMES =
|
||||||
R.string.clr_red,
|
arrayOf(
|
||||||
R.string.clr_pink,
|
R.string.clr_red,
|
||||||
R.string.clr_purple,
|
R.string.clr_pink,
|
||||||
R.string.clr_deep_purple,
|
R.string.clr_purple,
|
||||||
R.string.clr_indigo,
|
R.string.clr_deep_purple,
|
||||||
R.string.clr_blue,
|
R.string.clr_indigo,
|
||||||
R.string.clr_deep_blue,
|
R.string.clr_blue,
|
||||||
R.string.clr_cyan,
|
R.string.clr_deep_blue,
|
||||||
R.string.clr_teal,
|
R.string.clr_cyan,
|
||||||
R.string.clr_green,
|
R.string.clr_teal,
|
||||||
R.string.clr_deep_green,
|
R.string.clr_green,
|
||||||
R.string.clr_lime,
|
R.string.clr_deep_green,
|
||||||
R.string.clr_yellow,
|
R.string.clr_lime,
|
||||||
R.string.clr_orange,
|
R.string.clr_yellow,
|
||||||
R.string.clr_brown,
|
R.string.clr_orange,
|
||||||
R.string.clr_grey,
|
R.string.clr_brown,
|
||||||
)
|
R.string.clr_grey,
|
||||||
|
)
|
||||||
|
|
||||||
private val ACCENT_THEMES = arrayOf(
|
private val ACCENT_THEMES =
|
||||||
R.style.Theme_Auxio_Red,
|
arrayOf(
|
||||||
R.style.Theme_Auxio_Pink,
|
R.style.Theme_Auxio_Red,
|
||||||
R.style.Theme_Auxio_Purple,
|
R.style.Theme_Auxio_Pink,
|
||||||
R.style.Theme_Auxio_DeepPurple,
|
R.style.Theme_Auxio_Purple,
|
||||||
R.style.Theme_Auxio_Indigo,
|
R.style.Theme_Auxio_DeepPurple,
|
||||||
R.style.Theme_Auxio_Blue,
|
R.style.Theme_Auxio_Indigo,
|
||||||
R.style.Theme_Auxio_DeepBlue,
|
R.style.Theme_Auxio_Blue,
|
||||||
R.style.Theme_Auxio_Cyan,
|
R.style.Theme_Auxio_DeepBlue,
|
||||||
R.style.Theme_Auxio_Teal,
|
R.style.Theme_Auxio_Cyan,
|
||||||
R.style.Theme_Auxio_Green,
|
R.style.Theme_Auxio_Teal,
|
||||||
R.style.Theme_Auxio_DeepGreen,
|
R.style.Theme_Auxio_Green,
|
||||||
R.style.Theme_Auxio_Lime,
|
R.style.Theme_Auxio_DeepGreen,
|
||||||
R.style.Theme_Auxio_Yellow,
|
R.style.Theme_Auxio_Lime,
|
||||||
R.style.Theme_Auxio_Orange,
|
R.style.Theme_Auxio_Yellow,
|
||||||
R.style.Theme_Auxio_Brown,
|
R.style.Theme_Auxio_Orange,
|
||||||
R.style.Theme_Auxio_Grey,
|
R.style.Theme_Auxio_Brown,
|
||||||
)
|
R.style.Theme_Auxio_Grey,
|
||||||
|
)
|
||||||
|
|
||||||
private val ACCENT_BLACK_THEMES = arrayOf(
|
private val ACCENT_BLACK_THEMES =
|
||||||
R.style.Theme_Auxio_Black_Red,
|
arrayOf(
|
||||||
R.style.Theme_Auxio_Black_Pink,
|
R.style.Theme_Auxio_Black_Red,
|
||||||
R.style.Theme_Auxio_Black_Purple,
|
R.style.Theme_Auxio_Black_Pink,
|
||||||
R.style.Theme_Auxio_Black_DeepPurple,
|
R.style.Theme_Auxio_Black_Purple,
|
||||||
R.style.Theme_Auxio_Black_Indigo,
|
R.style.Theme_Auxio_Black_DeepPurple,
|
||||||
R.style.Theme_Auxio_Black_Blue,
|
R.style.Theme_Auxio_Black_Indigo,
|
||||||
R.style.Theme_Auxio_Black_DeepBlue,
|
R.style.Theme_Auxio_Black_Blue,
|
||||||
R.style.Theme_Auxio_Black_Cyan,
|
R.style.Theme_Auxio_Black_DeepBlue,
|
||||||
R.style.Theme_Auxio_Black_Teal,
|
R.style.Theme_Auxio_Black_Cyan,
|
||||||
R.style.Theme_Auxio_Black_Green,
|
R.style.Theme_Auxio_Black_Teal,
|
||||||
R.style.Theme_Auxio_Black_DeepGreen,
|
R.style.Theme_Auxio_Black_Green,
|
||||||
R.style.Theme_Auxio_Black_Lime,
|
R.style.Theme_Auxio_Black_DeepGreen,
|
||||||
R.style.Theme_Auxio_Black_Yellow,
|
R.style.Theme_Auxio_Black_Lime,
|
||||||
R.style.Theme_Auxio_Black_Orange,
|
R.style.Theme_Auxio_Black_Yellow,
|
||||||
R.style.Theme_Auxio_Black_Brown,
|
R.style.Theme_Auxio_Black_Orange,
|
||||||
R.style.Theme_Auxio_Black_Grey,
|
R.style.Theme_Auxio_Black_Brown,
|
||||||
)
|
R.style.Theme_Auxio_Black_Grey,
|
||||||
|
)
|
||||||
|
|
||||||
private val ACCENT_PRIMARY_COLORS = arrayOf(
|
private val ACCENT_PRIMARY_COLORS =
|
||||||
R.color.red_primary,
|
arrayOf(
|
||||||
R.color.pink_primary,
|
R.color.red_primary,
|
||||||
R.color.purple_primary,
|
R.color.pink_primary,
|
||||||
R.color.deep_purple_primary,
|
R.color.purple_primary,
|
||||||
R.color.indigo_primary,
|
R.color.deep_purple_primary,
|
||||||
R.color.blue_primary,
|
R.color.indigo_primary,
|
||||||
R.color.deep_blue_primary,
|
R.color.blue_primary,
|
||||||
R.color.cyan_primary,
|
R.color.deep_blue_primary,
|
||||||
R.color.teal_primary,
|
R.color.cyan_primary,
|
||||||
R.color.green_primary,
|
R.color.teal_primary,
|
||||||
R.color.deep_green_primary,
|
R.color.green_primary,
|
||||||
R.color.lime_primary,
|
R.color.deep_green_primary,
|
||||||
R.color.yellow_primary,
|
R.color.lime_primary,
|
||||||
R.color.orange_primary,
|
R.color.yellow_primary,
|
||||||
R.color.brown_primary,
|
R.color.orange_primary,
|
||||||
R.color.grey_primary,
|
R.color.brown_primary,
|
||||||
)
|
R.color.grey_primary,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data object for an accent. In the UI this is known as a "Color Scheme."
|
* The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally
|
||||||
* This can be nominally used to gleam some attributes about a given color scheme, but this
|
* used to gleam some attributes about a given color scheme, but this is not recommended. Attributes
|
||||||
* is not recommended. Attributes are the better option in nearly all cases.
|
* are the better option in nearly all cases.
|
||||||
*
|
*
|
||||||
* @property name The name of this accent
|
* @property name The name of this accent
|
||||||
* @property theme The theme resource for this accent
|
* @property theme The theme resource for this accent
|
||||||
* @property blackTheme The black theme resource for this accent
|
* @property blackTheme The black theme resource for this accent
|
||||||
* @property primary The primary color resource for this accent
|
* @property primary The primary color resource for this accent
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
data class Accent(val index: Int) {
|
data class Accent(val index: Int) {
|
||||||
val name: Int get() = ACCENT_NAMES[index]
|
val name: Int
|
||||||
val theme: Int get() = ACCENT_THEMES[index]
|
get() = ACCENT_NAMES[index]
|
||||||
val blackTheme: Int get() = ACCENT_BLACK_THEMES[index]
|
val theme: Int
|
||||||
val primary: Int get() = ACCENT_PRIMARY_COLORS[index]
|
get() = ACCENT_THEMES[index]
|
||||||
|
val blackTheme: Int
|
||||||
|
get() = ACCENT_BLACK_THEMES[index]
|
||||||
|
val primary: Int
|
||||||
|
get() = ACCENT_PRIMARY_COLORS[index]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AccentAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.accent
|
package org.oxycblt.auxio.accent
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
|
@ -33,10 +32,8 @@ import org.oxycblt.auxio.util.stateList
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
* @param onSelect What to do when an accent is selected.
|
* @param onSelect What to do when an accent is selected.
|
||||||
*/
|
*/
|
||||||
class AccentAdapter(
|
class AccentAdapter(private var curAccent: Accent, private val onSelect: (accent: Accent) -> Unit) :
|
||||||
private var curAccent: Accent,
|
RecyclerView.Adapter<AccentAdapter.ViewHolder>() {
|
||||||
private val onSelect: (accent: Accent) -> Unit
|
|
||||||
) : RecyclerView.Adapter<AccentAdapter.ViewHolder>() {
|
|
||||||
private var selectedViewHolder: ViewHolder? = null
|
private var selectedViewHolder: ViewHolder? = null
|
||||||
|
|
||||||
override fun getItemCount(): Int = ACCENT_COUNT
|
override fun getItemCount(): Int = ACCENT_COUNT
|
||||||
|
|
@ -54,9 +51,8 @@ class AccentAdapter(
|
||||||
onSelect(accent)
|
onSelect(accent)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(
|
inner class ViewHolder(private val binding: ItemAccentBinding) :
|
||||||
private val binding: ItemAccentBinding
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(accent: Accent) {
|
fun bind(accent: Accent) {
|
||||||
setSelected(accent == curAccent)
|
setSelected(accent == curAccent)
|
||||||
|
|
@ -77,14 +73,15 @@ class AccentAdapter(
|
||||||
val context = binding.accent.context
|
val context = binding.accent.context
|
||||||
|
|
||||||
binding.accent.isEnabled = !isSelected
|
binding.accent.isEnabled = !isSelected
|
||||||
binding.accent.imageTintList = if (isSelected) {
|
binding.accent.imageTintList =
|
||||||
// Switch out the currently selected ViewHolder with this one.
|
if (isSelected) {
|
||||||
selectedViewHolder?.setSelected(false)
|
// Switch out the currently selected ViewHolder with this one.
|
||||||
selectedViewHolder = this
|
selectedViewHolder?.setSelected(false)
|
||||||
context.getAttrColorSafe(R.attr.colorSurface).stateList
|
selectedViewHolder = this
|
||||||
} else {
|
context.getAttrColorSafe(R.attr.colorSurface).stateList
|
||||||
context.getColorSafe(android.R.color.transparent).stateList
|
} else {
|
||||||
}
|
context.getColorSafe(android.R.color.transparent).stateList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AccentDialog.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.accent
|
package org.oxycblt.auxio.accent
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -52,10 +51,11 @@ class AccentCustomizeDialog : LifecycleDialog() {
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.accentRecycler.apply {
|
binding.accentRecycler.apply {
|
||||||
adapter = AccentAdapter(pendingAccent) { accent ->
|
adapter =
|
||||||
logD("Switching selected accent to $accent")
|
AccentAdapter(pendingAccent) { accent ->
|
||||||
pendingAccent = accent
|
logD("Switching selected accent to $accent")
|
||||||
}
|
pendingAccent = accent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Dialog created")
|
logD("Dialog created")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AutoGridLayoutManager.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,20 +14,20 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.accent
|
package org.oxycblt.auxio.accent
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.util.pxOfDp
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import org.oxycblt.auxio.util.pxOfDp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width
|
* A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width
|
||||||
* of the RecyclerView.
|
* of the RecyclerView. Adapted from this StackOverflow answer:
|
||||||
* Adapted from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986
|
* https://stackoverflow.com/a/30256880/14143986
|
||||||
*/
|
*/
|
||||||
class AccentGridLayoutManager(
|
class AccentGridLayoutManager(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -5,6 +22,7 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.util.Size as AndroidSize
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
|
|
@ -20,6 +38,8 @@ import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
||||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
import com.google.android.exoplayer2.metadata.id3.ApicFrame
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
|
@ -28,22 +48,17 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import android.util.Size as AndroidSize
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base implementation for all image fetchers in Auxio.
|
* The base implementation for all image fetchers in Auxio.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt TODO: Artist images
|
||||||
* TODO: Artist images
|
|
||||||
*/
|
*/
|
||||||
abstract class BaseFetcher : Fetcher {
|
abstract class BaseFetcher : Fetcher {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the artwork of an [album].
|
* Fetch the artwork of an [album]. This call respects user configuration and has proper
|
||||||
* This call respects user configuration and has proper redundancy in the case that
|
* redundancy in the case that an API fails to load.
|
||||||
* an API fails to load.
|
|
||||||
*/
|
*/
|
||||||
protected suspend fun fetchArt(context: Context, album: Album): InputStream? {
|
protected suspend fun fetchArt(context: Context, album: Album): InputStream? {
|
||||||
if (!settingsManager.showCovers) {
|
if (!settingsManager.showCovers) {
|
||||||
|
|
@ -67,9 +82,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
val uri = data.albumCoverUri
|
val uri = data.albumCoverUri
|
||||||
|
|
||||||
// Eliminate any chance that this blocking call might mess up the cancellation process
|
// Eliminate any chance that this blocking call might mess up the cancellation process
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
|
||||||
context.contentResolver.openInputStream(uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
||||||
|
|
@ -115,17 +128,13 @@ abstract class BaseFetcher : Fetcher {
|
||||||
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
// Get the embedded picture from MediaMetadataRetriever, which will return a full
|
||||||
// ByteArray of the cover without any compression artifacts.
|
// ByteArray of the cover without any compression artifacts.
|
||||||
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
// If its null [i.e there is no embedded cover], than just ignore it and move on
|
||||||
return ext.embeddedPicture?.let { coverBytes ->
|
return ext.embeddedPicture?.let { coverBytes -> ByteArrayInputStream(coverBytes) }
|
||||||
ByteArrayInputStream(coverBytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? {
|
private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? {
|
||||||
val uri = album.songs[0].uri
|
val uri = album.songs[0].uri
|
||||||
val future = MetadataRetriever.retrieveMetadata(
|
val future = MetadataRetriever.retrieveMetadata(context, MediaItem.fromUri(uri))
|
||||||
context, MediaItem.fromUri(uri)
|
|
||||||
)
|
|
||||||
|
|
||||||
// future.get is a blocking call that makes us spin until the future is done.
|
// future.get is a blocking call that makes us spin until the future is done.
|
||||||
// This is bad for a co-routine, as it prevents cancellation and by extension
|
// This is bad for a co-routine, as it prevents cancellation and by extension
|
||||||
|
|
@ -133,13 +142,14 @@ abstract class BaseFetcher : Fetcher {
|
||||||
// To fix this we wrap this around in a withContext call to make it suspend and make
|
// To fix this we wrap this around in a withContext call to make it suspend and make
|
||||||
// sure that the runner can do other coroutines.
|
// sure that the runner can do other coroutines.
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
val tracks = withContext(Dispatchers.IO) {
|
val tracks =
|
||||||
try {
|
withContext(Dispatchers.IO) {
|
||||||
future.get()
|
try {
|
||||||
} catch (e: Exception) {
|
future.get()
|
||||||
null
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (tracks == null || tracks.isEmpty) {
|
if (tracks == null || tracks.isEmpty) {
|
||||||
// Unrecognized format. This is expected, as ExoPlayer only supports a
|
// Unrecognized format. This is expected, as ExoPlayer only supports a
|
||||||
|
|
@ -201,14 +211,17 @@ abstract class BaseFetcher : Fetcher {
|
||||||
* Create a mosaic image from multiple streams of image data, Code adapted from Phonograph
|
* Create a mosaic image from multiple streams of image data, Code adapted from Phonograph
|
||||||
* https://github.com/kabouzeid/Phonograph
|
* https://github.com/kabouzeid/Phonograph
|
||||||
*/
|
*/
|
||||||
protected suspend fun createMosaic(context: Context, streams: List<InputStream>, size: Size): FetchResult? {
|
protected suspend fun createMosaic(
|
||||||
|
context: Context,
|
||||||
|
streams: List<InputStream>,
|
||||||
|
size: Size
|
||||||
|
): FetchResult? {
|
||||||
if (streams.size < 4) {
|
if (streams.size < 4) {
|
||||||
return streams.firstOrNull()?.let { stream ->
|
return streams.firstOrNull()?.let { stream ->
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
source = ImageSource(stream.source().buffer(), context),
|
source = ImageSource(stream.source().buffer(), context),
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,15 +229,11 @@ abstract class BaseFetcher : Fetcher {
|
||||||
// get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a
|
// get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a
|
||||||
// 512x512 mosaic.
|
// 512x512 mosaic.
|
||||||
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize())
|
||||||
val mosaicFrameSize = Size(
|
val mosaicFrameSize =
|
||||||
Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)
|
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
|
||||||
)
|
|
||||||
|
|
||||||
val mosaicBitmap = Bitmap.createBitmap(
|
val mosaicBitmap =
|
||||||
mosaicSize.width,
|
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888)
|
||||||
mosaicSize.height,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
val canvas = Canvas(mosaicBitmap)
|
val canvas = Canvas(mosaicBitmap)
|
||||||
|
|
||||||
var x = 0
|
var x = 0
|
||||||
|
|
@ -239,11 +248,9 @@ abstract class BaseFetcher : Fetcher {
|
||||||
|
|
||||||
// Run the bitmap through a transform to make sure it's a square of the desired
|
// Run the bitmap through a transform to make sure it's a square of the desired
|
||||||
// resolution.
|
// resolution.
|
||||||
val bitmap = SquareFrameTransform.INSTANCE
|
val bitmap =
|
||||||
.transform(
|
SquareFrameTransform.INSTANCE.transform(
|
||||||
BitmapFactory.decodeStream(stream),
|
BitmapFactory.decodeStream(stream), mosaicFrameSize)
|
||||||
mosaicFrameSize
|
|
||||||
)
|
|
||||||
|
|
||||||
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
|
||||||
|
|
||||||
|
|
@ -261,8 +268,7 @@ abstract class BaseFetcher : Fetcher {
|
||||||
return DrawableResult(
|
return DrawableResult(
|
||||||
drawable = mosaicBitmap.toDrawable(context.resources),
|
drawable = mosaicBitmap.toDrawable(context.resources),
|
||||||
isSampled = true,
|
isSampled = true,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Dimension.mosaicSize(): Int {
|
private fun Dimension.mosaicSize(): Int {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* CoilUtils.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -38,27 +37,19 @@ import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
// --- BINDING ADAPTERS ---
|
// --- BINDING ADAPTERS ---
|
||||||
|
|
||||||
/**
|
/** Bind the album art for a [song]. */
|
||||||
* Bind the album art for a [song].
|
|
||||||
*/
|
|
||||||
@BindingAdapter("albumArt")
|
@BindingAdapter("albumArt")
|
||||||
fun ImageView.bindAlbumArt(song: Song?) = load(song, R.drawable.ic_album)
|
fun ImageView.bindAlbumArt(song: Song?) = load(song, R.drawable.ic_album)
|
||||||
|
|
||||||
/**
|
/** Bind the album art for an [album]. */
|
||||||
* Bind the album art for an [album].
|
|
||||||
*/
|
|
||||||
@BindingAdapter("albumArt")
|
@BindingAdapter("albumArt")
|
||||||
fun ImageView.bindAlbumArt(album: Album?) = load(album, R.drawable.ic_album)
|
fun ImageView.bindAlbumArt(album: Album?) = load(album, R.drawable.ic_album)
|
||||||
|
|
||||||
/**
|
/** Bind the image for an [artist] */
|
||||||
* Bind the image for an [artist]
|
|
||||||
*/
|
|
||||||
@BindingAdapter("artistImage")
|
@BindingAdapter("artistImage")
|
||||||
fun ImageView.bindArtistImage(artist: Artist?) = load(artist, R.drawable.ic_artist)
|
fun ImageView.bindArtistImage(artist: Artist?) = load(artist, R.drawable.ic_artist)
|
||||||
|
|
||||||
/**
|
/** Bind the image for a [genre] */
|
||||||
* Bind the image for a [genre]
|
|
||||||
*/
|
|
||||||
@BindingAdapter("genreImage")
|
@BindingAdapter("genreImage")
|
||||||
fun ImageView.bindGenreImage(genre: Genre?) = load(genre, R.drawable.ic_genre)
|
fun ImageView.bindGenreImage(genre: Genre?) = load(genre, R.drawable.ic_genre)
|
||||||
|
|
||||||
|
|
@ -74,23 +65,14 @@ fun <T : Music> ImageView.load(music: T?, @DrawableRes error: Int) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a bitmap for a [song]. [onDone] will be called with the loaded bitmap, or null if loading
|
* Get a bitmap for a [song]. [onDone] will be called with the loaded bitmap, or null if loading
|
||||||
* failed/shouldn't occur.
|
* failed/shouldn't occur. **This not meant for UIs, instead use the Binding Adapters.**
|
||||||
* **This not meant for UIs, instead use the Binding Adapters.**
|
|
||||||
*/
|
*/
|
||||||
fun loadBitmap(
|
fun loadBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||||
context: Context,
|
|
||||||
song: Song,
|
|
||||||
onDone: (Bitmap?) -> Unit
|
|
||||||
) {
|
|
||||||
context.imageLoader.enqueue(
|
context.imageLoader.enqueue(
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(song.album)
|
.data(song.album)
|
||||||
.size(Size.ORIGINAL)
|
.size(Size.ORIGINAL)
|
||||||
.transformations(SquareFrameTransform())
|
.transformations(SquareFrameTransform())
|
||||||
.target(
|
.target(onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) })
|
||||||
onError = { onDone(null) },
|
.build())
|
||||||
onSuccess = { onDone(it.toBitmap()) }
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
|
|
@ -9,8 +26,8 @@ import coil.transition.Transition
|
||||||
import coil.transition.TransitionTarget
|
import coil.transition.TransitionTarget
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A copy of [CrossfadeTransition.Factory] that applies a transition to error results.
|
* A copy of [CrossfadeTransition.Factory] that applies a transition to error results. You know.
|
||||||
* You know. Like they used to.
|
* Like they used to.
|
||||||
* @author Coil Team
|
* @author Coil Team
|
||||||
*/
|
*/
|
||||||
class CrossfadeFactory : Transition.Factory {
|
class CrossfadeFactory : Transition.Factory {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Fetchers.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -27,6 +26,7 @@ import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
|
import kotlin.math.min
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
|
@ -34,23 +34,19 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetcher that returns the album art for a given [Album] or [Song], depending on the factory used.
|
* Fetcher that returns the album art for a given [Album] or [Song], depending on the factory used.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumArtFetcher private constructor(
|
class AlbumArtFetcher private constructor(private val context: Context, private val album: Album) :
|
||||||
private val context: Context,
|
BaseFetcher() {
|
||||||
private val album: Album
|
|
||||||
) : BaseFetcher() {
|
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
return fetchArt(context, album)?.let { stream ->
|
return fetchArt(context, album)?.let { stream ->
|
||||||
SourceResult(
|
SourceResult(
|
||||||
source = ImageSource(stream.source().buffer(), context),
|
source = ImageSource(stream.source().buffer(), context),
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,17 +67,15 @@ class AlbumArtFetcher private constructor(
|
||||||
* Fetcher that fetches the image for an [Artist]
|
* Fetcher that fetches the image for an [Artist]
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistImageFetcher private constructor(
|
class ArtistImageFetcher
|
||||||
|
private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
private val artist: Artist,
|
private val artist: Artist,
|
||||||
) : BaseFetcher() {
|
) : BaseFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val albums = Sort.ByName(true)
|
val albums = Sort.ByName(true).sortAlbums(artist.albums)
|
||||||
.sortAlbums(artist.albums)
|
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||||
val results = albums.mapAtMost(4) { album ->
|
|
||||||
fetchArt(context, album)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createMosaic(context, results, size)
|
return createMosaic(context, results, size)
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +91,8 @@ class ArtistImageFetcher private constructor(
|
||||||
* Fetcher that fetches the image for a [Genre]
|
* Fetcher that fetches the image for a [Genre]
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreImageFetcher private constructor(
|
class GenreImageFetcher
|
||||||
|
private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
private val genre: Genre,
|
private val genre: Genre,
|
||||||
|
|
@ -105,9 +100,7 @@ class GenreImageFetcher private constructor(
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
// We don't need to sort here, as the way we
|
// We don't need to sort here, as the way we
|
||||||
val albums = genre.songs.groupBy { it.album }.keys
|
val albums = genre.songs.groupBy { it.album }.keys
|
||||||
val results = albums.mapAtMost(4) { album ->
|
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||||
fetchArt(context, album)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createMosaic(context, results, size)
|
return createMosaic(context, results, size)
|
||||||
}
|
}
|
||||||
|
|
@ -120,10 +113,13 @@ class GenreImageFetcher private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map at most [n] items from a collection. [transform] is called for each item that is eligible.
|
* Map at most [n] items from a collection. [transform] is called for each item that is eligible. If
|
||||||
* If null is returned, then that item will be skipped.
|
* null is returned, then that item will be skipped.
|
||||||
*/
|
*/
|
||||||
private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(n: Int, transform: (T) -> R?): List<R> {
|
private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(
|
||||||
|
n: Int,
|
||||||
|
transform: (T) -> R?
|
||||||
|
): List<R> {
|
||||||
val until = min(size, n)
|
val until = min(size, n)
|
||||||
val out = mutableListOf<R>()
|
val out = mutableListOf<R>()
|
||||||
|
|
||||||
|
|
@ -132,9 +128,7 @@ private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(n: Int, transform:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
transform(item)?.let {
|
transform(item)?.let { out.add(it) }
|
||||||
out.add(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import coil.key.Keyer
|
import coil.key.Keyer
|
||||||
|
|
@ -5,9 +22,7 @@ import coil.request.Options
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/**
|
/** A basic keyer for music data. */
|
||||||
* A basic keyer for music data.
|
|
||||||
*/
|
|
||||||
class MusicKeyer : Keyer<Music> {
|
class MusicKeyer : Keyer<Music> {
|
||||||
override fun key(data: Music, options: Options): String {
|
override fun key(data: Music, options: Options): String {
|
||||||
return if (data is Song) {
|
return if (data is Song) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -11,24 +28,24 @@ import org.oxycblt.auxio.util.getColorSafe
|
||||||
import org.oxycblt.auxio.util.stateList
|
import org.oxycblt.auxio.util.stateList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AppCompatImageView] that applies the specified cornerRadius attribute if the user
|
* An [AppCompatImageView] that applies the specified cornerRadius attribute if the user has enabled
|
||||||
* has enabled the "Round album covers" option. We don't round album covers by default as
|
* the "Round album covers" option. We don't round album covers by default as it desecrates album
|
||||||
* it desecrates album artwork, but if the user desires it we do have an option to enable it.
|
* artwork, but if the user desires it we do have an option to enable it.
|
||||||
*/
|
*/
|
||||||
class RoundableImageView @JvmOverloads constructor(
|
class RoundableImageView
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
AppCompatImageView(context, attrs, defStyleAttr) {
|
||||||
) : AppCompatImageView(context, attrs, defStyleAttr) {
|
|
||||||
init {
|
init {
|
||||||
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.RoundableImageView)
|
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.RoundableImageView)
|
||||||
val cornerRadius = styledAttrs.getDimension(R.styleable.RoundableImageView_cornerRadius, 0f)
|
val cornerRadius = styledAttrs.getDimension(R.styleable.RoundableImageView_cornerRadius, 0f)
|
||||||
styledAttrs.recycle()
|
styledAttrs.recycle()
|
||||||
|
|
||||||
background = MaterialShapeDrawable().apply {
|
background =
|
||||||
setCornerSize(cornerRadius)
|
MaterialShapeDrawable().apply {
|
||||||
fillColor = context.getColorSafe(android.R.color.transparent).stateList
|
setCornerSize(cornerRadius)
|
||||||
}
|
fillColor = context.getColorSafe(android.R.color.transparent).stateList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.coil
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
|
@ -7,8 +24,8 @@ import coil.transform.Transformation
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transformation that performs a center crop-style transformation on an image, however unlike
|
* A transformation that performs a center crop-style transformation on an image, however unlike the
|
||||||
* the actual ScaleType, this isn't affected by any hacks we do with ImageView itself.
|
* actual ScaleType, this isn't affected by any hacks we do with ImageView itself.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SquareFrameTransform : Transformation {
|
class SquareFrameTransform : Transformation {
|
||||||
|
|
@ -29,12 +46,7 @@ class SquareFrameTransform : Transformation {
|
||||||
|
|
||||||
if (dstSize != wantedWidth || dstSize != wantedHeight) {
|
if (dstSize != wantedWidth || dstSize != wantedHeight) {
|
||||||
// Desired size differs from the cropped size, resize the bitmap.
|
// Desired size differs from the cropped size, resize the bitmap.
|
||||||
return Bitmap.createScaledBitmap(
|
return Bitmap.createScaledBitmap(dst, wantedWidth, wantedHeight, true)
|
||||||
dst,
|
|
||||||
wantedWidth,
|
|
||||||
wantedHeight,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AlbumDetailFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -58,11 +57,12 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
detailModel.setAlbum(args.albumId)
|
detailModel.setAlbum(args.albumId)
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
||||||
val detailAdapter = AlbumDetailAdapter(
|
val detailAdapter =
|
||||||
playbackModel, detailModel,
|
AlbumDetailAdapter(
|
||||||
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
playbackModel,
|
||||||
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) }
|
detailModel,
|
||||||
)
|
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
|
||||||
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) })
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
|
@ -75,13 +75,11 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
playbackModel.addToQueue(detailModel.curAlbum.value!!)
|
playbackModel.addToQueue(detailModel.curAlbum.value!!)
|
||||||
requireContext().showToast(R.string.lbl_queue_added)
|
requireContext().showToast(R.string.lbl_queue_added)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,15 +93,11 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
|
|
||||||
// -- DETAILVIEWMODEL SETUP ---
|
// -- DETAILVIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.albumData.observe(viewLifecycleOwner) { data ->
|
detailModel.albumData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) }
|
||||||
detailAdapter.submitList(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
showMenu(config) { id ->
|
showMenu(config) { id -> id == R.id.option_sort_asc }
|
||||||
id == R.id.option_sort_asc
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,9 +112,8 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
detailModel.finishNavToItem()
|
detailModel.finishNavToItem()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
AlbumDetailFragmentDirections.actionShowAlbum(item.album.id)
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,20 +126,17 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
detailModel.finishNavToItem()
|
detailModel.finishNavToItem()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
AlbumDetailFragmentDirections.actionShowAlbum(item.id)
|
.navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always launch a new ArtistDetailFragment.
|
// Always launch a new ArtistDetailFragment.
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
AlbumDetailFragmentDirections.actionShowArtist(item.id)
|
.navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> {}
|
null -> {}
|
||||||
else -> logW("Unsupported navigation item ${item::class.java}")
|
else -> logW("Unsupported navigation item ${item::class.java}")
|
||||||
}
|
}
|
||||||
|
|
@ -158,8 +148,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
updateQueueActions(song, binding)
|
updateQueueActions(song, binding)
|
||||||
|
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
|
||||||
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id
|
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id) {
|
||||||
) {
|
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
|
@ -172,9 +161,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Updates the queue actions when */
|
||||||
* Updates the queue actions when
|
|
||||||
*/
|
|
||||||
private fun updateQueueActions(song: Song?, binding: FragmentDetailBinding) {
|
private fun updateQueueActions(song: Song?, binding: FragmentDetailBinding) {
|
||||||
for (item in binding.detailToolbar.menu.children) {
|
for (item in binding.detailToolbar.menu.children) {
|
||||||
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) {
|
||||||
|
|
@ -183,9 +170,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Scroll to an song using its [id]. */
|
||||||
* Scroll to an song using its [id].
|
|
||||||
*/
|
|
||||||
private fun scrollToItem(
|
private fun scrollToItem(
|
||||||
id: Long,
|
id: Long,
|
||||||
binding: FragmentDetailBinding,
|
binding: FragmentDetailBinding,
|
||||||
|
|
@ -198,8 +183,7 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
binding.detailRecycler.post {
|
binding.detailRecycler.post {
|
||||||
// Make sure to increment the position to make up for the detail header
|
// Make sure to increment the position to make up for the detail header
|
||||||
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
binding.detailRecycler.layoutManager?.startSmoothScroll(
|
||||||
CenterSmoothScroller(requireContext(), pos)
|
CenterSmoothScroller(requireContext(), pos))
|
||||||
)
|
|
||||||
|
|
||||||
// If the recyclerview can scroll, its certain that it will have to scroll to
|
// If the recyclerview can scroll, its certain that it will have to scroll to
|
||||||
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
// correctly center the playing item, so make sure that the Toolbar is lifted in
|
||||||
|
|
@ -210,13 +194,11 @@ class AlbumDetailFragment : DetailFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [LinearSmoothScroller] subclass that centers the item on the screen instead of
|
* [LinearSmoothScroller] subclass that centers the item on the screen instead of snapping to
|
||||||
* snapping to the top or bottom.
|
* the top or bottom.
|
||||||
*/
|
*/
|
||||||
private class CenterSmoothScroller(
|
private class CenterSmoothScroller(context: Context, target: Int) :
|
||||||
context: Context,
|
LinearSmoothScroller(context) {
|
||||||
target: Int
|
|
||||||
) : LinearSmoothScroller(context) {
|
|
||||||
init {
|
init {
|
||||||
targetPosition = target
|
targetPosition = target
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* ArtistDetailFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -53,24 +52,19 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
detailModel.setArtist(args.artistId)
|
detailModel.setArtist(args.artistId)
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
val binding = FragmentDetailBinding.inflate(layoutInflater)
|
||||||
val detailAdapter = ArtistDetailAdapter(
|
val detailAdapter =
|
||||||
playbackModel,
|
ArtistDetailAdapter(
|
||||||
doOnClick = { data ->
|
playbackModel,
|
||||||
if (!detailModel.isNavigating) {
|
doOnClick = { data ->
|
||||||
detailModel.setNavigating(true)
|
if (!detailModel.isNavigating) {
|
||||||
|
detailModel.setNavigating(true)
|
||||||
|
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
ArtistDetailFragmentDirections.actionShowAlbum(data.id)
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(data.id))
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
},
|
doOnSongClick = { data -> playbackModel.playSong(data, PlaybackMode.IN_ARTIST) },
|
||||||
doOnSongClick = { data ->
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) })
|
||||||
playbackModel.playSong(data, PlaybackMode.IN_ARTIST)
|
|
||||||
},
|
|
||||||
doOnLongClick = { view, data ->
|
|
||||||
newMenu(view, data, ActionMenu.FLAG_IN_ARTIST)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
|
@ -91,9 +85,7 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
|
|
||||||
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
detailModel.showMenu.observe(viewLifecycleOwner) { config ->
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
showMenu(config) { id ->
|
showMenu(config) { id -> id != R.id.option_sort_artist }
|
||||||
id != R.id.option_sort_artist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,26 +98,20 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
detailModel.finishNavToItem()
|
detailModel.finishNavToItem()
|
||||||
} else {
|
} else {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
ArtistDetailFragmentDirections.actionShowArtist(item.id)
|
.navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
ArtistDetailFragmentDirections.actionShowAlbum(item.id)
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is Song -> {
|
is Song -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)
|
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> {}
|
null -> {}
|
||||||
else -> logW("Unsupported navigation item ${item::class.java}")
|
else -> logW("Unsupported navigation item ${item::class.java}")
|
||||||
}
|
}
|
||||||
|
|
@ -143,8 +129,7 @@ class ArtistDetailFragment : DetailFragment() {
|
||||||
// Highlight songs if they are being played
|
// Highlight songs if they are being played
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
|
||||||
playbackModel.parent.value?.id == detailModel.curArtist.value?.id
|
playbackModel.parent.value?.id == detailModel.curArtist.value?.id) {
|
||||||
) {
|
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
|
|
@ -12,24 +29,23 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import java.lang.Exception
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.EdgeAppBarLayout
|
import org.oxycblt.auxio.ui.EdgeAppBarLayout
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail
|
* An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail
|
||||||
* recyclerview is scrolled beyond it's first item (a.k.a the header). This is used instead of
|
* recyclerview is scrolled beyond it's first item (a.k.a the header). This is used instead of
|
||||||
* CollapsingToolbarLayout since that thing is a mess with crippling bugs and state issues.
|
* CollapsingToolbarLayout since that thing is a mess with crippling bugs and state issues. This
|
||||||
* This just works.
|
* just works.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailAppBarLayout @JvmOverloads constructor(
|
class DetailAppBarLayout
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
EdgeAppBarLayout(context, attrs, defStyleAttr) {
|
||||||
) : EdgeAppBarLayout(context, attrs, defStyleAttr) {
|
|
||||||
private var mTitleView: AppCompatTextView? = null
|
private var mTitleView: AppCompatTextView? = null
|
||||||
private var mRecycler: RecyclerView? = null
|
private var mRecycler: RecyclerView? = null
|
||||||
|
|
||||||
|
|
@ -50,16 +66,17 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
||||||
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
|
||||||
|
|
||||||
// Reflect to get the actual title view to do transformations on
|
// Reflect to get the actual title view to do transformations on
|
||||||
val newTitleView = try {
|
val newTitleView =
|
||||||
Toolbar::class.java.getDeclaredField("mTitleTextView").run {
|
try {
|
||||||
isAccessible = true
|
Toolbar::class.java.getDeclaredField("mTitleTextView").run {
|
||||||
get(toolbar) as AppCompatTextView
|
isAccessible = true
|
||||||
|
get(toolbar) as AppCompatTextView
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Could not get toolbar title view (likely an internal code change)")
|
||||||
|
e.logTraceOrThrow()
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
logE("Could not get toolbar title view (likely an internal code change)")
|
|
||||||
e.logTraceOrThrow()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
newTitleView.alpha = 0f
|
newTitleView.alpha = 0f
|
||||||
mTitleView = newTitleView
|
mTitleView = newTitleView
|
||||||
|
|
@ -103,21 +120,21 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
||||||
|
|
||||||
if (titleView?.alpha == to) return
|
if (titleView?.alpha == to) return
|
||||||
|
|
||||||
mTitleAnimator = ValueAnimator.ofFloat(from, to).apply {
|
mTitleAnimator =
|
||||||
addUpdateListener {
|
ValueAnimator.ofFloat(from, to).apply {
|
||||||
titleView?.alpha = it.animatedValue as Float
|
addUpdateListener { titleView?.alpha = it.animatedValue as Float }
|
||||||
|
|
||||||
|
duration =
|
||||||
|
resources.getInteger(R.integer.detail_app_bar_title_anim_duration).toLong()
|
||||||
|
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
duration = resources.getInteger(R.integer.detail_app_bar_title_anim_duration).toLong()
|
|
||||||
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Behavior @JvmOverloads constructor(
|
class Behavior
|
||||||
context: Context? = null,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null
|
constructor(context: Context? = null, attrs: AttributeSet? = null) :
|
||||||
) : AppBarLayout.Behavior(context, attrs) {
|
AppBarLayout.Behavior(context, attrs) {
|
||||||
override fun onNestedPreScroll(
|
override fun onNestedPreScroll(
|
||||||
coordinatorLayout: CoordinatorLayout,
|
coordinatorLayout: CoordinatorLayout,
|
||||||
child: AppBarLayout,
|
child: AppBarLayout,
|
||||||
|
|
@ -132,8 +149,8 @@ class DetailAppBarLayout @JvmOverloads constructor(
|
||||||
val appBar = child as DetailAppBarLayout
|
val appBar = child as DetailAppBarLayout
|
||||||
val recycler = appBar.findRecyclerView()
|
val recycler = appBar.findRecyclerView()
|
||||||
|
|
||||||
val showTitle = (recycler.layoutManager as LinearLayoutManager)
|
val showTitle =
|
||||||
.findFirstVisibleItemPosition() > 0
|
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0
|
||||||
|
|
||||||
appBar.setTitleVisibility(showTitle)
|
appBar.setTitleVisibility(showTitle)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* DetailFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
|
|
@ -70,21 +69,15 @@ abstract class DetailFragment : Fragment() {
|
||||||
inflateMenu(menuId)
|
inflateMenu(menuId)
|
||||||
}
|
}
|
||||||
|
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMenuClick?.let { onClick ->
|
onMenuClick?.let { onClick ->
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item -> onClick(item.itemId) }
|
||||||
onClick(item.itemId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shortcut method for recyclerview setup */
|
||||||
* Shortcut method for recyclerview setup
|
|
||||||
*/
|
|
||||||
protected fun setupRecycler(
|
protected fun setupRecycler(
|
||||||
binding: FragmentDetailBinding,
|
binding: FragmentDetailBinding,
|
||||||
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
detailAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>,
|
||||||
|
|
@ -99,10 +92,14 @@ abstract class DetailFragment : Fragment() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut method for spinning up the sorting [PopupMenu]
|
* Shortcut method for spinning up the sorting [PopupMenu]
|
||||||
* @param config The initial configuration to apply to the menu. This is provided by [DetailViewModel.showMenu].
|
* @param config The initial configuration to apply to the menu. This is provided by
|
||||||
|
* [DetailViewModel.showMenu].
|
||||||
* @param showItem Which menu items to keep
|
* @param showItem Which menu items to keep
|
||||||
*/
|
*/
|
||||||
protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) {
|
protected fun showMenu(
|
||||||
|
config: DetailViewModel.MenuConfig,
|
||||||
|
showItem: ((Int) -> Boolean)? = null
|
||||||
|
) {
|
||||||
logD("Launching menu [$config]")
|
logD("Launching menu [$config]")
|
||||||
|
|
||||||
PopupMenu(config.anchor.context, config.anchor).apply {
|
PopupMenu(config.anchor.context, config.anchor).apply {
|
||||||
|
|
@ -120,9 +117,7 @@ abstract class DetailFragment : Fragment() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
setOnDismissListener {
|
setOnDismissListener { detailModel.finishShowMenu(null) }
|
||||||
detailModel.finishShowMenu(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showItem != null) {
|
if (showItem != null) {
|
||||||
for (item in menu.children) {
|
for (item in menu.children) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* DetailViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -47,22 +46,26 @@ class DetailViewModel : ViewModel() {
|
||||||
// --- CURRENT VALUES ---
|
// --- CURRENT VALUES ---
|
||||||
|
|
||||||
private val mCurGenre = MutableLiveData<Genre?>()
|
private val mCurGenre = MutableLiveData<Genre?>()
|
||||||
val curGenre: LiveData<Genre?> get() = mCurGenre
|
val curGenre: LiveData<Genre?>
|
||||||
|
get() = mCurGenre
|
||||||
|
|
||||||
private val mGenreData = MutableLiveData(listOf<Item>())
|
private val mGenreData = MutableLiveData(listOf<Item>())
|
||||||
val genreData: LiveData<List<Item>> = mGenreData
|
val genreData: LiveData<List<Item>> = mGenreData
|
||||||
|
|
||||||
private val mCurArtist = MutableLiveData<Artist?>()
|
private val mCurArtist = MutableLiveData<Artist?>()
|
||||||
val curArtist: LiveData<Artist?> get() = mCurArtist
|
val curArtist: LiveData<Artist?>
|
||||||
|
get() = mCurArtist
|
||||||
|
|
||||||
private val mArtistData = MutableLiveData(listOf<Item>())
|
private val mArtistData = MutableLiveData(listOf<Item>())
|
||||||
val artistData: LiveData<List<Item>> = mArtistData
|
val artistData: LiveData<List<Item>> = mArtistData
|
||||||
|
|
||||||
private val mCurAlbum = MutableLiveData<Album?>()
|
private val mCurAlbum = MutableLiveData<Album?>()
|
||||||
val curAlbum: LiveData<Album?> get() = mCurAlbum
|
val curAlbum: LiveData<Album?>
|
||||||
|
get() = mCurAlbum
|
||||||
|
|
||||||
private val mAlbumData = MutableLiveData(listOf<Item>())
|
private val mAlbumData = MutableLiveData(listOf<Item>())
|
||||||
val albumData: LiveData<List<Item>> get() = mAlbumData
|
val albumData: LiveData<List<Item>>
|
||||||
|
get() = mAlbumData
|
||||||
|
|
||||||
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
data class MenuConfig(val anchor: View, val sortMode: Sort)
|
||||||
|
|
||||||
|
|
@ -72,7 +75,8 @@ class DetailViewModel : ViewModel() {
|
||||||
private val mNavToItem = MutableLiveData<Item?>()
|
private val mNavToItem = MutableLiveData<Item?>()
|
||||||
|
|
||||||
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
/** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */
|
||||||
val navToItem: LiveData<Item?> get() = mNavToItem
|
val navToItem: LiveData<Item?>
|
||||||
|
get() = mNavToItem
|
||||||
|
|
||||||
var isNavigating = false
|
var isNavigating = false
|
||||||
private set
|
private set
|
||||||
|
|
@ -101,10 +105,7 @@ class DetailViewModel : ViewModel() {
|
||||||
refreshAlbumData()
|
refreshAlbumData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Mark that the menu process is done with the new [Sort]. Pass null if there was no change. */
|
||||||
* Mark that the menu process is done with the new [Sort].
|
|
||||||
* Pass null if there was no change.
|
|
||||||
*/
|
|
||||||
fun finishShowMenu(newMode: Sort?) {
|
fun finishShowMenu(newMode: Sort?) {
|
||||||
mShowMenu.value = null
|
mShowMenu.value = null
|
||||||
|
|
||||||
|
|
@ -130,23 +131,17 @@ class DetailViewModel : ViewModel() {
|
||||||
currentMenuContext = null
|
currentMenuContext = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Navigate to an item, whether a song/album/artist */
|
||||||
* Navigate to an item, whether a song/album/artist
|
|
||||||
*/
|
|
||||||
fun navToItem(item: Item) {
|
fun navToItem(item: Item) {
|
||||||
mNavToItem.value = item
|
mNavToItem.value = item
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Mark that the navigation process is done. */
|
||||||
* Mark that the navigation process is done.
|
|
||||||
*/
|
|
||||||
fun finishNavToItem() {
|
fun finishNavToItem() {
|
||||||
mNavToItem.value = null
|
mNavToItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the current navigation status to [isNavigating] */
|
||||||
* Update the current navigation status to [isNavigating]
|
|
||||||
*/
|
|
||||||
fun setNavigating(navigating: Boolean) {
|
fun setNavigating(navigating: Boolean) {
|
||||||
isNavigating = navigating
|
isNavigating = navigating
|
||||||
}
|
}
|
||||||
|
|
@ -165,9 +160,7 @@ class DetailViewModel : ViewModel() {
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
currentMenuContext = DisplayMode.SHOW_GENRES
|
currentMenuContext = DisplayMode.SHOW_GENRES
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
||||||
}
|
}))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
data.addAll(settingsManager.detailGenreSort.sortGenre(curGenre.value!!))
|
data.addAll(settingsManager.detailGenreSort.sortGenre(curGenre.value!!))
|
||||||
|
|
||||||
|
|
@ -179,12 +172,7 @@ class DetailViewModel : ViewModel() {
|
||||||
val artist = requireNotNull(curArtist.value)
|
val artist = requireNotNull(curArtist.value)
|
||||||
val data = mutableListOf<Item>(artist)
|
val data = mutableListOf<Item>(artist)
|
||||||
|
|
||||||
data.add(
|
data.add(Header(id = -2, string = R.string.lbl_albums))
|
||||||
Header(
|
|
||||||
id = -2,
|
|
||||||
string = R.string.lbl_albums
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
data.addAll(Sort.ByYear(false).sortAlbums(artist.albums))
|
data.addAll(Sort.ByYear(false).sortAlbums(artist.albums))
|
||||||
|
|
||||||
|
|
@ -197,9 +185,7 @@ class DetailViewModel : ViewModel() {
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
currentMenuContext = DisplayMode.SHOW_ARTISTS
|
currentMenuContext = DisplayMode.SHOW_ARTISTS
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort)
|
||||||
}
|
}))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
data.addAll(settingsManager.detailArtistSort.sortArtist(artist))
|
data.addAll(settingsManager.detailArtistSort.sortArtist(artist))
|
||||||
|
|
||||||
|
|
@ -220,9 +206,7 @@ class DetailViewModel : ViewModel() {
|
||||||
onClick = { view ->
|
onClick = { view ->
|
||||||
currentMenuContext = DisplayMode.SHOW_ALBUMS
|
currentMenuContext = DisplayMode.SHOW_ALBUMS
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort)
|
||||||
}
|
}))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
data.addAll(settingsManager.detailAlbumSort.sortAlbum(curAlbum.value!!))
|
data.addAll(settingsManager.detailAlbumSort.sortAlbum(curAlbum.value!!))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* GenreDetailFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -53,15 +52,11 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
detailModel.setGenre(args.genreId)
|
detailModel.setGenre(args.genreId)
|
||||||
|
|
||||||
val binding = FragmentDetailBinding.inflate(inflater)
|
val binding = FragmentDetailBinding.inflate(inflater)
|
||||||
val detailAdapter = GenreDetailAdapter(
|
val detailAdapter =
|
||||||
playbackModel,
|
GenreDetailAdapter(
|
||||||
doOnClick = { song ->
|
playbackModel,
|
||||||
playbackModel.playSong(song, PlaybackMode.IN_GENRE)
|
doOnClick = { song -> playbackModel.playSong(song, PlaybackMode.IN_GENRE) },
|
||||||
},
|
doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_GENRE) })
|
||||||
doOnLongClick = { view, data ->
|
|
||||||
newMenu(view, data, ActionMenu.FLAG_IN_GENRE)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
|
@ -75,34 +70,26 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
|
|
||||||
// --- DETAILVIEWMODEL SETUP ---
|
// --- DETAILVIEWMODEL SETUP ---
|
||||||
|
|
||||||
detailModel.genreData.observe(viewLifecycleOwner) { data ->
|
detailModel.genreData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) }
|
||||||
detailAdapter.submitList(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
// All items will launch new detail fragments.
|
// All items will launch new detail fragments.
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
logD("Navigating to another artist")
|
logD("Navigating to another artist")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
GenreDetailFragmentDirections.actionShowArtist(item.id)
|
.navigate(GenreDetailFragmentDirections.actionShowArtist(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
logD("Navigating to another album")
|
logD("Navigating to another album")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
GenreDetailFragmentDirections.actionShowAlbum(item.id)
|
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is Song -> {
|
is Song -> {
|
||||||
logD("Navigating to another song")
|
logD("Navigating to another song")
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
GenreDetailFragmentDirections.actionShowAlbum(item.album.id)
|
.navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> {}
|
null -> {}
|
||||||
else -> logW("Unsupported navigation command ${item::class.java}")
|
else -> logW("Unsupported navigation command ${item::class.java}")
|
||||||
}
|
}
|
||||||
|
|
@ -112,8 +99,7 @@ class GenreDetailFragment : DetailFragment() {
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
||||||
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
|
if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
|
||||||
playbackModel.parent.value?.id == detailModel.curGenre.value!!.id
|
playbackModel.parent.value?.id == detailModel.curGenre.value!!.id) {
|
||||||
) {
|
|
||||||
detailAdapter.highlightSong(song, binding.detailRecycler)
|
detailAdapter.highlightSong(song, binding.detailRecycler)
|
||||||
} else {
|
} else {
|
||||||
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
// Clear the ViewHolders if the mode isn't ALL_SONGS
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AlbumDetailAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -63,16 +62,11 @@ class AlbumDetailAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
ALBUM_DETAIL_ITEM_TYPE -> AlbumDetailViewHolder(
|
ALBUM_DETAIL_ITEM_TYPE ->
|
||||||
ItemDetailBinding.inflate(parent.context.inflater)
|
AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||||
)
|
ALBUM_SONG_ITEM_TYPE ->
|
||||||
|
AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater))
|
||||||
ALBUM_SONG_ITEM_TYPE -> AlbumSongViewHolder(
|
|
||||||
ItemAlbumSongBinding.inflate(parent.context.inflater)
|
|
||||||
)
|
|
||||||
|
|
||||||
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
else -> error("Invalid ViewHolder item type $viewType")
|
else -> error("Invalid ViewHolder item type $viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,8 +78,7 @@ class AlbumDetailAdapter(
|
||||||
is Album -> (holder as AlbumDetailViewHolder).bind(item)
|
is Album -> (holder as AlbumDetailViewHolder).bind(item)
|
||||||
is Song -> (holder as AlbumSongViewHolder).bind(item)
|
is Song -> (holder as AlbumSongViewHolder).bind(item)
|
||||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||||
else -> {
|
else -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (holder is Highlightable) {
|
if (holder is Highlightable) {
|
||||||
|
|
@ -114,9 +107,7 @@ class AlbumDetailAdapter(
|
||||||
|
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
// Use existing data instead of having to re-sort it.
|
// Use existing data instead of having to re-sort it.
|
||||||
val pos = currentList.indexOfFirst { item ->
|
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||||
item.id == song.id && item is Song
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||||
|
|
@ -130,9 +121,8 @@ class AlbumDetailAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AlbumDetailViewHolder(
|
inner class AlbumDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||||
private val binding: ItemDetailBinding
|
BaseViewHolder<Album>(binding) {
|
||||||
) : BaseViewHolder<Album>(binding) {
|
|
||||||
|
|
||||||
override fun onBind(data: Album) {
|
override fun onBind(data: Album) {
|
||||||
binding.detailCover.apply {
|
binding.detailCover.apply {
|
||||||
|
|
@ -144,27 +134,21 @@ class AlbumDetailAdapter(
|
||||||
|
|
||||||
binding.detailSubhead.apply {
|
binding.detailSubhead.apply {
|
||||||
text = data.artist.resolvedName
|
text = data.artist.resolvedName
|
||||||
setOnClickListener {
|
setOnClickListener { detailModel.navToItem(data.artist) }
|
||||||
detailModel.navToItem(data.artist)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailInfo.apply {
|
binding.detailInfo.apply {
|
||||||
text = context.getString(
|
text =
|
||||||
R.string.fmt_three,
|
context.getString(
|
||||||
data.year?.toString() ?: context.getString(R.string.def_date),
|
R.string.fmt_three,
|
||||||
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size),
|
data.year?.toString() ?: context.getString(R.string.def_date),
|
||||||
data.totalDuration
|
context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size),
|
||||||
)
|
data.totalDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener {
|
binding.detailPlayButton.setOnClickListener { playbackModel.playAlbum(data, false) }
|
||||||
playbackModel.playAlbum(data, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener {
|
binding.detailShuffleButton.setOnClickListener { playbackModel.playAlbum(data, true) }
|
||||||
playbackModel.playAlbum(data, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* ArtistDetailAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -70,22 +69,14 @@ class ArtistDetailAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
ARTIST_DETAIL_ITEM_TYPE -> ArtistDetailViewHolder(
|
ARTIST_DETAIL_ITEM_TYPE ->
|
||||||
ItemDetailBinding.inflate(parent.context.inflater)
|
ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||||
)
|
ARTIST_ALBUM_ITEM_TYPE ->
|
||||||
|
ArtistAlbumViewHolder(ItemArtistAlbumBinding.inflate(parent.context.inflater))
|
||||||
ARTIST_ALBUM_ITEM_TYPE -> ArtistAlbumViewHolder(
|
ARTIST_SONG_ITEM_TYPE ->
|
||||||
ItemArtistAlbumBinding.inflate(parent.context.inflater)
|
ArtistSongViewHolder(ItemArtistSongBinding.inflate(parent.context.inflater))
|
||||||
)
|
|
||||||
|
|
||||||
ARTIST_SONG_ITEM_TYPE -> ArtistSongViewHolder(
|
|
||||||
ItemArtistSongBinding.inflate(parent.context.inflater)
|
|
||||||
)
|
|
||||||
|
|
||||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
else -> error("Invalid ViewHolder item type $viewType")
|
else -> error("Invalid ViewHolder item type $viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,8 +90,7 @@ class ArtistDetailAdapter(
|
||||||
is Song -> (holder as ArtistSongViewHolder).bind(item)
|
is Song -> (holder as ArtistSongViewHolder).bind(item)
|
||||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||||
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item)
|
||||||
else -> {
|
else -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (holder is Highlightable) {
|
if (holder is Highlightable) {
|
||||||
|
|
@ -133,9 +123,7 @@ class ArtistDetailAdapter(
|
||||||
|
|
||||||
if (album != null) {
|
if (album != null) {
|
||||||
// Use existing data instead of having to re-sort it.
|
// Use existing data instead of having to re-sort it.
|
||||||
val pos = currentList.indexOfFirst { item ->
|
val pos = currentList.indexOfFirst { item -> item.id == album.id && item is Album }
|
||||||
item.id == album.id && item is Album
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the ViewHolder if this album is visible, and highlight it if so.
|
// Check if the ViewHolder if this album is visible, and highlight it if so.
|
||||||
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
recycler.layoutManager?.findViewByPosition(pos)?.let { child ->
|
||||||
|
|
@ -163,9 +151,7 @@ class ArtistDetailAdapter(
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
// Use existing data instead of having to re-sort it.
|
// Use existing data instead of having to re-sort it.
|
||||||
// We also have to account for the album count when searching for the ViewHolder.
|
// We also have to account for the album count when searching for the ViewHolder.
|
||||||
val pos = currentList.indexOfFirst { item ->
|
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||||
item.id == song.id && item is Song
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||||
|
|
@ -179,39 +165,35 @@ class ArtistDetailAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ArtistDetailViewHolder(
|
inner class ArtistDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||||
private val binding: ItemDetailBinding
|
BaseViewHolder<Artist>(binding) {
|
||||||
) : BaseViewHolder<Artist>(binding) {
|
|
||||||
|
|
||||||
override fun onBind(data: Artist) {
|
override fun onBind(data: Artist) {
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
|
|
||||||
binding.detailCover.apply {
|
binding.detailCover.apply {
|
||||||
bindArtistImage(data)
|
bindArtistImage(data)
|
||||||
contentDescription = context.getString(
|
contentDescription =
|
||||||
R.string.desc_artist_image,
|
context.getString(R.string.desc_artist_image, data.resolvedName)
|
||||||
data.resolvedName
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailName.text = data.resolvedName
|
binding.detailName.text = data.resolvedName
|
||||||
|
|
||||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||||
// the most "Prominent" genre.
|
// the most "Prominent" genre.
|
||||||
binding.detailSubhead.text = data.songs
|
binding.detailSubhead.text =
|
||||||
.groupBy { it.genre.resolvedName }
|
data.songs
|
||||||
.entries.maxByOrNull { it.value.size }
|
.groupBy { it.genre.resolvedName }
|
||||||
?.key ?: context.getString(R.string.def_genre)
|
.entries
|
||||||
|
.maxByOrNull { it.value.size }
|
||||||
|
?.key
|
||||||
|
?: context.getString(R.string.def_genre)
|
||||||
|
|
||||||
binding.detailInfo.bindArtistInfo(data)
|
binding.detailInfo.bindArtistInfo(data)
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener {
|
binding.detailPlayButton.setOnClickListener { playbackModel.playArtist(data, false) }
|
||||||
playbackModel.playArtist(data, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener {
|
binding.detailShuffleButton.setOnClickListener { playbackModel.playArtist(data, true) }
|
||||||
playbackModel.playArtist(data, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* GenreDetailAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -60,16 +59,13 @@ class GenreDetailAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
GENRE_DETAIL_ITEM_TYPE -> GenreDetailViewHolder(
|
GENRE_DETAIL_ITEM_TYPE ->
|
||||||
ItemDetailBinding.inflate(parent.context.inflater)
|
GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater))
|
||||||
)
|
GENRE_SONG_ITEM_TYPE ->
|
||||||
|
GenreSongViewHolder(
|
||||||
GENRE_SONG_ITEM_TYPE -> GenreSongViewHolder(
|
ItemGenreSongBinding.inflate(parent.context.inflater),
|
||||||
ItemGenreSongBinding.inflate(parent.context.inflater),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
else -> error("Bad ViewHolder item type $viewType")
|
else -> error("Bad ViewHolder item type $viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +106,7 @@ class GenreDetailAdapter(
|
||||||
|
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
// Use existing data instead of having to re-sort it.
|
// Use existing data instead of having to re-sort it.
|
||||||
val pos = currentList.indexOfFirst { item ->
|
val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song }
|
||||||
item.id == song.id && item is Song
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
// Check if the ViewHolder for this song is visible, if it is then highlight it.
|
||||||
// If the ViewHolder is not visible, then the adapter should take care of it if
|
// If the ViewHolder is not visible, then the adapter should take care of it if
|
||||||
|
|
@ -126,31 +120,23 @@ class GenreDetailAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class GenreDetailViewHolder(
|
inner class GenreDetailViewHolder(private val binding: ItemDetailBinding) :
|
||||||
private val binding: ItemDetailBinding
|
BaseViewHolder<Genre>(binding) {
|
||||||
) : BaseViewHolder<Genre>(binding) {
|
|
||||||
override fun onBind(data: Genre) {
|
override fun onBind(data: Genre) {
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
|
|
||||||
binding.detailCover.apply {
|
binding.detailCover.apply {
|
||||||
bindGenreImage(data)
|
bindGenreImage(data)
|
||||||
contentDescription = context.getString(
|
contentDescription = context.getString(R.string.desc_genre_image, data.resolvedName)
|
||||||
R.string.desc_genre_image,
|
|
||||||
data.resolvedName
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailName.text = data.resolvedName
|
binding.detailName.text = data.resolvedName
|
||||||
binding.detailSubhead.bindGenreInfo(data)
|
binding.detailSubhead.bindGenreInfo(data)
|
||||||
binding.detailInfo.text = data.totalDuration
|
binding.detailInfo.text = data.totalDuration
|
||||||
|
|
||||||
binding.detailPlayButton.setOnClickListener {
|
binding.detailPlayButton.setOnClickListener { playbackModel.playGenre(data, false) }
|
||||||
playbackModel.playGenre(data, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailShuffleButton.setOnClickListener {
|
binding.detailShuffleButton.setOnClickListener { playbackModel.playGenre(data, true) }
|
||||||
playbackModel.playGenre(data, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Highlightable.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,12 +14,10 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.recycler
|
package org.oxycblt.auxio.detail.recycler
|
||||||
|
|
||||||
/**
|
/** Interface that allows the highlighting of certain ViewHolders */
|
||||||
* Interface that allows the highlighting of certain ViewHolders
|
|
||||||
*/
|
|
||||||
interface Highlightable {
|
interface Highlightable {
|
||||||
fun setHighlighted(isHighlighted: Boolean)
|
fun setHighlighted(isHighlighted: Boolean)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -11,10 +28,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
* - On medium screens, use only text
|
* - On medium screens, use only text
|
||||||
* - On large screens, use text and an icon
|
* - On large screens, use text and an icon
|
||||||
*/
|
*/
|
||||||
class AdaptiveTabStrategy(
|
class AdaptiveTabStrategy(context: Context, private val homeModel: HomeViewModel) :
|
||||||
context: Context,
|
TabLayoutMediator.TabConfigurationStrategy {
|
||||||
private val homeModel: HomeViewModel
|
|
||||||
) : TabLayoutMediator.TabConfigurationStrategy {
|
|
||||||
private val width = context.resources.configuration.smallestScreenWidthDp
|
private val width = context.resources.configuration.smallestScreenWidthDp
|
||||||
|
|
||||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||||
|
|
@ -23,19 +38,15 @@ class AdaptiveTabStrategy(
|
||||||
when {
|
when {
|
||||||
width < 370 -> {
|
width < 370 -> {
|
||||||
logD("Using icon-only configuration")
|
logD("Using icon-only configuration")
|
||||||
tab.setIcon(tabMode.icon)
|
tab.setIcon(tabMode.icon).setContentDescription(tabMode.string)
|
||||||
.setContentDescription(tabMode.string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
width < 640 -> {
|
width < 640 -> {
|
||||||
logD("Using text-only configuration")
|
logD("Using text-only configuration")
|
||||||
tab.setText(tabMode.string)
|
tab.setText(tabMode.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
logD("Using icon-and-text configuration")
|
logD("Using icon-and-text configuration")
|
||||||
tab.setIcon(tabMode.icon)
|
tab.setIcon(tabMode.icon).setText(tabMode.string)
|
||||||
.setText(tabMode.string)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* EdgeFloatingActionButton.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -30,11 +29,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* A container for a FloatingActionButton that enables edge-to-edge support.
|
* A container for a FloatingActionButton that enables edge-to-edge support.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class EdgeFabContainer @JvmOverloads constructor(
|
class EdgeFabContainer
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
init {
|
init {
|
||||||
clipToPadding = false
|
clipToPadding = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MainFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -52,11 +51,10 @@ import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail
|
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
||||||
* views for each respective item.
|
* respective item.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt TODO: Make tabs invisible when there is only one TODO: Add duration and song
|
||||||
* TODO: Make tabs invisible when there is only one
|
* count sorts
|
||||||
* TODO: Add duration and song count sorts
|
|
||||||
*/
|
*/
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
@ -83,35 +81,37 @@ class HomeFragment : Fragment() {
|
||||||
logD("Navigating to search")
|
logD("Navigating to search")
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_settings -> {
|
R.id.action_settings -> {
|
||||||
logD("Navigating to settings")
|
logD("Navigating to settings")
|
||||||
parentFragment?.parentFragment?.findNavController()?.navigate(
|
parentFragment
|
||||||
MainFragmentDirections.actionShowSettings()
|
?.parentFragment
|
||||||
)
|
?.findNavController()
|
||||||
|
?.navigate(MainFragmentDirections.actionShowSettings())
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_about -> {
|
R.id.action_about -> {
|
||||||
logD("Navigating to about")
|
logD("Navigating to about")
|
||||||
parentFragment?.parentFragment?.findNavController()?.navigate(
|
parentFragment
|
||||||
MainFragmentDirections.actionShowAbout()
|
?.parentFragment
|
||||||
)
|
?.findNavController()
|
||||||
|
?.navigate(MainFragmentDirections.actionShowAbout())
|
||||||
}
|
}
|
||||||
|
R.id.submenu_sorting -> {}
|
||||||
R.id.submenu_sorting -> { }
|
|
||||||
|
|
||||||
R.id.option_sort_asc -> {
|
R.id.option_sort_asc -> {
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
|
val new =
|
||||||
.ascending(item.isChecked)
|
homeModel
|
||||||
|
.getSortForDisplay(homeModel.curTab.value!!)
|
||||||
|
.ascending(item.isChecked)
|
||||||
homeModel.updateCurrentSort(new)
|
homeModel.updateCurrentSort(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting option was selected, mark it as selected and update the mode
|
// Sorting option was selected, mark it as selected and update the mode
|
||||||
else -> {
|
else -> {
|
||||||
item.isChecked = true
|
item.isChecked = true
|
||||||
val new = homeModel.getSortForDisplay(homeModel.curTab.value!!)
|
val new =
|
||||||
.assignId(item.itemId)
|
homeModel
|
||||||
|
.getSortForDisplay(homeModel.curTab.value!!)
|
||||||
|
.assignId(item.itemId)
|
||||||
homeModel.updateCurrentSort(requireNotNull(new))
|
homeModel.updateCurrentSort(requireNotNull(new))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,12 +129,14 @@ class HomeFragment : Fragment() {
|
||||||
// scroll events being registered as horizontal scroll events. Reflect into the
|
// scroll events being registered as horizontal scroll events. Reflect into the
|
||||||
// internal recyclerview and change the touch slope so that touch actions will
|
// internal recyclerview and change the touch slope so that touch actions will
|
||||||
// act more as a scroll than as a swipe.
|
// act more as a scroll than as a swipe.
|
||||||
// Derived from: https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
// Derived from:
|
||||||
|
// https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
||||||
try {
|
try {
|
||||||
val recycler = ViewPager2::class.java.getDeclaredField("mRecyclerView").run {
|
val recycler =
|
||||||
isAccessible = true
|
ViewPager2::class.java.getDeclaredField("mRecyclerView").run {
|
||||||
get(binding.homePager)
|
isAccessible = true
|
||||||
}
|
get(binding.homePager)
|
||||||
|
}
|
||||||
|
|
||||||
RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
|
RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
|
||||||
isAccessible = true
|
isAccessible = true
|
||||||
|
|
@ -152,18 +154,17 @@ class HomeFragment : Fragment() {
|
||||||
// page transitions.
|
// page transitions.
|
||||||
offscreenPageLimit = homeModel.tabs.size
|
offscreenPageLimit = homeModel.tabs.size
|
||||||
|
|
||||||
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
registerOnPageChangeCallback(
|
||||||
override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position)
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
})
|
override fun onPageSelected(position: Int) =
|
||||||
|
homeModel.updateCurrentTab(position)
|
||||||
|
})
|
||||||
|
|
||||||
TabLayoutMediator(
|
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
|
||||||
binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel)
|
.attach()
|
||||||
).attach()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeFab.setOnClickListener {
|
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
|
||||||
playbackModel.shuffleAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
|
@ -213,18 +214,12 @@ class HomeFragment : Fragment() {
|
||||||
// the tab changes.
|
// the tab changes.
|
||||||
when (tab) {
|
when (tab) {
|
||||||
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab)
|
||||||
|
DisplayMode.SHOW_ALBUMS ->
|
||||||
DisplayMode.SHOW_ALBUMS -> updateSortMenu(sortItem, tab) { id ->
|
updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album }
|
||||||
id != R.id.option_sort_album
|
DisplayMode.SHOW_ARTISTS ->
|
||||||
}
|
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
||||||
|
DisplayMode.SHOW_GENRES ->
|
||||||
DisplayMode.SHOW_ARTISTS -> updateSortMenu(sortItem, tab) { id ->
|
updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc }
|
||||||
id == R.id.option_sort_asc
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayMode.SHOW_GENRES -> updateSortMenu(sortItem, tab) { id ->
|
|
||||||
id == R.id.option_sort_asc
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId
|
binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId
|
||||||
|
|
@ -236,24 +231,19 @@ class HomeFragment : Fragment() {
|
||||||
// This is only here just in case a collapsing toolbar is re-added.
|
// This is only here just in case a collapsing toolbar is re-added.
|
||||||
binding.homeAppbar.post {
|
binding.homeAppbar.post {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> findNavController().navigate(
|
is Song ->
|
||||||
HomeFragmentDirections.actionShowAlbum(item.album.id)
|
findNavController()
|
||||||
)
|
.navigate(HomeFragmentDirections.actionShowAlbum(item.album.id))
|
||||||
|
is Album ->
|
||||||
is Album -> findNavController().navigate(
|
findNavController()
|
||||||
HomeFragmentDirections.actionShowAlbum(item.id)
|
.navigate(HomeFragmentDirections.actionShowAlbum(item.id))
|
||||||
)
|
is Artist ->
|
||||||
|
findNavController()
|
||||||
is Artist -> findNavController().navigate(
|
.navigate(HomeFragmentDirections.actionShowArtist(item.id))
|
||||||
HomeFragmentDirections.actionShowArtist(item.id)
|
is Genre ->
|
||||||
)
|
findNavController()
|
||||||
|
.navigate(HomeFragmentDirections.actionShowGenre(item.id))
|
||||||
is Genre -> findNavController().navigate(
|
else -> {}
|
||||||
HomeFragmentDirections.actionShowGenre(item.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,12 +273,14 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DisplayMode.viewId: Int get() = when (this) {
|
private val DisplayMode.viewId: Int
|
||||||
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
get() =
|
||||||
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
when (this) {
|
||||||
DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list
|
DisplayMode.SHOW_SONGS -> R.id.home_song_list
|
||||||
DisplayMode.SHOW_GENRES -> R.id.home_genre_list
|
DisplayMode.SHOW_ALBUMS -> R.id.home_album_list
|
||||||
}
|
DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list
|
||||||
|
DisplayMode.SHOW_GENRES -> R.id.home_genre_list
|
||||||
|
}
|
||||||
|
|
||||||
private inner class HomePagerAdapter :
|
private inner class HomePagerAdapter :
|
||||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* HomeViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home
|
package org.oxycblt.auxio.home
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
|
@ -42,31 +41,34 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mSongs = MutableLiveData(listOf<Song>())
|
private val mSongs = MutableLiveData(listOf<Song>())
|
||||||
val songs: LiveData<List<Song>> get() = mSongs
|
val songs: LiveData<List<Song>>
|
||||||
|
get() = mSongs
|
||||||
|
|
||||||
private val mAlbums = MutableLiveData(listOf<Album>())
|
private val mAlbums = MutableLiveData(listOf<Album>())
|
||||||
val albums: LiveData<List<Album>> get() = mAlbums
|
val albums: LiveData<List<Album>>
|
||||||
|
get() = mAlbums
|
||||||
|
|
||||||
private val mArtists = MutableLiveData(listOf<Artist>())
|
private val mArtists = MutableLiveData(listOf<Artist>())
|
||||||
val artists: LiveData<List<Artist>> get() = mArtists
|
val artists: LiveData<List<Artist>>
|
||||||
|
get() = mArtists
|
||||||
|
|
||||||
private val mGenres = MutableLiveData(listOf<Genre>())
|
private val mGenres = MutableLiveData(listOf<Genre>())
|
||||||
val genres: LiveData<List<Genre>> get() = mGenres
|
val genres: LiveData<List<Genre>>
|
||||||
|
get() = mGenres
|
||||||
|
|
||||||
var tabs: List<DisplayMode> = visibleTabs
|
var tabs: List<DisplayMode> = visibleTabs
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/** Internal getter for getting the visible library tabs */
|
/** Internal getter for getting the visible library tabs */
|
||||||
private val visibleTabs: List<DisplayMode> get() = settingsManager.libTabs
|
private val visibleTabs: List<DisplayMode>
|
||||||
.filterIsInstance<Tab.Visible>().map { it.mode }
|
get() = settingsManager.libTabs.filterIsInstance<Tab.Visible>().map { it.mode }
|
||||||
|
|
||||||
private val mCurTab = MutableLiveData(tabs[0])
|
private val mCurTab = MutableLiveData(tabs[0])
|
||||||
val curTab: LiveData<DisplayMode> = mCurTab
|
val curTab: LiveData<DisplayMode> = mCurTab
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker to recreate all library tabs, usually initiated by a settings change.
|
* Marker to recreate all library tabs, usually initiated by a settings change. When this flag
|
||||||
* When this flag is set, all tabs (and their respective viewpager fragments) will be
|
* is set, all tabs (and their respective viewpager fragments) will be recreated from scratch.
|
||||||
* recreated from scratch.
|
|
||||||
*/
|
*/
|
||||||
private val mRecreateTabs = MutableLiveData(false)
|
private val mRecreateTabs = MutableLiveData(false)
|
||||||
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
val recreateTabs: LiveData<Boolean> = mRecreateTabs
|
||||||
|
|
@ -86,9 +88,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the current tab based off of the new ViewPager position. */
|
||||||
* Update the current tab based off of the new ViewPager position.
|
|
||||||
*/
|
|
||||||
fun updateCurrentTab(pos: Int) {
|
fun updateCurrentTab(pos: Int) {
|
||||||
logD("Updating current tab to ${tabs[pos]}")
|
logD("Updating current tab to ${tabs[pos]}")
|
||||||
mCurTab.value = tabs[pos]
|
mCurTab.value = tabs[pos]
|
||||||
|
|
@ -107,9 +107,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the currently displayed item's [Sort]. */
|
||||||
* Update the currently displayed item's [Sort].
|
|
||||||
*/
|
|
||||||
fun updateCurrentSort(sort: Sort) {
|
fun updateCurrentSort(sort: Sort) {
|
||||||
logD("Updating ${mCurTab.value} sort to $sort")
|
logD("Updating ${mCurTab.value} sort to $sort")
|
||||||
when (mCurTab.value) {
|
when (mCurTab.value) {
|
||||||
|
|
@ -117,29 +115,25 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
settingsManager.libSongSort = sort
|
settingsManager.libSongSort = sort
|
||||||
mSongs.value = sort.sortSongs(mSongs.value!!)
|
mSongs.value = sort.sortSongs(mSongs.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
settingsManager.libAlbumSort = sort
|
settingsManager.libAlbumSort = sort
|
||||||
mAlbums.value = sort.sortAlbums(mAlbums.value!!)
|
mAlbums.value = sort.sortAlbums(mAlbums.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
settingsManager.libArtistSort = sort
|
settingsManager.libArtistSort = sort
|
||||||
mArtists.value = sort.sortParents(mArtists.value!!)
|
mArtists.value = sort.sortParents(mArtists.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
settingsManager.libGenreSort = sort
|
settingsManager.libGenreSort = sort
|
||||||
mGenres.value = sort.sortParents(mGenres.value!!)
|
mGenres.value = sort.sortParents(mGenres.value!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the fast scroll state. This is used to control the FAB visibility whenever
|
* Update the fast scroll state. This is used to control the FAB visibility whenever the user
|
||||||
* the user begins to fast scroll.
|
* begins to fast scroll.
|
||||||
*/
|
*/
|
||||||
fun updateFastScrolling(scrolling: Boolean) {
|
fun updateFastScrolling(scrolling: Boolean) {
|
||||||
mFastScrolling.value = scrolling
|
mFastScrolling.value = scrolling
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Md2PopupBackground.java is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,6 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.fastscroll
|
package org.oxycblt.auxio.home.fastscroll
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -30,19 +30,18 @@ import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import kotlin.math.sqrt
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||||
import org.oxycblt.auxio.util.getDimenOffsetSafe
|
import org.oxycblt.auxio.util.getDimenOffsetSafe
|
||||||
import kotlin.math.sqrt
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The custom drawable used as FastScrollRecyclerView's popup background.
|
* The custom drawable used as FastScrollRecyclerView's popup background. This is an adaptation from
|
||||||
* This is an adaptation from AndroidFastScroll's MD2 theme.
|
* AndroidFastScroll's MD2 theme.
|
||||||
*
|
*
|
||||||
* Attributions as per the Apache 2.0 license:
|
* Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang
|
||||||
* ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
|
* [https://github.com/zhanghai] PROJECT: Android Fast Scroll
|
||||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
* [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/]
|
||||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
|
||||||
*
|
*
|
||||||
* !!! MODIFICATIONS !!!:
|
* !!! MODIFICATIONS !!!:
|
||||||
* - Use modified Auxio resources instead of AFS resources
|
* - Use modified Auxio resources instead of AFS resources
|
||||||
|
|
@ -53,11 +52,12 @@ import kotlin.math.sqrt
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @author Hai Zhang, OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class FastScrollPopupDrawable(context: Context) : Drawable() {
|
class FastScrollPopupDrawable(context: Context) : Drawable() {
|
||||||
private val paint: Paint = Paint().apply {
|
private val paint: Paint =
|
||||||
isAntiAlias = true
|
Paint().apply {
|
||||||
color = context.getAttrColorSafe(R.attr.colorSecondary)
|
isAntiAlias = true
|
||||||
style = Paint.Style.FILL
|
color = context.getAttrColorSafe(R.attr.colorSecondary)
|
||||||
}
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
private val path = Path()
|
private val path = Path()
|
||||||
private val matrix = Matrix()
|
private val matrix = Matrix()
|
||||||
|
|
@ -86,13 +86,14 @@ class FastScrollPopupDrawable(context: Context) : Drawable() {
|
||||||
// Paths don't need to be convex on android Q, but the API was mislabeled and so
|
// Paths don't need to be convex on android Q, but the API was mislabeled and so
|
||||||
// we still have to use this method.
|
// we still have to use this method.
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path)
|
||||||
|
else ->
|
||||||
else -> if (!path.isConvex) {
|
if (!path.isConvex) {
|
||||||
// The outline path must be convex before Q, but we may run into floating point
|
// The outline path must be convex before Q, but we may run into floating point
|
||||||
// errors caused by calculations involving sqrt(2) or OEM implementation differences,
|
// errors caused by calculations involving sqrt(2) or OEM implementation
|
||||||
// so in this case we just omit the shadow instead of crashing.
|
// differences,
|
||||||
super.getOutline(outline)
|
// so in this case we just omit the shadow instead of crashing.
|
||||||
}
|
super.getOutline(outline)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,11 +154,15 @@ class FastScrollPopupDrawable(context: Context) : Drawable() {
|
||||||
sweepAngle: Float
|
sweepAngle: Float
|
||||||
) {
|
) {
|
||||||
path.arcTo(
|
path.arcTo(
|
||||||
centerX - radius, centerY - radius, centerX + radius, centerY + radius,
|
centerX - radius,
|
||||||
startAngle, sweepAngle, false
|
centerY - radius,
|
||||||
)
|
centerX + radius,
|
||||||
|
centerY + radius,
|
||||||
|
startAngle,
|
||||||
|
sweepAngle,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isRtl: Boolean get() =
|
private val isRtl: Boolean
|
||||||
DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* FastScrollRecyclerView.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.fastscroll
|
package org.oxycblt.auxio.home.fastscroll
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -41,6 +40,7 @@ import androidx.core.widget.TextViewCompat
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlin.math.abs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.canScroll
|
import org.oxycblt.auxio.util.canScroll
|
||||||
import org.oxycblt.auxio.util.getAttrColorSafe
|
import org.oxycblt.auxio.util.getAttrColorSafe
|
||||||
|
|
@ -48,20 +48,18 @@ import org.oxycblt.auxio.util.getDimenOffsetSafe
|
||||||
import org.oxycblt.auxio.util.getDimenSizeSafe
|
import org.oxycblt.auxio.util.getDimenSizeSafe
|
||||||
import org.oxycblt.auxio.util.getDrawableSafe
|
import org.oxycblt.auxio.util.getDrawableSafe
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
* A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of
|
||||||
* Hai Zhang's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
|
* Hai Zhang's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements.
|
||||||
*
|
*
|
||||||
* Attributions as per the Apache 2.0 license:
|
* Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang
|
||||||
* ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai]
|
* [https://github.com/zhanghai] PROJECT: Android Fast Scroll
|
||||||
* PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll]
|
* [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/]
|
||||||
* MODIFIER: OxygenCobalt [https://github.com/]
|
|
||||||
*
|
*
|
||||||
* !!! MODIFICATIONS !!!:
|
* !!! MODIFICATIONS !!!:
|
||||||
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant
|
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with
|
||||||
* with multiple views
|
* multiple views
|
||||||
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
|
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
|
||||||
* - FastScroller overlay was merged into RecyclerView instance
|
* - FastScroller overlay was merged into RecyclerView instance
|
||||||
* - Removed FastScrollerBuilder
|
* - Removed FastScrollerBuilder
|
||||||
|
|
@ -75,17 +73,16 @@ import kotlin.math.abs
|
||||||
*
|
*
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @author Hai Zhang, OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView @JvmOverloads constructor(
|
class FastScrollRecyclerView
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
RecyclerView(context, attrs, defStyleAttr) {
|
||||||
) : RecyclerView(context, attrs, defStyleAttr) {
|
|
||||||
/** Callback to provide a string to be shown on the popup when an item is passed */
|
/** Callback to provide a string to be shown on the popup when an item is passed */
|
||||||
var popupProvider: ((Int) -> String)? = null
|
var popupProvider: ((Int) -> String)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A listener for when a drag event occurs. The value will be true if a drag has begun,
|
* A listener for when a drag event occurs. The value will be true if a drag has begun, and
|
||||||
* and false if a drag ended.
|
* false if a drag ended.
|
||||||
*/
|
*/
|
||||||
var onDragListener: ((Boolean) -> Unit)? = null
|
var onDragListener: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
|
|
@ -128,35 +125,37 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
|
val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb)
|
||||||
|
|
||||||
trackView = View(context)
|
trackView = View(context)
|
||||||
thumbView = View(context).apply {
|
thumbView =
|
||||||
alpha = 0f
|
View(context).apply {
|
||||||
background = thumbDrawable
|
alpha = 0f
|
||||||
}
|
background = thumbDrawable
|
||||||
|
|
||||||
popupView = AppCompatTextView(context).apply {
|
|
||||||
alpha = 0f
|
|
||||||
layoutParams = FrameLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
|
|
||||||
minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width)
|
|
||||||
minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large)
|
|
||||||
|
|
||||||
(layoutParams as FrameLayout.LayoutParams).apply {
|
|
||||||
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
||||||
marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
|
popupView =
|
||||||
setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
|
AppCompatTextView(context).apply {
|
||||||
|
alpha = 0f
|
||||||
|
layoutParams =
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
|
||||||
background = FastScrollPopupDrawable(context)
|
minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width)
|
||||||
elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
|
minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large)
|
||||||
ellipsize = TextUtils.TruncateAt.MIDDLE
|
|
||||||
gravity = Gravity.CENTER
|
(layoutParams as FrameLayout.LayoutParams).apply {
|
||||||
includeFontPadding = false
|
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
||||||
isSingleLine = true
|
marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge)
|
||||||
|
setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary))
|
||||||
|
|
||||||
|
background = FastScrollPopupDrawable(context)
|
||||||
|
elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat()
|
||||||
|
ellipsize = TextUtils.TruncateAt.MIDDLE
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
includeFontPadding = false
|
||||||
|
isSingleLine = true
|
||||||
|
}
|
||||||
|
|
||||||
thumbWidth = thumbDrawable.intrinsicWidth
|
thumbWidth = thumbDrawable.intrinsicWidth
|
||||||
thumbHeight = thumbDrawable.intrinsicHeight
|
thumbHeight = thumbDrawable.intrinsicHeight
|
||||||
|
|
@ -168,33 +167,28 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
overlay.add(thumbView)
|
overlay.add(thumbView)
|
||||||
overlay.add(popupView)
|
overlay.add(popupView)
|
||||||
|
|
||||||
addItemDecoration(object : ItemDecoration() {
|
addItemDecoration(
|
||||||
override fun onDraw(
|
object : ItemDecoration() {
|
||||||
canvas: Canvas,
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) {
|
||||||
parent: RecyclerView,
|
onPreDraw()
|
||||||
state: State
|
}
|
||||||
) {
|
})
|
||||||
onPreDraw()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
|
// We use a listener instead of overriding onTouchEvent so that we don't conflict with
|
||||||
// RecyclerView touch events.
|
// RecyclerView touch events.
|
||||||
addOnItemTouchListener(object : SimpleOnItemTouchListener() {
|
addOnItemTouchListener(
|
||||||
override fun onTouchEvent(
|
object : SimpleOnItemTouchListener() {
|
||||||
recyclerView: RecyclerView,
|
override fun onTouchEvent(recyclerView: RecyclerView, event: MotionEvent) {
|
||||||
event: MotionEvent
|
onItemTouch(event)
|
||||||
) {
|
}
|
||||||
onItemTouch(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInterceptTouchEvent(
|
override fun onInterceptTouchEvent(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
event: MotionEvent
|
event: MotionEvent
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return onItemTouch(event)
|
return onItemTouch(event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
// --- RECYCLERVIEW EVENT MANAGEMENT ---
|
||||||
|
|
@ -206,33 +200,34 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
thumbView.layoutDirection = layoutDirection
|
thumbView.layoutDirection = layoutDirection
|
||||||
popupView.layoutDirection = layoutDirection
|
popupView.layoutDirection = layoutDirection
|
||||||
|
|
||||||
val trackLeft = if (isRtl) {
|
val trackLeft =
|
||||||
scrollerPadding.left
|
if (isRtl) {
|
||||||
} else {
|
scrollerPadding.left
|
||||||
width - scrollerPadding.right - thumbWidth
|
} else {
|
||||||
}
|
width - scrollerPadding.right - thumbWidth
|
||||||
|
}
|
||||||
|
|
||||||
trackView.layout(
|
trackView.layout(
|
||||||
trackLeft, scrollerPadding.top, trackLeft + thumbWidth,
|
trackLeft, scrollerPadding.top, trackLeft + thumbWidth, height - scrollerPadding.bottom)
|
||||||
height - scrollerPadding.bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
val thumbLeft = if (isRtl) {
|
val thumbLeft =
|
||||||
scrollerPadding.left
|
if (isRtl) {
|
||||||
} else {
|
scrollerPadding.left
|
||||||
width - scrollerPadding.right - thumbWidth
|
} else {
|
||||||
}
|
width - scrollerPadding.right - thumbWidth
|
||||||
|
}
|
||||||
|
|
||||||
val thumbTop = scrollerPadding.top + thumbOffset
|
val thumbTop = scrollerPadding.top + thumbOffset
|
||||||
|
|
||||||
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
|
thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight)
|
||||||
|
|
||||||
val firstPos = firstAdapterPos
|
val firstPos = firstAdapterPos
|
||||||
val popupText = if (firstPos != NO_POSITION) {
|
val popupText =
|
||||||
popupProvider?.invoke(firstPos) ?: ""
|
if (firstPos != NO_POSITION) {
|
||||||
} else {
|
popupProvider?.invoke(firstPos) ?: ""
|
||||||
""
|
} else {
|
||||||
}
|
""
|
||||||
|
}
|
||||||
|
|
||||||
popupView.isInvisible = popupText.isEmpty()
|
popupView.isInvisible = popupText.isEmpty()
|
||||||
|
|
||||||
|
|
@ -242,58 +237,67 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
if (popupView.text != popupText) {
|
if (popupView.text != popupText) {
|
||||||
popupView.text = popupText
|
popupView.text = popupText
|
||||||
|
|
||||||
val widthMeasureSpec = ViewGroup.getChildMeasureSpec(
|
val widthMeasureSpec =
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
ViewGroup.getChildMeasureSpec(
|
||||||
scrollerPadding.left + scrollerPadding.right + thumbWidth +
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
popupLayoutParams.leftMargin + popupLayoutParams.rightMargin,
|
scrollerPadding.left +
|
||||||
popupLayoutParams.width
|
scrollerPadding.right +
|
||||||
)
|
thumbWidth +
|
||||||
|
popupLayoutParams.leftMargin +
|
||||||
|
popupLayoutParams.rightMargin,
|
||||||
|
popupLayoutParams.width)
|
||||||
|
|
||||||
val heightMeasureSpec = ViewGroup.getChildMeasureSpec(
|
val heightMeasureSpec =
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
ViewGroup.getChildMeasureSpec(
|
||||||
scrollerPadding.top + scrollerPadding.bottom + popupLayoutParams.topMargin +
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
||||||
popupLayoutParams.bottomMargin,
|
scrollerPadding.top +
|
||||||
popupLayoutParams.height
|
scrollerPadding.bottom +
|
||||||
)
|
popupLayoutParams.topMargin +
|
||||||
|
popupLayoutParams.bottomMargin,
|
||||||
|
popupLayoutParams.height)
|
||||||
|
|
||||||
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
popupView.measure(widthMeasureSpec, heightMeasureSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
val popupWidth = popupView.measuredWidth
|
val popupWidth = popupView.measuredWidth
|
||||||
val popupHeight = popupView.measuredHeight
|
val popupHeight = popupView.measuredHeight
|
||||||
val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
val popupLeft =
|
||||||
scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||||
} else {
|
scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin
|
||||||
width - scrollerPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth
|
} else {
|
||||||
}
|
width -
|
||||||
|
scrollerPadding.right -
|
||||||
|
thumbWidth -
|
||||||
|
popupLayoutParams.rightMargin -
|
||||||
|
popupWidth
|
||||||
|
}
|
||||||
|
|
||||||
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
|
// We handle RTL separately, so it's okay if Gravity.RIGHT is used here
|
||||||
@SuppressLint("RtlHardcoded")
|
@SuppressLint("RtlHardcoded")
|
||||||
val popupAnchorY = when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
|
val popupAnchorY =
|
||||||
Gravity.CENTER_HORIZONTAL -> popupHeight / 2
|
when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
|
||||||
Gravity.RIGHT -> popupHeight
|
Gravity.CENTER_HORIZONTAL -> popupHeight / 2
|
||||||
else -> 0
|
Gravity.RIGHT -> popupHeight
|
||||||
}
|
else -> 0
|
||||||
|
|
||||||
val thumbAnchorY = when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
|
||||||
Gravity.CENTER_VERTICAL -> {
|
|
||||||
thumbView.paddingTop + (
|
|
||||||
thumbHeight - thumbView.paddingTop - thumbView.paddingBottom
|
|
||||||
) / 2
|
|
||||||
}
|
}
|
||||||
Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom
|
|
||||||
else -> thumbView.paddingTop
|
|
||||||
}
|
|
||||||
|
|
||||||
val popupTop = MathUtils.clamp(
|
val thumbAnchorY =
|
||||||
thumbTop + thumbAnchorY - popupAnchorY,
|
when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||||
scrollerPadding.top + popupLayoutParams.topMargin,
|
Gravity.CENTER_VERTICAL -> {
|
||||||
height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight
|
thumbView.paddingTop +
|
||||||
)
|
(thumbHeight - thumbView.paddingTop - thumbView.paddingBottom) / 2
|
||||||
|
}
|
||||||
|
Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom
|
||||||
|
else -> thumbView.paddingTop
|
||||||
|
}
|
||||||
|
|
||||||
popupView.layout(
|
val popupTop =
|
||||||
popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight
|
MathUtils.clamp(
|
||||||
)
|
thumbTop + thumbAnchorY - popupAnchorY,
|
||||||
|
scrollerPadding.top + popupLayoutParams.topMargin,
|
||||||
|
height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight)
|
||||||
|
|
||||||
|
popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,9 +319,10 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
|
|
||||||
updatePadding(
|
updatePadding(
|
||||||
initialPadding.left, initialPadding.top, initialPadding.right,
|
initialPadding.left,
|
||||||
initialPadding.bottom + bars.bottom
|
initialPadding.top,
|
||||||
)
|
initialPadding.right,
|
||||||
|
initialPadding.bottom + bars.bottom)
|
||||||
|
|
||||||
scrollerPadding.bottom = bars.bottom
|
scrollerPadding.bottom = bars.bottom
|
||||||
|
|
||||||
|
|
@ -358,24 +363,25 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
if (isInViewTouchTarget(thumbView, eventX, eventY)) {
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
dragStartThumbOffset =
|
||||||
|
(eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
if (!dragging && isInViewTouchTarget(trackView, downX, downY) &&
|
if (!dragging &&
|
||||||
abs(eventY - downY) > touchSlop
|
isInViewTouchTarget(trackView, downX, downY) &&
|
||||||
) {
|
abs(eventY - downY) > touchSlop) {
|
||||||
if (isInViewTouchTarget(thumbView, downX, downY)) {
|
if (isInViewTouchTarget(thumbView, downX, downY)) {
|
||||||
dragStartY = lastY
|
dragStartY = lastY
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
} else {
|
} else {
|
||||||
dragStartY = eventY
|
dragStartY = eventY
|
||||||
dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
dragStartThumbOffset =
|
||||||
|
(eventY - scrollerPadding.top - thumbHeight / 2f).toInt()
|
||||||
scrollToThumbOffset(dragStartThumbOffset)
|
scrollToThumbOffset(dragStartThumbOffset)
|
||||||
}
|
}
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
|
|
@ -386,7 +392,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
scrollToThumbOffset(thumbOffset)
|
scrollToThumbOffset(thumbOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,9 +438,9 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
private fun scrollToThumbOffset(thumbOffset: Int) {
|
private fun scrollToThumbOffset(thumbOffset: Int) {
|
||||||
val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange)
|
val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange)
|
||||||
|
|
||||||
val scrollOffset = (
|
val scrollOffset =
|
||||||
scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange
|
(scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() -
|
||||||
).toInt() - paddingTop
|
paddingTop
|
||||||
|
|
||||||
scrollTo(scrollOffset)
|
scrollTo(scrollOffset)
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +466,6 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
targetPosition *= mgr.spanCount
|
targetPosition *= mgr.spanCount
|
||||||
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
|
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
is LinearLayoutManager -> {
|
is LinearLayoutManager -> {
|
||||||
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
|
mgr.scrollToPositionWithOffset(targetPosition, trueOffset)
|
||||||
}
|
}
|
||||||
|
|
@ -538,10 +542,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateView(view: View, alpha: Float) {
|
private fun animateView(view: View, alpha: Float) {
|
||||||
view.animate()
|
view.animate().alpha(alpha).setDuration(ANIM_MILLIS).start()
|
||||||
.alpha(alpha)
|
|
||||||
.setDuration(ANIM_MILLIS)
|
|
||||||
.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- LAYOUT STATE ---
|
// --- LAYOUT STATE ---
|
||||||
|
|
@ -601,11 +602,12 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val itemCount: Int
|
private val itemCount: Int
|
||||||
get() = when (val mgr = layoutManager) {
|
get() =
|
||||||
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
|
when (val mgr = layoutManager) {
|
||||||
is LinearLayoutManager -> mgr.itemCount
|
is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1
|
||||||
else -> 0
|
is LinearLayoutManager -> mgr.itemCount
|
||||||
}
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ANIM_MILLIS = 150L
|
private const val ANIM_MILLIS = 150L
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AlbumListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -49,14 +48,12 @@ class AlbumListFragment : HomeListFragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
val adapter = AlbumAdapter(
|
val adapter =
|
||||||
doOnClick = { album ->
|
AlbumAdapter(
|
||||||
findNavController().navigate(
|
doOnClick = { album ->
|
||||||
HomeFragmentDirections.actionShowAlbum(album.id)
|
findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id))
|
||||||
)
|
},
|
||||||
},
|
::newMenu)
|
||||||
::newMenu
|
|
||||||
)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums)
|
setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums)
|
||||||
|
|
||||||
|
|
@ -70,16 +67,13 @@ class AlbumListFragment : HomeListFragment() {
|
||||||
// Change how we display the popup depending on the mode.
|
// Change how we display the popup depending on the mode.
|
||||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
||||||
// By Name -> Use Name
|
// By Name -> Use Name
|
||||||
is Sort.ByName -> album.name.sliceArticle()
|
is Sort.ByName -> album.name.sliceArticle().first().uppercase()
|
||||||
.first().uppercase()
|
|
||||||
|
|
||||||
// By Artist -> Use Artist Name
|
// By Artist -> Use Artist Name
|
||||||
is Sort.ByArtist -> album.artist.resolvedName.sliceArticle()
|
is Sort.ByArtist -> album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||||
.first().uppercase()
|
|
||||||
|
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.ByYear -> album.year?.toString()
|
is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date)
|
||||||
?: getString(R.string.def_date)
|
|
||||||
|
|
||||||
// Unsupported sort, error gracefully
|
// Unsupported sort, error gracefully
|
||||||
else -> ""
|
else -> ""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AlbumListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -47,14 +46,12 @@ class ArtistListFragment : HomeListFragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
val adapter = ArtistAdapter(
|
val adapter =
|
||||||
doOnClick = { artist ->
|
ArtistAdapter(
|
||||||
findNavController().navigate(
|
doOnClick = { artist ->
|
||||||
HomeFragmentDirections.actionShowArtist(artist.id)
|
findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id))
|
||||||
)
|
},
|
||||||
},
|
::newMenu)
|
||||||
::newMenu
|
|
||||||
)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists)
|
setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists)
|
||||||
|
|
||||||
|
|
@ -63,8 +60,7 @@ class ArtistListFragment : HomeListFragment() {
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
get() = { idx ->
|
get() = { idx ->
|
||||||
homeModel.artists.value!![idx].resolvedName
|
homeModel.artists.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
||||||
.sliceArticle().first().uppercase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistAdapter(
|
class ArtistAdapter(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AlbumListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -47,14 +46,12 @@ class GenreListFragment : HomeListFragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
val adapter = GenreAdapter(
|
val adapter =
|
||||||
doOnClick = { Genre ->
|
GenreAdapter(
|
||||||
findNavController().navigate(
|
doOnClick = { Genre ->
|
||||||
HomeFragmentDirections.actionShowGenre(Genre.id)
|
findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id))
|
||||||
)
|
},
|
||||||
},
|
::newMenu)
|
||||||
::newMenu
|
|
||||||
)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres)
|
setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres)
|
||||||
|
|
||||||
|
|
@ -63,8 +60,7 @@ class GenreListFragment : HomeListFragment() {
|
||||||
|
|
||||||
override val listPopupProvider: (Int) -> String
|
override val listPopupProvider: (Int) -> String
|
||||||
get() = { idx ->
|
get() = { idx ->
|
||||||
homeModel.genres.value!![idx].resolvedName
|
homeModel.genres.value!![idx].resolvedName.sliceArticle().first().uppercase()
|
||||||
.sliceArticle().first().uppercase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreAdapter(
|
class GenreAdapter(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* HomeListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -38,9 +37,7 @@ abstract class HomeListFragment : Fragment() {
|
||||||
protected val homeModel: HomeViewModel by activityViewModels()
|
protected val homeModel: HomeViewModel by activityViewModels()
|
||||||
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
protected val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
/**
|
/** The popup provider to use for the fast scroller view. */
|
||||||
* The popup provider to use for the fast scroller view.
|
|
||||||
*/
|
|
||||||
abstract val listPopupProvider: (Int) -> String
|
abstract val listPopupProvider: (Int) -> String
|
||||||
|
|
||||||
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
protected fun <T : Item, VH : RecyclerView.ViewHolder> setupRecycler(
|
||||||
|
|
@ -56,18 +53,15 @@ abstract class HomeListFragment : Fragment() {
|
||||||
applySpans()
|
applySpans()
|
||||||
|
|
||||||
popupProvider = listPopupProvider
|
popupProvider = listPopupProvider
|
||||||
onDragListener = { dragging ->
|
onDragListener = { dragging -> homeModel.updateFastScrolling(dragging) }
|
||||||
homeModel.updateFastScrolling(dragging)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure that this RecyclerView has data before startup
|
// Make sure that this RecyclerView has data before startup
|
||||||
homeData.observe(viewLifecycleOwner) { data ->
|
homeData.observe(viewLifecycleOwner) { data -> homeAdapter.updateData(data) }
|
||||||
homeAdapter.updateData(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
abstract class HomeAdapter<T : Item, VH : RecyclerView.ViewHolder> :
|
||||||
|
RecyclerView.Adapter<VH>() {
|
||||||
protected var data = listOf<T>()
|
protected var data = listOf<T>()
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SongListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.list
|
package org.oxycblt.auxio.home.list
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -47,12 +46,7 @@ class SongListFragment : HomeListFragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
val adapter = SongsAdapter(
|
val adapter = SongsAdapter(doOnClick = { song -> playbackModel.playSong(song) }, ::newMenu)
|
||||||
doOnClick = { song ->
|
|
||||||
playbackModel.playSong(song)
|
|
||||||
},
|
|
||||||
::newMenu
|
|
||||||
)
|
|
||||||
|
|
||||||
setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs)
|
setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs)
|
||||||
|
|
||||||
|
|
@ -68,21 +62,17 @@ class SongListFragment : HomeListFragment() {
|
||||||
// based off the names of the parent objects and not the child objects.
|
// based off the names of the parent objects and not the child objects.
|
||||||
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||||
// Name -> Use name
|
// Name -> Use name
|
||||||
is Sort.ByName -> song.name.sliceArticle()
|
is Sort.ByName -> song.name.sliceArticle().first().uppercase()
|
||||||
.first().uppercase()
|
|
||||||
|
|
||||||
// Artist -> Use Artist Name
|
// Artist -> Use Artist Name
|
||||||
is Sort.ByArtist ->
|
is Sort.ByArtist ->
|
||||||
song.album.artist.resolvedName
|
song.album.artist.resolvedName.sliceArticle().first().uppercase()
|
||||||
.sliceArticle().first().uppercase()
|
|
||||||
|
|
||||||
// Album -> Use Album Name
|
// Album -> Use Album Name
|
||||||
is Sort.ByAlbum -> song.album.name.sliceArticle()
|
is Sort.ByAlbum -> song.album.name.sliceArticle().first().uppercase()
|
||||||
.first().uppercase()
|
|
||||||
|
|
||||||
// Year -> Use Full Year
|
// Year -> Use Full Year
|
||||||
is Sort.ByYear -> song.album.year?.toString()
|
is Sort.ByYear -> song.album.year?.toString() ?: getString(R.string.def_date)
|
||||||
?: getString(R.string.def_date)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Tab.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,24 +14,24 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data representation of a library tab.
|
* A data representation of a library tab. A tab can come in two moves, [Visible] or [Invisible].
|
||||||
* A tab can come in two moves, [Visible] or [Invisible]. Invisibility means that the tab
|
* Invisibility means that the tab will still be present in the customization menu, but will not be
|
||||||
* will still be present in the customization menu, but will not be shown on the home UI.
|
* shown on the home UI.
|
||||||
*
|
*
|
||||||
* Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs cannot
|
* Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs cannot
|
||||||
* be serialized on their own. Instead, they are saved as a sequence of tabs as shown below:
|
* be serialized on their own. Instead, they are saved as a sequence of tabs as shown below:
|
||||||
*
|
*
|
||||||
* 0bTAB1_TAB2_TAB3_TAB4_TAB5
|
* 0bTAB1_TAB2_TAB3_TAB4_TAB5
|
||||||
*
|
*
|
||||||
* Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists.
|
* Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists. Each
|
||||||
* Each chunk in a sequence is represented as:
|
* chunk in a sequence is represented as:
|
||||||
*
|
*
|
||||||
* VTTT
|
* VTTT
|
||||||
*
|
*
|
||||||
|
|
@ -49,14 +48,12 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
data class Invisible(override val mode: DisplayMode) : Tab(mode)
|
data class Invisible(override val mode: DisplayMode) : Tab(mode)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The length a well-formed tab sequence should be **/
|
/** The length a well-formed tab sequence should be */
|
||||||
const val SEQUENCE_LEN = 4
|
const val SEQUENCE_LEN = 4
|
||||||
/** The default tab sequence, represented in integer form **/
|
/** The default tab sequence, represented in integer form */
|
||||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
||||||
|
|
||||||
/**
|
/** Convert an array [tabs] into a sequence of tabs. */
|
||||||
* Convert an array [tabs] into a sequence of tabs.
|
|
||||||
*/
|
|
||||||
fun toSequence(tabs: Array<Tab>): Int {
|
fun toSequence(tabs: Array<Tab>): Int {
|
||||||
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
|
// Like when deserializing, make sure there are no duplicate tabs for whatever reason.
|
||||||
val distinct = tabs.distinctBy { it.mode }
|
val distinct = tabs.distinctBy { it.mode }
|
||||||
|
|
@ -65,10 +62,11 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
var shift = SEQUENCE_LEN * 4
|
var shift = SEQUENCE_LEN * 4
|
||||||
|
|
||||||
for (tab in distinct) {
|
for (tab in distinct) {
|
||||||
val bin = when (tab) {
|
val bin =
|
||||||
is Visible -> 1.shl(3) or tab.mode.ordinal
|
when (tab) {
|
||||||
is Invisible -> tab.mode.ordinal
|
is Visible -> 1.shl(3) or tab.mode.ordinal
|
||||||
}
|
is Invisible -> tab.mode.ordinal
|
||||||
|
}
|
||||||
|
|
||||||
sequence = sequence or bin.shl(shift)
|
sequence = sequence or bin.shl(shift)
|
||||||
shift -= 4
|
shift -= 4
|
||||||
|
|
@ -77,9 +75,7 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
return sequence
|
return sequence
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert a [sequence] into an array of tabs. */
|
||||||
* Convert a [sequence] into an array of tabs.
|
|
||||||
*/
|
|
||||||
fun fromSequence(sequence: Int): Array<Tab>? {
|
fun fromSequence(sequence: Int): Array<Tab>? {
|
||||||
val tabs = mutableListOf<Tab>()
|
val tabs = mutableListOf<Tab>()
|
||||||
|
|
||||||
|
|
@ -88,20 +84,22 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) {
|
for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) {
|
||||||
val chunk = sequence.shr(shift) and 0b1111
|
val chunk = sequence.shr(shift) and 0b1111
|
||||||
|
|
||||||
val mode = when (chunk and 7) {
|
val mode =
|
||||||
0 -> DisplayMode.SHOW_SONGS
|
when (chunk and 7) {
|
||||||
1 -> DisplayMode.SHOW_ALBUMS
|
0 -> DisplayMode.SHOW_SONGS
|
||||||
2 -> DisplayMode.SHOW_ARTISTS
|
1 -> DisplayMode.SHOW_ALBUMS
|
||||||
3 -> DisplayMode.SHOW_GENRES
|
2 -> DisplayMode.SHOW_ARTISTS
|
||||||
else -> continue
|
3 -> DisplayMode.SHOW_GENRES
|
||||||
}
|
else -> continue
|
||||||
|
}
|
||||||
|
|
||||||
// Figure out the visibility
|
// Figure out the visibility
|
||||||
tabs += if (chunk and 1.shl(3) != 0) {
|
tabs +=
|
||||||
Visible(mode)
|
if (chunk and 1.shl(3) != 0) {
|
||||||
} else {
|
Visible(mode)
|
||||||
Invisible(mode)
|
} else {
|
||||||
}
|
Invisible(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are no duplicate tabs
|
// Make sure there are no duplicate tabs
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* TabAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -31,7 +30,8 @@ class TabAdapter(
|
||||||
private val getTabs: () -> Array<Tab>,
|
private val getTabs: () -> Array<Tab>,
|
||||||
private val onTabSwitch: (Tab) -> Unit,
|
private val onTabSwitch: (Tab) -> Unit,
|
||||||
) : RecyclerView.Adapter<TabAdapter.TabViewHolder>() {
|
) : RecyclerView.Adapter<TabAdapter.TabViewHolder>() {
|
||||||
private val tabs: Array<Tab> get() = getTabs()
|
private val tabs: Array<Tab>
|
||||||
|
get() = getTabs()
|
||||||
|
|
||||||
override fun getItemCount(): Int = Tab.SEQUENCE_LEN
|
override fun getItemCount(): Int = Tab.SEQUENCE_LEN
|
||||||
|
|
||||||
|
|
@ -43,13 +43,12 @@ class TabAdapter(
|
||||||
holder.bind(tabs[position])
|
holder.bind(tabs[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class TabViewHolder(
|
inner class TabViewHolder(private val binding: ItemTabBinding) :
|
||||||
private val binding: ItemTabBinding
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
init {
|
||||||
binding.root.layoutParams = RecyclerView.LayoutParams(
|
binding.root.layoutParams =
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
|
RecyclerView.LayoutParams(
|
||||||
)
|
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* CustomizeListDialog.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -32,8 +31,8 @@ import org.oxycblt.auxio.ui.LifecycleDialog
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel
|
* The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel and
|
||||||
* and serializes it's state instead of
|
* serializes it's state instead of
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class TabCustomizeDialog : LifecycleDialog() {
|
class TabCustomizeDialog : LifecycleDialog() {
|
||||||
|
|
@ -58,27 +57,28 @@ class TabCustomizeDialog : LifecycleDialog() {
|
||||||
// Set up adapter & drag callback
|
// Set up adapter & drag callback
|
||||||
val callback = TabDragCallback { pendingTabs }
|
val callback = TabDragCallback { pendingTabs }
|
||||||
val helper = ItemTouchHelper(callback)
|
val helper = ItemTouchHelper(callback)
|
||||||
val tabAdapter = TabAdapter(
|
val tabAdapter =
|
||||||
helper,
|
TabAdapter(
|
||||||
getTabs = { pendingTabs },
|
helper,
|
||||||
onTabSwitch = { tab ->
|
getTabs = { pendingTabs },
|
||||||
// Don't find the specific tab [Which might be outdated due to the nature
|
onTabSwitch = { tab ->
|
||||||
// of how ViewHolders are bound], but instead simply look for the mode in
|
// Don't find the specific tab [Which might be outdated due to the nature
|
||||||
// the list of pending tabs and update that instead.
|
// of how ViewHolders are bound], but instead simply look for the mode in
|
||||||
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
|
// the list of pending tabs and update that instead.
|
||||||
if (index != -1) {
|
val index = pendingTabs.indexOfFirst { it.mode == tab.mode }
|
||||||
val curTab = pendingTabs[index]
|
if (index != -1) {
|
||||||
logD("Updating tab $curTab to $tab")
|
val curTab = pendingTabs[index]
|
||||||
pendingTabs[index] = when (curTab) {
|
logD("Updating tab $curTab to $tab")
|
||||||
is Tab.Visible -> Tab.Invisible(curTab.mode)
|
pendingTabs[index] =
|
||||||
is Tab.Invisible -> Tab.Visible(curTab.mode)
|
when (curTab) {
|
||||||
|
is Tab.Visible -> Tab.Invisible(curTab.mode)
|
||||||
|
is Tab.Invisible -> Tab.Visible(curTab.mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
|
(requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
.isEnabled = pendingTabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
callback.addTabAdapter(tabAdapter)
|
callback.addTabAdapter(tabAdapter)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* QueueDragCallback.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
|
@ -24,21 +23,18 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple [ItemTouchHelper.Callback] that handles dragging items in the tab customization menu.
|
* A simple [ItemTouchHelper.Callback] that handles dragging items in the tab customization menu.
|
||||||
* Unlike QueueAdapter's ItemTouchHelper, this one is bare and simple.
|
* Unlike QueueAdapter's ItemTouchHelper, this one is bare and simple. TODO: Consider unifying the
|
||||||
* TODO: Consider unifying the shared behavior between this and QueueDragCallback into a single
|
* shared behavior between this and QueueDragCallback into a single class.
|
||||||
* class.
|
|
||||||
*/
|
*/
|
||||||
class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.Callback() {
|
class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.Callback() {
|
||||||
private val tabs: Array<Tab> get() = getTabs()
|
private val tabs: Array<Tab>
|
||||||
|
get() = getTabs()
|
||||||
private lateinit var tabAdapter: TabAdapter
|
private lateinit var tabAdapter: TabAdapter
|
||||||
|
|
||||||
override fun getMovementFlags(
|
override fun getMovementFlags(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
viewHolder: RecyclerView.ViewHolder
|
viewHolder: RecyclerView.ViewHolder
|
||||||
): Int = makeFlag(
|
): Int = makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN)
|
||||||
ItemTouchHelper.ACTION_STATE_DRAG,
|
|
||||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onChildDraw(
|
override fun onChildDraw(
|
||||||
c: Canvas,
|
c: Canvas,
|
||||||
|
|
@ -76,8 +72,8 @@ class TabDragCallback(private val getTabs: () -> Array<Tab>) : ItemTouchHelper.C
|
||||||
override fun isLongPressDragEnabled(): Boolean = false
|
override fun isLongPressDragEnabled(): Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the tab adapter to this callback.
|
* Add the tab adapter to this callback. Done because there's a circular dependency between the
|
||||||
* Done because there's a circular dependency between the two objects
|
* two objects
|
||||||
*/
|
*/
|
||||||
fun addTabAdapter(adapter: TabAdapter) {
|
fun addTabAdapter(adapter: TabAdapter) {
|
||||||
tabAdapter = adapter
|
tabAdapter = adapter
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Models.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
|
|
@ -27,18 +26,15 @@ import androidx.annotation.StringRes
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
/**
|
/** The base for all items in Auxio. */
|
||||||
* The base for all items in Auxio.
|
|
||||||
*/
|
|
||||||
sealed class Item {
|
sealed class Item {
|
||||||
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
/** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */
|
||||||
abstract val id: Long
|
abstract val id: Long
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Item] variant that represents a music item.
|
* [Item] variant that represents a music item. TODO: Make name the actual display name and move raw
|
||||||
* TODO: Make name the actual display name and move raw names (including file names) to a new
|
* names (including file names) to a new field called rawName.
|
||||||
* field called rawName.
|
|
||||||
*/
|
*/
|
||||||
sealed class Music : Item() {
|
sealed class Music : Item() {
|
||||||
/** The raw name of this item. */
|
/** The raw name of this item. */
|
||||||
|
|
@ -46,21 +42,19 @@ sealed class Music : Item() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Music] variant that denotes that this object is a parent of other data objects, such
|
* [Music] variant that denotes that this object is a parent of other data objects, such as an
|
||||||
* as an [Album] or [Artist]
|
* [Album] or [Artist]
|
||||||
* @property resolvedName
|
* @property resolvedName
|
||||||
*/
|
*/
|
||||||
sealed class MusicParent : Music() {
|
sealed class MusicParent : Music() {
|
||||||
/**
|
/**
|
||||||
* A name resolved from it's raw form to a form suitable to be shown in a ui.
|
* A name resolved from it's raw form to a form suitable to be shown in a ui. Ex. "unknown"
|
||||||
* Ex. "unknown" would become Unknown Artist, (124) would become its proper genre name, etc.
|
* would become Unknown Artist, (124) would become its proper genre name, etc.
|
||||||
*/
|
*/
|
||||||
abstract val resolvedName: String
|
abstract val resolvedName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The data object for a song. */
|
||||||
* The data object for a song.
|
|
||||||
*/
|
|
||||||
data class Song(
|
data class Song(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
/** The file name of this song, excluding the full path. */
|
/** The file name of this song, excluding the full path. */
|
||||||
|
|
@ -82,57 +76,69 @@ data class Song(
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalMediaStoreAlbumArtistName: String?,
|
val internalMediaStoreAlbumArtistName: String?,
|
||||||
) : Music() {
|
) : Music() {
|
||||||
override val id: Long get() {
|
override val id: Long
|
||||||
var result = name.hashCode().toLong()
|
get() {
|
||||||
result = 31 * result + album.name.hashCode()
|
var result = name.hashCode().toLong()
|
||||||
result = 31 * result + album.artist.name.hashCode()
|
result = 31 * result + album.name.hashCode()
|
||||||
result = 31 * result + (track ?: 0)
|
result = 31 * result + album.artist.name.hashCode()
|
||||||
result = 31 * result + duration.hashCode()
|
result = 31 * result + (track ?: 0)
|
||||||
return result
|
result = 31 * result + duration.hashCode()
|
||||||
}
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/** The URI for this song. */
|
/** The URI for this song. */
|
||||||
val uri: Uri get() = ContentUris.withAppendedId(
|
val uri: Uri
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId
|
get() =
|
||||||
)
|
ContentUris.withAppendedId(
|
||||||
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId)
|
||||||
/** The duration of this song, in seconds (rounded down) */
|
/** The duration of this song, in seconds (rounded down) */
|
||||||
val seconds: Long get() = duration / 1000
|
val seconds: Long
|
||||||
|
get() = duration / 1000
|
||||||
/** The seconds of this song, but as a duration. */
|
/** The seconds of this song, but as a duration. */
|
||||||
val formattedDuration: String get() = seconds.toDuration(false)
|
val formattedDuration: String
|
||||||
|
get() = seconds.toDuration(false)
|
||||||
|
|
||||||
private var mAlbum: Album? = null
|
private var mAlbum: Album? = null
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
val album: Album get() = requireNotNull(mAlbum)
|
val album: Album
|
||||||
|
get() = requireNotNull(mAlbum)
|
||||||
|
|
||||||
private var mGenre: Genre? = null
|
private var mGenre: Genre? = null
|
||||||
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
/** The genre of this song. Will be an "unknown genre" if the song does not have any. */
|
||||||
val genre: Genre get() = requireNotNull(mGenre)
|
val genre: Genre
|
||||||
|
get() = requireNotNull(mGenre)
|
||||||
|
|
||||||
/** An album name resolved to this song in particular. */
|
/** An album name resolved to this song in particular. */
|
||||||
val resolvedAlbumName: String get() =
|
val resolvedAlbumName: String
|
||||||
album.resolvedName
|
get() = album.resolvedName
|
||||||
|
|
||||||
/** An artist name resolved to this song in particular. */
|
/** An artist name resolved to this song in particular. */
|
||||||
val resolvedArtistName: String get() =
|
val resolvedArtistName: String
|
||||||
internalMediaStoreArtistName ?: album.artist.resolvedName
|
get() = internalMediaStoreArtistName ?: album.artist.resolvedName
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalAlbumGroupingId: Long get() {
|
val internalAlbumGroupingId: Long
|
||||||
var result = internalGroupingArtistName.lowercase().hashCode().toLong()
|
get() {
|
||||||
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
|
var result = internalGroupingArtistName.lowercase().hashCode().toLong()
|
||||||
return result
|
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
|
||||||
}
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalGroupingArtistName: String get() = internalMediaStoreAlbumArtistName
|
val internalGroupingArtistName: String
|
||||||
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
get() =
|
||||||
|
internalMediaStoreAlbumArtistName
|
||||||
|
?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingAlbum: Boolean get() = mAlbum == null
|
val internalIsMissingAlbum: Boolean
|
||||||
|
get() = mAlbum == null
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingArtist: Boolean get() = mAlbum?.internalIsMissingArtist ?: true
|
val internalIsMissingArtist: Boolean
|
||||||
/** Internal field. Do not use. **/
|
get() = mAlbum?.internalIsMissingArtist ?: true
|
||||||
val internalIsMissingGenre: Boolean get() = mGenre == null
|
/** Internal field. Do not use. */
|
||||||
|
val internalIsMissingGenre: Boolean
|
||||||
|
get() = mGenre == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun internalLinkAlbum(album: Album) {
|
fun internalLinkAlbum(album: Album) {
|
||||||
|
|
@ -145,9 +151,7 @@ data class Song(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The data object for an album. */
|
||||||
* The data object for an album.
|
|
||||||
*/
|
|
||||||
data class Album(
|
data class Album(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
/** The latest year of the songs in this album. Null if none of the songs had metadata. */
|
/** The latest year of the songs in this album. Null if none of the songs had metadata. */
|
||||||
|
|
@ -165,34 +169,37 @@ data class Album(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val id: Long get() {
|
override val id: Long
|
||||||
var result = name.hashCode().toLong()
|
get() {
|
||||||
result = 31 * result + artist.name.hashCode()
|
var result = name.hashCode().toLong()
|
||||||
result = 31 * result + (year ?: 0)
|
result = 31 * result + artist.name.hashCode()
|
||||||
return result
|
result = 31 * result + (year ?: 0)
|
||||||
}
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
override val resolvedName: String
|
override val resolvedName: String
|
||||||
get() = name
|
get() = name
|
||||||
|
|
||||||
/** The formatted total duration of this album */
|
/** The formatted total duration of this album */
|
||||||
val totalDuration: String get() =
|
val totalDuration: String
|
||||||
songs.sumOf { it.seconds }.toDuration(false)
|
get() = songs.sumOf { it.seconds }.toDuration(false)
|
||||||
|
|
||||||
private var mArtist: Artist? = null
|
private var mArtist: Artist? = null
|
||||||
/** The parent artist of this album. */
|
/** The parent artist of this album. */
|
||||||
val artist: Artist get() = requireNotNull(mArtist)
|
val artist: Artist
|
||||||
|
get() = requireNotNull(mArtist)
|
||||||
|
|
||||||
/** The artist name, resolved to this album in particular. */
|
/** The artist name, resolved to this album in particular. */
|
||||||
val resolvedArtistName: String get() =
|
val resolvedArtistName: String
|
||||||
artist.resolvedName
|
get() = artist.resolvedName
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalArtistGroupingId: Long get() =
|
val internalArtistGroupingId: Long
|
||||||
internalGroupingArtistName.lowercase().hashCode().toLong()
|
get() = internalGroupingArtistName.lowercase().hashCode().toLong()
|
||||||
|
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val internalIsMissingArtist: Boolean get() = mArtist == null
|
val internalIsMissingArtist: Boolean
|
||||||
|
get() = mArtist == null
|
||||||
|
|
||||||
/** Internal method. Do not use. */
|
/** Internal method. Do not use. */
|
||||||
fun internalLinkArtist(artist: Artist) {
|
fun internalLinkArtist(artist: Artist) {
|
||||||
|
|
@ -201,8 +208,8 @@ data class Album(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish)
|
* The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album
|
||||||
* album artist or artist field, not the individual performers of an artist.
|
* artist or artist field, not the individual performers of an artist.
|
||||||
*/
|
*/
|
||||||
data class Artist(
|
data class Artist(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
|
|
@ -222,9 +229,7 @@ data class Artist(
|
||||||
val songs = albums.flatMap { it.songs }
|
val songs = albums.flatMap { it.songs }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The data object for a genre. */
|
||||||
* The data object for a genre.
|
|
||||||
*/
|
|
||||||
data class Genre(
|
data class Genre(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val resolvedName: String,
|
override val resolvedName: String,
|
||||||
|
|
@ -239,13 +244,11 @@ data class Genre(
|
||||||
override val id = name.hashCode().toLong()
|
override val id = name.hashCode().toLong()
|
||||||
|
|
||||||
/** The formatted total duration of this genre */
|
/** The formatted total duration of this genre */
|
||||||
val totalDuration: String get() =
|
val totalDuration: String
|
||||||
songs.sumOf { it.seconds }.toDuration(false)
|
get() = songs.sumOf { it.seconds }.toDuration(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A data object used solely for the "Header" UI element. */
|
||||||
* A data object used solely for the "Header" UI element.
|
|
||||||
*/
|
|
||||||
data class Header(
|
data class Header(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
/** The string resource used for the header. */
|
/** The string resource used for the header. */
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
|
|
@ -15,53 +32,52 @@ import org.oxycblt.auxio.util.logD
|
||||||
/**
|
/**
|
||||||
* This class acts as the base for most the black magic required to get a remotely sensible music
|
* This class acts as the base for most the black magic required to get a remotely sensible music
|
||||||
* indexing system while still optimizing for time. I would recommend you leave this module now
|
* indexing system while still optimizing for time. I would recommend you leave this module now
|
||||||
* before you lose your sanity trying to understand the hoops I had to jump through for this
|
* before you lose your sanity trying to understand the hoops I had to jump through for this system,
|
||||||
* system, but if you really want to stay, here's a debrief on why this code is so awful.
|
* but if you really want to stay, here's a debrief on why this code is so awful.
|
||||||
*
|
*
|
||||||
* MediaStore is not a good API. It is not even a bad API. Calling it a bad API is an insult to
|
* MediaStore is not a good API. It is not even a bad API. Calling it a bad API is an insult to
|
||||||
* other bad android APIs, like CoordinatorLayout or InputMethodManager. No. MediaStore is a
|
* other bad android APIs, like CoordinatorLayout or InputMethodManager. No. MediaStore is a crime
|
||||||
* crime against humanity and probably a way to summon Zalgo if you look at it the wrong way.
|
* against humanity and probably a way to summon Zalgo if you look at it the wrong way.
|
||||||
*
|
*
|
||||||
* You think that if you wanted to query a song's genre from a media database, you could just
|
* You think that if you wanted to query a song's genre from a media database, you could just put
|
||||||
* put "genre" in the query and it would return it, right? But not with MediaStore! No, that's
|
* "genre" in the query and it would return it, right? But not with MediaStore! No, that's too
|
||||||
* too straightforward for this contract that was dropped on it's head as a baby. So instead, you
|
* straightforward for this contract that was dropped on it's head as a baby. So instead, you have
|
||||||
* have to query for each genre, query all the songs in each genre, and then iterate through those
|
* to query for each genre, query all the songs in each genre, and then iterate through those songs
|
||||||
* songs to link every song with their genre. This is not documented anywhere, and the
|
* to link every song with their genre. This is not documented anywhere, and the O(mom im scared)
|
||||||
* O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's
|
* algorithm you have to run to get it working single-handedly DOUBLES Auxio's loading times. At no
|
||||||
* loading times. At no point have the devs considered that this system is absolutely insane, and
|
* point have the devs considered that this system is absolutely insane, and instead focused on
|
||||||
* instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their
|
* adding infuriat- I mean nice proprietary extensions to MediaStore for their own Google Play
|
||||||
* own Google Play Music, and of course every Google Play Music user knew how great that turned
|
* Music, and of course every Google Play Music user knew how great that turned out!
|
||||||
* out!
|
|
||||||
*
|
*
|
||||||
* It's not even ergonomics that makes this API bad. It's base implementation is completely borked
|
* It's not even ergonomics that makes this API bad. It's base implementation is completely borked
|
||||||
* as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files?
|
* as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files? I
|
||||||
* I sure didn't, until I decided to upgrade my music collection to ID3v2.4 and FLAC only to see
|
* sure didn't, until I decided to upgrade my music collection to ID3v2.4 and FLAC only to see that
|
||||||
* that the metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or
|
* the metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or DATE tag.
|
||||||
* DATE tag. Once again, this is because internally android uses an ancient in-house metadata
|
* Once again, this is because internally android uses an ancient in-house metadata parser to get
|
||||||
* parser to get everything indexed, and so far they have not bothered to modernize this parser
|
* everything indexed, and so far they have not bothered to modernize this parser or even switch it
|
||||||
* or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 has
|
* to something more powerful like Taglib, not even in Android 12. ID3v2.4 has been around for *21
|
||||||
* been around for *21 years.* *It can drink now.* All of my what.
|
* years.* *It can drink now.* All of my what.
|
||||||
*
|
*
|
||||||
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
|
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
|
||||||
* table, so we have to go for the less efficient "make a big query on all the songs lol" method
|
* table, so we have to go for the less efficient "make a big query on all the songs lol" method so
|
||||||
* so that songs don't end up fragmented across artists. Pretty much every OEM has added some
|
* that songs don't end up fragmented across artists. Pretty much every OEM has added some extension
|
||||||
* extension or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH)
|
* or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH) crippling the
|
||||||
* crippling the normal tables so that you're railroaded into their music app. The way I do
|
* normal tables so that you're railroaded into their music app. The way I do blacklisting relies on
|
||||||
* blacklisting relies on a semi-deprecated method, and the supposedly "modern" method is SLOWER and
|
* a semi-deprecated method, and the supposedly "modern" method is SLOWER and causes even more
|
||||||
* causes even more problems since I have to manage databases across version boundaries. Sometimes
|
* problems since I have to manage databases across version boundaries. Sometimes music will have a
|
||||||
* music will have a deformed clone that I can't filter out, sometimes Genres will just break for
|
* deformed clone that I can't filter out, sometimes Genres will just break for no reason, and
|
||||||
* no reason, and sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to
|
* sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to Latin-1 to *Shift
|
||||||
* Latin-1 to *Shift JIS* WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY
|
* JIS* WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY
|
||||||
*
|
*
|
||||||
* Is there anything we can do about it? No. Google has routinely shut down issues that begged google
|
* Is there anything we can do about it? No. Google has routinely shut down issues that begged
|
||||||
* to fix glaring issues with MediaStore or to just take the API behind the woodshed and shoot it.
|
* google to fix glaring issues with MediaStore or to just take the API behind the woodshed and
|
||||||
* Largely because they have zero incentive to improve it given how "obscure" local music listening
|
* shoot it. Largely because they have zero incentive to improve it given how "obscure" local music
|
||||||
* is. As a result, some players like Vanilla and VLC just hack their own pseudo-MediaStore
|
* listening is. As a result, some players like Vanilla and VLC just hack their own
|
||||||
* implementation from their own (better) parsers, but this is both infeasible for Auxio due to how
|
* pseudo-MediaStore implementation from their own (better) parsers, but this is both infeasible for
|
||||||
* incredibly slow it is to get a file handle from the android sandbox AND how much harder it is to
|
* Auxio due to how incredibly slow it is to get a file handle from the android sandbox AND how much
|
||||||
* manage a database of your own media that mirrors the filesystem perfectly. And even if I set
|
* harder it is to manage a database of your own media that mirrors the filesystem perfectly. And
|
||||||
* aside those crippling issues and changed my indexer to that, it would face the even larger
|
* even if I set aside those crippling issues and changed my indexer to that, it would face the even
|
||||||
* problem of how google keeps trying to kill the filesystem and force you into their
|
* larger problem of how google keeps trying to kill the filesystem and force you into their
|
||||||
* ContentResolver API. In the future MediaStore could be the only system we have, which is also the
|
* ContentResolver API. In the future MediaStore could be the only system we have, which is also the
|
||||||
* day that greenland melts and birthdays stop happening forever.
|
* day that greenland melts and birthdays stop happening forever.
|
||||||
*
|
*
|
||||||
|
|
@ -94,38 +110,30 @@ class MusicLoader {
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
if (song.internalIsMissingAlbum ||
|
if (song.internalIsMissingAlbum ||
|
||||||
song.internalIsMissingArtist ||
|
song.internalIsMissingArtist ||
|
||||||
song.internalIsMissingGenre
|
song.internalIsMissingGenre) {
|
||||||
) {
|
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
"Found malformed song: ${song.name} [" +
|
"Found malformed song: ${song.name} [" +
|
||||||
"album: ${!song.internalIsMissingAlbum} " +
|
"album: ${!song.internalIsMissingAlbum} " +
|
||||||
"artist: ${!song.internalIsMissingArtist} " +
|
"artist: ${!song.internalIsMissingArtist} " +
|
||||||
"genre: ${!song.internalIsMissingGenre}]"
|
"genre: ${!song.internalIsMissingGenre}]")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Library(
|
return Library(genres, artists, albums, songs)
|
||||||
genres,
|
|
||||||
artists,
|
|
||||||
albums,
|
|
||||||
songs
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a content resolver in a way that does not mangle metadata on
|
* Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See
|
||||||
* certain OEM skins. See https://github.com/OxygenCobalt/Auxio/issues/50
|
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
||||||
* for more info.
|
|
||||||
*/
|
*/
|
||||||
private val Context.contentResolverSafe: ContentResolver get() =
|
private val Context.contentResolverSafe: ContentResolver
|
||||||
applicationContext.contentResolver
|
get() = applicationContext.contentResolver
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the initial query over the song database, including excluded directory
|
* Does the initial query over the song database, including excluded directory checks. The songs
|
||||||
* checks. The songs returned by this function are **not** well-formed. The
|
* returned by this function are **not** well-formed. The companion [buildAlbums],
|
||||||
* companion [buildAlbums], [buildArtists], and [readGenres] functions must be
|
* [buildArtists], and [readGenres] functions must be called with the returned list so that all
|
||||||
* called with the returned list so that all songs are properly linked up.
|
* songs are properly linked up.
|
||||||
*/
|
*/
|
||||||
private fun loadSongs(context: Context): List<Song> {
|
private fun loadSongs(context: Context): List<Song> {
|
||||||
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
||||||
|
|
@ -146,89 +154,100 @@ class MusicLoader {
|
||||||
var songs = mutableListOf<Song>()
|
var songs = mutableListOf<Song>()
|
||||||
|
|
||||||
context.contentResolverSafe.query(
|
context.contentResolverSafe.query(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MediaStore.Audio.AudioColumns._ID,
|
MediaStore.Audio.AudioColumns._ID,
|
||||||
MediaStore.Audio.AudioColumns.TITLE,
|
MediaStore.Audio.AudioColumns.TITLE,
|
||||||
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
|
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
|
||||||
MediaStore.Audio.AudioColumns.TRACK,
|
MediaStore.Audio.AudioColumns.TRACK,
|
||||||
MediaStore.Audio.AudioColumns.DURATION,
|
MediaStore.Audio.AudioColumns.DURATION,
|
||||||
MediaStore.Audio.AudioColumns.YEAR,
|
MediaStore.Audio.AudioColumns.YEAR,
|
||||||
MediaStore.Audio.AudioColumns.ALBUM,
|
MediaStore.Audio.AudioColumns.ALBUM,
|
||||||
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
MediaStore.Audio.AudioColumns.ALBUM_ID,
|
||||||
MediaStore.Audio.AudioColumns.ARTIST,
|
MediaStore.Audio.AudioColumns.ARTIST,
|
||||||
AUDIO_COLUMN_ALBUM_ARTIST
|
AUDIO_COLUMN_ALBUM_ARTIST),
|
||||||
),
|
selector,
|
||||||
selector, args.toTypedArray(), null
|
args.toTypedArray(),
|
||||||
)?.use { cursor ->
|
null)
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
?.use { cursor ->
|
||||||
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||||
val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||||
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
val fileIndex =
|
||||||
val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||||
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
val durationIndex =
|
||||||
val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||||
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||||
val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||||
|
val albumIdIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
|
||||||
|
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
|
||||||
|
val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val title = cursor.getString(titleIndex)
|
val title = cursor.getString(titleIndex)
|
||||||
val fileName = cursor.getString(fileIndex)
|
val fileName = cursor.getString(fileIndex)
|
||||||
|
|
||||||
// The TRACK field is for some reason formatted as DTTT, where D is the disk
|
// The TRACK field is for some reason formatted as DTTT, where D is the disk
|
||||||
// and T is the track. This is dumb and insane and forces me to mangle track
|
// and T is the track. This is dumb and insane and forces me to mangle track
|
||||||
// numbers above 1000, but there is nothing we can do that won't break the app
|
// numbers above 1000, but there is nothing we can do that won't break the app
|
||||||
// below API 30.
|
// below API 30.
|
||||||
// TODO: Disk number support?
|
// TODO: Disk number support?
|
||||||
val track = cursor.getIntOrNull(trackIndex)?.mod(1000)
|
val track = cursor.getIntOrNull(trackIndex)?.mod(1000)
|
||||||
|
|
||||||
val duration = cursor.getLong(durationIndex)
|
val duration = cursor.getLong(durationIndex)
|
||||||
val year = cursor.getIntOrNull(yearIndex)
|
val year = cursor.getIntOrNull(yearIndex)
|
||||||
|
|
||||||
val album = cursor.getString(albumIndex)
|
val album = cursor.getString(albumIndex)
|
||||||
val albumId = cursor.getLong(albumIdIndex)
|
val albumId = cursor.getLong(albumIdIndex)
|
||||||
|
|
||||||
// If the artist field is <unknown>, make it null. This makes handling the
|
// If the artist field is <unknown>, make it null. This makes handling the
|
||||||
// insanity of the artist field easier later on.
|
// insanity of the artist field easier later on.
|
||||||
val artist = cursor.getStringOrNull(artistIndex)?.run {
|
val artist =
|
||||||
if (this == MediaStore.UNKNOWN_STRING) {
|
cursor.getStringOrNull(artistIndex)?.run {
|
||||||
null
|
if (this == MediaStore.UNKNOWN_STRING) {
|
||||||
} else {
|
null
|
||||||
this
|
} else {
|
||||||
}
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val albumArtist = cursor.getStringOrNull(albumArtistIndex)
|
||||||
|
|
||||||
|
// Note: Directory parsing is currently disabled until artist images are added.
|
||||||
|
// val dirs = cursor.getStringOrNull(dataIndex)?.run {
|
||||||
|
// substringBeforeLast("/", "").ifEmpty { null }
|
||||||
|
// }
|
||||||
|
|
||||||
|
songs.add(
|
||||||
|
Song(
|
||||||
|
title,
|
||||||
|
fileName,
|
||||||
|
duration,
|
||||||
|
track,
|
||||||
|
id,
|
||||||
|
year,
|
||||||
|
album,
|
||||||
|
albumId,
|
||||||
|
artist,
|
||||||
|
albumArtist,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
val albumArtist = cursor.getStringOrNull(albumArtistIndex)
|
|
||||||
|
|
||||||
// Note: Directory parsing is currently disabled until artist images are added.
|
|
||||||
// val dirs = cursor.getStringOrNull(dataIndex)?.run {
|
|
||||||
// substringBeforeLast("/", "").ifEmpty { null }
|
|
||||||
// }
|
|
||||||
|
|
||||||
songs.add(
|
|
||||||
Song(
|
|
||||||
title,
|
|
||||||
fileName,
|
|
||||||
duration,
|
|
||||||
track,
|
|
||||||
id,
|
|
||||||
year,
|
|
||||||
album,
|
|
||||||
albumId,
|
|
||||||
artist,
|
|
||||||
albumArtist,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplicate songs to prevent (most) deformed music clones
|
// Deduplicate songs to prevent (most) deformed music clones
|
||||||
songs = songs.distinctBy {
|
songs =
|
||||||
it.name to it.internalMediaStoreAlbumName to it.internalMediaStoreArtistName to
|
songs
|
||||||
it.internalMediaStoreAlbumArtistName to it.track to it.duration
|
.distinctBy {
|
||||||
}.toMutableList()
|
it.name to
|
||||||
|
it.internalMediaStoreAlbumName to
|
||||||
|
it.internalMediaStoreArtistName to
|
||||||
|
it.internalMediaStoreAlbumArtistName to
|
||||||
|
it.track to
|
||||||
|
it.duration
|
||||||
|
}
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
logD("Successfully loaded ${songs.size} songs")
|
logD("Successfully loaded ${songs.size} songs")
|
||||||
|
|
||||||
|
|
@ -236,17 +255,17 @@ class MusicLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group songs up into their respective albums. Instead of using the unreliable album or
|
* Group songs up into their respective albums. Instead of using the unreliable album or artist
|
||||||
* artist databases, we instead group up songs by their *lowercase* artist and album name
|
* databases, we instead group up songs by their *lowercase* artist and album name to create
|
||||||
* to create albums. This serves two purposes:
|
* albums. This serves two purposes:
|
||||||
* 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN".
|
* 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN". This
|
||||||
* This makes sure both of those are resolved into a single artist called "Rammstein"
|
* makes sure both of those are resolved into a single artist called "Rammstein"
|
||||||
* 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This
|
* 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This ensures
|
||||||
* ensures that all songs are unified under a single album.
|
* that all songs are unified under a single album.
|
||||||
*
|
*
|
||||||
* This does come with some costs, it's far slower than using the album ID itself, and
|
* This does come with some costs, it's far slower than using the album ID itself, and it may
|
||||||
* it may result in an unrelated album art being selected depending on the song chosen
|
* result in an unrelated album art being selected depending on the song chosen as the template,
|
||||||
* as the template, but it seems to work pretty well.
|
* but it seems to work pretty well.
|
||||||
*/
|
*/
|
||||||
private fun buildAlbums(songs: List<Song>): List<Album> {
|
private fun buildAlbums(songs: List<Song>): List<Album> {
|
||||||
val albums = mutableListOf<Album>()
|
val albums = mutableListOf<Album>()
|
||||||
|
|
@ -259,15 +278,14 @@ class MusicLoader {
|
||||||
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||||
// weird years like "0" wont show up if there are alternatives.
|
// weird years like "0" wont show up if there are alternatives.
|
||||||
// TODO: Weigh songs with null years lower than songs with zero years
|
// TODO: Weigh songs with null years lower than songs with zero years
|
||||||
val templateSong = requireNotNull(
|
val templateSong =
|
||||||
albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 }
|
requireNotNull(albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 })
|
||||||
)
|
|
||||||
val albumName = templateSong.internalMediaStoreAlbumName
|
val albumName = templateSong.internalMediaStoreAlbumName
|
||||||
val albumYear = templateSong.internalMediaStoreYear
|
val albumYear = templateSong.internalMediaStoreYear
|
||||||
val albumCoverUri = ContentUris.withAppendedId(
|
val albumCoverUri =
|
||||||
Uri.parse("content://media/external/audio/albumart"),
|
ContentUris.withAppendedId(
|
||||||
templateSong.internalMediaStoreAlbumId
|
Uri.parse("content://media/external/audio/albumart"),
|
||||||
)
|
templateSong.internalMediaStoreAlbumId)
|
||||||
val artistName = templateSong.internalGroupingArtistName
|
val artistName = templateSong.internalGroupingArtistName
|
||||||
|
|
||||||
albums.add(
|
albums.add(
|
||||||
|
|
@ -277,8 +295,7 @@ class MusicLoader {
|
||||||
albumCoverUri,
|
albumCoverUri,
|
||||||
albumSongs,
|
albumSongs,
|
||||||
artistName,
|
artistName,
|
||||||
)
|
))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully built ${albums.size} albums")
|
logD("Successfully built ${albums.size} albums")
|
||||||
|
|
@ -287,8 +304,8 @@ class MusicLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group up albums into artists. This also requires a de-duplication step due to some
|
* Group up albums into artists. This also requires a de-duplication step due to some edge cases
|
||||||
* edge cases where [buildAlbums] could not detect duplicates.
|
* where [buildAlbums] could not detect duplicates.
|
||||||
*/
|
*/
|
||||||
private fun buildArtists(context: Context, albums: List<Album>): List<Artist> {
|
private fun buildArtists(context: Context, albums: List<Album>): List<Artist> {
|
||||||
val artists = mutableListOf<Artist>()
|
val artists = mutableListOf<Artist>()
|
||||||
|
|
@ -297,19 +314,14 @@ class MusicLoader {
|
||||||
for (entry in albumsByArtist) {
|
for (entry in albumsByArtist) {
|
||||||
val templateAlbum = entry.value[0]
|
val templateAlbum = entry.value[0]
|
||||||
val artistName = templateAlbum.internalGroupingArtistName
|
val artistName = templateAlbum.internalGroupingArtistName
|
||||||
val resolvedName = when (templateAlbum.internalGroupingArtistName) {
|
val resolvedName =
|
||||||
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist)
|
when (templateAlbum.internalGroupingArtistName) {
|
||||||
else -> artistName
|
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist)
|
||||||
}
|
else -> artistName
|
||||||
|
}
|
||||||
val artistAlbums = entry.value
|
val artistAlbums = entry.value
|
||||||
|
|
||||||
artists.add(
|
artists.add(Artist(artistName, resolvedName, artistAlbums))
|
||||||
Artist(
|
|
||||||
artistName,
|
|
||||||
resolvedName,
|
|
||||||
artistAlbums
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully built ${artists.size} artists")
|
logD("Successfully built ${artists.size} artists")
|
||||||
|
|
@ -318,50 +330,45 @@ class MusicLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read all genres and link them up to the given songs. This is the code that
|
* Read all genres and link them up to the given songs. This is the code that requires me to
|
||||||
* requires me to make dozens of useless queries just to link genres up.
|
* make dozens of useless queries just to link genres up.
|
||||||
*/
|
*/
|
||||||
private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
|
private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
|
||||||
val genres = mutableListOf<Genre>()
|
val genres = mutableListOf<Genre>()
|
||||||
|
|
||||||
context.contentResolverSafe.query(
|
context.contentResolverSafe.query(
|
||||||
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
|
||||||
arrayOf(
|
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME),
|
||||||
MediaStore.Audio.Genres._ID,
|
null,
|
||||||
MediaStore.Audio.Genres.NAME
|
null,
|
||||||
),
|
null)
|
||||||
null, null, null
|
?.use { cursor ->
|
||||||
)?.use { cursor ->
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
|
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
||||||
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names are
|
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names
|
||||||
// resolved as usual, but null values don't make sense and are often junk anyway,
|
// are
|
||||||
// so we skip genres that have them.
|
// resolved as usual, but null values don't make sense and are often junk
|
||||||
val id = cursor.getLong(idIndex)
|
// anyway,
|
||||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
// so we skip genres that have them.
|
||||||
val resolvedName = name.genreNameCompat ?: name
|
val id = cursor.getLong(idIndex)
|
||||||
val genreSongs = queryGenreSongs(context, id, songs) ?: continue
|
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||||
|
val resolvedName = name.genreNameCompat ?: name
|
||||||
|
val genreSongs = queryGenreSongs(context, id, songs) ?: continue
|
||||||
|
|
||||||
genres.add(
|
genres.add(Genre(name, resolvedName, genreSongs))
|
||||||
Genre(
|
}
|
||||||
name,
|
|
||||||
resolvedName,
|
|
||||||
genreSongs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
|
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
|
||||||
if (songsWithoutGenres.isNotEmpty()) {
|
if (songsWithoutGenres.isNotEmpty()) {
|
||||||
// Songs that don't have a genre will be thrown into an unknown genre.
|
// Songs that don't have a genre will be thrown into an unknown genre.
|
||||||
val unknownGenre = Genre(
|
val unknownGenre =
|
||||||
name = MediaStore.UNKNOWN_STRING,
|
Genre(
|
||||||
resolvedName = context.getString(R.string.def_genre),
|
name = MediaStore.UNKNOWN_STRING,
|
||||||
songsWithoutGenres
|
resolvedName = context.getString(R.string.def_genre),
|
||||||
)
|
songsWithoutGenres)
|
||||||
|
|
||||||
genres.add(unknownGenre)
|
genres.add(unknownGenre)
|
||||||
}
|
}
|
||||||
|
|
@ -372,60 +379,63 @@ class MusicLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the genre name from an ID3(v2) constant. See [genreConstantTable] for the
|
* Decodes the genre name from an ID3(v2) constant. See [genreConstantTable] for the genre
|
||||||
* genre constant map that Auxio uses.
|
* constant map that Auxio uses.
|
||||||
*/
|
*/
|
||||||
private val String.genreNameCompat: String? get() {
|
private val String.genreNameCompat: String?
|
||||||
if (isDigitsOnly()) {
|
get() {
|
||||||
// ID3v1, just parse as an integer
|
if (isDigitsOnly()) {
|
||||||
return genreConstantTable.getOrNull(toInt())
|
// ID3v1, just parse as an integer
|
||||||
}
|
return genreConstantTable.getOrNull(toInt())
|
||||||
|
|
||||||
if (startsWith('(') && endsWith(')')) {
|
|
||||||
// ID3v2.3/ID3v2.4, parse out the parentheses and get the integer
|
|
||||||
// Any genres formatted as "(CHARS)" will be ignored.
|
|
||||||
val genreInt = substring(1 until lastIndex).toIntOrNull()
|
|
||||||
if (genreInt != null) {
|
|
||||||
return genreConstantTable.getOrNull(genreInt)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Current name is fine.
|
if (startsWith('(') && endsWith(')')) {
|
||||||
return null
|
// ID3v2.3/ID3v2.4, parse out the parentheses and get the integer
|
||||||
}
|
// Any genres formatted as "(CHARS)" will be ignored.
|
||||||
|
val genreInt = substring(1 until lastIndex).toIntOrNull()
|
||||||
|
if (genreInt != null) {
|
||||||
|
return genreConstantTable.getOrNull(genreInt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current name is fine.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries the genre songs for [genreId]. Some genres are insane and don't contain songs
|
* Queries the genre songs for [genreId]. Some genres are insane and don't contain songs for
|
||||||
* for some reason, so if that's the case then this function will return null.
|
* some reason, so if that's the case then this function will return null.
|
||||||
*/
|
*/
|
||||||
private fun queryGenreSongs(context: Context, genreId: Long, songs: List<Song>): List<Song>? {
|
private fun queryGenreSongs(context: Context, genreId: Long, songs: List<Song>): List<Song>? {
|
||||||
val genreSongs = mutableListOf<Song>()
|
val genreSongs = mutableListOf<Song>()
|
||||||
|
|
||||||
// Don't even bother blacklisting here as useless iterations are less expensive than IO
|
// Don't even bother blacklisting here as useless iterations are less expensive than IO
|
||||||
context.contentResolverSafe.query(
|
context.contentResolverSafe.query(
|
||||||
MediaStore.Audio.Genres.Members.getContentUri("external", genreId),
|
MediaStore.Audio.Genres.Members.getContentUri("external", genreId),
|
||||||
arrayOf(MediaStore.Audio.Genres.Members._ID),
|
arrayOf(MediaStore.Audio.Genres.Members._ID),
|
||||||
null, null, null
|
null,
|
||||||
)?.use { cursor ->
|
null,
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
null)
|
||||||
|
?.use { cursor ->
|
||||||
|
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
songs.find { it.internalMediaStoreId == id }?.let { song ->
|
songs.find { it.internalMediaStoreId == id }?.let { song ->
|
||||||
genreSongs.add(song)
|
genreSongs.add(song)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return genreSongs.ifEmpty { null }
|
return genreSongs.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* The album_artist MediaStore field has existed since at least API 21, but until API
|
* The album_artist MediaStore field has existed since at least API 21, but until API 30 it
|
||||||
* 30 it was a proprietary extension for Google Play Music and was not documented.
|
* was a proprietary extension for Google Play Music and was not documented. Since this
|
||||||
* Since this field probably works on all versions Auxio supports, we suppress the
|
* field probably works on all versions Auxio supports, we suppress the warning about using
|
||||||
* warning about using a possibly-unsupported constant.
|
* a possibly-unsupported constant.
|
||||||
*/
|
*/
|
||||||
@Suppress("InlinedApi")
|
@Suppress("InlinedApi")
|
||||||
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
||||||
|
|
@ -434,42 +444,205 @@ class MusicLoader {
|
||||||
* A complete table of all the constant genre values for ID3(v2), including non-standard
|
* A complete table of all the constant genre values for ID3(v2), including non-standard
|
||||||
* extensions.
|
* extensions.
|
||||||
*/
|
*/
|
||||||
private val genreConstantTable = arrayOf(
|
private val genreConstantTable =
|
||||||
// ID3 Standard
|
arrayOf(
|
||||||
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop",
|
// ID3 Standard
|
||||||
"Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock",
|
"Blues",
|
||||||
"Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack",
|
"Classic Rock",
|
||||||
"Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance",
|
"Country",
|
||||||
"Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise",
|
"Dance",
|
||||||
"AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
|
"Disco",
|
||||||
"Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic",
|
"Funk",
|
||||||
"Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
|
"Grunge",
|
||||||
"Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret",
|
"Hip-Hop",
|
||||||
"New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
|
"Jazz",
|
||||||
"Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock",
|
"Metal",
|
||||||
|
"New Age",
|
||||||
|
"Oldies",
|
||||||
|
"Other",
|
||||||
|
"Pop",
|
||||||
|
"R&B",
|
||||||
|
"Rap",
|
||||||
|
"Reggae",
|
||||||
|
"Rock",
|
||||||
|
"Techno",
|
||||||
|
"Industrial",
|
||||||
|
"Alternative",
|
||||||
|
"Ska",
|
||||||
|
"Death Metal",
|
||||||
|
"Pranks",
|
||||||
|
"Soundtrack",
|
||||||
|
"Euro-Techno",
|
||||||
|
"Ambient",
|
||||||
|
"Trip-Hop",
|
||||||
|
"Vocal",
|
||||||
|
"Jazz+Funk",
|
||||||
|
"Fusion",
|
||||||
|
"Trance",
|
||||||
|
"Classical",
|
||||||
|
"Instrumental",
|
||||||
|
"Acid",
|
||||||
|
"House",
|
||||||
|
"Game",
|
||||||
|
"Sound Clip",
|
||||||
|
"Gospel",
|
||||||
|
"Noise",
|
||||||
|
"AlternRock",
|
||||||
|
"Bass",
|
||||||
|
"Soul",
|
||||||
|
"Punk",
|
||||||
|
"Space",
|
||||||
|
"Meditative",
|
||||||
|
"Instrumental Pop",
|
||||||
|
"Instrumental Rock",
|
||||||
|
"Ethnic",
|
||||||
|
"Gothic",
|
||||||
|
"Darkwave",
|
||||||
|
"Techno-Industrial",
|
||||||
|
"Electronic",
|
||||||
|
"Pop-Folk",
|
||||||
|
"Eurodance",
|
||||||
|
"Dream",
|
||||||
|
"Southern Rock",
|
||||||
|
"Comedy",
|
||||||
|
"Cult",
|
||||||
|
"Gangsta",
|
||||||
|
"Top 40",
|
||||||
|
"Christian Rap",
|
||||||
|
"Pop/Funk",
|
||||||
|
"Jungle",
|
||||||
|
"Native American",
|
||||||
|
"Cabaret",
|
||||||
|
"New Wave",
|
||||||
|
"Psychadelic",
|
||||||
|
"Rave",
|
||||||
|
"Showtunes",
|
||||||
|
"Trailer",
|
||||||
|
"Lo-Fi",
|
||||||
|
"Tribal",
|
||||||
|
"Acid Punk",
|
||||||
|
"Acid Jazz",
|
||||||
|
"Polka",
|
||||||
|
"Retro",
|
||||||
|
"Musical",
|
||||||
|
"Rock & Roll",
|
||||||
|
"Hard Rock",
|
||||||
|
|
||||||
// Winamp extensions, more or less a de-facto standard
|
// Winamp extensions, more or less a de-facto standard
|
||||||
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin",
|
"Folk",
|
||||||
"Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
|
"Folk-Rock",
|
||||||
"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus",
|
"National Folk",
|
||||||
"Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music",
|
"Swing",
|
||||||
"Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam",
|
"Fast Fusion",
|
||||||
"Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul",
|
"Bebob",
|
||||||
"Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
|
"Latin",
|
||||||
"Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "Britpop",
|
"Revival",
|
||||||
"Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal",
|
"Celtic",
|
||||||
"Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa",
|
"Bluegrass",
|
||||||
"Thrash Metal", "Anime", "JPop", "Synthpop",
|
"Avantgarde",
|
||||||
|
"Gothic Rock",
|
||||||
|
"Progressive Rock",
|
||||||
|
"Psychedelic Rock",
|
||||||
|
"Symphonic Rock",
|
||||||
|
"Slow Rock",
|
||||||
|
"Big Band",
|
||||||
|
"Chorus",
|
||||||
|
"Easy Listening",
|
||||||
|
"Acoustic",
|
||||||
|
"Humour",
|
||||||
|
"Speech",
|
||||||
|
"Chanson",
|
||||||
|
"Opera",
|
||||||
|
"Chamber Music",
|
||||||
|
"Sonata",
|
||||||
|
"Symphony",
|
||||||
|
"Booty Bass",
|
||||||
|
"Primus",
|
||||||
|
"Porn Groove",
|
||||||
|
"Satire",
|
||||||
|
"Slow Jam",
|
||||||
|
"Club",
|
||||||
|
"Tango",
|
||||||
|
"Samba",
|
||||||
|
"Folklore",
|
||||||
|
"Ballad",
|
||||||
|
"Power Ballad",
|
||||||
|
"Rhythmic Soul",
|
||||||
|
"Freestyle",
|
||||||
|
"Duet",
|
||||||
|
"Punk Rock",
|
||||||
|
"Drum Solo",
|
||||||
|
"A capella",
|
||||||
|
"Euro-House",
|
||||||
|
"Dance Hall",
|
||||||
|
"Goa",
|
||||||
|
"Drum & Bass",
|
||||||
|
"Club-House",
|
||||||
|
"Hardcore",
|
||||||
|
"Terror",
|
||||||
|
"Indie",
|
||||||
|
"Britpop",
|
||||||
|
"Negerpunk",
|
||||||
|
"Polsk Punk",
|
||||||
|
"Beat",
|
||||||
|
"Christian Gangsta",
|
||||||
|
"Heavy Metal",
|
||||||
|
"Black Metal",
|
||||||
|
"Crossover",
|
||||||
|
"Contemporary Christian",
|
||||||
|
"Christian Rock",
|
||||||
|
"Merengue",
|
||||||
|
"Salsa",
|
||||||
|
"Thrash Metal",
|
||||||
|
"Anime",
|
||||||
|
"JPop",
|
||||||
|
"Synthpop",
|
||||||
|
|
||||||
// Winamp 5.6+ extensions, also used by EasyTAG.
|
// Winamp 5.6+ extensions, also used by EasyTAG.
|
||||||
// I only include this because post-rock is a based genre and deserves a slot.
|
// I only include this because post-rock is a based genre and deserves a slot.
|
||||||
"Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout",
|
"Abstract",
|
||||||
"Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental",
|
"Art Rock",
|
||||||
"Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock",
|
"Baroque",
|
||||||
"Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk",
|
"Bhangra",
|
||||||
"Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music",
|
"Big Beat",
|
||||||
"Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast",
|
"Breakbeat",
|
||||||
"Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient"
|
"Chillout",
|
||||||
)
|
"Downtempo",
|
||||||
|
"Dub",
|
||||||
|
"EBM",
|
||||||
|
"Eclectic",
|
||||||
|
"Electro",
|
||||||
|
"Electroclash",
|
||||||
|
"Emo",
|
||||||
|
"Experimental",
|
||||||
|
"Garage",
|
||||||
|
"Global",
|
||||||
|
"IDM",
|
||||||
|
"Illbient",
|
||||||
|
"Industro-Goth",
|
||||||
|
"Jam Band",
|
||||||
|
"Krautrock",
|
||||||
|
"Leftfield",
|
||||||
|
"Lounge",
|
||||||
|
"Math Rock",
|
||||||
|
"New Romantic",
|
||||||
|
"Nu-Breakz",
|
||||||
|
"Post-Punk",
|
||||||
|
"Post-Rock",
|
||||||
|
"Psytrance",
|
||||||
|
"Shoegaze",
|
||||||
|
"Space Rock",
|
||||||
|
"Trop Rock",
|
||||||
|
"World Music",
|
||||||
|
"Neoclassical",
|
||||||
|
"Audiobook",
|
||||||
|
"Audio Theatre",
|
||||||
|
"Neue Deutsche Welle",
|
||||||
|
"Podcast",
|
||||||
|
"Indie Rock",
|
||||||
|
"G-Funk",
|
||||||
|
"Dubstep",
|
||||||
|
"Garage Rock",
|
||||||
|
"Psybient")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MusicStore.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
|
@ -25,41 +24,42 @@ import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.lang.Exception
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main storage for music items.
|
* The main storage for music items. Getting an instance of this object is more complicated as it
|
||||||
* Getting an instance of this object is more complicated as it loads asynchronously.
|
* loads asynchronously. See the companion object for more. TODO: Add automatic rescanning [major
|
||||||
* See the companion object for more.
|
* change]
|
||||||
* TODO: Add automatic rescanning [major change]
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicStore private constructor() {
|
class MusicStore private constructor() {
|
||||||
private var mGenres = listOf<Genre>()
|
private var mGenres = listOf<Genre>()
|
||||||
val genres: List<Genre> get() = mGenres
|
val genres: List<Genre>
|
||||||
|
get() = mGenres
|
||||||
|
|
||||||
private var mArtists = listOf<Artist>()
|
private var mArtists = listOf<Artist>()
|
||||||
val artists: List<Artist> get() = mArtists
|
val artists: List<Artist>
|
||||||
|
get() = mArtists
|
||||||
|
|
||||||
private var mAlbums = listOf<Album>()
|
private var mAlbums = listOf<Album>()
|
||||||
val albums: List<Album> get() = mAlbums
|
val albums: List<Album>
|
||||||
|
get() = mAlbums
|
||||||
|
|
||||||
private var mSongs = listOf<Song>()
|
private var mSongs = listOf<Song>()
|
||||||
val songs: List<Song> get() = mSongs
|
val songs: List<Song>
|
||||||
|
get() = mSongs
|
||||||
|
|
||||||
/**
|
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
||||||
* Load/Sort the entire music library. Should always be ran on a coroutine.
|
|
||||||
*/
|
|
||||||
private fun load(context: Context): Response {
|
private fun load(context: Context): Response {
|
||||||
logD("Starting initial music load")
|
logD("Starting initial music load")
|
||||||
|
|
||||||
val notGranted = ContextCompat.checkSelfPermission(
|
val notGranted =
|
||||||
context, Manifest.permission.READ_EXTERNAL_STORAGE
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
) == PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
if (notGranted) {
|
if (notGranted) {
|
||||||
return Response.Err(ErrorKind.NO_PERMS)
|
return Response.Err(ErrorKind.NO_PERMS)
|
||||||
|
|
@ -69,8 +69,7 @@ class MusicStore private constructor() {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
val loader = MusicLoader()
|
val loader = MusicLoader()
|
||||||
val library = loader.load(context)
|
val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC)
|
||||||
?: return Response.Err(ErrorKind.NO_MUSIC)
|
|
||||||
|
|
||||||
mSongs = library.songs
|
mSongs = library.songs
|
||||||
mAlbums = library.albums
|
mAlbums = library.albums
|
||||||
|
|
@ -87,27 +86,22 @@ class MusicStore private constructor() {
|
||||||
return Response.Ok(this)
|
return Response.Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Find a song in a faster manner using an ID for its album as well. */
|
||||||
* Find a song in a faster manner using an ID for its album as well.
|
|
||||||
*/
|
|
||||||
fun findSongFast(songId: Long, albumId: Long): Song? {
|
fun findSongFast(songId: Long, albumId: Long): Song? {
|
||||||
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
|
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content uri.
|
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
|
||||||
|
* uri.
|
||||||
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
||||||
*/
|
*/
|
||||||
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
||||||
resolver.query(
|
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor
|
||||||
uri,
|
->
|
||||||
arrayOf(OpenableColumns.DISPLAY_NAME),
|
|
||||||
null, null, null
|
|
||||||
)?.use { cursor ->
|
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
val fileName = cursor.getString(
|
val fileName =
|
||||||
cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||||
)
|
|
||||||
|
|
||||||
return songs.find { it.fileName == fileName }
|
return songs.find { it.fileName == fileName }
|
||||||
}
|
}
|
||||||
|
|
@ -116,9 +110,8 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A response that [MusicStore] returns when loading music.
|
* A response that [MusicStore] returns when loading music. And before you ask, yes, I do like
|
||||||
* And before you ask, yes, I do like rust.
|
* rust. TODO: Add the exception to the "FAILED" ErrorKind
|
||||||
* TODO: Add the exception to the "FAILED" ErrorKind
|
|
||||||
*/
|
*/
|
||||||
sealed class Response {
|
sealed class Response {
|
||||||
class Ok(val musicStore: MusicStore) : Response()
|
class Ok(val musicStore: MusicStore) : Response()
|
||||||
|
|
@ -126,12 +119,13 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ErrorKind {
|
enum class ErrorKind {
|
||||||
NO_PERMS, NO_MUSIC, FAILED
|
NO_PERMS,
|
||||||
|
NO_MUSIC,
|
||||||
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile
|
@Volatile private var RESPONSE: Response? = null
|
||||||
private var RESPONSE: Response? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the loading process for this instance. This must be ran on a background
|
* Initialize the loading process for this instance. This must be ran on a background
|
||||||
|
|
@ -145,41 +139,41 @@ class MusicStore private constructor() {
|
||||||
return currentInstance
|
return currentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = withContext(Dispatchers.IO) {
|
val response =
|
||||||
val response = MusicStore().load(context)
|
withContext(Dispatchers.IO) {
|
||||||
synchronized(this) {
|
val response = MusicStore().load(context)
|
||||||
RESPONSE = response
|
synchronized(this) { RESPONSE = response }
|
||||||
|
response
|
||||||
}
|
}
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Await the successful creation of a [MusicStore] instance. The co-routine calling
|
* Await the successful creation of a [MusicStore] instance. The co-routine calling this
|
||||||
* this will block until the successful creation of a [MusicStore], in which it will
|
* will block until the successful creation of a [MusicStore], in which it will then be
|
||||||
* then be returned.
|
* returned.
|
||||||
*/
|
*/
|
||||||
suspend fun awaitInstance() = withContext(Dispatchers.Default) {
|
suspend fun awaitInstance() =
|
||||||
// We have to do a withContext call so we don't block the JVM thread
|
withContext(Dispatchers.Default) {
|
||||||
val musicStore: MusicStore
|
// We have to do a withContext call so we don't block the JVM thread
|
||||||
|
val musicStore: MusicStore
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val response = RESPONSE
|
val response = RESPONSE
|
||||||
|
|
||||||
if (response is Response.Ok) {
|
if (response is Response.Ok) {
|
||||||
musicStore = response.musicStore
|
musicStore = response.musicStore
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
musicStore
|
||||||
}
|
}
|
||||||
|
|
||||||
musicStore
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maybe get a MusicStore instance. This is useful if you are running code while the
|
* Maybe get a MusicStore instance. This is useful if you are running code while the loading
|
||||||
* loading process may still be going on.
|
* process may still be going on.
|
||||||
*
|
*
|
||||||
* @return null if the music store instance is still loading or if the loading process has
|
* @return null if the music store instance is still loading or if the loading process has
|
||||||
* encountered an error. An instance is returned otherwise.
|
* encountered an error. An instance is returned otherwise.
|
||||||
|
|
@ -195,9 +189,8 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require a MusicStore instance. This function is dangerous and should only be used if
|
* Require a MusicStore instance. This function is dangerous and should only be used if it's
|
||||||
* it's guaranteed that the caller's code will only be called after the initial loading
|
* guaranteed that the caller's code will only be called after the initial loading process.
|
||||||
* process.
|
|
||||||
*/
|
*/
|
||||||
fun requireInstance(): MusicStore {
|
fun requireInstance(): MusicStore {
|
||||||
return requireNotNull(maybeGetInstance()) {
|
return requireNotNull(maybeGetInstance()) {
|
||||||
|
|
@ -205,9 +198,7 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Check if this instance has successfully loaded or not. */
|
||||||
* Check if this instance has successfully loaded or not.
|
|
||||||
*/
|
|
||||||
fun loaded(): Boolean {
|
fun loaded(): Boolean {
|
||||||
return maybeGetInstance() != null
|
return maybeGetInstance() != null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MusicUtils.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
|
@ -30,8 +29,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a [Long] of seconds into a string duration.
|
* Convert a [Long] of seconds into a string duration.
|
||||||
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then
|
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
|
||||||
* --:-- will be returned if the second value is 0.
|
* will be returned if the second value is 0.
|
||||||
*/
|
*/
|
||||||
fun Long.toDuration(isElapsed: Boolean): String {
|
fun Long.toDuration(isElapsed: Boolean): String {
|
||||||
if (!isElapsed && this == 0L) {
|
if (!isElapsed && this == 0L) {
|
||||||
|
|
@ -58,11 +57,7 @@ fun TextView.bindSongInfo(song: Song?) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text = context.getString(
|
text = context.getString(R.string.fmt_two, song.resolvedArtistName, song.resolvedAlbumName)
|
||||||
R.string.fmt_two,
|
|
||||||
song.resolvedArtistName,
|
|
||||||
song.resolvedAlbumName
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("albumInfo")
|
@BindingAdapter("albumInfo")
|
||||||
|
|
@ -72,11 +67,11 @@ fun TextView.bindAlbumInfo(album: Album?) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text = context.getString(
|
text =
|
||||||
R.string.fmt_two,
|
context.getString(
|
||||||
album.resolvedArtistName,
|
R.string.fmt_two,
|
||||||
context.getPluralSafe(R.plurals.fmt_song_count, album.songs.size)
|
album.resolvedArtistName,
|
||||||
)
|
context.getPluralSafe(R.plurals.fmt_song_count, album.songs.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("artistInfo")
|
@BindingAdapter("artistInfo")
|
||||||
|
|
@ -86,11 +81,11 @@ fun TextView.bindArtistInfo(artist: Artist?) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text = context.getString(
|
text =
|
||||||
R.string.fmt_two,
|
context.getString(
|
||||||
context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size),
|
R.string.fmt_two,
|
||||||
context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size)
|
context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size),
|
||||||
)
|
context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("genreInfo")
|
@BindingAdapter("genreInfo")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* MusicViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -33,8 +32,8 @@ class MusicViewModel : ViewModel() {
|
||||||
private var isBusy = false
|
private var isBusy = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate the loading process. This is done here since HomeFragment will be the first
|
* Initiate the loading process. This is done here since HomeFragment will be the first fragment
|
||||||
* fragment navigated to and because SnackBars will have the best UX here.
|
* navigated to and because SnackBars will have the best UX here.
|
||||||
*/
|
*/
|
||||||
fun loadMusic(context: Context) {
|
fun loadMusic(context: Context) {
|
||||||
if (mLoaderResponse.value != null || isBusy) {
|
if (mLoaderResponse.value != null || isBusy) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* BlacklistDatabase.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
|
@ -28,9 +27,9 @@ import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.queryAll
|
import org.oxycblt.auxio.util.queryAll
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database for storing excluded directories.
|
* Database for storing excluded directories. Note that the paths stored here will not work with
|
||||||
* Note that the paths stored here will not work with MediaStore unless you append a "%" at the end.
|
* MediaStore unless you append a "%" at the end. Yes. I know Room exists. But that would needlessly
|
||||||
* Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
|
* bloat my app and has crippling bugs.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||||
|
|
@ -47,9 +46,7 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
|
||||||
onUpgrade(db, newVersion, oldVersion)
|
onUpgrade(db, newVersion, oldVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Write a list of [paths] to the database. */
|
||||||
* Write a list of [paths] to the database.
|
|
||||||
*/
|
|
||||||
fun writePaths(paths: List<String>) {
|
fun writePaths(paths: List<String>) {
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
|
|
@ -58,21 +55,14 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
|
||||||
logD("Deleted paths db")
|
logD("Deleted paths db")
|
||||||
|
|
||||||
for (path in paths) {
|
for (path in paths) {
|
||||||
insert(
|
insert(TABLE_NAME, null, ContentValues(1).apply { put(COLUMN_PATH, path) })
|
||||||
TABLE_NAME, null,
|
|
||||||
ContentValues(1).apply {
|
|
||||||
put(COLUMN_PATH, path)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Successfully wrote ${paths.size} paths to db")
|
logD("Successfully wrote ${paths.size} paths to db")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Get the current list of paths from the database. */
|
||||||
* Get the current list of paths from the database.
|
|
||||||
*/
|
|
||||||
fun readPaths(): List<String> {
|
fun readPaths(): List<String> {
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
|
|
@ -97,12 +87,9 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
|
||||||
const val TABLE_NAME = "blacklist_dirs_table"
|
const val TABLE_NAME = "blacklist_dirs_table"
|
||||||
const val COLUMN_PATH = "COLUMN_PATH"
|
const val COLUMN_PATH = "COLUMN_PATH"
|
||||||
|
|
||||||
@Volatile
|
@Volatile private var INSTANCE: ExcludedDatabase? = null
|
||||||
private var INSTANCE: ExcludedDatabase? = null
|
|
||||||
|
|
||||||
/**
|
/** Get/Instantiate the single instance of [ExcludedDatabase]. */
|
||||||
* Get/Instantiate the single instance of [ExcludedDatabase].
|
|
||||||
*/
|
|
||||||
fun getInstance(context: Context): ExcludedDatabase {
|
fun getInstance(context: Context): ExcludedDatabase {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* BlacklistDialog.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -57,13 +56,10 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
): View {
|
): View {
|
||||||
val binding = DialogExcludedBinding.inflate(inflater)
|
val binding = DialogExcludedBinding.inflate(inflater)
|
||||||
|
|
||||||
val adapter = ExcludedEntryAdapter { path ->
|
val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) }
|
||||||
excludedModel.removePath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
val launcher = registerForActivityResult(
|
val launcher =
|
||||||
ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
|
||||||
)
|
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
|
@ -131,9 +127,9 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
|
|
||||||
private fun parseDocTreePath(uri: Uri): String? {
|
private fun parseDocTreePath(uri: Uri): String? {
|
||||||
// Turn the raw URI into a document tree URI
|
// Turn the raw URI into a document tree URI
|
||||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(
|
val docUri =
|
||||||
uri, DocumentsContract.getTreeDocumentId(uri)
|
DocumentsContract.buildDocumentUriUsingTree(
|
||||||
)
|
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||||
|
|
||||||
// Turn it into a semi-usable path
|
// Turn it into a semi-usable path
|
||||||
val typeAndPath = DocumentsContract.getTreeDocumentId(docUri).split(":")
|
val typeAndPath = DocumentsContract.getTreeDocumentId(docUri).split(":")
|
||||||
|
|
@ -153,15 +149,11 @@ class ExcludedDialog : LifecycleDialog() {
|
||||||
|
|
||||||
private fun saveAndRestart() {
|
private fun saveAndRestart() {
|
||||||
excludedModel.save {
|
excludedModel.save {
|
||||||
playbackModel.savePlaybackState(requireContext()) {
|
playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() }
|
||||||
requireContext().hardRestart()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Get *just* the root path, nothing else is really needed. */
|
||||||
* Get *just* the root path, nothing else is really needed.
|
|
||||||
*/
|
|
||||||
private fun getRootPath(): String {
|
private fun getRootPath(): String {
|
||||||
return Environment.getExternalStorageDirectory().absolutePath
|
return Environment.getExternalStorageDirectory().absolutePath
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* BlacklistEntryAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -28,9 +27,8 @@ import org.oxycblt.auxio.util.inflater
|
||||||
* Adapter that shows the excluded directories and their "Clear" button.
|
* Adapter that shows the excluded directories and their "Clear" button.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedEntryAdapter(
|
class ExcludedEntryAdapter(private val onClear: (String) -> Unit) :
|
||||||
private val onClear: (String) -> Unit
|
RecyclerView.Adapter<ExcludedEntryAdapter.ViewHolder>() {
|
||||||
) : RecyclerView.Adapter<ExcludedEntryAdapter.ViewHolder>() {
|
|
||||||
private var paths = mutableListOf<String>()
|
private var paths = mutableListOf<String>()
|
||||||
|
|
||||||
override fun getItemCount() = paths.size
|
override fun getItemCount() = paths.size
|
||||||
|
|
@ -49,21 +47,18 @@ class ExcludedEntryAdapter(
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ViewHolder(
|
inner class ViewHolder(private val binding: ItemExcludedDirBinding) :
|
||||||
private val binding: ItemExcludedDirBinding
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
init {
|
init {
|
||||||
binding.root.layoutParams = RecyclerView.LayoutParams(
|
binding.root.layoutParams =
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
|
RecyclerView.LayoutParams(
|
||||||
)
|
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(path: String) {
|
fun bind(path: String) {
|
||||||
binding.excludedPath.text = path
|
binding.excludedPath.text = path
|
||||||
binding.excludedPath.requestLayout()
|
binding.excludedPath.requestLayout()
|
||||||
binding.excludedClear.setOnClickListener {
|
binding.excludedClear.setOnClickListener { onClear(path) }
|
||||||
onClear(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* BlacklistViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.excluded
|
package org.oxycblt.auxio.music.excluded
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -30,29 +29,28 @@ import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal
|
* ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal of
|
||||||
* of paths. Use [Factory] to instantiate this.
|
* paths. Use [Factory] to instantiate this. TODO: Unify with MusicViewModel
|
||||||
* TODO: Unify with MusicViewModel
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {
|
||||||
private val mPaths = MutableLiveData(mutableListOf<String>())
|
private val mPaths = MutableLiveData(mutableListOf<String>())
|
||||||
val paths: LiveData<MutableList<String>> get() = mPaths
|
val paths: LiveData<MutableList<String>>
|
||||||
|
get() = mPaths
|
||||||
|
|
||||||
private var dbPaths = listOf<String>()
|
private var dbPaths = listOf<String>()
|
||||||
|
|
||||||
/**
|
/** Check if changes have been made to the ViewModel's paths. */
|
||||||
* Check if changes have been made to the ViewModel's paths.
|
val isModified: Boolean
|
||||||
*/
|
get() = dbPaths != paths.value
|
||||||
val isModified: Boolean get() = dbPaths != paths.value
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadDatabasePaths()
|
loadDatabasePaths()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a path to this ViewModel. It will not write the path to the database unless
|
* Add a path to this ViewModel. It will not write the path to the database unless [save] is
|
||||||
* [save] is called.
|
* called.
|
||||||
*/
|
*/
|
||||||
fun addPath(path: String) {
|
fun addPath(path: String) {
|
||||||
if (!mPaths.value!!.contains(path)) {
|
if (!mPaths.value!!.contains(path)) {
|
||||||
|
|
@ -70,9 +68,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
mPaths.value = mPaths.value
|
mPaths.value = mPaths.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Save the pending paths to the database. [onDone] will be called on completion. */
|
||||||
* Save the pending paths to the database. [onDone] will be called on completion.
|
|
||||||
*/
|
|
||||||
fun save(onDone: () -> Unit) {
|
fun save(onDone: () -> Unit) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
@ -80,24 +76,18 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo
|
||||||
dbPaths = mPaths.value!!
|
dbPaths = mPaths.value!!
|
||||||
onDone()
|
onDone()
|
||||||
this@ExcludedViewModel.logD(
|
this@ExcludedViewModel.logD(
|
||||||
"Path save completed successfully in ${System.currentTimeMillis() - start}ms"
|
"Path save completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Load the paths stored in the database to this ViewModel, will erase any pending changes. */
|
||||||
* Load the paths stored in the database to this ViewModel, will erase any pending changes.
|
|
||||||
*/
|
|
||||||
private fun loadDatabasePaths() {
|
private fun loadDatabasePaths() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
dbPaths = excludedDatabase.readPaths()
|
dbPaths = excludedDatabase.readPaths()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() }
|
||||||
mPaths.value = dbPaths.toMutableList()
|
|
||||||
}
|
|
||||||
this@ExcludedViewModel.logD(
|
this@ExcludedViewModel.logD(
|
||||||
"Path load completed successfully in ${System.currentTimeMillis() - start}ms"
|
"Path load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* CompactPlaybackView.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -35,14 +34,13 @@ import org.oxycblt.auxio.util.inflater
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view displaying the playback state in a compact manner. This is only meant to be used
|
* A view displaying the playback state in a compact manner. This is only meant to be used by
|
||||||
* by [PlaybackLayout].
|
* [PlaybackLayout].
|
||||||
*/
|
*/
|
||||||
class PlaybackBarView @JvmOverloads constructor(
|
class PlaybackBarView
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||||
defStyleAttr: Int = 0
|
ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
|
||||||
private val binding = ViewPlaybackBarBinding.inflate(context.inflater, this, true)
|
private val binding = ViewPlaybackBarBinding.inflate(context.inflater, this, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -52,35 +50,29 @@ class PlaybackBarView @JvmOverloads constructor(
|
||||||
// we use colorSecondary instead of colorSurfaceVariant. This is because
|
// we use colorSecondary instead of colorSurfaceVariant. This is because
|
||||||
// colorSurfaceVariant is used with the assumption that the view that is using it is
|
// colorSurfaceVariant is used with the assumption that the view that is using it is
|
||||||
// not elevated and is therefore not colored. This view is elevated.
|
// not elevated and is therefore not colored. This view is elevated.
|
||||||
binding.playbackProgressBar.trackColor = MaterialColors.compositeARGBWithAlpha(
|
binding.playbackProgressBar.trackColor =
|
||||||
context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()
|
MaterialColors.compositeARGBWithAlpha(
|
||||||
)
|
context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
// Since we swipe up this view, we need to make sure it does not collide with
|
// Since we swipe up this view, we need to make sure it does not collide with
|
||||||
// any gesture events. So, apply the system gesture insets if present and then
|
// any gesture events. So, apply the system gesture insets if present and then
|
||||||
// only default to the system bar insets when there are no other options.
|
// only default to the system bar insets when there are no other options.
|
||||||
val gesturePadding = when {
|
val gesturePadding =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
when {
|
||||||
insets.getInsets(WindowInsets.Type.systemGestures()).bottom
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
|
insets.getInsets(WindowInsets.Type.systemGestures()).bottom
|
||||||
|
}
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
|
||||||
|
@Suppress("DEPRECATION") insets.systemGestureInsets.bottom
|
||||||
|
}
|
||||||
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
insets.systemGestureInsets.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePadding(
|
updatePadding(
|
||||||
bottom =
|
bottom =
|
||||||
if (gesturePadding != 0)
|
if (gesturePadding != 0) gesturePadding else insets.systemBarInsetsCompat.bottom)
|
||||||
gesturePadding
|
|
||||||
else
|
|
||||||
insets.systemBarInsetsCompat.bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
|
|
@ -91,23 +83,15 @@ class PlaybackBarView @JvmOverloads constructor(
|
||||||
viewLifecycleOwner: LifecycleOwner
|
viewLifecycleOwner: LifecycleOwner
|
||||||
) {
|
) {
|
||||||
setOnLongClickListener {
|
setOnLongClickListener {
|
||||||
playbackModel.song.value?.let { song ->
|
playbackModel.song.value?.let { song -> detailModel.navToItem(song) }
|
||||||
detailModel.navToItem(song)
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackSkipPrev?.setOnClickListener {
|
binding.playbackSkipPrev?.setOnClickListener { playbackModel.skipPrev() }
|
||||||
playbackModel.skipPrev()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackPlayPause.setOnClickListener {
|
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlayingStatus() }
|
||||||
playbackModel.invertPlayingStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackSkipNext?.setOnClickListener {
|
binding.playbackSkipNext?.setOnClickListener { playbackModel.skipNext() }
|
||||||
playbackModel.skipNext()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!!
|
binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -14,20 +31,19 @@ import org.oxycblt.auxio.util.getDrawableSafe
|
||||||
/**
|
/**
|
||||||
* An [AppCompatImageButton] designed for the buttons used in the playback display.
|
* An [AppCompatImageButton] designed for the buttons used in the playback display.
|
||||||
*
|
*
|
||||||
* Auxio's playback buttons have never followed the typical 24dp icon size that all
|
* Auxio's playback buttons have never followed the typical 24dp icon size that all other UI
|
||||||
* other UI elements do, mostly because those icons just look bad at that size with
|
* elements do, mostly because those icons just look bad at that size with all the gobs of
|
||||||
* all the gobs of whitespace surrounding them. So, this view resizes the icons to a
|
* whitespace surrounding them. So, this view resizes the icons to a fixed 32dp in a way that
|
||||||
* fixed 32dp in a way that doesn't require a whole new icon set.
|
* doesn't require a whole new icon set.
|
||||||
*
|
*
|
||||||
* This view also enables use of an "indicator", which is a dot that can denote when a
|
* This view also enables use of an "indicator", which is a dot that can denote when a button is
|
||||||
* button is active. This is useful for the shuffle/loop buttons, as at times highlighting
|
* active. This is useful for the shuffle/loop buttons, as at times highlighting them is not enough
|
||||||
* them is not enough to differentiate them.
|
* to differentiate them.
|
||||||
*/
|
*/
|
||||||
class PlaybackButton @JvmOverloads constructor(
|
class PlaybackButton
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
AppCompatImageButton(context, attrs, defStyleAttr) {
|
||||||
) : AppCompatImageButton(context, attrs, defStyleAttr) {
|
|
||||||
private val iconSize = context.getDimenSizeSafe(R.dimen.size_playback_icon)
|
private val iconSize = context.getDimenSizeSafe(R.dimen.size_playback_icon)
|
||||||
private val centerMatrix = Matrix()
|
private val centerMatrix = Matrix()
|
||||||
private val matrixSrc = RectF()
|
private val matrixSrc = RectF()
|
||||||
|
|
@ -55,31 +71,35 @@ class PlaybackButton @JvmOverloads constructor(
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
|
||||||
imageMatrix = centerMatrix.apply {
|
imageMatrix =
|
||||||
reset()
|
centerMatrix.apply {
|
||||||
drawable?.let { drawable ->
|
reset()
|
||||||
// Android is too good to allow us to set a fixed image size, so we instead need
|
drawable?.let { drawable ->
|
||||||
// to define a matrix to scale an image directly.
|
// Android is too good to allow us to set a fixed image size, so we instead need
|
||||||
|
// to define a matrix to scale an image directly.
|
||||||
|
|
||||||
// First scale the icon up to the desired size.
|
// First scale the icon up to the desired size.
|
||||||
matrixSrc.set(0f, 0f, drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat())
|
matrixSrc.set(
|
||||||
matrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
|
0f,
|
||||||
centerMatrix.setRectToRect(matrixSrc, matrixDst, Matrix.ScaleToFit.CENTER)
|
0f,
|
||||||
|
drawable.intrinsicWidth.toFloat(),
|
||||||
|
drawable.intrinsicHeight.toFloat())
|
||||||
|
matrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat())
|
||||||
|
centerMatrix.setRectToRect(matrixSrc, matrixDst, Matrix.ScaleToFit.CENTER)
|
||||||
|
|
||||||
// Then actually center it into the icon, which the previous call does not actually do.
|
// Then actually center it into the icon, which the previous call does not
|
||||||
centerMatrix.postTranslate(
|
// actually do.
|
||||||
(measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f
|
centerMatrix.postTranslate(
|
||||||
)
|
(measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Put the indicator right below the icon.
|
// Put the indicator right below the icon.
|
||||||
val x = (measuredWidth - indicatorDrawable.intrinsicWidth) / 2
|
val x = (measuredWidth - indicatorDrawable.intrinsicWidth) / 2
|
||||||
val y = ((measuredHeight - iconSize) / 2) + iconSize
|
val y = ((measuredHeight - iconSize) / 2) + iconSize
|
||||||
|
|
||||||
indicatorDrawable.bounds.set(
|
indicatorDrawable.bounds.set(
|
||||||
x, y, x + indicatorDrawable.intrinsicWidth, y + indicatorDrawable.intrinsicHeight
|
x, y, x + indicatorDrawable.intrinsicWidth, y + indicatorDrawable.intrinsicHeight)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -38,8 +37,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
/**
|
/**
|
||||||
* A [Fragment] that displays more information about the song, along with more media controls.
|
* A [Fragment] that displays more information about the song, along with more media controls.
|
||||||
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
* Instantiation is done by the navigation component, **do not instantiate this fragment manually.**
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt TODO: Handle RTL correctly in the playback buttons
|
||||||
* TODO: Handle RTL correctly in the playback buttons
|
|
||||||
*/
|
*/
|
||||||
class PlaybackFragment : Fragment() {
|
class PlaybackFragment : Fragment() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
@ -66,18 +64,13 @@ class PlaybackFragment : Fragment() {
|
||||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||||
val bars = insets.systemBarInsetsCompat
|
val bars = insets.systemBarInsetsCompat
|
||||||
|
|
||||||
binding.root.updatePadding(
|
binding.root.updatePadding(top = bars.top, bottom = bars.bottom)
|
||||||
top = bars.top,
|
|
||||||
bottom = bars.bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playbackToolbar.apply {
|
binding.playbackToolbar.apply {
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener { navigateUp() }
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
setOnMenuItemClickListener { item ->
|
||||||
if (item.itemId == R.id.action_queue) {
|
if (item.itemId == R.id.action_queue) {
|
||||||
|
|
@ -96,9 +89,7 @@ class PlaybackFragment : Fragment() {
|
||||||
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
|
binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition
|
||||||
|
|
||||||
// Abuse the play/pause FAB (see style definition for more info)
|
// Abuse the play/pause FAB (see style definition for more info)
|
||||||
binding.playbackPlayPause.post {
|
binding.playbackPlayPause.post { binding.playbackPlayPause.stateListAnimator = null }
|
||||||
binding.playbackPlayPause.stateListAnimator = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP --
|
// --- VIEWMODEL SETUP --
|
||||||
|
|
||||||
|
|
@ -114,8 +105,8 @@ class PlaybackFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
playbackModel.parent.observe(viewLifecycleOwner) { parent ->
|
||||||
binding.playbackToolbar.subtitle = parent?.resolvedName
|
binding.playbackToolbar.subtitle =
|
||||||
?: getString(R.string.lbl_all_songs)
|
parent?.resolvedName ?: getString(R.string.lbl_all_songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling ->
|
||||||
|
|
@ -123,11 +114,12 @@ class PlaybackFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackModel.loopMode.observe(viewLifecycleOwner) { loopMode ->
|
playbackModel.loopMode.observe(viewLifecycleOwner) { loopMode ->
|
||||||
val resId = when (loopMode) {
|
val resId =
|
||||||
LoopMode.NONE, null -> R.drawable.ic_loop
|
when (loopMode) {
|
||||||
LoopMode.ALL -> R.drawable.ic_loop_on
|
LoopMode.NONE, null -> R.drawable.ic_loop
|
||||||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
LoopMode.ALL -> R.drawable.ic_loop_on
|
||||||
}
|
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||||
|
}
|
||||||
|
|
||||||
binding.playbackLoop.apply {
|
binding.playbackLoop.apply {
|
||||||
isActivated = loopMode != LoopMode.NONE
|
isActivated = loopMode != LoopMode.NONE
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -19,6 +36,9 @@ import androidx.core.view.isInvisible
|
||||||
import androidx.customview.widget.ViewDragHelper
|
import androidx.customview.widget.ViewDragHelper
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.DetailViewModel
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
|
|
@ -32,31 +52,30 @@ import org.oxycblt.auxio.util.pxOfDp
|
||||||
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.stateList
|
import org.oxycblt.auxio.util.stateList
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This layout handles pretty much every aspect of the playback UI flow, notably the playback
|
* This layout handles pretty much every aspect of the playback UI flow, notably the playback bar
|
||||||
* bar and it's ability to slide up into the playback view. It's a blend of Hai Zhang's
|
* and it's ability to slide up into the playback view. It's a blend of Hai Zhang's
|
||||||
* PersistentBarLayout and Umano's SlidingUpPanelLayout, albeit heavily minified to remove
|
* PersistentBarLayout and Umano's SlidingUpPanelLayout, albeit heavily minified to remove
|
||||||
* extraneous use cases and updated to support the latest SDK level and androidx tools.
|
* extraneous use cases and updated to support the latest SDK level and androidx tools.
|
||||||
*
|
*
|
||||||
* **Note:** If you want to adapt this layout into your own app. Good luck. This layout has been
|
* **Note:** If you want to adapt this layout into your own app. Good luck. This layout has been
|
||||||
* reduced to Auxio's use case in particular and is really hard to understand since it has a ton
|
* reduced to Auxio's use case in particular and is really hard to understand since it has a ton of
|
||||||
* of state and view magic. I tried my best to document it, but it's probably not the most friendly
|
* state and view magic. I tried my best to document it, but it's probably not the most friendly or
|
||||||
* or extendable. You have been warned.
|
* extendable. You have been warned.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt (With help from Umano and Hai Zhang)
|
* @author OxygenCobalt (With help from Umano and Hai Zhang) TODO: Find a better way to handle
|
||||||
* TODO: Find a better way to handle PlaybackFragment in general (navigation, creation)
|
* PlaybackFragment in general (navigation, creation)
|
||||||
*/
|
*/
|
||||||
class PlaybackLayout @JvmOverloads constructor(
|
class PlaybackLayout
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
|
||||||
defStyle: Int = 0
|
ViewGroup(context, attrs, defStyle) {
|
||||||
) : ViewGroup(context, attrs, defStyle) {
|
|
||||||
private enum class PanelState {
|
private enum class PanelState {
|
||||||
EXPANDED, COLLAPSED, HIDDEN, DRAGGING
|
EXPANDED,
|
||||||
|
COLLAPSED,
|
||||||
|
HIDDEN,
|
||||||
|
DRAGGING
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var contentView: View
|
private lateinit var contentView: View
|
||||||
|
|
@ -67,20 +86,19 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
private val playbackContainerBg: MaterialShapeDrawable
|
private val playbackContainerBg: MaterialShapeDrawable
|
||||||
private val playbackFragment = PlaybackFragment()
|
private val playbackFragment = PlaybackFragment()
|
||||||
|
|
||||||
/**
|
/** The drag helper that animates and dispatches drag events to the panels. */
|
||||||
* The drag helper that animates and dispatches drag events to the panels.
|
private val dragHelper =
|
||||||
*/
|
ViewDragHelper.create(this, DragHelperCallback()).apply {
|
||||||
private val dragHelper = ViewDragHelper.create(this, DragHelperCallback()).apply {
|
minVelocity = MIN_FLING_VEL * resources.displayMetrics.density
|
||||||
minVelocity = MIN_FLING_VEL * resources.displayMetrics.density
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current window insets.
|
* The current window insets. Important since this layout must play a long with Auxio's
|
||||||
* Important since this layout must play a long with Auxio's edge-to-edge functionality.
|
* edge-to-edge functionality.
|
||||||
*/
|
*/
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
|
|
||||||
/** The current panel state. Can be [PanelState.DRAGGING]*/
|
/** The current panel state. Can be [PanelState.DRAGGING] */
|
||||||
private var panelState = INIT_PANEL_STATE
|
private var panelState = INIT_PANEL_STATE
|
||||||
|
|
||||||
/** The last panel state before a drag event began. */
|
/** The last panel state before a drag event began. */
|
||||||
|
|
@ -90,10 +108,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
private var panelRange = 0
|
private var panelRange = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relative offset of this panel as a percentage of [panelRange].
|
* The relative offset of this panel as a percentage of [panelRange]. A value of 1 means a fully
|
||||||
* A value of 1 means a fully expanded panel.
|
* expanded panel. A value of 0 means a collapsed panel. A value below 0 means a hidden panel.
|
||||||
* A value of 0 means a collapsed panel.
|
|
||||||
* A value below 0 means a hidden panel.
|
|
||||||
*/
|
*/
|
||||||
private var panelOffset = 0f
|
private var panelOffset = 0f
|
||||||
|
|
||||||
|
|
@ -105,88 +121,96 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
private val elevationNormal = context.getDimenSafe(R.dimen.elevation_normal)
|
private val elevationNormal = context.getDimenSafe(R.dimen.elevation_normal)
|
||||||
|
|
||||||
/** See [isDragging] */
|
/** See [isDragging] */
|
||||||
private val dragStateField = ViewDragHelper::class.java.getDeclaredField("mDragState").apply {
|
private val dragStateField =
|
||||||
isAccessible = true
|
ViewDragHelper::class.java.getDeclaredField("mDragState").apply { isAccessible = true }
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setWillNotDraw(false)
|
setWillNotDraw(false)
|
||||||
|
|
||||||
// Set up our playback views. Doing this allows us to abstract away the implementation
|
// Set up our playback views. Doing this allows us to abstract away the implementation
|
||||||
// of these views from the user of this layout [MainFragment].
|
// of these views from the user of this layout [MainFragment].
|
||||||
playbackContainerView = FrameLayout(context).apply {
|
playbackContainerView =
|
||||||
id = R.id.playback_container
|
FrameLayout(context).apply {
|
||||||
|
id = R.id.playback_container
|
||||||
|
|
||||||
isClickable = true
|
isClickable = true
|
||||||
isFocusable = false
|
isFocusable = false
|
||||||
isFocusableInTouchMode = false
|
isFocusableInTouchMode = false
|
||||||
|
|
||||||
playbackContainerBg = MaterialShapeDrawable.createWithElevationOverlay(context).apply {
|
playbackContainerBg =
|
||||||
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
|
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
|
||||||
elevation = context.pxOfDp(elevationNormal).toFloat()
|
fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList
|
||||||
|
elevation = context.pxOfDp(elevationNormal).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The way we fade out the elevation overlay is not by actually reducing the
|
||||||
|
// elevation
|
||||||
|
// but by fading out the background drawable itself. To be safe, we apply this
|
||||||
|
// background drawable to a layer list with another colorSurface shape drawable,
|
||||||
|
// just
|
||||||
|
// in case weird things happen if background drawable is completely transparent.
|
||||||
|
background =
|
||||||
|
(context.getDrawableSafe(R.drawable.ui_panel_bg) as LayerDrawable).apply {
|
||||||
|
setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableDropShadowCompat()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The way we fade out the elevation overlay is not by actually reducing the elevation
|
playbackBarView =
|
||||||
// but by fading out the background drawable itself. To be safe, we apply this
|
PlaybackBarView(context).apply {
|
||||||
// background drawable to a layer list with another colorSurface shape drawable, just
|
id = R.id.playback_bar
|
||||||
// in case weird things happen if background drawable is completely transparent.
|
|
||||||
background = (context.getDrawableSafe(R.drawable.ui_panel_bg) as LayerDrawable).apply {
|
|
||||||
setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg)
|
|
||||||
}
|
|
||||||
|
|
||||||
disableDropShadowCompat()
|
playbackContainerView.addView(this)
|
||||||
}
|
|
||||||
|
|
||||||
playbackBarView = PlaybackBarView(context).apply {
|
(layoutParams as FrameLayout.LayoutParams).apply {
|
||||||
id = R.id.playback_bar
|
width = LayoutParams.MATCH_PARENT
|
||||||
|
height = LayoutParams.WRAP_CONTENT
|
||||||
|
gravity = Gravity.TOP
|
||||||
|
}
|
||||||
|
|
||||||
playbackContainerView.addView(this)
|
// The bar view if clicked will expand into the full panel
|
||||||
|
setOnClickListener {
|
||||||
(layoutParams as FrameLayout.LayoutParams).apply {
|
if (canSlide && panelState != PanelState.EXPANDED) {
|
||||||
width = LayoutParams.MATCH_PARENT
|
applyState(PanelState.EXPANDED)
|
||||||
height = LayoutParams.WRAP_CONTENT
|
}
|
||||||
gravity = Gravity.TOP
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bar view if clicked will expand into the full panel
|
|
||||||
setOnClickListener {
|
|
||||||
if (canSlide && panelState != PanelState.EXPANDED) {
|
|
||||||
applyState(PanelState.EXPANDED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
playbackPanelView = FrameLayout(context).apply {
|
playbackPanelView =
|
||||||
playbackContainerView.addView(this)
|
FrameLayout(context).apply {
|
||||||
|
playbackContainerView.addView(this)
|
||||||
|
|
||||||
(layoutParams as FrameLayout.LayoutParams).apply {
|
(layoutParams as FrameLayout.LayoutParams).apply {
|
||||||
width = LayoutParams.MATCH_PARENT
|
width = LayoutParams.MATCH_PARENT
|
||||||
height = LayoutParams.MATCH_PARENT
|
height = LayoutParams.MATCH_PARENT
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
id = R.id.playback_panel
|
||||||
|
|
||||||
|
// Make sure we add our fragment to this view. This is actually a replace operation
|
||||||
|
// since we don't want to stack fragments but we can't ensure that this view doesn't
|
||||||
|
// already have a fragment attached.
|
||||||
|
try {
|
||||||
|
(context as AppCompatActivity)
|
||||||
|
.supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.playback_panel, playbackFragment)
|
||||||
|
.commit()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Band-aid to stop the app crashing if we have to swap out the content view
|
||||||
|
// without warning (which we have to do sometimes because android is the worst
|
||||||
|
// thing ever)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id = R.id.playback_panel
|
|
||||||
|
|
||||||
// Make sure we add our fragment to this view. This is actually a replace operation
|
|
||||||
// since we don't want to stack fragments but we can't ensure that this view doesn't
|
|
||||||
// already have a fragment attached.
|
|
||||||
try {
|
|
||||||
(context as AppCompatActivity).supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.playback_panel, playbackFragment)
|
|
||||||
.commit()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Band-aid to stop the app crashing if we have to swap out the content view
|
|
||||||
// without warning (which we have to do sometimes because android is the worst
|
|
||||||
// thing ever)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// / --- CONTROL METHODS ---
|
// / --- CONTROL METHODS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the song that this layout is showing. This will be reflected in the compact view
|
* Update the song that this layout is showing. This will be reflected in the compact view at
|
||||||
* at the bottom of the screen.
|
* the bottom of the screen.
|
||||||
*/
|
*/
|
||||||
fun setup(
|
fun setup(
|
||||||
playbackModel: PlaybackViewModel,
|
playbackModel: PlaybackViewModel,
|
||||||
|
|
@ -195,9 +219,7 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
) {
|
) {
|
||||||
setSong(playbackModel.song.value)
|
setSong(playbackModel.song.value)
|
||||||
|
|
||||||
playbackModel.song.observe(viewLifecycleOwner) { song ->
|
playbackModel.song.observe(viewLifecycleOwner) { song -> setSong(song) }
|
||||||
setSong(song)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackBarView.setup(playbackModel, detailModel, viewLifecycleOwner)
|
playbackBarView.setup(playbackModel, detailModel, viewLifecycleOwner)
|
||||||
}
|
}
|
||||||
|
|
@ -243,7 +265,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLaidOut) {
|
if (!isLaidOut) {
|
||||||
// Not laid out, just apply the state and let the measure + layout steps apply it for us.
|
// Not laid out, just apply the state and let the measure + layout steps apply it for
|
||||||
|
// us.
|
||||||
setPanelStateInternal(state)
|
setPanelStateInternal(state)
|
||||||
} else {
|
} else {
|
||||||
// We are laid out. In this case we actually animate to our desired target.
|
// We are laid out. In this case we actually animate to our desired target.
|
||||||
|
|
@ -293,11 +316,12 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
if (!isLaidOut) {
|
if (!isLaidOut) {
|
||||||
// This is our first layout, so make sure we know what offset we should work with
|
// This is our first layout, so make sure we know what offset we should work with
|
||||||
// before we measure our content
|
// before we measure our content
|
||||||
panelOffset = when (panelState) {
|
panelOffset =
|
||||||
PanelState.EXPANDED -> 1.0f
|
when (panelState) {
|
||||||
PanelState.HIDDEN -> computePanelOffset(measuredHeight)
|
PanelState.EXPANDED -> 1.0f
|
||||||
else -> 0f
|
PanelState.HIDDEN -> computePanelOffset(measuredHeight)
|
||||||
}
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
updatePanelTransition()
|
updatePanelTransition()
|
||||||
}
|
}
|
||||||
|
|
@ -315,9 +339,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
// Note that these views will always be a fixed MATCH_PARENT. This is intentional,
|
// Note that these views will always be a fixed MATCH_PARENT. This is intentional,
|
||||||
// as it reduces the logic we have to deal with regarding WRAP_CONTENT views.
|
// as it reduces the logic we have to deal with regarding WRAP_CONTENT views.
|
||||||
val contentWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
|
val contentWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
|
||||||
val contentHeightSpec = MeasureSpec.makeMeasureSpec(
|
val contentHeightSpec =
|
||||||
measuredHeight - barHeightAdjusted, MeasureSpec.EXACTLY
|
MeasureSpec.makeMeasureSpec(measuredHeight - barHeightAdjusted, MeasureSpec.EXACTLY)
|
||||||
)
|
|
||||||
|
|
||||||
contentView.measure(contentWidthSpec, contentHeightSpec)
|
contentView.measure(contentWidthSpec, contentHeightSpec)
|
||||||
}
|
}
|
||||||
|
|
@ -330,8 +353,7 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
0,
|
0,
|
||||||
panelTop,
|
panelTop,
|
||||||
playbackContainerView.measuredWidth,
|
playbackContainerView.measuredWidth,
|
||||||
playbackContainerView.measuredHeight + panelTop
|
playbackContainerView.measuredHeight + panelTop)
|
||||||
)
|
|
||||||
|
|
||||||
layoutContent()
|
layoutContent()
|
||||||
}
|
}
|
||||||
|
|
@ -352,9 +374,7 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
canvas.clipRect(tRect)
|
canvas.clipRect(tRect)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.drawChild(canvas, child, drawingTime).also {
|
return super.drawChild(canvas, child, drawingTime).also { canvas.restoreToCount(save) }
|
||||||
canvas.restoreToCount(save)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
|
|
@ -369,8 +389,8 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply window insets to the content views in this layouts. This is done separately as at
|
* Apply window insets to the content views in this layouts. This is done separately as at times
|
||||||
* times we want to re-inset the content views but not re-inset the bar view.
|
* we want to re-inset the content views but not re-inset the bar view.
|
||||||
*/
|
*/
|
||||||
private fun applyContentWindowInsets() {
|
private fun applyContentWindowInsets() {
|
||||||
val insets = lastInsets
|
val insets = lastInsets
|
||||||
|
|
@ -379,9 +399,7 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Adjust window insets to line up with the panel */
|
||||||
* Adjust window insets to line up with the panel
|
|
||||||
*/
|
|
||||||
private fun adjustInsets(insets: WindowInsets): WindowInsets {
|
private fun adjustInsets(insets: WindowInsets): WindowInsets {
|
||||||
// We kind to do a reverse-measure to figure out how we should inset this view.
|
// We kind to do a reverse-measure to figure out how we should inset this view.
|
||||||
// Find how much space is lost by the panel and then combine that with the
|
// Find how much space is lost by the panel and then combine that with the
|
||||||
|
|
@ -390,21 +408,20 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
|
val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight
|
||||||
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
|
val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0)
|
||||||
return insets.replaceSystemBarInsetsCompat(
|
return insets.replaceSystemBarInsetsCompat(
|
||||||
bars.left, bars.top, bars.right, adjustedBottomInset
|
bars.left, bars.top, bars.right, adjustedBottomInset)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(): Parcelable = Bundle().apply {
|
override fun onSaveInstanceState(): Parcelable =
|
||||||
putParcelable("superState", super.onSaveInstanceState())
|
Bundle().apply {
|
||||||
putSerializable(
|
putParcelable("superState", super.onSaveInstanceState())
|
||||||
KEY_PANEL_STATE,
|
putSerializable(
|
||||||
if (panelState != PanelState.DRAGGING) {
|
KEY_PANEL_STATE,
|
||||||
panelState
|
if (panelState != PanelState.DRAGGING) {
|
||||||
} else {
|
panelState
|
||||||
lastIdlePanelState
|
} else {
|
||||||
}
|
lastIdlePanelState
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(state: Parcelable) {
|
override fun onRestoreInstanceState(state: Parcelable) {
|
||||||
if (state is Bundle) {
|
if (state is Bundle) {
|
||||||
|
|
@ -425,13 +442,14 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
|
|
||||||
return if (!canSlide) {
|
return if (!canSlide) {
|
||||||
super.onTouchEvent(ev)
|
super.onTouchEvent(ev)
|
||||||
} else try {
|
} else
|
||||||
dragHelper.processTouchEvent(ev)
|
try {
|
||||||
true
|
dragHelper.processTouchEvent(ev)
|
||||||
} catch (ex: Exception) {
|
true
|
||||||
// Ignore the pointer out of range exception
|
} catch (ex: Exception) {
|
||||||
false
|
// Ignore the pointer out of range exception
|
||||||
}
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
|
@ -454,10 +472,10 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
val pointerUnder = playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt())
|
val pointerUnder = playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt())
|
||||||
val motionUnder = playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt())
|
val motionUnder =
|
||||||
|
playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt())
|
||||||
|
|
||||||
if (!(pointerUnder || motionUnder) || ady > dragSlop && adx > ady) {
|
if (!(pointerUnder || motionUnder) || ady > dragSlop && adx > ady) {
|
||||||
// Pointer has moved beyond our control, do not intercept this event
|
// Pointer has moved beyond our control, do not intercept this event
|
||||||
|
|
@ -465,7 +483,6 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP ->
|
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP ->
|
||||||
if (dragHelper.isDragging) {
|
if (dragHelper.isDragging) {
|
||||||
// Stopped pressing while we were dragging, let the drag helper handle it
|
// Stopped pressing while we were dragging, let the drag helper handle it
|
||||||
|
|
@ -504,11 +521,12 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
get() {
|
get() {
|
||||||
// We can't grab the drag state outside of a callback, but that's stupid and I don't
|
// We can't grab the drag state outside of a callback, but that's stupid and I don't
|
||||||
// want to vendor ViewDragHelper so I just do reflection instead.
|
// want to vendor ViewDragHelper so I just do reflection instead.
|
||||||
val state = try {
|
val state =
|
||||||
dragStateField.get(this)
|
try {
|
||||||
} catch (e: Exception) {
|
dragStateField.get(this)
|
||||||
ViewDragHelper.STATE_IDLE
|
} catch (e: Exception) {
|
||||||
}
|
ViewDragHelper.STATE_IDLE
|
||||||
|
}
|
||||||
|
|
||||||
return state == ViewDragHelper.STATE_DRAGGING
|
return state == ViewDragHelper.STATE_DRAGGING
|
||||||
}
|
}
|
||||||
|
|
@ -524,9 +542,9 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do the nice view animations that occur whenever we slide up the playback panel.
|
* Do the nice view animations that occur whenever we slide up the playback panel. The way I
|
||||||
* The way I transition is largely inspired by Android 12's notification panel, with the
|
* transition is largely inspired by Android 12's notification panel, with the compact view
|
||||||
* compact view fading out completely before the panel view fades in.
|
* fading out completely before the panel view fades in.
|
||||||
*/
|
*/
|
||||||
private fun updatePanelTransition() {
|
private fun updatePanelTransition() {
|
||||||
val ratio = max(panelOffset, 0f)
|
val ratio = max(panelOffset, 0f)
|
||||||
|
|
@ -566,8 +584,7 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
params.leftMargin,
|
params.leftMargin,
|
||||||
(bars.top * halfOutRatio).toInt(),
|
(bars.top * halfOutRatio).toInt(),
|
||||||
params.rightMargin,
|
params.rightMargin,
|
||||||
params.bottomMargin
|
params.bottomMargin)
|
||||||
)
|
|
||||||
|
|
||||||
// Poke the layout only when we changed something
|
// Poke the layout only when we changed something
|
||||||
if (params.topMargin != oldTopMargin) {
|
if (params.topMargin != oldTopMargin) {
|
||||||
|
|
@ -592,9 +609,9 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
private fun smoothSlideTo(offset: Float) {
|
private fun smoothSlideTo(offset: Float) {
|
||||||
logD("Smooth sliding to $offset")
|
logD("Smooth sliding to $offset")
|
||||||
|
|
||||||
val okay = dragHelper.smoothSlideViewTo(
|
val okay =
|
||||||
playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset)
|
dragHelper.smoothSlideViewTo(
|
||||||
)
|
playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset))
|
||||||
|
|
||||||
if (okay) {
|
if (okay) {
|
||||||
postInvalidateOnAnimation()
|
postInvalidateOnAnimation()
|
||||||
|
|
@ -621,7 +638,6 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
setPanelStateInternal(PanelState.HIDDEN)
|
setPanelStateInternal(PanelState.HIDDEN)
|
||||||
playbackContainerView.visibility = INVISIBLE
|
playbackContainerView.visibility = INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> setPanelStateInternal(PanelState.EXPANDED)
|
else -> setPanelStateInternal(PanelState.EXPANDED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -658,16 +674,17 @@ class PlaybackLayout @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
|
||||||
val newOffset = when {
|
val newOffset =
|
||||||
// Swipe Up -> Expand to top
|
when {
|
||||||
yvel < 0 -> 1f
|
// Swipe Up -> Expand to top
|
||||||
// Swipe down -> Collapse to bottom
|
yvel < 0 -> 1f
|
||||||
yvel > 0 -> 0f
|
// Swipe down -> Collapse to bottom
|
||||||
// No velocity, far enough from middle to expand to top
|
yvel > 0 -> 0f
|
||||||
panelOffset >= 0.5f -> 1f
|
// No velocity, far enough from middle to expand to top
|
||||||
// Collapse to bottom
|
panelOffset >= 0.5f -> 1f
|
||||||
else -> 0f
|
// Collapse to bottom
|
||||||
}
|
else -> 0f
|
||||||
|
}
|
||||||
|
|
||||||
dragHelper.settleCapturedViewAt(releasedChild.left, computePanelTopPosition(newOffset))
|
dragHelper.settleCapturedViewAt(releasedChild.left, computePanelTopPosition(newOffset))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackSeeker.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -33,20 +32,22 @@ import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.stateList
|
import org.oxycblt.auxio.util.stateList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom view that bundles together a seekbar with a current duration and a total duration.
|
* A custom view that bundles together a seekbar with a current duration and a total duration. The
|
||||||
* The sub-views are specifically laid out so that the seekbar has an adequate touch height while
|
* sub-views are specifically laid out so that the seekbar has an adequate touch height while still
|
||||||
* still not having gobs of whitespace everywhere.
|
* not having gobs of whitespace everywhere. TODO: Add smooth seeking [i.e seeking in sub-second
|
||||||
* TODO: Add smooth seeking [i.e seeking in sub-second values]
|
* values]
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class PlaybackSeekBar @JvmOverloads constructor(
|
class PlaybackSeekBar
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) :
|
||||||
defStyleRes: Int = 0
|
ConstraintLayout(context, attrs, defStyleRes),
|
||||||
) : ConstraintLayout(context, attrs, defStyleRes), Slider.OnChangeListener, Slider.OnSliderTouchListener {
|
Slider.OnChangeListener,
|
||||||
|
Slider.OnSliderTouchListener {
|
||||||
private val binding = ViewSeekBarBinding.inflate(context.inflater, this, true)
|
private val binding = ViewSeekBarBinding.inflate(context.inflater, this, true)
|
||||||
private val isSeeking: Boolean get() = binding.playbackDurationCurrent.isActivated
|
private val isSeeking: Boolean
|
||||||
|
get() = binding.playbackDurationCurrent.isActivated
|
||||||
|
|
||||||
var onConfirmListener: ((Long) -> Unit)? = null
|
var onConfirmListener: ((Long) -> Unit)? = null
|
||||||
|
|
||||||
|
|
@ -55,9 +56,10 @@ class PlaybackSeekBar @JvmOverloads constructor(
|
||||||
binding.seekBar.addOnSliderTouchListener(this)
|
binding.seekBar.addOnSliderTouchListener(this)
|
||||||
|
|
||||||
// Override the inactive color so that it lines up with the playback progress bar.
|
// Override the inactive color so that it lines up with the playback progress bar.
|
||||||
binding.seekBar.trackInactiveTintList = MaterialColors.compositeARGBWithAlpha(
|
binding.seekBar.trackInactiveTintList =
|
||||||
context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()
|
MaterialColors.compositeARGBWithAlpha(
|
||||||
).stateList
|
context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt())
|
||||||
|
.stateList
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setProgress(seconds: Long) {
|
fun setProgress(seconds: Long) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -41,13 +40,14 @@ import org.oxycblt.auxio.util.logE
|
||||||
/**
|
/**
|
||||||
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
||||||
*
|
*
|
||||||
* **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this provides
|
* **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this
|
||||||
* an interface that properly sanitizes input and abstracts functions unlike the master class.**
|
* provides an interface that properly sanitizes input and abstracts functions unlike the master
|
||||||
|
* class.**
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
*
|
||||||
* TODO: Completely rework this module to support the new music rescan system,
|
* TODO: Completely rework this module to support the new music rescan system, proper android auto
|
||||||
* proper android auto and external exposing, and so on.
|
* and external exposing, and so on.
|
||||||
* - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS.
|
* - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS.
|
||||||
*/
|
*/
|
||||||
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// Playback
|
// Playback
|
||||||
|
|
@ -68,21 +68,29 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
private var mIntentUri: Uri? = null
|
private var mIntentUri: Uri? = null
|
||||||
|
|
||||||
/** The current song. */
|
/** The current song. */
|
||||||
val song: LiveData<Song?> get() = mSong
|
val song: LiveData<Song?>
|
||||||
|
get() = mSong
|
||||||
/** The current model that is being played from, such as an [Album] or [Artist] */
|
/** The current model that is being played from, such as an [Album] or [Artist] */
|
||||||
val parent: LiveData<MusicParent?> get() = mParent
|
val parent: LiveData<MusicParent?>
|
||||||
|
get() = mParent
|
||||||
|
|
||||||
val isPlaying: LiveData<Boolean> get() = mIsPlaying
|
val isPlaying: LiveData<Boolean>
|
||||||
val isShuffling: LiveData<Boolean> get() = mIsShuffling
|
get() = mIsPlaying
|
||||||
|
val isShuffling: LiveData<Boolean>
|
||||||
|
get() = mIsShuffling
|
||||||
/** The current repeat mode, see [LoopMode] for more information */
|
/** The current repeat mode, see [LoopMode] for more information */
|
||||||
val loopMode: LiveData<LoopMode> get() = mLoopMode
|
val loopMode: LiveData<LoopMode>
|
||||||
|
get() = mLoopMode
|
||||||
/** The current playback position, in seconds */
|
/** The current playback position, in seconds */
|
||||||
val position: LiveData<Long> get() = mPosition
|
val position: LiveData<Long>
|
||||||
|
get() = mPosition
|
||||||
|
|
||||||
/** The queue, without the previous items. */
|
/** The queue, without the previous items. */
|
||||||
val nextUp: LiveData<List<Song>> get() = mNextUp
|
val nextUp: LiveData<List<Song>>
|
||||||
|
get() = mNextUp
|
||||||
/** The current [PlaybackMode] that also determines the queue */
|
/** The current [PlaybackMode] that also determines the queue */
|
||||||
val playbackMode: LiveData<PlaybackMode> get() = mMode
|
val playbackMode: LiveData<PlaybackMode>
|
||||||
|
get() = mMode
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
@ -102,8 +110,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// --- PLAYING FUNCTIONS ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a [song] with the [mode] specified. [mode] will default to the preferred song
|
* Play a [song] with the [mode] specified. [mode] will default to the preferred song playback
|
||||||
* playback mode of the user if not specified.
|
* mode of the user if not specified.
|
||||||
*/
|
*/
|
||||||
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
||||||
playbackManager.playSong(song, mode)
|
playbackManager.playSong(song, mode)
|
||||||
|
|
@ -152,8 +160,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play using a file [uri].
|
* Play using a file [uri]. This will not play instantly during the initial startup sequence.
|
||||||
* This will not play instantly during the initial startup sequence.
|
|
||||||
*/
|
*/
|
||||||
fun playWithUri(uri: Uri, context: Context) {
|
fun playWithUri(uri: Uri, context: Context) {
|
||||||
// Check if everything is already running to run the URI play
|
// Check if everything is already running to run the URI play
|
||||||
|
|
@ -166,54 +173,41 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */
|
||||||
* Play with a file URI.
|
|
||||||
* This is called after [playWithUri] once its deemed safe to do so.
|
|
||||||
*/
|
|
||||||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||||
logD("Playing with uri $uri")
|
logD("Playing with uri $uri")
|
||||||
|
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||||
musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
|
musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
|
||||||
playSong(song)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shuffle all songs */
|
||||||
* Shuffle all songs
|
|
||||||
*/
|
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
playbackManager.shuffleAll()
|
playbackManager.shuffleAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- POSITION FUNCTIONS ---
|
// --- POSITION FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Update the position and push it to [PlaybackStateManager] */
|
||||||
* Update the position and push it to [PlaybackStateManager]
|
|
||||||
*/
|
|
||||||
fun setPosition(progress: Long) {
|
fun setPosition(progress: Long) {
|
||||||
playbackManager.seekTo((progress * 1000))
|
playbackManager.seekTo((progress * 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Skip to the next song. */
|
||||||
* Skip to the next song.
|
|
||||||
*/
|
|
||||||
fun skipNext() {
|
fun skipNext() {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Skip to the previous song. */
|
||||||
* Skip to the previous song.
|
|
||||||
*/
|
|
||||||
fun skipPrev() {
|
fun skipPrev() {
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a queue item using it's recyclerview adapter index. If the indices are valid,
|
* Remove a queue item using it's recyclerview adapter index. If the indices are valid, [apply]
|
||||||
* [apply] is called just before the change is committed so that the adapter can be updated.
|
* is called just before the change is committed so that the adapter can be updated.
|
||||||
*/
|
*/
|
||||||
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
|
fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) {
|
||||||
val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
|
val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size)
|
||||||
|
|
@ -223,8 +217,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Move queue items using their recyclerview adapter indices. If the indices are valid,
|
* Move queue items using their recyclerview adapter indices. If the indices are valid, [apply]
|
||||||
* [apply] is called just before the change is committed so that the adapter can be updated.
|
* is called just before the change is committed so that the adapter can be updated.
|
||||||
*/
|
*/
|
||||||
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
|
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean {
|
||||||
val delta = (playbackManager.queue.size - mNextUp.value!!.size)
|
val delta = (playbackManager.queue.size - mNextUp.value!!.size)
|
||||||
|
|
@ -239,53 +233,39 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a [Song] to the top of the queue. */
|
||||||
* Add a [Song] to the top of the queue.
|
|
||||||
*/
|
|
||||||
fun playNext(song: Song) {
|
fun playNext(song: Song) {
|
||||||
playbackManager.playNext(song)
|
playbackManager.playNext(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add an [Album] to the top of the queue. */
|
||||||
* Add an [Album] to the top of the queue.
|
|
||||||
*/
|
|
||||||
fun playNext(album: Album) {
|
fun playNext(album: Album) {
|
||||||
playbackManager.playNext(settingsManager.detailAlbumSort.sortAlbum(album))
|
playbackManager.playNext(settingsManager.detailAlbumSort.sortAlbum(album))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a [Song] to the end of the queue. */
|
||||||
* Add a [Song] to the end of the queue.
|
|
||||||
*/
|
|
||||||
fun addToQueue(song: Song) {
|
fun addToQueue(song: Song) {
|
||||||
playbackManager.addToQueue(song)
|
playbackManager.addToQueue(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add an [Album] to the end of the queue. */
|
||||||
* Add an [Album] to the end of the queue.
|
|
||||||
*/
|
|
||||||
fun addToQueue(album: Album) {
|
fun addToQueue(album: Album) {
|
||||||
playbackManager.addToQueue(settingsManager.detailAlbumSort.sortAlbum(album))
|
playbackManager.addToQueue(settingsManager.detailAlbumSort.sortAlbum(album))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Flip the playing status, e.g from playing to paused */
|
||||||
* Flip the playing status, e.g from playing to paused
|
|
||||||
*/
|
|
||||||
fun invertPlayingStatus() {
|
fun invertPlayingStatus() {
|
||||||
playbackManager.setPlaying(!playbackManager.isPlaying)
|
playbackManager.setPlaying(!playbackManager.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Flip the shuffle status, e.g from on to off. Will keep song by default. */
|
||||||
* Flip the shuffle status, e.g from on to off. Will keep song by default.
|
|
||||||
*/
|
|
||||||
fun invertShuffleStatus() {
|
fun invertShuffleStatus() {
|
||||||
playbackManager.setShuffling(!playbackManager.isShuffling, true)
|
playbackManager.setShuffling(!playbackManager.isShuffling, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Increment the loop status, e.g from off to loop once */
|
||||||
* Increment the loop status, e.g from off to loop once
|
|
||||||
*/
|
|
||||||
fun incrementLoopStatus() {
|
fun incrementLoopStatus() {
|
||||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||||
}
|
}
|
||||||
|
|
@ -293,8 +273,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
// --- SAVE/RESTORE FUNCTIONS ---
|
// --- SAVE/RESTORE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force save the current [PlaybackStateManager] state to the database.
|
* Force save the current [PlaybackStateManager] state to the database. Called by
|
||||||
* Called by SettingsListFragment.
|
* SettingsListFragment.
|
||||||
*/
|
*/
|
||||||
fun savePlaybackState(context: Context, onDone: () -> Unit) {
|
fun savePlaybackState(context: Context, onDone: () -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -320,15 +300,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
playbackManager.markRestored()
|
playbackManager.markRestored()
|
||||||
} else if (!playbackManager.isRestored) {
|
} else if (!playbackManager.isRestored) {
|
||||||
// Otherwise just restore
|
// Otherwise just restore
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { playbackManager.restoreFromDatabase(context) }
|
||||||
playbackManager.restoreFromDatabase(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to restore the current playback state from an existing
|
* Attempt to restore the current playback state from an existing [PlaybackStateManager]
|
||||||
* [PlaybackStateManager] instance.
|
* instance.
|
||||||
*/
|
*/
|
||||||
private fun restorePlaybackState() {
|
private fun restorePlaybackState() {
|
||||||
logD("Attempting to restore playback state")
|
logD("Attempting to restore playback state")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* QueueAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -47,9 +46,8 @@ import org.oxycblt.auxio.util.stateList
|
||||||
* @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used
|
* @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueAdapter(
|
class QueueAdapter(private val touchHelper: ItemTouchHelper) :
|
||||||
private val touchHelper: ItemTouchHelper
|
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|
||||||
private var data = mutableListOf<Item>()
|
private var data = mutableListOf<Item>()
|
||||||
private var listDiffer = AsyncListDiffer(this, DiffCallback())
|
private var listDiffer = AsyncListDiffer(this, DiffCallback())
|
||||||
|
|
||||||
|
|
@ -60,16 +58,14 @@ class QueueAdapter(
|
||||||
is Song -> QUEUE_SONG_ITEM_TYPE
|
is Song -> QUEUE_SONG_ITEM_TYPE
|
||||||
is Header -> HeaderViewHolder.ITEM_TYPE
|
is Header -> HeaderViewHolder.ITEM_TYPE
|
||||||
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
|
is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE
|
||||||
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder(
|
QUEUE_SONG_ITEM_TYPE ->
|
||||||
ItemQueueSongBinding.inflate(parent.context.inflater)
|
QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater))
|
||||||
)
|
|
||||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||||
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context)
|
||||||
else -> error("Invalid ViewHolder item type $viewType")
|
else -> error("Invalid ViewHolder item type $viewType")
|
||||||
|
|
@ -86,8 +82,8 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit data using [AsyncListDiffer].
|
* Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes
|
||||||
* **Only use this if you have no idea what changes occurred to the data**
|
* occurred to the data**
|
||||||
*/
|
*/
|
||||||
fun submitList(newData: MutableList<Item>) {
|
fun submitList(newData: MutableList<Item>) {
|
||||||
if (data != newData) {
|
if (data != newData) {
|
||||||
|
|
@ -96,39 +92,32 @@ class QueueAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Move Items. Used since [submitList] will cause QueueAdapter to freak out. */
|
||||||
* Move Items.
|
|
||||||
* Used since [submitList] will cause QueueAdapter to freak out.
|
|
||||||
*/
|
|
||||||
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
fun moveItems(adapterFrom: Int, adapterTo: Int) {
|
||||||
data.add(adapterTo, data.removeAt(adapterFrom))
|
data.add(adapterTo, data.removeAt(adapterFrom))
|
||||||
notifyItemMoved(adapterFrom, adapterTo)
|
notifyItemMoved(adapterFrom, adapterTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Remove an item. Used since [submitList] will cause QueueAdapter to freak out. */
|
||||||
* Remove an item.
|
|
||||||
* Used since [submitList] will cause QueueAdapter to freak out.
|
|
||||||
*/
|
|
||||||
fun removeItem(adapterIndex: Int) {
|
fun removeItem(adapterIndex: Int) {
|
||||||
data.removeAt(adapterIndex)
|
data.removeAt(adapterIndex)
|
||||||
notifyItemRemoved(adapterIndex)
|
notifyItemRemoved(adapterIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Generic ViewHolder for a queue song */
|
||||||
* Generic ViewHolder for a queue song
|
|
||||||
*/
|
|
||||||
inner class QueueSongViewHolder(
|
inner class QueueSongViewHolder(
|
||||||
private val binding: ItemQueueSongBinding,
|
private val binding: ItemQueueSongBinding,
|
||||||
) : BaseViewHolder<Song>(binding) {
|
) : BaseViewHolder<Song>(binding) {
|
||||||
val bodyView: View get() = binding.body
|
val bodyView: View
|
||||||
val backgroundView: View get() = binding.background
|
get() = binding.body
|
||||||
|
val backgroundView: View
|
||||||
|
get() = binding.background
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.body.background = MaterialShapeDrawable.createWithElevationOverlay(
|
binding.body.background =
|
||||||
binding.root.context
|
MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply {
|
||||||
).apply {
|
fillColor = (binding.body.background as ColorDrawable).color.stateList
|
||||||
fillColor = (binding.body.background as ColorDrawable).color.stateList
|
}
|
||||||
}
|
|
||||||
|
|
||||||
binding.root.disableDropShadowCompat()
|
binding.root.disableDropShadowCompat()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* QueueDragCallback.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
|
@ -24,19 +23,19 @@ import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
import org.oxycblt.auxio.util.getDimenSafe
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
|
import org.oxycblt.auxio.util.getDimenSafe
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A highly customized [ItemTouchHelper.Callback] that handles the queue system while basically
|
* A highly customized [ItemTouchHelper.Callback] that handles the queue system while basically
|
||||||
* rebuilding most the "Material-y" aspects of an editable list because Google's implementations
|
* rebuilding most the "Material-y" aspects of an editable list because Google's implementations are
|
||||||
* are hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
||||||
|
|
@ -59,17 +58,14 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
): Int {
|
): Int {
|
||||||
// Fix to make QueueFragment scroll slower when an item is scrolled out of bounds.
|
// Fix to make QueueFragment scroll slower when an item is scrolled out of bounds.
|
||||||
// Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe
|
// Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe
|
||||||
val standardSpeed = super.interpolateOutOfBoundsScroll(
|
val standardSpeed =
|
||||||
recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll
|
super.interpolateOutOfBoundsScroll(
|
||||||
)
|
recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll)
|
||||||
|
|
||||||
val clampedAbsVelocity = max(
|
val clampedAbsVelocity =
|
||||||
MINIMUM_INITIAL_DRAG_VELOCITY,
|
max(
|
||||||
min(
|
MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||||
abs(standardSpeed),
|
min(abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY))
|
||||||
MAXIMUM_INITIAL_DRAG_VELOCITY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return clampedAbsVelocity * sign(viewSizeOutOfBounds.toDouble()).toInt()
|
return clampedAbsVelocity * sign(viewSizeOutOfBounds.toDouble()).toInt()
|
||||||
}
|
}
|
||||||
|
|
@ -94,12 +90,12 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
|
|
||||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
val bg = holder.bodyView.background as MaterialShapeDrawable
|
||||||
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
|
val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small)
|
||||||
holder.itemView.animate()
|
holder
|
||||||
|
.itemView
|
||||||
|
.animate()
|
||||||
.translationZ(elevation)
|
.translationZ(elevation)
|
||||||
.setDuration(100)
|
.setDuration(100)
|
||||||
.setUpdateListener {
|
.setUpdateListener { bg.elevation = holder.itemView.translationZ }
|
||||||
bg.elevation = holder.itemView.translationZ
|
|
||||||
}
|
|
||||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||||
.start()
|
.start()
|
||||||
|
|
||||||
|
|
@ -132,7 +128,9 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
logD("Dropping queue item")
|
logD("Dropping queue item")
|
||||||
|
|
||||||
val bg = holder.bodyView.background as MaterialShapeDrawable
|
val bg = holder.bodyView.background as MaterialShapeDrawable
|
||||||
holder.itemView.animate()
|
holder
|
||||||
|
.itemView
|
||||||
|
.animate()
|
||||||
.translationZ(0.0f)
|
.translationZ(0.0f)
|
||||||
.setDuration(100)
|
.setDuration(100)
|
||||||
.setUpdateListener { bg.elevation = holder.itemView.translationZ }
|
.setUpdateListener { bg.elevation = holder.itemView.translationZ }
|
||||||
|
|
@ -154,9 +152,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
val from = viewHolder.bindingAdapterPosition
|
val from = viewHolder.bindingAdapterPosition
|
||||||
val to = target.bindingAdapterPosition
|
val to = target.bindingAdapterPosition
|
||||||
|
|
||||||
return playbackModel.moveQueueDataItems(from, to) {
|
return playbackModel.moveQueueDataItems(from, to) { queueAdapter.moveItems(from, to) }
|
||||||
queueAdapter.moveItems(from, to)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
|
@ -168,8 +164,8 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
||||||
override fun isLongPressDragEnabled(): Boolean = false
|
override fun isLongPressDragEnabled(): Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the queue adapter to this callback.
|
* Add the queue adapter to this callback. Done because there's a circular dependency between
|
||||||
* Done because there's a circular dependency between the two objects
|
* the two objects
|
||||||
*/
|
*/
|
||||||
fun addQueueAdapter(adapter: QueueAdapter) {
|
fun addQueueAdapter(adapter: QueueAdapter) {
|
||||||
queueAdapter = adapter
|
queueAdapter = adapter
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* QueueFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.queue
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -54,9 +53,7 @@ class QueueFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.queueToolbar.setNavigationOnClickListener {
|
binding.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.queueRecycler.apply {
|
binding.queueRecycler.apply {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* LoopMode.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,11 +22,11 @@ package org.oxycblt.auxio.playback.state
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
enum class LoopMode {
|
enum class LoopMode {
|
||||||
NONE, ALL, TRACK;
|
NONE,
|
||||||
|
ALL,
|
||||||
|
TRACK;
|
||||||
|
|
||||||
/**
|
/** Increment the LoopMode, e.g from [NONE] to [ALL] */
|
||||||
* Increment the LoopMode, e.g from [NONE] to [ALL]
|
|
||||||
*/
|
|
||||||
fun increment(): LoopMode {
|
fun increment(): LoopMode {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
NONE -> ALL
|
NONE -> ALL
|
||||||
|
|
@ -53,15 +52,12 @@ enum class LoopMode {
|
||||||
private const val INT_ALL = 0xA101
|
private const val INT_ALL = 0xA101
|
||||||
private const val INT_TRACK = 0xA102
|
private const val INT_TRACK = 0xA102
|
||||||
|
|
||||||
/**
|
/** Convert an int [constant] into a LoopMode, or null if it isn't valid. */
|
||||||
* Convert an int [constant] into a LoopMode, or null if it isn't valid.
|
|
||||||
*/
|
|
||||||
fun fromInt(constant: Int): LoopMode? {
|
fun fromInt(constant: Int): LoopMode? {
|
||||||
return when (constant) {
|
return when (constant) {
|
||||||
INT_NONE -> NONE
|
INT_NONE -> NONE
|
||||||
INT_ALL -> ALL
|
INT_ALL -> ALL
|
||||||
INT_TRACK -> TRACK
|
INT_TRACK -> TRACK
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackMode.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackStateDatabase.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
|
@ -32,8 +31,8 @@ import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.queryAll
|
import org.oxycblt.auxio.util.queryAll
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A SQLite database for managing the persistent playback state and queue.
|
* A SQLite database for managing the persistent playback state and queue. Yes. I know Room exists.
|
||||||
* Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
|
* But that would needlessly bloat my app and has crippling bugs.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackStateDatabase(context: Context) :
|
class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
@ -58,9 +57,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
// --- DATABASE CONSTRUCTION FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Create a table for this database. */
|
||||||
* Create a table for this database.
|
|
||||||
*/
|
|
||||||
private fun createTable(database: SQLiteDatabase, tableName: String) {
|
private fun createTable(database: SQLiteDatabase, tableName: String) {
|
||||||
val command = StringBuilder()
|
val command = StringBuilder()
|
||||||
command.append("CREATE TABLE IF NOT EXISTS $tableName(")
|
command.append("CREATE TABLE IF NOT EXISTS $tableName(")
|
||||||
|
|
@ -74,11 +71,10 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
database.execSQL(command.toString())
|
database.execSQL(command.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Construct a [StateColumns] table */
|
||||||
* Construct a [StateColumns] table
|
|
||||||
*/
|
|
||||||
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
||||||
command.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
command
|
||||||
|
.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
|
||||||
.append("${StateColumns.COLUMN_SONG_HASH} LONG,")
|
.append("${StateColumns.COLUMN_SONG_HASH} LONG,")
|
||||||
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
.append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
|
||||||
.append("${StateColumns.COLUMN_PARENT_HASH} LONG,")
|
.append("${StateColumns.COLUMN_PARENT_HASH} LONG,")
|
||||||
|
|
@ -90,11 +86,10 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Construct a [QueueColumns] table */
|
||||||
* Construct a [QueueColumns] table
|
|
||||||
*/
|
|
||||||
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
||||||
command.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
command
|
||||||
|
.append("${QueueColumns.ID} LONG PRIMARY KEY,")
|
||||||
.append("${QueueColumns.SONG_HASH} INTEGER NOT NULL,")
|
.append("${QueueColumns.SONG_HASH} INTEGER NOT NULL,")
|
||||||
.append("${QueueColumns.ALBUM_HASH} INTEGER NOT NULL)")
|
.append("${QueueColumns.ALBUM_HASH} INTEGER NOT NULL)")
|
||||||
|
|
||||||
|
|
@ -126,30 +121,31 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
val song = cursor.getLongOrNull(songIndex)?.let { id ->
|
val song =
|
||||||
musicStore.songs.find { it.id == id }
|
cursor.getLongOrNull(songIndex)?.let { id -> musicStore.songs.find { it.id == id } }
|
||||||
}
|
|
||||||
|
|
||||||
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
|
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
|
||||||
|
|
||||||
val parent = cursor.getLongOrNull(parentIndex)?.let { id ->
|
val parent =
|
||||||
when (mode) {
|
cursor.getLongOrNull(parentIndex)?.let { id ->
|
||||||
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id }
|
when (mode) {
|
||||||
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id }
|
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id }
|
||||||
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id }
|
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id }
|
||||||
PlaybackMode.ALL_SONGS -> null
|
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id }
|
||||||
|
PlaybackMode.ALL_SONGS -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
state = SavedState(
|
state =
|
||||||
song = song,
|
SavedState(
|
||||||
position = cursor.getLong(posIndex),
|
song = song,
|
||||||
parent = parent,
|
position = cursor.getLong(posIndex),
|
||||||
queueIndex = cursor.getInt(indexIndex),
|
parent = parent,
|
||||||
playbackMode = mode,
|
queueIndex = cursor.getInt(indexIndex),
|
||||||
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
playbackMode = mode,
|
||||||
loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
|
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
||||||
)
|
loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
|
||||||
|
)
|
||||||
|
|
||||||
logD("Successfully read playback state: $state")
|
logD("Successfully read playback state: $state")
|
||||||
}
|
}
|
||||||
|
|
@ -157,9 +153,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Clear the previously written [SavedState] and write a new one. */
|
||||||
* Clear the previously written [SavedState] and write a new one.
|
|
||||||
*/
|
|
||||||
fun writeState(state: SavedState) {
|
fun writeState(state: SavedState) {
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
|
|
@ -168,16 +162,17 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
this@PlaybackStateDatabase.logD("Wiped state db")
|
this@PlaybackStateDatabase.logD("Wiped state db")
|
||||||
|
|
||||||
val stateData = ContentValues(10).apply {
|
val stateData =
|
||||||
put(StateColumns.COLUMN_ID, 0)
|
ContentValues(10).apply {
|
||||||
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
|
put(StateColumns.COLUMN_ID, 0)
|
||||||
put(StateColumns.COLUMN_POSITION, state.position)
|
put(StateColumns.COLUMN_SONG_HASH, state.song?.id)
|
||||||
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
|
put(StateColumns.COLUMN_POSITION, state.position)
|
||||||
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
|
put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id)
|
||||||
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt())
|
put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
|
||||||
put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
|
put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt())
|
||||||
put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt())
|
put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
|
||||||
}
|
put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
insert(TABLE_NAME_STATE, null, stateData)
|
insert(TABLE_NAME_STATE, null, stateData)
|
||||||
}
|
}
|
||||||
|
|
@ -202,9 +197,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
|
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
|
||||||
?.let { song ->
|
?.let { song -> queue.add(song) }
|
||||||
queue.add(song)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,16 +206,12 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
return queue
|
return queue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Write a queue to the database. */
|
||||||
* Write a queue to the database.
|
|
||||||
*/
|
|
||||||
fun writeQueue(queue: MutableList<Song>) {
|
fun writeQueue(queue: MutableList<Song>) {
|
||||||
assertBackgroundThread()
|
assertBackgroundThread()
|
||||||
|
|
||||||
val database = writableDatabase
|
val database = writableDatabase
|
||||||
database.transaction {
|
database.transaction { delete(TABLE_NAME_QUEUE, null, null) }
|
||||||
delete(TABLE_NAME_QUEUE, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
logD("Wiped queue db")
|
logD("Wiped queue db")
|
||||||
|
|
||||||
|
|
@ -243,11 +232,12 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
val song = queue[i]
|
val song = queue[i]
|
||||||
i++
|
i++
|
||||||
|
|
||||||
val itemData = ContentValues(4).apply {
|
val itemData =
|
||||||
put(QueueColumns.ID, idStart + i)
|
ContentValues(4).apply {
|
||||||
put(QueueColumns.SONG_HASH, song.id)
|
put(QueueColumns.ID, idStart + i)
|
||||||
put(QueueColumns.ALBUM_HASH, song.album.id)
|
put(QueueColumns.SONG_HASH, song.id)
|
||||||
}
|
put(QueueColumns.ALBUM_HASH, song.album.id)
|
||||||
|
}
|
||||||
|
|
||||||
insert(TABLE_NAME_QUEUE, null, itemData)
|
insert(TABLE_NAME_QUEUE, null, itemData)
|
||||||
}
|
}
|
||||||
|
|
@ -295,12 +285,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
const val TABLE_NAME_STATE = "playback_state_table"
|
const val TABLE_NAME_STATE = "playback_state_table"
|
||||||
const val TABLE_NAME_QUEUE = "queue_table"
|
const val TABLE_NAME_QUEUE = "queue_table"
|
||||||
|
|
||||||
@Volatile
|
@Volatile private var INSTANCE: PlaybackStateDatabase? = null
|
||||||
private var INSTANCE: PlaybackStateDatabase? = null
|
|
||||||
|
|
||||||
/**
|
/** Get/Instantiate the single instance of [PlaybackStateDatabase]. */
|
||||||
* Get/Instantiate the single instance of [PlaybackStateDatabase].
|
|
||||||
*/
|
|
||||||
fun getInstance(context: Context): PlaybackStateDatabase {
|
fun getInstance(context: Context): PlaybackStateDatabase {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackStateManager.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.state
|
package org.oxycblt.auxio.playback.state
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -35,8 +34,10 @@ import org.oxycblt.auxio.util.logE
|
||||||
* Master class (and possible god object) for the playback state.
|
* Master class (and possible god object) for the playback state.
|
||||||
*
|
*
|
||||||
* This should ***NOT*** be used outside of the playback module.
|
* This should ***NOT*** be used outside of the playback module.
|
||||||
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
* - If you want to use the playback state in the UI, use
|
||||||
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService].
|
* [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
||||||
|
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use
|
||||||
|
* [org.oxycblt.auxio.playback.system.PlaybackService].
|
||||||
*
|
*
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [PlaybackStateManager.getInstance].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
|
@ -90,27 +91,38 @@ class PlaybackStateManager private constructor() {
|
||||||
private var mHasPlayed = false
|
private var mHasPlayed = false
|
||||||
|
|
||||||
/** The currently playing song. Null if there isn't one */
|
/** The currently playing song. Null if there isn't one */
|
||||||
val song: Song? get() = mSong
|
val song: Song?
|
||||||
|
get() = mSong
|
||||||
/** The parent the queue is based on, null if all_songs */
|
/** The parent the queue is based on, null if all_songs */
|
||||||
val parent: MusicParent? get() = mParent
|
val parent: MusicParent?
|
||||||
|
get() = mParent
|
||||||
/** The current playback progress */
|
/** The current playback progress */
|
||||||
val position: Long get() = mPosition
|
val position: Long
|
||||||
|
get() = mPosition
|
||||||
/** The current queue determined by [parent] and [playbackMode] */
|
/** The current queue determined by [parent] and [playbackMode] */
|
||||||
val queue: List<Song> get() = mQueue
|
val queue: List<Song>
|
||||||
|
get() = mQueue
|
||||||
/** The current position in the queue */
|
/** The current position in the queue */
|
||||||
val index: Int get() = mIndex
|
val index: Int
|
||||||
|
get() = mIndex
|
||||||
/** The current [PlaybackMode] */
|
/** The current [PlaybackMode] */
|
||||||
val playbackMode: PlaybackMode get() = mPlaybackMode
|
val playbackMode: PlaybackMode
|
||||||
|
get() = mPlaybackMode
|
||||||
/** Whether playback is paused or not */
|
/** Whether playback is paused or not */
|
||||||
val isPlaying: Boolean get() = mIsPlaying
|
val isPlaying: Boolean
|
||||||
|
get() = mIsPlaying
|
||||||
/** Whether the queue is shuffled */
|
/** Whether the queue is shuffled */
|
||||||
val isShuffling: Boolean get() = mIsShuffling
|
val isShuffling: Boolean
|
||||||
|
get() = mIsShuffling
|
||||||
/** The current [LoopMode] */
|
/** The current [LoopMode] */
|
||||||
val loopMode: LoopMode get() = mLoopMode
|
val loopMode: LoopMode
|
||||||
|
get() = mLoopMode
|
||||||
/** Whether this instance has already been restored */
|
/** Whether this instance has already been restored */
|
||||||
val isRestored: Boolean get() = mIsRestored
|
val isRestored: Boolean
|
||||||
|
get() = mIsRestored
|
||||||
/** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */
|
/** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */
|
||||||
val hasPlayed: Boolean get() = mHasPlayed
|
val hasPlayed: Boolean
|
||||||
|
get() = mHasPlayed
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
|
|
@ -119,16 +131,14 @@ class PlaybackStateManager private constructor() {
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a [PlaybackStateManager.Callback] to this instance.
|
* Add a [PlaybackStateManager.Callback] to this instance. Make sure to remove the callback with
|
||||||
* Make sure to remove the callback with [removeCallback] when done.
|
* [removeCallback] when done.
|
||||||
*/
|
*/
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Remove a [PlaybackStateManager.Callback] bound to this instance. */
|
||||||
* Remove a [PlaybackStateManager.Callback] bound to this instance.
|
|
||||||
*/
|
|
||||||
fun removeCallback(callback: Callback) {
|
fun removeCallback(callback: Callback) {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
}
|
}
|
||||||
|
|
@ -149,17 +159,14 @@ class PlaybackStateManager private constructor() {
|
||||||
mParent = null
|
mParent = null
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = musicStore.songs.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMode.IN_GENRE -> {
|
PlaybackMode.IN_GENRE -> {
|
||||||
mParent = song.genre
|
mParent = song.genre
|
||||||
mQueue = song.genre.songs.toMutableList()
|
mQueue = song.genre.songs.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMode.IN_ARTIST -> {
|
PlaybackMode.IN_ARTIST -> {
|
||||||
mParent = song.album.artist
|
mParent = song.album.artist
|
||||||
mQueue = song.album.artist.songs.toMutableList()
|
mQueue = song.album.artist.songs.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackMode.IN_ALBUM -> {
|
PlaybackMode.IN_ALBUM -> {
|
||||||
mParent = song.album
|
mParent = song.album
|
||||||
mQueue = song.album.songs.toMutableList()
|
mQueue = song.album.songs.toMutableList()
|
||||||
|
|
@ -188,12 +195,10 @@ class PlaybackStateManager private constructor() {
|
||||||
mQueue = parent.songs.toMutableList()
|
mQueue = parent.songs.toMutableList()
|
||||||
mPlaybackMode = PlaybackMode.IN_ALBUM
|
mPlaybackMode = PlaybackMode.IN_ALBUM
|
||||||
}
|
}
|
||||||
|
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
mQueue = parent.songs.toMutableList()
|
mQueue = parent.songs.toMutableList()
|
||||||
mPlaybackMode = PlaybackMode.IN_ARTIST
|
mPlaybackMode = PlaybackMode.IN_ARTIST
|
||||||
}
|
}
|
||||||
|
|
||||||
is Genre -> {
|
is Genre -> {
|
||||||
mQueue = parent.songs.toMutableList()
|
mQueue = parent.songs.toMutableList()
|
||||||
mPlaybackMode = PlaybackMode.IN_GENRE
|
mPlaybackMode = PlaybackMode.IN_GENRE
|
||||||
|
|
@ -204,9 +209,7 @@ class PlaybackStateManager private constructor() {
|
||||||
updatePlayback(mQueue[0])
|
updatePlayback(mQueue[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shuffle all songs. */
|
||||||
* Shuffle all songs.
|
|
||||||
*/
|
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||||
|
|
||||||
|
|
@ -218,9 +221,7 @@ class PlaybackStateManager private constructor() {
|
||||||
updatePlayback(mQueue[0])
|
updatePlayback(mQueue[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the playback to a new [song], doing all the required logic. */
|
||||||
* Update the playback to a new [song], doing all the required logic.
|
|
||||||
*/
|
|
||||||
private fun updatePlayback(song: Song, shouldPlay: Boolean = true) {
|
private fun updatePlayback(song: Song, shouldPlay: Boolean = true) {
|
||||||
mSong = song
|
mSong = song
|
||||||
mPosition = 0
|
mPosition = 0
|
||||||
|
|
@ -229,9 +230,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// --- QUEUE FUNCTIONS ---
|
// --- QUEUE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Go to the next song, along with doing all the checks that entails. */
|
||||||
* Go to the next song, along with doing all the checks that entails.
|
|
||||||
*/
|
|
||||||
fun next() {
|
fun next() {
|
||||||
// Increment the index, if it cannot be incremented any further, then
|
// Increment the index, if it cannot be incremented any further, then
|
||||||
// loop and pause/resume playback depending on the setting
|
// loop and pause/resume playback depending on the setting
|
||||||
|
|
@ -246,9 +245,7 @@ class PlaybackStateManager private constructor() {
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Go to the previous song, doing any checks that are needed. */
|
||||||
* Go to the previous song, doing any checks that are needed.
|
|
||||||
*/
|
|
||||||
fun prev() {
|
fun prev() {
|
||||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
||||||
if (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) {
|
if (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) {
|
||||||
|
|
@ -266,9 +263,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// --- QUEUE EDITING FUNCTIONS ---
|
// --- QUEUE EDITING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Remove a queue item at [index]. Will ignore invalid indexes. */
|
||||||
* Remove a queue item at [index]. Will ignore invalid indexes.
|
|
||||||
*/
|
|
||||||
fun removeQueueItem(index: Int): Boolean {
|
fun removeQueueItem(index: Int): Boolean {
|
||||||
if (index > mQueue.size || index < 0) {
|
if (index > mQueue.size || index < 0) {
|
||||||
logE("Index is out of bounds, did not remove queue item")
|
logE("Index is out of bounds, did not remove queue item")
|
||||||
|
|
@ -281,9 +276,7 @@ class PlaybackStateManager private constructor() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */
|
||||||
* Move a queue item at [from] to a position at [to]. Will ignore invalid indexes.
|
|
||||||
*/
|
|
||||||
fun moveQueueItems(from: Int, to: Int): Boolean {
|
fun moveQueueItems(from: Int, to: Int): Boolean {
|
||||||
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
|
if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) {
|
||||||
logE("Indices were out of bounds, did not move queue item")
|
logE("Indices were out of bounds, did not move queue item")
|
||||||
|
|
@ -296,9 +289,7 @@ class PlaybackStateManager private constructor() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a [song] to the top of the queue. */
|
||||||
* Add a [song] to the top of the queue.
|
|
||||||
*/
|
|
||||||
fun playNext(song: Song) {
|
fun playNext(song: Song) {
|
||||||
if (mQueue.isEmpty()) {
|
if (mQueue.isEmpty()) {
|
||||||
return
|
return
|
||||||
|
|
@ -308,9 +299,7 @@ class PlaybackStateManager private constructor() {
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a list of [songs] to the top of the queue. */
|
||||||
* Add a list of [songs] to the top of the queue.
|
|
||||||
*/
|
|
||||||
fun playNext(songs: List<Song>) {
|
fun playNext(songs: List<Song>) {
|
||||||
if (mQueue.isEmpty()) {
|
if (mQueue.isEmpty()) {
|
||||||
return
|
return
|
||||||
|
|
@ -320,29 +309,21 @@ class PlaybackStateManager private constructor() {
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a [song] to the end of the queue. */
|
||||||
* Add a [song] to the end of the queue.
|
|
||||||
*/
|
|
||||||
fun addToQueue(song: Song) {
|
fun addToQueue(song: Song) {
|
||||||
mQueue.add(song)
|
mQueue.add(song)
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Add a list of [songs] to the end of the queue. */
|
||||||
* Add a list of [songs] to the end of the queue.
|
|
||||||
*/
|
|
||||||
fun addToQueue(songs: List<Song>) {
|
fun addToQueue(songs: List<Song>) {
|
||||||
mQueue.addAll(songs)
|
mQueue.addAll(songs)
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Force any callbacks to receive a queue update. */
|
||||||
* Force any callbacks to receive a queue update.
|
|
||||||
*/
|
|
||||||
private fun pushQueueUpdate() {
|
private fun pushQueueUpdate() {
|
||||||
callbacks.forEach {
|
callbacks.forEach { it.onQueueUpdate(mQueue, mIndex) }
|
||||||
it.onQueueUpdate(mQueue, mIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SHUFFLE FUNCTIONS ---
|
// --- SHUFFLE FUNCTIONS ---
|
||||||
|
|
@ -392,16 +373,17 @@ class PlaybackStateManager private constructor() {
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val musicStore = MusicStore.maybeGetInstance() ?: return
|
||||||
val lastSong = mSong
|
val lastSong = mSong
|
||||||
|
|
||||||
mQueue = when (mPlaybackMode) {
|
mQueue =
|
||||||
PlaybackMode.ALL_SONGS ->
|
when (mPlaybackMode) {
|
||||||
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
PlaybackMode.ALL_SONGS ->
|
||||||
PlaybackMode.IN_ALBUM ->
|
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
||||||
settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList()
|
PlaybackMode.IN_ALBUM ->
|
||||||
PlaybackMode.IN_ARTIST ->
|
settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList()
|
||||||
settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList()
|
PlaybackMode.IN_ARTIST ->
|
||||||
PlaybackMode.IN_GENRE ->
|
settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList()
|
||||||
settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList()
|
PlaybackMode.IN_GENRE ->
|
||||||
}
|
settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
if (keepSong) {
|
if (keepSong) {
|
||||||
mIndex = mQueue.indexOf(lastSong)
|
mIndex = mQueue.indexOf(lastSong)
|
||||||
|
|
@ -412,9 +394,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// --- STATE FUNCTIONS ---
|
// --- STATE FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Set whether this instance is currently [playing]. */
|
||||||
* Set whether this instance is currently [playing].
|
|
||||||
*/
|
|
||||||
fun setPlaying(playing: Boolean) {
|
fun setPlaying(playing: Boolean) {
|
||||||
if (mIsPlaying != playing) {
|
if (mIsPlaying != playing) {
|
||||||
if (playing) {
|
if (playing) {
|
||||||
|
|
@ -449,39 +429,29 @@ class PlaybackStateManager private constructor() {
|
||||||
callbacks.forEach { it.onSeek(position) }
|
callbacks.forEach { it.onSeek(position) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Rewind to the beginning of a song. */
|
||||||
* Rewind to the beginning of a song.
|
|
||||||
*/
|
|
||||||
fun rewind() {
|
fun rewind() {
|
||||||
seekTo(0)
|
seekTo(0)
|
||||||
setPlaying(true)
|
setPlaying(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Loop playback around to the beginning. */
|
||||||
* Loop playback around to the beginning.
|
|
||||||
*/
|
|
||||||
fun loop() {
|
fun loop() {
|
||||||
seekTo(0)
|
seekTo(0)
|
||||||
setPlaying(!settingsManager.pauseOnLoop)
|
setPlaying(!settingsManager.pauseOnLoop)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Set the [LoopMode] to [mode]. */
|
||||||
* Set the [LoopMode] to [mode].
|
|
||||||
*/
|
|
||||||
fun setLoopMode(mode: LoopMode) {
|
fun setLoopMode(mode: LoopMode) {
|
||||||
mLoopMode = mode
|
mLoopMode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Mark whether this instance has played or not */
|
||||||
* Mark whether this instance has played or not
|
|
||||||
*/
|
|
||||||
fun setHasPlayed(hasPlayed: Boolean) {
|
fun setHasPlayed(hasPlayed: Boolean) {
|
||||||
mHasPlayed = hasPlayed
|
mHasPlayed = hasPlayed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Mark this instance as restored. */
|
||||||
* Mark this instance as restored.
|
|
||||||
*/
|
|
||||||
fun markRestored() {
|
fun markRestored() {
|
||||||
mIsRestored = true
|
mIsRestored = true
|
||||||
}
|
}
|
||||||
|
|
@ -503,16 +473,19 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
database.writeState(
|
database.writeState(
|
||||||
PlaybackStateDatabase.SavedState(
|
PlaybackStateDatabase.SavedState(
|
||||||
mSong, mPosition, mParent, mIndex,
|
mSong,
|
||||||
mPlaybackMode, mIsShuffling, mLoopMode,
|
mPosition,
|
||||||
)
|
mParent,
|
||||||
)
|
mIndex,
|
||||||
|
mPlaybackMode,
|
||||||
|
mIsShuffling,
|
||||||
|
mLoopMode,
|
||||||
|
))
|
||||||
|
|
||||||
database.writeQueue(mQueue)
|
database.writeQueue(mQueue)
|
||||||
|
|
||||||
this@PlaybackStateManager.logD(
|
this@PlaybackStateManager.logD(
|
||||||
"State save completed successfully in ${System.currentTimeMillis() - start}ms"
|
"State save completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,9 +522,7 @@ class PlaybackStateManager private constructor() {
|
||||||
markRestored()
|
markRestored()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Unpack a [playbackState] into this instance. */
|
||||||
* Unpack a [playbackState] into this instance.
|
|
||||||
*/
|
|
||||||
private fun unpackFromPlaybackState(playbackState: PlaybackStateDatabase.SavedState) {
|
private fun unpackFromPlaybackState(playbackState: PlaybackStateDatabase.SavedState) {
|
||||||
// Turn the simplified information from PlaybackState into usable data.
|
// Turn the simplified information from PlaybackState into usable data.
|
||||||
|
|
||||||
|
|
@ -573,26 +544,23 @@ class PlaybackStateManager private constructor() {
|
||||||
pushQueueUpdate()
|
pushQueueUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Do a sanity check to make sure the parent was not lost in the restore process. */
|
||||||
* Do a sanity check to make sure the parent was not lost in the restore process.
|
|
||||||
*/
|
|
||||||
private fun doParentSanityCheck() {
|
private fun doParentSanityCheck() {
|
||||||
// Check if the parent was lost while in the DB.
|
// Check if the parent was lost while in the DB.
|
||||||
if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) {
|
if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) {
|
||||||
logD("Parent lost, attempting restore")
|
logD("Parent lost, attempting restore")
|
||||||
|
|
||||||
mParent = when (mPlaybackMode) {
|
mParent =
|
||||||
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album
|
when (mPlaybackMode) {
|
||||||
PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist
|
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album
|
||||||
PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre
|
PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist
|
||||||
PlaybackMode.ALL_SONGS -> null
|
PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre
|
||||||
}
|
PlaybackMode.ALL_SONGS -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Do a sanity check to make sure that the index lines up with the current song. */
|
||||||
* Do a sanity check to make sure that the index lines up with the current song.
|
|
||||||
*/
|
|
||||||
private fun doIndexSanityCheck() {
|
private fun doIndexSanityCheck() {
|
||||||
// Be careful with how we handle the queue since a possible index de-sync
|
// Be careful with how we handle the queue since a possible index de-sync
|
||||||
// could easily result in an OOB crash.
|
// could easily result in an OOB crash.
|
||||||
|
|
@ -639,9 +607,8 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface for receiving updates from [PlaybackStateManager].
|
* The interface for receiving updates from [PlaybackStateManager]. Add the callback to
|
||||||
* Add the callback to [PlaybackStateManager] using [addCallback],
|
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
|
||||||
* remove them on destruction with [removeCallback].
|
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onSongUpdate(song: Song?) {}
|
fun onSongUpdate(song: Song?) {}
|
||||||
|
|
@ -658,12 +625,9 @@ class PlaybackStateManager private constructor() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val REWIND_THRESHOLD = 3000L
|
private const val REWIND_THRESHOLD = 3000L
|
||||||
|
|
||||||
@Volatile
|
@Volatile private var INSTANCE: PlaybackStateManager? = null
|
||||||
private var INSTANCE: PlaybackStateManager? = null
|
|
||||||
|
|
||||||
/**
|
/** Get/Instantiate the single instance of [PlaybackStateManager]. */
|
||||||
* Get/Instantiate the single instance of [PlaybackStateManager].
|
|
||||||
*/
|
|
||||||
fun getInstance(): PlaybackStateManager {
|
fun getInstance(): PlaybackStateManager {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AudioReactor.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -28,22 +27,20 @@ import androidx.media.AudioManagerCompat
|
||||||
import com.google.android.exoplayer2.metadata.Metadata
|
import com.google.android.exoplayer2.metadata.Metadata
|
||||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
||||||
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
||||||
|
import kotlin.math.pow
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import kotlin.math.pow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the current volume and playback state across ReplayGain and AudioFocus events.
|
* Manages the current volume and playback state across ReplayGain and AudioFocus events.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AudioReactor(
|
class AudioReactor(context: Context, private val callback: (Float) -> Unit) :
|
||||||
context: Context,
|
AudioManager.OnAudioFocusChangeListener, SettingsManager.Callback {
|
||||||
private val callback: (Float) -> Unit
|
|
||||||
) : AudioManager.OnAudioFocusChangeListener, SettingsManager.Callback {
|
|
||||||
private data class Gain(val track: Float, val album: Float)
|
private data class Gain(val track: Float, val album: Float)
|
||||||
private data class GainTag(val key: String, val value: Float)
|
private data class GainTag(val key: String, val value: Float)
|
||||||
|
|
||||||
|
|
@ -51,16 +48,16 @@ class AudioReactor(
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
private val audioManager = context.getSystemServiceSafe(AudioManager::class)
|
||||||
|
|
||||||
private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
|
private val request =
|
||||||
.setWillPauseWhenDucked(false)
|
AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
|
||||||
.setAudioAttributes(
|
.setWillPauseWhenDucked(false)
|
||||||
AudioAttributesCompat.Builder()
|
.setAudioAttributes(
|
||||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
|
AudioAttributesCompat.Builder()
|
||||||
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
|
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
|
||||||
.build()
|
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
|
||||||
)
|
.build())
|
||||||
.setOnAudioFocusChangeListener(this)
|
.setOnAudioFocusChangeListener(this)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private var pauseWasTransient = false
|
private var pauseWasTransient = false
|
||||||
|
|
||||||
|
|
@ -82,19 +79,16 @@ class AudioReactor(
|
||||||
settingsManager.addCallback(this)
|
settingsManager.addCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Request the android system for audio focus */
|
||||||
* Request the android system for audio focus
|
|
||||||
*/
|
|
||||||
fun requestFocus() {
|
fun requestFocus() {
|
||||||
logD("Requesting audio focus")
|
logD("Requesting audio focus")
|
||||||
AudioManagerCompat.requestAudioFocus(audioManager, request)
|
AudioManagerCompat.requestAudioFocus(audioManager, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the rough volume adjustment for [Metadata] with ReplayGain tags.
|
* Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is based off
|
||||||
* This is based off Vanilla Music's implementation.
|
* Vanilla Music's implementation. TODO: Add ReplayGain pre-amp TODO: Add positive ReplayGain
|
||||||
* TODO: Add ReplayGain pre-amp
|
* values
|
||||||
* TODO: Add positive ReplayGain values
|
|
||||||
*/
|
*/
|
||||||
fun applyReplayGain(metadata: Metadata?) {
|
fun applyReplayGain(metadata: Metadata?) {
|
||||||
if (metadata == null) {
|
if (metadata == null) {
|
||||||
|
|
@ -104,49 +98,44 @@ class AudioReactor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayGain is configurable, so determine what to do based off of the mode.
|
// ReplayGain is configurable, so determine what to do based off of the mode.
|
||||||
val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) {
|
val useAlbumGain: (Gain) -> Boolean =
|
||||||
ReplayGainMode.OFF -> {
|
when (settingsManager.replayGainMode) {
|
||||||
logD("ReplayGain is off")
|
ReplayGainMode.OFF -> {
|
||||||
volume = 1f
|
logD("ReplayGain is off")
|
||||||
return
|
volume = 1f
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// User wants track gain to be preferred. Default to album gain only if there
|
||||||
|
// is no track gain.
|
||||||
|
ReplayGainMode.TRACK -> { gain -> gain.track == 0f }
|
||||||
|
|
||||||
|
// User wants album gain to be preferred. Default to track gain only if there
|
||||||
|
// is no album gain.
|
||||||
|
ReplayGainMode.ALBUM -> { gain -> gain.album != 0f }
|
||||||
|
|
||||||
|
// User wants album gain to be used when in an album, track gain otherwise.
|
||||||
|
ReplayGainMode.DYNAMIC -> { _ ->
|
||||||
|
playbackManager.parent is Album &&
|
||||||
|
playbackManager.song?.album == playbackManager.parent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User wants track gain to be preferred. Default to album gain only if there
|
|
||||||
// is no track gain.
|
|
||||||
ReplayGainMode.TRACK ->
|
|
||||||
{ gain ->
|
|
||||||
gain.track == 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
// User wants album gain to be preferred. Default to track gain only if there
|
|
||||||
// is no album gain.
|
|
||||||
ReplayGainMode.ALBUM ->
|
|
||||||
{ gain ->
|
|
||||||
gain.album != 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
// User wants album gain to be used when in an album, track gain otherwise.
|
|
||||||
ReplayGainMode.DYNAMIC ->
|
|
||||||
{ _ ->
|
|
||||||
playbackManager.parent is Album &&
|
|
||||||
playbackManager.song?.album == playbackManager.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val gain = parseReplayGain(metadata)
|
val gain = parseReplayGain(metadata)
|
||||||
|
|
||||||
val adjust = if (gain != null) {
|
val adjust =
|
||||||
if (useAlbumGain(gain)) {
|
if (gain != null) {
|
||||||
logD("Using album gain")
|
if (useAlbumGain(gain)) {
|
||||||
gain.album
|
logD("Using album gain")
|
||||||
|
gain.album
|
||||||
|
} else {
|
||||||
|
logD("Using track gain")
|
||||||
|
gain.track
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logD("Using track gain")
|
// No gain tags were present
|
||||||
gain.track
|
0f
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// No gain tags were present
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final adjustment along the volume curve.
|
// Final adjustment along the volume curve.
|
||||||
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
|
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
|
||||||
|
|
@ -171,12 +160,10 @@ class AudioReactor(
|
||||||
key = entry.description?.uppercase()
|
key = entry.description?.uppercase()
|
||||||
value = entry.value
|
value = entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
is VorbisComment -> {
|
is VorbisComment -> {
|
||||||
key = entry.key
|
key = entry.key
|
||||||
value = entry.value
|
value = entry.value
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> continue
|
else -> continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,9 +213,7 @@ class AudioReactor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Abandon the current focus request and any callbacks */
|
||||||
* Abandon the current focus request and any callbacks
|
|
||||||
*/
|
|
||||||
fun release() {
|
fun release() {
|
||||||
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request)
|
AudioManagerCompat.abandonAudioFocusRequest(audioManager, request)
|
||||||
settingsManager.removeCallback(this)
|
settingsManager.removeCallback(this)
|
||||||
|
|
@ -302,11 +287,6 @@ class AudioReactor(
|
||||||
const val R128_TRACK = "R128_TRACK_GAIN"
|
const val R128_TRACK = "R128_TRACK_GAIN"
|
||||||
const val R128_ALBUM = "R128_ALBUM_GAIN"
|
const val R128_ALBUM = "R128_ALBUM_GAIN"
|
||||||
|
|
||||||
val REPLAY_GAIN_TAGS = arrayOf(
|
val REPLAY_GAIN_TAGS = arrayOf(RG_TRACK, RG_ALBUM, R128_ALBUM, R128_TRACK)
|
||||||
RG_TRACK,
|
|
||||||
RG_ALBUM,
|
|
||||||
R128_ALBUM,
|
|
||||||
R128_TRACK
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
|
@ -8,14 +25,14 @@ import androidx.core.content.ContextCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON
|
* Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON intent
|
||||||
* intent to determine the media apps on a system. *Auxio does not expose this.* Auxio exposes
|
* to determine the media apps on a system. *Auxio does not expose this.* Auxio exposes a
|
||||||
* a MediaSession that an app should control instead through the much better MediaController API.
|
* MediaSession that an app should control instead through the much better MediaController API. But
|
||||||
* But who cares about that, we need to make sure the 3% of barely functioning TouchWiz devices
|
* who cares about that, we need to make sure the 3% of barely functioning TouchWiz devices running
|
||||||
* running KitKat don't break! To prevent Auxio from not showing up at all in these apps, we
|
* KitKat don't break! To prevent Auxio from not showing up at all in these apps, we declare a
|
||||||
* declare a BroadcastReceiver that deliberately handles this event. This also means that Auxio
|
* BroadcastReceiver that deliberately handles this event. This also means that Auxio will start
|
||||||
* will start without warning if you use the media buttons while the app exists, because I guess
|
* without warning if you use the media buttons while the app exists, because I guess we just have
|
||||||
* we just have to deal with this.
|
* to deal with this.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MediaButtonReceiver : BroadcastReceiver() {
|
class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackNotification.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -37,15 +36,14 @@ import org.oxycblt.auxio.util.newBroadcastIntent
|
||||||
import org.oxycblt.auxio.util.newMainIntent
|
import org.oxycblt.auxio.util.newMainIntent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
|
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have to be
|
||||||
* to be delivered manually.
|
* delivered manually.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class PlaybackNotification private constructor(
|
class PlaybackNotification
|
||||||
private val context: Context,
|
private constructor(private val context: Context, mediaToken: MediaSessionCompat.Token) :
|
||||||
mediaToken: MediaSessionCompat.Token
|
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||||
) : NotificationCompat.Builder(context, CHANNEL_ID) {
|
|
||||||
init {
|
init {
|
||||||
setSmallIcon(R.drawable.ic_auxio)
|
setSmallIcon(R.drawable.ic_auxio)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
|
@ -61,11 +59,7 @@ class PlaybackNotification private constructor(
|
||||||
addAction(buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
addAction(buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
||||||
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit))
|
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit))
|
||||||
|
|
||||||
setStyle(
|
setStyle(MediaStyle().setMediaSession(mediaToken).setShowActionsInCompactView(1, 2, 3))
|
||||||
MediaStyle()
|
|
||||||
.setMediaSession(mediaToken)
|
|
||||||
.setShowActionsInCompactView(1, 2, 3)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Don't connect to PlaybackStateManager here. This is because it's possible for this
|
// Don't connect to PlaybackStateManager here. This is because it's possible for this
|
||||||
// notification to not be updated by PlaybackStateManager before PlaybackService pushes
|
// notification to not be updated by PlaybackStateManager before PlaybackService pushes
|
||||||
|
|
@ -96,30 +90,22 @@ class PlaybackNotification private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Set the playing icon on the notification */
|
||||||
* Set the playing icon on the notification
|
|
||||||
*/
|
|
||||||
fun setPlaying(isPlaying: Boolean) {
|
fun setPlaying(isPlaying: Boolean) {
|
||||||
mActions[2] = buildPlayPauseAction(context, isPlaying)
|
mActions[2] = buildPlayPauseAction(context, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the first action to reflect the [loopMode] given. */
|
||||||
* Update the first action to reflect the [loopMode] given.
|
|
||||||
*/
|
|
||||||
fun setLoop(loopMode: LoopMode) {
|
fun setLoop(loopMode: LoopMode) {
|
||||||
mActions[0] = buildLoopAction(context, loopMode)
|
mActions[0] = buildLoopAction(context, loopMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the first action to reflect whether the queue is shuffled or not */
|
||||||
* Update the first action to reflect whether the queue is shuffled or not
|
|
||||||
*/
|
|
||||||
fun setShuffle(isShuffling: Boolean) {
|
fun setShuffle(isShuffling: Boolean) {
|
||||||
mActions[0] = buildShuffleAction(context, isShuffling)
|
mActions[0] = buildShuffleAction(context, isShuffling)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Apply the current [parent] to the header of the notification. */
|
||||||
* Apply the current [parent] to the header of the notification.
|
|
||||||
*/
|
|
||||||
fun setParent(parent: MusicParent?) {
|
fun setParent(parent: MusicParent?) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
||||||
|
|
||||||
|
|
@ -138,15 +124,13 @@ class PlaybackNotification private constructor(
|
||||||
return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes)
|
return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildLoopAction(
|
private fun buildLoopAction(context: Context, loopMode: LoopMode): NotificationCompat.Action {
|
||||||
context: Context,
|
val drawableRes =
|
||||||
loopMode: LoopMode
|
when (loopMode) {
|
||||||
): NotificationCompat.Action {
|
LoopMode.NONE -> R.drawable.ic_remote_loop_off
|
||||||
val drawableRes = when (loopMode) {
|
LoopMode.ALL -> R.drawable.ic_loop
|
||||||
LoopMode.NONE -> R.drawable.ic_remote_loop_off
|
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||||
LoopMode.ALL -> R.drawable.ic_loop
|
}
|
||||||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildAction(context, PlaybackService.ACTION_LOOP, drawableRes)
|
return buildAction(context, PlaybackService.ACTION_LOOP, drawableRes)
|
||||||
}
|
}
|
||||||
|
|
@ -155,7 +139,8 @@ class PlaybackNotification private constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
isShuffled: Boolean
|
isShuffled: Boolean
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_remote_shuffle_off
|
val drawableRes =
|
||||||
|
if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_remote_shuffle_off
|
||||||
|
|
||||||
return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes)
|
return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes)
|
||||||
}
|
}
|
||||||
|
|
@ -165,10 +150,9 @@ class PlaybackNotification private constructor(
|
||||||
actionName: String,
|
actionName: String,
|
||||||
@DrawableRes iconRes: Int
|
@DrawableRes iconRes: Int
|
||||||
): NotificationCompat.Action {
|
): NotificationCompat.Action {
|
||||||
val action = NotificationCompat.Action.Builder(
|
val action =
|
||||||
iconRes, actionName,
|
NotificationCompat.Action.Builder(
|
||||||
context.newBroadcastIntent(actionName)
|
iconRes, actionName, context.newBroadcastIntent(actionName))
|
||||||
)
|
|
||||||
|
|
||||||
return action.build()
|
return action.build()
|
||||||
}
|
}
|
||||||
|
|
@ -177,19 +161,18 @@ class PlaybackNotification private constructor(
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK"
|
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK"
|
||||||
const val NOTIFICATION_ID = 0xA0A0
|
const val NOTIFICATION_ID = 0xA0A0
|
||||||
|
|
||||||
/**
|
/** Build a new instance of [PlaybackNotification]. */
|
||||||
* Build a new instance of [PlaybackNotification].
|
|
||||||
*/
|
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
context: Context,
|
||||||
notificationManager: NotificationManager,
|
notificationManager: NotificationManager,
|
||||||
mediaSession: MediaSessionCompat
|
mediaSession: MediaSessionCompat
|
||||||
): PlaybackNotification {
|
): PlaybackNotification {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
CHANNEL_ID, context.getString(R.string.info_channel_name),
|
NotificationChannel(
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
CHANNEL_ID,
|
||||||
)
|
context.getString(R.string.info_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackService.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
|
@ -69,11 +68,12 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
* - Headset management
|
* - Headset management
|
||||||
* - Widgets
|
* - Widgets
|
||||||
*
|
*
|
||||||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback],
|
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
|
||||||
* so therefore there's no need to bind to it to deliver commands.
|
* therefore there's no need to bind to it to deliver commands.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
class PlaybackService :
|
||||||
|
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||||
// Player components
|
// Player components
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
|
|
@ -126,22 +126,20 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
.setUsage(C.USAGE_MEDIA)
|
.setUsage(C.USAGE_MEDIA)
|
||||||
.setContentType(C.CONTENT_TYPE_MUSIC)
|
.setContentType(C.CONTENT_TYPE_MUSIC)
|
||||||
.build(),
|
.build(),
|
||||||
false
|
false)
|
||||||
)
|
|
||||||
|
|
||||||
audioReactor = AudioReactor(this) { volume ->
|
audioReactor =
|
||||||
logD("Updating player volume to $volume")
|
AudioReactor(this) { volume ->
|
||||||
player.volume = volume
|
logD("Updating player volume to $volume")
|
||||||
}
|
player.volume = volume
|
||||||
|
}
|
||||||
|
|
||||||
// --- SYSTEM SETUP ---
|
// --- SYSTEM SETUP ---
|
||||||
|
|
||||||
widgets = WidgetController(this)
|
widgets = WidgetController(this)
|
||||||
|
|
||||||
// Set up the media button callbacks
|
// Set up the media button callbacks
|
||||||
mediaSession = MediaSessionCompat(this, packageName).apply {
|
mediaSession = MediaSessionCompat(this, packageName).apply { isActive = true }
|
||||||
isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
connector = PlaybackSessionConnector(this, player, mediaSession)
|
connector = PlaybackSessionConnector(this, player, mediaSession)
|
||||||
|
|
||||||
|
|
@ -215,7 +213,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
when (state) {
|
when (state) {
|
||||||
Player.STATE_READY -> startPolling()
|
Player.STATE_READY -> startPolling()
|
||||||
|
|
||||||
Player.STATE_ENDED -> {
|
Player.STATE_ENDED -> {
|
||||||
if (playbackManager.loopMode == LoopMode.TRACK) {
|
if (playbackManager.loopMode == LoopMode.TRACK) {
|
||||||
playbackManager.loop()
|
playbackManager.loop()
|
||||||
|
|
@ -223,7 +220,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -347,17 +343,14 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
// --- OTHER FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/** Create the [ExoPlayer] instance. */
|
||||||
* Create the [ExoPlayer] instance.
|
|
||||||
*/
|
|
||||||
private fun newPlayer(): ExoPlayer {
|
private fun newPlayer(): ExoPlayer {
|
||||||
// Since Auxio is a music player, only specify an audio renderer to save
|
// Since Auxio is a music player, only specify an audio renderer to save
|
||||||
// battery/apk size/cache size
|
// battery/apk size/cache size
|
||||||
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
|
||||||
arrayOf(
|
arrayOf(
|
||||||
MediaCodecAudioRenderer(this, MediaCodecSelector.DEFAULT, handler, audioListener),
|
MediaCodecAudioRenderer(this, MediaCodecSelector.DEFAULT, handler, audioListener),
|
||||||
LibflacAudioRenderer(handler, audioListener)
|
LibflacAudioRenderer(handler, audioListener))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
|
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
|
||||||
|
|
@ -369,9 +362,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Fully restore the notification and playback state */
|
||||||
* Fully restore the notification and playback state
|
|
||||||
*/
|
|
||||||
private fun restore() {
|
private fun restore() {
|
||||||
logD("Restoring the service state")
|
logD("Restoring the service state")
|
||||||
|
|
||||||
|
|
@ -387,16 +378,16 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
widgets.update()
|
widgets.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Start polling the position on a coroutine. */
|
||||||
* Start polling the position on a coroutine.
|
|
||||||
*/
|
|
||||||
private fun startPolling() {
|
private fun startPolling() {
|
||||||
val pollFlow = flow {
|
val pollFlow =
|
||||||
while (true) {
|
flow {
|
||||||
emit(player.currentPosition)
|
while (true) {
|
||||||
delay(POS_POLL_INTERVAL)
|
emit(player.currentPosition)
|
||||||
}
|
delay(POS_POLL_INTERVAL)
|
||||||
}.conflate()
|
}
|
||||||
|
}
|
||||||
|
.conflate()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
pollFlow.takeWhile { player.isPlaying }.collect { pos ->
|
pollFlow.takeWhile { player.isPlaying }.collect { pos ->
|
||||||
|
|
@ -416,9 +407,9 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
// Specify that this is a media service, if supported.
|
// Specify that this is a media service, if supported.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(
|
startForeground(
|
||||||
PlaybackNotification.NOTIFICATION_ID, notification.build(),
|
PlaybackNotification.NOTIFICATION_ID,
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
notification.build(),
|
||||||
)
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
} else {
|
} else {
|
||||||
startForeground(PlaybackNotification.NOTIFICATION_ID, notification.build())
|
startForeground(PlaybackNotification.NOTIFICATION_ID, notification.build())
|
||||||
}
|
}
|
||||||
|
|
@ -427,24 +418,19 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
} else {
|
} else {
|
||||||
// If we are already in foreground just update the notification
|
// If we are already in foreground just update the notification
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
PlaybackNotification.NOTIFICATION_ID, notification.build()
|
PlaybackNotification.NOTIFICATION_ID, notification.build())
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Stop the foreground state and hide the notification */
|
||||||
* Stop the foreground state and hide the notification
|
|
||||||
*/
|
|
||||||
private fun stopForegroundAndNotification() {
|
private fun stopForegroundAndNotification() {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
notificationManager.cancel(PlaybackNotification.NOTIFICATION_ID)
|
notificationManager.cancel(PlaybackNotification.NOTIFICATION_ID)
|
||||||
isForeground = false
|
isForeground = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
||||||
* A [BroadcastReceiver] for receiving general playback events from the system.
|
|
||||||
*/
|
|
||||||
private inner class PlaybackReceiver : BroadcastReceiver() {
|
private inner class PlaybackReceiver : BroadcastReceiver() {
|
||||||
private var initialHeadsetPlugEventHandled = false
|
private var initialHeadsetPlugEventHandled = false
|
||||||
|
|
||||||
|
|
@ -477,56 +463,44 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
|
||||||
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
|
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug()
|
||||||
|
|
||||||
// --- AUXIO EVENTS ---
|
// --- AUXIO EVENTS ---
|
||||||
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(
|
ACTION_PLAY_PAUSE -> playbackManager.setPlaying(!playbackManager.isPlaying)
|
||||||
!playbackManager.isPlaying
|
ACTION_LOOP -> playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||||
)
|
ACTION_SHUFFLE ->
|
||||||
|
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
|
||||||
ACTION_LOOP -> playbackManager.setLoopMode(
|
|
||||||
playbackManager.loopMode.increment()
|
|
||||||
)
|
|
||||||
|
|
||||||
ACTION_SHUFFLE -> playbackManager.setShuffling(
|
|
||||||
!playbackManager.isShuffling, keepSong = true
|
|
||||||
)
|
|
||||||
|
|
||||||
ACTION_SKIP_PREV -> playbackManager.prev()
|
ACTION_SKIP_PREV -> playbackManager.prev()
|
||||||
ACTION_SKIP_NEXT -> playbackManager.next()
|
ACTION_SKIP_NEXT -> playbackManager.next()
|
||||||
|
|
||||||
ACTION_EXIT -> {
|
ACTION_EXIT -> {
|
||||||
playbackManager.setPlaying(false)
|
playbackManager.setPlaying(false)
|
||||||
stopForegroundAndNotification()
|
stopForegroundAndNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
|
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume from a headset plug event in the case that the quirk is enabled.
|
* Resume from a headset plug event in the case that the quirk is enabled. This
|
||||||
* This functionality remains a quirk for two reasons:
|
* functionality remains a quirk for two reasons:
|
||||||
* 1. Automatically resuming more or less overrides all other audio streams, which
|
* 1. Automatically resuming more or less overrides all other audio streams, which is not
|
||||||
* is not that friendly
|
* that friendly
|
||||||
* 2. There is a bug where playback will always start when this service starts, mostly
|
* 2. There is a bug where playback will always start when this service starts, mostly due
|
||||||
* due to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but
|
* to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but I fear
|
||||||
* I fear that it may not work on OEM skins that for whatever reason don't make this
|
* that it may not work on OEM skins that for whatever reason don't make this action fire.
|
||||||
* action fire.
|
* TODO: Figure out how players like Retro are able to get autoplay working with bluetooth
|
||||||
* TODO: Figure out how players like Retro are able to get autoplay working with
|
* headsets
|
||||||
* bluetooth headsets
|
|
||||||
*/
|
*/
|
||||||
private fun maybeResumeFromPlug() {
|
private fun maybeResumeFromPlug() {
|
||||||
if (playbackManager.song != null &&
|
if (playbackManager.song != null &&
|
||||||
settingsManager.headsetAutoplay &&
|
settingsManager.headsetAutoplay &&
|
||||||
initialHeadsetPlugEventHandled
|
initialHeadsetPlugEventHandled) {
|
||||||
) {
|
|
||||||
logD("Device connected, resuming")
|
logD("Device connected, resuming")
|
||||||
playbackManager.setPlaying(true)
|
playbackManager.setPlaying(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause from a headset plug.
|
* Pause from a headset plug. TODO: Find a way to centralize this stuff into a single
|
||||||
* TODO: Find a way to centralize this stuff into a single BroadcastReciever instead
|
* BroadcastReciever instead of the weird disjointed arrangement between MediaSession and
|
||||||
* of the weird disjointed arrangement between MediaSession and this.
|
* this.
|
||||||
*/
|
*/
|
||||||
private fun pauseFromPlug() {
|
private fun pauseFromPlug() {
|
||||||
if (playbackManager.song != null) {
|
if (playbackManager.song != null) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* PlaybackSessionConnector.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -32,8 +31,8 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player],
|
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], and
|
||||||
* and [PlaybackStateManager].
|
* [PlaybackStateManager].
|
||||||
*/
|
*/
|
||||||
class PlaybackSessionConnector(
|
class PlaybackSessionConnector(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
|
@ -84,12 +83,13 @@ class PlaybackSessionConnector(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetRepeatMode(repeatMode: Int) {
|
override fun onSetRepeatMode(repeatMode: Int) {
|
||||||
val mode = when (repeatMode) {
|
val mode =
|
||||||
PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL
|
when (repeatMode) {
|
||||||
PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL
|
PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL
|
||||||
PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK
|
PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL
|
||||||
else -> LoopMode.NONE
|
PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK
|
||||||
}
|
else -> LoopMode.NONE
|
||||||
|
}
|
||||||
|
|
||||||
playbackManager.setLoopMode(mode)
|
playbackManager.setLoopMode(mode)
|
||||||
}
|
}
|
||||||
|
|
@ -98,8 +98,7 @@ class PlaybackSessionConnector(
|
||||||
playbackManager.setShuffling(
|
playbackManager.setShuffling(
|
||||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
|
||||||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
|
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
|
||||||
true
|
true)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|
@ -117,15 +116,16 @@ class PlaybackSessionConnector(
|
||||||
|
|
||||||
val artistName = song.resolvedArtistName
|
val artistName = song.resolvedArtistName
|
||||||
|
|
||||||
val builder = MediaMetadataCompat.Builder()
|
val builder =
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName)
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName)
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName)
|
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.resolvedAlbumName)
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName)
|
||||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.resolvedAlbumName)
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
||||||
|
|
||||||
// Load the cover asynchronously. This is the entire reason I don't use a plain
|
// Load the cover asynchronously. This is the entire reason I don't use a plain
|
||||||
// MediaSessionConnector, which AFAIK makes it impossible to load this the way I do
|
// MediaSessionConnector, which AFAIK makes it impossible to load this the way I do
|
||||||
|
|
@ -144,14 +144,12 @@ class PlaybackSessionConnector(
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
if (events.containsAny(
|
if (events.containsAny(
|
||||||
Player.EVENT_POSITION_DISCONTINUITY,
|
Player.EVENT_POSITION_DISCONTINUITY,
|
||||||
Player.EVENT_PLAYBACK_STATE_CHANGED,
|
Player.EVENT_PLAYBACK_STATE_CHANGED,
|
||||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||||
Player.EVENT_IS_PLAYING_CHANGED,
|
Player.EVENT_IS_PLAYING_CHANGED,
|
||||||
Player.EVENT_REPEAT_MODE_CHANGED,
|
Player.EVENT_REPEAT_MODE_CHANGED,
|
||||||
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED
|
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||||
)
|
|
||||||
) {
|
|
||||||
invalidateSessionState()
|
invalidateSessionState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,24 +160,20 @@ class PlaybackSessionConnector(
|
||||||
logD("Updating media session state")
|
logD("Updating media session state")
|
||||||
|
|
||||||
// Position updates arrive faster when you upload STATE_PAUSED for some insane reason.
|
// Position updates arrive faster when you upload STATE_PAUSED for some insane reason.
|
||||||
val state = PlaybackStateCompat.Builder()
|
val state =
|
||||||
.setActions(ACTIONS)
|
PlaybackStateCompat.Builder()
|
||||||
.setBufferedPosition(player.bufferedPosition)
|
.setActions(ACTIONS)
|
||||||
.setState(
|
.setBufferedPosition(player.bufferedPosition)
|
||||||
PlaybackStateCompat.STATE_PAUSED,
|
.setState(
|
||||||
player.currentPosition,
|
PlaybackStateCompat.STATE_PAUSED,
|
||||||
1.0f,
|
player.currentPosition,
|
||||||
SystemClock.elapsedRealtime()
|
1.0f,
|
||||||
)
|
SystemClock.elapsedRealtime())
|
||||||
|
|
||||||
mediaSession.setPlaybackState(state.build())
|
mediaSession.setPlaybackState(state.build())
|
||||||
|
|
||||||
state.setState(
|
state.setState(
|
||||||
getPlayerState(),
|
getPlayerState(), player.currentPosition, 1.0f, SystemClock.elapsedRealtime())
|
||||||
player.currentPosition,
|
|
||||||
1.0f,
|
|
||||||
SystemClock.elapsedRealtime()
|
|
||||||
)
|
|
||||||
|
|
||||||
mediaSession.setPlaybackState(state.build())
|
mediaSession.setPlaybackState(state.build())
|
||||||
}
|
}
|
||||||
|
|
@ -199,14 +193,15 @@ class PlaybackSessionConnector(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTIONS = PlaybackStateCompat.ACTION_PLAY or
|
const val ACTIONS =
|
||||||
PlaybackStateCompat.ACTION_PAUSE or
|
PlaybackStateCompat.ACTION_PLAY or
|
||||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
PlaybackStateCompat.ACTION_PAUSE or
|
||||||
PlaybackStateCompat.ACTION_SET_REPEAT_MODE or
|
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||||
PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or
|
PlaybackStateCompat.ACTION_SET_REPEAT_MODE or
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||||
PlaybackStateCompat.ACTION_SEEK_TO or
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||||
PlaybackStateCompat.ACTION_STOP
|
PlaybackStateCompat.ACTION_SEEK_TO or
|
||||||
|
PlaybackStateCompat.ACTION_STOP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
/**
|
/** Represents the current setting for ReplayGain. */
|
||||||
* Represents the current setting for ReplayGain.
|
|
||||||
*/
|
|
||||||
enum class ReplayGainMode {
|
enum class ReplayGainMode {
|
||||||
/** Do not apply ReplayGain. */
|
/** Do not apply ReplayGain. */
|
||||||
OFF,
|
OFF,
|
||||||
|
|
@ -13,9 +28,7 @@ enum class ReplayGainMode {
|
||||||
/** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */
|
/** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */
|
||||||
DYNAMIC;
|
DYNAMIC;
|
||||||
|
|
||||||
/**
|
/** Converts this type to an integer constant. */
|
||||||
* Converts this type to an integer constant.
|
|
||||||
*/
|
|
||||||
fun toInt(): Int {
|
fun toInt(): Int {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
OFF -> INT_OFF
|
OFF -> INT_OFF
|
||||||
|
|
@ -31,9 +44,7 @@ enum class ReplayGainMode {
|
||||||
private const val INT_ALBUM = 0xA112
|
private const val INT_ALBUM = 0xA112
|
||||||
private const val INT_DYNAMIC = 0xA113
|
private const val INT_DYNAMIC = 0xA113
|
||||||
|
|
||||||
/**
|
/** Converts an integer constant to this type. */
|
||||||
* Converts an integer constant to this type.
|
|
||||||
*/
|
|
||||||
fun fromInt(value: Int): ReplayGainMode? {
|
fun fromInt(value: Int): ReplayGainMode? {
|
||||||
return when (value) {
|
return when (value) {
|
||||||
INT_OFF -> OFF
|
INT_OFF -> OFF
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SearchAdapter.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.search
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -58,24 +57,15 @@ class SearchAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from(
|
GenreViewHolder.ITEM_TYPE ->
|
||||||
parent.context, doOnClick, doOnLongClick
|
GenreViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||||
)
|
ArtistViewHolder.ITEM_TYPE ->
|
||||||
|
ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||||
ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from(
|
AlbumViewHolder.ITEM_TYPE ->
|
||||||
parent.context, doOnClick, doOnLongClick
|
AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||||
)
|
SongViewHolder.ITEM_TYPE ->
|
||||||
|
SongViewHolder.from(parent.context, doOnClick, doOnLongClick)
|
||||||
AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from(
|
|
||||||
parent.context, doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
|
|
||||||
SongViewHolder.ITEM_TYPE -> SongViewHolder.from(
|
|
||||||
parent.context, doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
|
|
||||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||||
|
|
||||||
else -> error("Invalid ViewHolder item type")
|
else -> error("Invalid ViewHolder item type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SearchFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.search
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -67,24 +66,21 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
val imm = requireContext().getSystemServiceSafe(InputMethodManager::class)
|
||||||
|
|
||||||
val searchAdapter = SearchAdapter(
|
val searchAdapter =
|
||||||
doOnClick = { item ->
|
SearchAdapter(doOnClick = { item -> onItemSelection(item, imm) }, ::newMenu)
|
||||||
onItemSelection(item, imm)
|
|
||||||
},
|
|
||||||
::newMenu
|
|
||||||
)
|
|
||||||
// --- UI SETUP --
|
// --- UI SETUP --
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
binding.searchToolbar.apply {
|
binding.searchToolbar.apply {
|
||||||
val itemId = when (searchModel.filterMode) {
|
val itemId =
|
||||||
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
when (searchModel.filterMode) {
|
||||||
DisplayMode.SHOW_ALBUMS -> R.id.option_filter_albums
|
DisplayMode.SHOW_SONGS -> R.id.option_filter_songs
|
||||||
DisplayMode.SHOW_ARTISTS -> R.id.option_filter_artists
|
DisplayMode.SHOW_ALBUMS -> R.id.option_filter_albums
|
||||||
DisplayMode.SHOW_GENRES -> R.id.option_filter_genres
|
DisplayMode.SHOW_ARTISTS -> R.id.option_filter_artists
|
||||||
null -> R.id.option_filter_all
|
DisplayMode.SHOW_GENRES -> R.id.option_filter_genres
|
||||||
}
|
null -> R.id.option_filter_all
|
||||||
|
}
|
||||||
|
|
||||||
menu.findItem(itemId).isChecked = true
|
menu.findItem(itemId).isChecked = true
|
||||||
|
|
||||||
|
|
@ -114,9 +110,7 @@ class SearchFragment : Fragment() {
|
||||||
if (!launchedKeyboard) {
|
if (!launchedKeyboard) {
|
||||||
// Auto-open the keyboard when this view is shown
|
// Auto-open the keyboard when this view is shown
|
||||||
requestFocus()
|
requestFocus()
|
||||||
postDelayed(200) {
|
postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
|
||||||
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
|
||||||
|
|
||||||
launchedKeyboard = true
|
launchedKeyboard = true
|
||||||
}
|
}
|
||||||
|
|
@ -125,9 +119,7 @@ class SearchFragment : Fragment() {
|
||||||
binding.searchRecycler.apply {
|
binding.searchRecycler.apply {
|
||||||
adapter = searchAdapter
|
adapter = searchAdapter
|
||||||
|
|
||||||
applySpans { pos ->
|
applySpans { pos -> searchAdapter.currentList[pos] is Header }
|
||||||
searchAdapter.currentList[pos] is Header
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
@ -148,15 +140,14 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
detailModel.navToItem.observe(viewLifecycleOwner) { item ->
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
when (item) {
|
.navigate(
|
||||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
when (item) {
|
||||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id)
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||||
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||||
else -> return@observe
|
else -> return@observe
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +168,7 @@ class SearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that handles when an [item] is selected.
|
* Function that handles when an [item] is selected. Handles all datatypes that are selectable.
|
||||||
* Handles all datatypes that are selectable.
|
|
||||||
*/
|
*/
|
||||||
private fun onItemSelection(item: Music, imm: InputMethodManager) {
|
private fun onItemSelection(item: Music, imm: InputMethodManager) {
|
||||||
if (item is Song) {
|
if (item is Song) {
|
||||||
|
|
@ -192,20 +182,20 @@ class SearchFragment : Fragment() {
|
||||||
|
|
||||||
logD("Navigating to the detail fragment for ${item.name}")
|
logD("Navigating to the detail fragment for ${item.name}")
|
||||||
|
|
||||||
findNavController().navigate(
|
findNavController()
|
||||||
when (item) {
|
.navigate(
|
||||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
when (item) {
|
||||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
is Genre -> SearchFragmentDirections.actionShowGenre(item.id)
|
||||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
is Artist -> SearchFragmentDirections.actionShowArtist(item.id)
|
||||||
|
is Album -> SearchFragmentDirections.actionShowAlbum(item.id)
|
||||||
|
|
||||||
// If given model wasn't valid, then reset the navigation status
|
// If given model wasn't valid, then reset the navigation status
|
||||||
// and abort the navigation.
|
// and abort the navigation.
|
||||||
else -> {
|
else -> {
|
||||||
searchModel.setNavigating(false)
|
searchModel.setNavigating(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SearchViewModel.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.search
|
package org.oxycblt.auxio.search
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
|
@ -23,6 +22,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import java.text.Normalizer
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Header
|
import org.oxycblt.auxio.music.Header
|
||||||
|
|
@ -34,7 +34,6 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.text.Normalizer
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [ViewModel] for the search functionality
|
* The [ViewModel] for the search functionality
|
||||||
|
|
@ -47,9 +46,12 @@ class SearchViewModel : ViewModel() {
|
||||||
private var mLastQuery = ""
|
private var mLastQuery = ""
|
||||||
|
|
||||||
/** Current search results from the last [search] call. */
|
/** Current search results from the last [search] call. */
|
||||||
val searchResults: LiveData<List<Item>> get() = mSearchResults
|
val searchResults: LiveData<List<Item>>
|
||||||
val isNavigating: Boolean get() = mIsNavigating
|
get() = mSearchResults
|
||||||
val filterMode: DisplayMode? get() = mFilterMode
|
val isNavigating: Boolean
|
||||||
|
get() = mIsNavigating
|
||||||
|
val filterMode: DisplayMode?
|
||||||
|
get() = mFilterMode
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
|
|
@ -63,8 +65,7 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [query] to perform a search of the music library.
|
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||||
* Will push results to [searchResults].
|
|
||||||
*/
|
*/
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
val musicStore = MusicStore.maybeGetInstance()
|
val musicStore = MusicStore.maybeGetInstance()
|
||||||
|
|
@ -118,18 +119,17 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the current filter mode with a menu [id].
|
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
|
||||||
* New value will be pushed to [filterMode].
|
|
||||||
*/
|
*/
|
||||||
fun updateFilterModeWithId(@IdRes id: Int) {
|
fun updateFilterModeWithId(@IdRes id: Int) {
|
||||||
mFilterMode = when (id) {
|
mFilterMode =
|
||||||
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
when (id) {
|
||||||
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
||||||
R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS
|
R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS
|
||||||
R.id.option_filter_genres -> DisplayMode.SHOW_GENRES
|
R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS
|
||||||
|
R.id.option_filter_genres -> DisplayMode.SHOW_GENRES
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Updating filter mode to $mFilterMode")
|
logD("Updating filter mode to $mFilterMode")
|
||||||
|
|
||||||
|
|
@ -139,17 +139,18 @@ class SearchViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut that will run a ignoreCase filter on a list and only return
|
* Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
|
||||||
* a value if the resulting list is empty.
|
* list is empty.
|
||||||
*/
|
*/
|
||||||
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
|
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
|
||||||
val filtered = filter {
|
val filtered = filter {
|
||||||
// Ensure the name we match with is correct.
|
// Ensure the name we match with is correct.
|
||||||
val name = if (it is MusicParent) {
|
val name =
|
||||||
it.resolvedName
|
if (it is MusicParent) {
|
||||||
} else {
|
it.resolvedName
|
||||||
it.name
|
} else {
|
||||||
}
|
it.name
|
||||||
|
}
|
||||||
|
|
||||||
// First see if the normal item name will work. If that fails, try the "normalized"
|
// First see if the normal item name will work. If that fails, try the "normalized"
|
||||||
// [e.g all accented/unicode chars become latin chars] instead. Hopefully this
|
// [e.g all accented/unicode chars become latin chars] instead. Hopefully this
|
||||||
|
|
@ -182,8 +183,8 @@ class SearchViewModel : ViewModel() {
|
||||||
when (Character.getType(cp)) {
|
when (Character.getType(cp)) {
|
||||||
// Character.NON_SPACING_MARK and Character.COMBINING_SPACING_MARK were added
|
// Character.NON_SPACING_MARK and Character.COMBINING_SPACING_MARK were added
|
||||||
// by normalizer
|
// by normalizer
|
||||||
6, 8 -> continue
|
6,
|
||||||
|
8 -> continue
|
||||||
else -> sb.appendCodePoint(cp)
|
else -> sb.appendCodePoint(cp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,9 +192,7 @@ class SearchViewModel : ViewModel() {
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Update the current navigation status to [isNavigating] */
|
||||||
* Update the current navigation status to [isNavigating]
|
|
||||||
*/
|
|
||||||
fun setNavigating(isNavigating: Boolean) {
|
fun setNavigating(isNavigating: Boolean) {
|
||||||
mIsNavigating = isNavigating
|
mIsNavigating = isNavigating
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* AboutFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
|
@ -59,9 +58,7 @@ class AboutFragment : Fragment() {
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.aboutToolbar.setNavigationOnClickListener {
|
binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.aboutVersion.text = BuildConfig.VERSION_NAME
|
binding.aboutVersion.text = BuildConfig.VERSION_NAME
|
||||||
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
|
binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) }
|
||||||
|
|
@ -69,9 +66,7 @@ class AboutFragment : Fragment() {
|
||||||
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
|
binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) }
|
||||||
|
|
||||||
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
homeModel.songs.observe(viewLifecycleOwner) { songs ->
|
||||||
binding.aboutSongCount.text = getString(
|
binding.aboutSongCount.text = getString(R.string.fmt_songs_loaded, songs.size)
|
||||||
R.string.fmt_songs_loaded, songs.size
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Dialog created")
|
logD("Dialog created")
|
||||||
|
|
@ -79,15 +74,12 @@ class AboutFragment : Fragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Go through the process of opening a [link] in a browser. */
|
||||||
* Go through the process of opening a [link] in a browser.
|
|
||||||
*/
|
|
||||||
private fun openLinkInBrowser(link: String) {
|
private fun openLinkInBrowser(link: String) {
|
||||||
logD("Opening $link")
|
logD("Opening $link")
|
||||||
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(
|
val browserIntent =
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
// Android 11 seems to now handle the app chooser situations on its own now
|
// Android 11 seems to now handle the app chooser situations on its own now
|
||||||
|
|
@ -104,9 +96,12 @@ class AboutFragment : Fragment() {
|
||||||
// not work in all cases, especially when no default app was set. If that is the
|
// not work in all cases, especially when no default app was set. If that is the
|
||||||
// case, we will try to manually handle these cases before we try to launch the
|
// case, we will try to manually handle these cases before we try to launch the
|
||||||
// browser.
|
// browser.
|
||||||
val pkgName = requireContext().packageManager.resolveActivity(
|
val pkgName =
|
||||||
browserIntent, PackageManager.MATCH_DEFAULT_ONLY
|
requireContext()
|
||||||
)?.activityInfo?.packageName
|
.packageManager
|
||||||
|
.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
?.activityInfo
|
||||||
|
?.packageName
|
||||||
|
|
||||||
if (pkgName != null) {
|
if (pkgName != null) {
|
||||||
if (pkgName == "android") {
|
if (pkgName == "android") {
|
||||||
|
|
@ -130,9 +125,10 @@ class AboutFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAppChooser(intent: Intent) {
|
private fun openAppChooser(intent: Intent) {
|
||||||
val chooserIntent = Intent(Intent.ACTION_CHOOSER)
|
val chooserIntent =
|
||||||
.putExtra(Intent.EXTRA_INTENT, intent)
|
Intent(Intent.ACTION_CHOOSER)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.putExtra(Intent.EXTRA_INTENT, intent)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
startActivity(chooserIntent)
|
startActivity(chooserIntent)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SettingsCompat.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
|
@ -53,9 +52,7 @@ fun handleAccentCompat(prefs: SharedPreferences): Accent {
|
||||||
return Accent(prefs.getInt(SettingsManager.KEY_ACCENT, 5))
|
return Accent(prefs.getInt(SettingsManager.KEY_ACCENT, 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Cache of the old keys used in Auxio. */
|
||||||
* Cache of the old keys used in Auxio.
|
|
||||||
*/
|
|
||||||
private object OldKeys {
|
private object OldKeys {
|
||||||
const val KEY_ACCENT2 = "KEY_ACCENT2"
|
const val KEY_ACCENT2 = "KEY_ACCENT2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SettingsFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -39,9 +38,7 @@ class SettingsFragment : Fragment() {
|
||||||
val binding = FragmentSettingsBinding.inflate(inflater)
|
val binding = FragmentSettingsBinding.inflate(inflater)
|
||||||
|
|
||||||
binding.settingsToolbar.apply {
|
binding.settingsToolbar.apply {
|
||||||
setNavigationOnClickListener {
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
findNavController().navigateUp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SettingsListFragment.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -32,8 +31,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.accent.AccentCustomizeDialog
|
import org.oxycblt.auxio.accent.AccentCustomizeDialog
|
||||||
import org.oxycblt.auxio.music.excluded.ExcludedDialog
|
|
||||||
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
|
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
|
||||||
|
import org.oxycblt.auxio.music.excluded.ExcludedDialog
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.settings.pref.IntListPrefDialog
|
import org.oxycblt.auxio.settings.pref.IntListPrefDialog
|
||||||
import org.oxycblt.auxio.settings.pref.IntListPreference
|
import org.oxycblt.auxio.settings.pref.IntListPreference
|
||||||
|
|
@ -83,9 +82,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Recursively handle a preference, doing any specific actions on it. */
|
||||||
* Recursively handle a preference, doing any specific actions on it.
|
|
||||||
*/
|
|
||||||
private fun recursivelyHandlePreference(preference: Preference) {
|
private fun recursivelyHandlePreference(preference: Preference) {
|
||||||
if (!preference.isVisible) return
|
if (!preference.isVisible) return
|
||||||
|
|
||||||
|
|
@ -100,82 +97,80 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
SettingsManager.KEY_THEME -> {
|
SettingsManager.KEY_THEME -> {
|
||||||
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
|
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
|
||||||
|
|
||||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
|
onPreferenceChangeListener =
|
||||||
AppCompatDelegate.setDefaultNightMode(value as Int)
|
Preference.OnPreferenceChangeListener { _, value ->
|
||||||
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
|
AppCompatDelegate.setDefaultNightMode(value as Int)
|
||||||
true
|
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
|
||||||
}
|
true
|
||||||
}
|
|
||||||
|
|
||||||
SettingsManager.KEY_BLACK_THEME -> {
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
if (requireContext().isNight) {
|
|
||||||
requireActivity().recreate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
SettingsManager.KEY_BLACK_THEME -> {
|
||||||
|
onPreferenceClickListener =
|
||||||
|
Preference.OnPreferenceClickListener {
|
||||||
|
if (requireContext().isNight) {
|
||||||
|
requireActivity().recreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
SettingsManager.KEY_ACCENT -> {
|
SettingsManager.KEY_ACCENT -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener =
|
||||||
AccentCustomizeDialog().show(childFragmentManager, AccentCustomizeDialog.TAG)
|
Preference.OnPreferenceClickListener {
|
||||||
true
|
AccentCustomizeDialog()
|
||||||
}
|
.show(childFragmentManager, AccentCustomizeDialog.TAG)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
summary = context.getString(settingsManager.accent.name)
|
summary = context.getString(settingsManager.accent.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.KEY_LIB_TABS -> {
|
SettingsManager.KEY_LIB_TABS -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener =
|
||||||
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
|
Preference.OnPreferenceClickListener {
|
||||||
true
|
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
|
||||||
}
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> {
|
SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> {
|
||||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
|
onPreferenceChangeListener =
|
||||||
Coil.imageLoader(requireContext()).apply {
|
Preference.OnPreferenceChangeListener { _, _ ->
|
||||||
this.memoryCache?.clear()
|
Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() }
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.KEY_SAVE_STATE -> {
|
SettingsManager.KEY_SAVE_STATE -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener =
|
||||||
playbackModel.savePlaybackState(requireContext()) {
|
Preference.OnPreferenceClickListener {
|
||||||
requireContext().showToast(R.string.lbl_state_saved)
|
playbackModel.savePlaybackState(requireContext()) {
|
||||||
|
requireContext().showToast(R.string.lbl_state_saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.KEY_RELOAD -> {
|
SettingsManager.KEY_RELOAD -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener =
|
||||||
playbackModel.savePlaybackState(requireContext()) {
|
Preference.OnPreferenceClickListener {
|
||||||
requireContext().hardRestart()
|
playbackModel.savePlaybackState(requireContext()) {
|
||||||
|
requireContext().hardRestart()
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsManager.KEY_EXCLUDED -> {
|
SettingsManager.KEY_EXCLUDED -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener =
|
||||||
ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG)
|
Preference.OnPreferenceClickListener {
|
||||||
true
|
ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG)
|
||||||
}
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert an theme integer into an icon that can be used. */
|
||||||
* Convert an theme integer into an icon that can be used.
|
|
||||||
*/
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
private fun Int.toThemeIcon(): Int {
|
private fun Int.toThemeIcon(): Int {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SettingsManager.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -64,16 +63,17 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to display the LoopMode or the shuffle status on the notification.
|
* Whether to display the LoopMode or the shuffle status on the notification. False if loop,
|
||||||
* False if loop, true if shuffle.
|
* true if shuffle.
|
||||||
*/
|
*/
|
||||||
val useAltNotifAction: Boolean
|
val useAltNotifAction: Boolean
|
||||||
get() = prefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false)
|
get() = prefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false)
|
||||||
|
|
||||||
/** The current library tabs preferred by the user. */
|
/** The current library tabs preferred by the user. */
|
||||||
var libTabs: Array<Tab>
|
var libTabs: Array<Tab>
|
||||||
get() = Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
|
get() =
|
||||||
?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!!
|
Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT))
|
||||||
|
?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!!
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_LIB_TABS, Tab.toSequence(value))
|
putInt(KEY_LIB_TABS, Tab.toSequence(value))
|
||||||
|
|
@ -103,13 +103,15 @@ class SettingsManager private constructor(context: Context) :
|
||||||
|
|
||||||
/** The current ReplayGain configuration */
|
/** The current ReplayGain configuration */
|
||||||
val replayGainMode: ReplayGainMode
|
val replayGainMode: ReplayGainMode
|
||||||
get() = ReplayGainMode.fromInt(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE))
|
get() =
|
||||||
?: ReplayGainMode.OFF
|
ReplayGainMode.fromInt(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE))
|
||||||
|
?: ReplayGainMode.OFF
|
||||||
|
|
||||||
/** What queue to create when a song is selected (ex. From All Songs or Search) */
|
/** What queue to create when a song is selected (ex. From All Songs or Search) */
|
||||||
val songPlaybackMode: PlaybackMode
|
val songPlaybackMode: PlaybackMode
|
||||||
get() = PlaybackMode.fromInt(prefs.getInt(KEY_SONG_PLAYBACK_MODE, Int.MIN_VALUE))
|
get() =
|
||||||
?: PlaybackMode.ALL_SONGS
|
PlaybackMode.fromInt(prefs.getInt(KEY_SONG_PLAYBACK_MODE, Int.MIN_VALUE))
|
||||||
|
?: PlaybackMode.ALL_SONGS
|
||||||
|
|
||||||
/** Whether shuffle should stay on when a new song is selected. */
|
/** Whether shuffle should stay on when a new song is selected. */
|
||||||
val keepShuffle: Boolean
|
val keepShuffle: Boolean
|
||||||
|
|
@ -119,7 +121,9 @@ class SettingsManager private constructor(context: Context) :
|
||||||
val rewindWithPrev: Boolean
|
val rewindWithPrev: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PREV_REWIND, true)
|
get() = prefs.getBoolean(KEY_PREV_REWIND, true)
|
||||||
|
|
||||||
/** Whether [org.oxycblt.auxio.playback.state.LoopMode.TRACK] should pause when the track repeats */
|
/**
|
||||||
|
* Whether [org.oxycblt.auxio.playback.state.LoopMode.TRACK] should pause when the track repeats
|
||||||
|
*/
|
||||||
val pauseOnLoop: Boolean
|
val pauseOnLoop: Boolean
|
||||||
get() = prefs.getBoolean(KEY_LOOP_PAUSE, false)
|
get() = prefs.getBoolean(KEY_LOOP_PAUSE, false)
|
||||||
|
|
||||||
|
|
@ -133,10 +137,9 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The song sort mode on HomeFragment **/
|
/** The song sort mode on HomeFragment */
|
||||||
var libSongSort: Sort
|
var libSongSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE))
|
get() = Sort.fromInt(prefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
?: Sort.ByName(true)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_LIB_SONGS_SORT, value.toInt())
|
putInt(KEY_LIB_SONGS_SORT, value.toInt())
|
||||||
|
|
@ -144,10 +147,9 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The album sort mode on HomeFragment **/
|
/** The album sort mode on HomeFragment */
|
||||||
var libAlbumSort: Sort
|
var libAlbumSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE))
|
get() = Sort.fromInt(prefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
?: Sort.ByName(true)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_LIB_ALBUMS_SORT, value.toInt())
|
putInt(KEY_LIB_ALBUMS_SORT, value.toInt())
|
||||||
|
|
@ -155,10 +157,9 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The artist sort mode on HomeFragment **/
|
/** The artist sort mode on HomeFragment */
|
||||||
var libArtistSort: Sort
|
var libArtistSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE))
|
get() = Sort.fromInt(prefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
?: Sort.ByName(true)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_LIB_ARTISTS_SORT, value.toInt())
|
putInt(KEY_LIB_ARTISTS_SORT, value.toInt())
|
||||||
|
|
@ -166,10 +167,9 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The genre sort mode on HomeFragment **/
|
/** The genre sort mode on HomeFragment */
|
||||||
var libGenreSort: Sort
|
var libGenreSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE))
|
get() = Sort.fromInt(prefs.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
?: Sort.ByName(true)
|
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_LIB_GENRES_SORT, value.toInt())
|
putInt(KEY_LIB_GENRES_SORT, value.toInt())
|
||||||
|
|
@ -177,10 +177,10 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The detail album sort mode **/
|
/** The detail album sort mode */
|
||||||
var detailAlbumSort: Sort
|
var detailAlbumSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE))
|
get() =
|
||||||
?: Sort.ByName(true)
|
Sort.fromInt(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_DETAIL_ALBUM_SORT, value.toInt())
|
putInt(KEY_DETAIL_ALBUM_SORT, value.toInt())
|
||||||
|
|
@ -188,10 +188,10 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The detail artist sort mode **/
|
/** The detail artist sort mode */
|
||||||
var detailArtistSort: Sort
|
var detailArtistSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE))
|
get() =
|
||||||
?: Sort.ByYear(false)
|
Sort.fromInt(prefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) ?: Sort.ByYear(false)
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_DETAIL_ARTIST_SORT, value.toInt())
|
putInt(KEY_DETAIL_ARTIST_SORT, value.toInt())
|
||||||
|
|
@ -199,10 +199,10 @@ class SettingsManager private constructor(context: Context) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The detail genre sort mode **/
|
/** The detail genre sort mode */
|
||||||
var detailGenreSort: Sort
|
var detailGenreSort: Sort
|
||||||
get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE))
|
get() =
|
||||||
?: Sort.ByName(true)
|
Sort.fromInt(prefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true)
|
||||||
set(value) {
|
set(value) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt(KEY_DETAIL_GENRE_SORT, value.toInt())
|
putInt(KEY_DETAIL_GENRE_SORT, value.toInt())
|
||||||
|
|
@ -226,29 +226,13 @@ class SettingsManager private constructor(context: Context) :
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
when (key) {
|
when (key) {
|
||||||
KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach {
|
KEY_USE_ALT_NOTIFICATION_ACTION ->
|
||||||
it.onNotifActionUpdate(useAltNotifAction)
|
callbacks.forEach { it.onNotifActionUpdate(useAltNotifAction) }
|
||||||
}
|
KEY_SHOW_COVERS -> callbacks.forEach { it.onShowCoverUpdate(showCovers) }
|
||||||
|
KEY_QUALITY_COVERS -> callbacks.forEach { it.onQualityCoverUpdate(useQualityCovers) }
|
||||||
KEY_SHOW_COVERS -> callbacks.forEach {
|
KEY_LIB_TABS -> callbacks.forEach { it.onLibTabsUpdate(libTabs) }
|
||||||
it.onShowCoverUpdate(showCovers)
|
KEY_AUDIO_FOCUS -> callbacks.forEach { it.onAudioFocusUpdate(doAudioFocus) }
|
||||||
}
|
KEY_REPLAY_GAIN -> callbacks.forEach { it.onReplayGainUpdate(replayGainMode) }
|
||||||
|
|
||||||
KEY_QUALITY_COVERS -> callbacks.forEach {
|
|
||||||
it.onQualityCoverUpdate(useQualityCovers)
|
|
||||||
}
|
|
||||||
|
|
||||||
KEY_LIB_TABS -> callbacks.forEach {
|
|
||||||
it.onLibTabsUpdate(libTabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
KEY_AUDIO_FOCUS -> callbacks.forEach {
|
|
||||||
it.onAudioFocusUpdate(doAudioFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
KEY_REPLAY_GAIN -> callbacks.forEach {
|
|
||||||
it.onReplayGainUpdate(replayGainMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,26 +288,21 @@ class SettingsManager private constructor(context: Context) :
|
||||||
const val KEY_DETAIL_ARTIST_SORT = "auxio_artist_sort"
|
const val KEY_DETAIL_ARTIST_SORT = "auxio_artist_sort"
|
||||||
const val KEY_DETAIL_GENRE_SORT = "auxio_genre_sort"
|
const val KEY_DETAIL_GENRE_SORT = "auxio_genre_sort"
|
||||||
|
|
||||||
@Volatile
|
@Volatile private var INSTANCE: SettingsManager? = null
|
||||||
private var INSTANCE: SettingsManager? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the single instance of [SettingsManager]. Done so that every object
|
* Init the single instance of [SettingsManager]. Done so that every object can have access
|
||||||
* can have access to it regardless of if it has a context.
|
* to it regardless of if it has a context.
|
||||||
*/
|
*/
|
||||||
fun init(context: Context): SettingsManager {
|
fun init(context: Context): SettingsManager {
|
||||||
if (INSTANCE == null) {
|
if (INSTANCE == null) {
|
||||||
synchronized(this) {
|
synchronized(this) { INSTANCE = SettingsManager(context) }
|
||||||
INSTANCE = SettingsManager(context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getInstance()
|
return getInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Get the single instance of [SettingsManager]. */
|
||||||
* Get the single instance of [SettingsManager].
|
|
||||||
*/
|
|
||||||
fun getInstance(): SettingsManager {
|
fun getInstance(): SettingsManager {
|
||||||
val instance = INSTANCE
|
val instance = INSTANCE
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* IntListPrefDialog.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.pref
|
package org.oxycblt.auxio.settings.pref
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -24,17 +23,15 @@ import androidx.preference.PreferenceFragmentCompat
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.ui.LifecycleDialog
|
import org.oxycblt.auxio.ui.LifecycleDialog
|
||||||
|
|
||||||
/**
|
/** The dialog shown whenever an [IntListPreference] is shown. */
|
||||||
* The dialog shown whenever an [IntListPreference] is shown.
|
|
||||||
*/
|
|
||||||
class IntListPrefDialog : LifecycleDialog() {
|
class IntListPrefDialog : LifecycleDialog() {
|
||||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
// Since we have to store the preference key as an argument, we have to find the
|
// Since we have to store the preference key as an argument, we have to find the
|
||||||
// preference we need to use manually.
|
// preference we need to use manually.
|
||||||
val pref = requireNotNull(
|
val pref =
|
||||||
(parentFragment as PreferenceFragmentCompat).preferenceManager
|
requireNotNull(
|
||||||
.findPreference<IntListPreference>(requireArguments().getString(ARG_KEY, null))
|
(parentFragment as PreferenceFragmentCompat).preferenceManager.findPreference<
|
||||||
)
|
IntListPreference>(requireArguments().getString(ARG_KEY, null)))
|
||||||
|
|
||||||
builder.setTitle(pref.title)
|
builder.setTitle(pref.title)
|
||||||
|
|
||||||
|
|
@ -52,9 +49,7 @@ class IntListPrefDialog : LifecycleDialog() {
|
||||||
|
|
||||||
fun from(pref: IntListPreference): IntListPrefDialog {
|
fun from(pref: IntListPreference): IntListPrefDialog {
|
||||||
return IntListPrefDialog().apply {
|
return IntListPrefDialog().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply { putString(ARG_KEY, pref.key) }
|
||||||
putString(ARG_KEY, pref.key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* IntListPreference.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.pref
|
package org.oxycblt.auxio.settings.pref
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -25,32 +24,34 @@ import androidx.preference.DialogPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
class IntListPreference @JvmOverloads constructor(
|
class IntListPreference
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle,
|
defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle,
|
||||||
defStyleRes: Int = 0
|
defStyleRes: Int = 0
|
||||||
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
// Reflect into Preference to get the (normally inaccessible) default value.
|
// Reflect into Preference to get the (normally inaccessible) default value.
|
||||||
private val defValueField = Preference::class.java.getDeclaredField("mDefaultValue").apply {
|
private val defValueField =
|
||||||
isAccessible = true
|
Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true }
|
||||||
}
|
|
||||||
|
|
||||||
val entries: Array<CharSequence>
|
val entries: Array<CharSequence>
|
||||||
val values: IntArray
|
val values: IntArray
|
||||||
private var currentValue: Int? = null
|
private var currentValue: Int? = null
|
||||||
private val defValue: Int get() = defValueField.get(this) as Int
|
private val defValue: Int
|
||||||
|
get() = defValueField.get(this) as Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val prefAttrs = context.obtainStyledAttributes(
|
val prefAttrs =
|
||||||
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes
|
context.obtainStyledAttributes(
|
||||||
)
|
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
|
||||||
|
|
||||||
entries = prefAttrs.getTextArray(R.styleable.IntListPreference_entries)
|
entries = prefAttrs.getTextArray(R.styleable.IntListPreference_entries)
|
||||||
|
|
||||||
values = context.resources.getIntArray(
|
values =
|
||||||
prefAttrs.getResourceId(R.styleable.IntListPreference_entryValues, -1)
|
context.resources.getIntArray(
|
||||||
)
|
prefAttrs.getResourceId(R.styleable.IntListPreference_entryValues, -1))
|
||||||
|
|
||||||
prefAttrs.recycle()
|
prefAttrs.recycle()
|
||||||
|
|
||||||
|
|
@ -80,9 +81,7 @@ class IntListPreference @JvmOverloads constructor(
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Set a value using the index of it in [values] */
|
||||||
* Set a value using the index of it in [values]
|
|
||||||
*/
|
|
||||||
fun setValueIndex(index: Int) {
|
fun setValueIndex(index: Int) {
|
||||||
setValue(values[index])
|
setValue(values[index])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings.pref
|
package org.oxycblt.auxio.settings.pref
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -11,10 +28,12 @@ import org.oxycblt.auxio.util.getColorStateListSafe
|
||||||
import org.oxycblt.auxio.util.getDrawableSafe
|
import org.oxycblt.auxio.util.getDrawableSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [SwitchPreferenceCompat] that emulates the M3 switches until the design team
|
* A [SwitchPreferenceCompat] that emulates the M3 switches until the design team actually bothers
|
||||||
* actually bothers to add them to MDC.
|
* to add them to MDC.
|
||||||
*/
|
*/
|
||||||
class M3SwitchPreference @JvmOverloads constructor(
|
class M3SwitchPreference
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = R.attr.switchPreferenceCompatStyle,
|
defStyleAttr: Int = R.attr.switchPreferenceCompatStyle,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* ActionMenu.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -52,12 +51,11 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE)
|
||||||
* @param activity [AppCompatActivity] required as both a context and ViewModelStore owner.
|
* @param activity [AppCompatActivity] required as both a context and ViewModelStore owner.
|
||||||
* @param anchor [View] This should be centered around
|
* @param anchor [View] This should be centered around
|
||||||
* @param data [Item] this menu corresponds to
|
* @param data [Item] this menu corresponds to
|
||||||
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
|
* @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM],
|
||||||
|
* [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details.
|
||||||
* @throws IllegalStateException When there is no menu for this specific datatype/flag
|
* @throws IllegalStateException When there is no menu for this specific datatype/flag
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt TODO: Stop scrolling when a menu is open TODO: Prevent duplicate menus from
|
||||||
* TODO: Stop scrolling when a menu is open
|
* showing up TODO: Maybe replace this with a bottom sheet?
|
||||||
* TODO: Prevent duplicate menus from showing up
|
|
||||||
* TODO: Maybe replace this with a bottom sheet?
|
|
||||||
*/
|
*/
|
||||||
class ActionMenu(
|
class ActionMenu(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
|
|
@ -98,9 +96,7 @@ class ActionMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Figure out what menu to use here, based on the data & flags */
|
||||||
* Figure out what menu to use here, based on the data & flags
|
|
||||||
*/
|
|
||||||
@MenuRes
|
@MenuRes
|
||||||
private fun determineMenu(): Int {
|
private fun determineMenu(): Int {
|
||||||
return when (data) {
|
return when (data) {
|
||||||
|
|
@ -109,31 +105,23 @@ class ActionMenu(
|
||||||
FLAG_NONE, FLAG_IN_GENRE -> R.menu.menu_song_actions
|
FLAG_NONE, FLAG_IN_GENRE -> R.menu.menu_song_actions
|
||||||
FLAG_IN_ALBUM -> R.menu.menu_album_song_actions
|
FLAG_IN_ALBUM -> R.menu.menu_album_song_actions
|
||||||
FLAG_IN_ARTIST -> R.menu.menu_artist_song_actions
|
FLAG_IN_ARTIST -> R.menu.menu_artist_song_actions
|
||||||
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
when (flag) {
|
when (flag) {
|
||||||
FLAG_NONE -> R.menu.menu_album_actions
|
FLAG_NONE -> R.menu.menu_album_actions
|
||||||
FLAG_IN_ARTIST -> R.menu.menu_artist_album_actions
|
FLAG_IN_ARTIST -> R.menu.menu_artist_album_actions
|
||||||
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Artist -> R.menu.menu_artist_actions
|
is Artist -> R.menu.menu_artist_actions
|
||||||
|
|
||||||
is Genre -> R.menu.menu_genre_actions
|
is Genre -> R.menu.menu_genre_actions
|
||||||
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Determine what to do when a MenuItem is clicked. */
|
||||||
* Determine what to do when a MenuItem is clicked.
|
|
||||||
*/
|
|
||||||
private fun onMenuClick(@IdRes id: Int) {
|
private fun onMenuClick(@IdRes id: Int) {
|
||||||
when (id) {
|
when (id) {
|
||||||
R.id.action_play -> {
|
R.id.action_play -> {
|
||||||
|
|
@ -141,59 +129,48 @@ class ActionMenu(
|
||||||
is Album -> playbackModel.playAlbum(data, false)
|
is Album -> playbackModel.playAlbum(data, false)
|
||||||
is Artist -> playbackModel.playArtist(data, false)
|
is Artist -> playbackModel.playArtist(data, false)
|
||||||
is Genre -> playbackModel.playGenre(data, false)
|
is Genre -> playbackModel.playGenre(data, false)
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_shuffle -> {
|
R.id.action_shuffle -> {
|
||||||
when (data) {
|
when (data) {
|
||||||
is Album -> playbackModel.playAlbum(data, true)
|
is Album -> playbackModel.playAlbum(data, true)
|
||||||
is Artist -> playbackModel.playArtist(data, true)
|
is Artist -> playbackModel.playArtist(data, true)
|
||||||
is Genre -> playbackModel.playGenre(data, true)
|
is Genre -> playbackModel.playGenre(data, true)
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_play_next -> {
|
R.id.action_play_next -> {
|
||||||
when (data) {
|
when (data) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
playbackModel.playNext(data)
|
playbackModel.playNext(data)
|
||||||
context.showToast(R.string.lbl_queue_added)
|
context.showToast(R.string.lbl_queue_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
playbackModel.playNext(data)
|
playbackModel.playNext(data)
|
||||||
context.showToast(R.string.lbl_queue_added)
|
context.showToast(R.string.lbl_queue_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_queue_add -> {
|
R.id.action_queue_add -> {
|
||||||
when (data) {
|
when (data) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
playbackModel.addToQueue(data)
|
playbackModel.addToQueue(data)
|
||||||
context.showToast(R.string.lbl_queue_added)
|
context.showToast(R.string.lbl_queue_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Album -> {
|
is Album -> {
|
||||||
playbackModel.addToQueue(data)
|
playbackModel.addToQueue(data)
|
||||||
context.showToast(R.string.lbl_queue_added)
|
context.showToast(R.string.lbl_queue_added)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_go_album -> {
|
R.id.action_go_album -> {
|
||||||
if (data is Song) {
|
if (data is Song) {
|
||||||
detailModel.navToItem(data.album)
|
detailModel.navToItem(data.album)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_go_artist -> {
|
R.id.action_go_artist -> {
|
||||||
if (data is Song) {
|
if (data is Song) {
|
||||||
detailModel.navToItem(data.album.artist)
|
detailModel.navToItem(data.album.artist)
|
||||||
|
|
@ -205,13 +182,22 @@ class ActionMenu(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** No Flags **/
|
/** No Flags */
|
||||||
const val FLAG_NONE = -1
|
const val FLAG_NONE = -1
|
||||||
/** Flag for when a menu is opened from an artist (See [org.oxycblt.auxio.detail.ArtistDetailFragment]) **/
|
/**
|
||||||
|
* Flag for when a menu is opened from an artist (See
|
||||||
|
* [org.oxycblt.auxio.detail.ArtistDetailFragment])
|
||||||
|
*/
|
||||||
const val FLAG_IN_ARTIST = 0
|
const val FLAG_IN_ARTIST = 0
|
||||||
/** Flag for when a menu is opened from an album (See [org.oxycblt.auxio.detail.AlbumDetailFragment]) **/
|
/**
|
||||||
|
* Flag for when a menu is opened from an album (See
|
||||||
|
* [org.oxycblt.auxio.detail.AlbumDetailFragment])
|
||||||
|
*/
|
||||||
const val FLAG_IN_ALBUM = 1
|
const val FLAG_IN_ALBUM = 1
|
||||||
/** Flag for when a menu is opened from a genre (See [org.oxycblt.auxio.detail.GenreDetailFragment]) **/
|
/**
|
||||||
|
* Flag for when a menu is opened from a genre (See
|
||||||
|
* [org.oxycblt.auxio.detail.GenreDetailFragment])
|
||||||
|
*/
|
||||||
const val FLAG_IN_GENRE = 2
|
const val FLAG_IN_GENRE = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* DiffCallback.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,15 +14,15 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.oxycblt.auxio.music.Item
|
import org.oxycblt.auxio.music.Item
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A re-usable diff callback for all [Item] implementations.
|
* A re-usable diff callback for all [Item] implementations. **Use this instead of creating a
|
||||||
* **Use this instead of creating a DiffCallback for each adapter.**
|
* DiffCallback for each adapter.**
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
class DiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* DisplayMode.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,15 +14,15 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enum for determining what items to show in a given list.
|
* An enum for determining what items to show in a given list. Note: **DO NOT RE-ARRANGE THE ENUM**.
|
||||||
* Note: **DO NOT RE-ARRANGE THE ENUM**. The ordinals are used to store library tabs, so doing
|
* The ordinals are used to store library tabs, so doing changing them would also change the meaning
|
||||||
* changing them would also change the meaning of tab instances.
|
* of tab instances.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
enum class DisplayMode {
|
enum class DisplayMode {
|
||||||
|
|
@ -32,19 +31,23 @@ enum class DisplayMode {
|
||||||
SHOW_ARTISTS,
|
SHOW_ARTISTS,
|
||||||
SHOW_GENRES;
|
SHOW_GENRES;
|
||||||
|
|
||||||
val string: Int get() = when (this) {
|
val string: Int
|
||||||
SHOW_SONGS -> R.string.lbl_songs
|
get() =
|
||||||
SHOW_ALBUMS -> R.string.lbl_albums
|
when (this) {
|
||||||
SHOW_ARTISTS -> R.string.lbl_artists
|
SHOW_SONGS -> R.string.lbl_songs
|
||||||
SHOW_GENRES -> R.string.lbl_genres
|
SHOW_ALBUMS -> R.string.lbl_albums
|
||||||
}
|
SHOW_ARTISTS -> R.string.lbl_artists
|
||||||
|
SHOW_GENRES -> R.string.lbl_genres
|
||||||
|
}
|
||||||
|
|
||||||
val icon: Int get() = when (this) {
|
val icon: Int
|
||||||
SHOW_SONGS -> R.drawable.ic_song
|
get() =
|
||||||
SHOW_ALBUMS -> R.drawable.ic_album
|
when (this) {
|
||||||
SHOW_ARTISTS -> R.drawable.ic_artist
|
SHOW_SONGS -> R.drawable.ic_song
|
||||||
SHOW_GENRES -> R.drawable.ic_genre
|
SHOW_ALBUMS -> R.drawable.ic_album
|
||||||
}
|
SHOW_ARTISTS -> R.drawable.ic_artist
|
||||||
|
SHOW_GENRES -> R.drawable.ic_genre
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val INT_NULL = 0xA107
|
private const val INT_NULL = 0xA107
|
||||||
|
|
@ -54,8 +57,8 @@ enum class DisplayMode {
|
||||||
private const val INT_SHOW_SONGS = 0xA10B
|
private const val INT_SHOW_SONGS = 0xA10B
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert this enum into an integer for filtering.
|
* Convert this enum into an integer for filtering. In this context, a null value means to
|
||||||
* In this context, a null value means to filter nothing.
|
* filter nothing.
|
||||||
* @return An integer constant for that display mode, or a constant for a null [DisplayMode]
|
* @return An integer constant for that display mode, or a constant for a null [DisplayMode]
|
||||||
*/
|
*/
|
||||||
fun toFilterInt(value: DisplayMode?): Int {
|
fun toFilterInt(value: DisplayMode?): Int {
|
||||||
|
|
@ -69,8 +72,8 @@ enum class DisplayMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a filtering integer to a [DisplayMode].
|
* Convert a filtering integer to a [DisplayMode]. In this context, a null value means to
|
||||||
* In this context, a null value means to filter nothing.
|
* filter nothing.
|
||||||
* @return A [DisplayMode] for this constant (including null)
|
* @return A [DisplayMode] for this constant (including null)
|
||||||
*/
|
*/
|
||||||
fun fromFilterInt(value: Int): DisplayMode? {
|
fun fromFilterInt(value: Int): DisplayMode? {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* LiftAppBarLayout.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -33,32 +32,31 @@ import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state
|
* An [AppBarLayout] that fixes a bug with the default implementation where the lifted state will
|
||||||
* will not properly respond to RecyclerView events.
|
* not properly respond to RecyclerView events. **Note:** This layout relies on
|
||||||
* **Note:** This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what
|
* [AppBarLayout.liftOnScrollTargetViewId] to figure out what scrolling view to use. Failure to
|
||||||
* scrolling view to use. Failure to specify this will result in the layout not working.
|
* specify this will result in the layout not working.
|
||||||
*/
|
*/
|
||||||
open class EdgeAppBarLayout @JvmOverloads constructor(
|
open class EdgeAppBarLayout
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
AppBarLayout(context, attrs, defStyleAttr) {
|
||||||
) : AppBarLayout(context, attrs, defStyleAttr) {
|
|
||||||
private var scrollingChild: View? = null
|
private var scrollingChild: View? = null
|
||||||
private val tConsumed = IntArray(2)
|
private val tConsumed = IntArray(2)
|
||||||
|
|
||||||
private val onPreDraw = ViewTreeObserver.OnPreDrawListener {
|
private val onPreDraw =
|
||||||
val child = findScrollingChild()
|
ViewTreeObserver.OnPreDrawListener {
|
||||||
|
val child = findScrollingChild()
|
||||||
|
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
val coordinator = parent as CoordinatorLayout
|
val coordinator = parent as CoordinatorLayout
|
||||||
(layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll(
|
(layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll(
|
||||||
coordinator, this, coordinator, 0, 0, tConsumed, 0
|
coordinator, this, coordinator, 0, 0, tConsumed, 0)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewTreeObserver.addOnPreDrawListener(onPreDraw)
|
viewTreeObserver.addOnPreDrawListener(onPreDraw)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* FuckedCoordinatorLayout.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -26,16 +25,15 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that fixes an issue where [CoordinatorLayout] will override [onApplyWindowInsets]
|
* Class that fixes an issue where [CoordinatorLayout] will override [onApplyWindowInsets] and
|
||||||
* and delegate the job to ***LAYOUT BEHAVIOR INSTANCES*** instead of the actual views.
|
* delegate the job to ***LAYOUT BEHAVIOR INSTANCES*** instead of the actual views.
|
||||||
*
|
*
|
||||||
* I can't believe I have to do this.
|
* I can't believe I have to do this.
|
||||||
*/
|
*/
|
||||||
class EdgeCoordinatorLayout @JvmOverloads constructor(
|
class EdgeCoordinatorLayout
|
||||||
context: Context,
|
@JvmOverloads
|
||||||
attrs: AttributeSet? = null,
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
@AttrRes defStyleAttr: Int = 0
|
CoordinatorLayout(context, attrs, defStyleAttr) {
|
||||||
) : CoordinatorLayout(context, attrs, defStyleAttr) {
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
for (child in children) {
|
for (child in children) {
|
||||||
child.onApplyWindowInsets(insets)
|
child.onApplyWindowInsets(insets)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* EdgeRecyclerView.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -26,14 +25,11 @@ import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/** A [RecyclerView] that automatically applies insets to itself. */
|
||||||
* A [RecyclerView] that automatically applies insets to itself.
|
class EdgeRecyclerView
|
||||||
*/
|
@JvmOverloads
|
||||||
class EdgeRecyclerView @JvmOverloads constructor(
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
context: Context,
|
RecyclerView(context, attrs, defStyleAttr) {
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
@AttrRes defStyleAttr: Int = 0
|
|
||||||
) : RecyclerView(context, attrs, defStyleAttr) {
|
|
||||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||||
updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
updatePadding(bottom = insets.systemBarInsetsCompat.bottom)
|
||||||
return insets
|
return insets
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* LifecycleDialog.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
|
@ -27,8 +26,8 @@ import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around [DialogFragment] that allows the usage of the standard Auxio lifecycle
|
* A wrapper around [DialogFragment] that allows the usage of the standard Auxio lifecycle override
|
||||||
* override [onCreateView] and [onDestroyView], but with a proper dialog being created.
|
* [onCreateView] and [onDestroyView], but with a proper dialog being created.
|
||||||
*/
|
*/
|
||||||
abstract class LifecycleDialog : AppCompatDialogFragment() {
|
abstract class LifecycleDialog : AppCompatDialogFragment() {
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Sort.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
|
|
@ -53,15 +52,15 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||||
class ByYear(override val isAscending: Boolean) : Sort(isAscending)
|
class ByYear(override val isAscending: Boolean) : Sort(isAscending)
|
||||||
|
|
||||||
/**
|
/** Get the corresponding item id for this sort. */
|
||||||
* Get the corresponding item id for this sort.
|
val itemId: Int
|
||||||
*/
|
get() =
|
||||||
val itemId: Int get() = when (this) {
|
when (this) {
|
||||||
is ByName -> R.id.option_sort_name
|
is ByName -> R.id.option_sort_name
|
||||||
is ByArtist -> R.id.option_sort_artist
|
is ByArtist -> R.id.option_sort_artist
|
||||||
is ByAlbum -> R.id.option_sort_album
|
is ByAlbum -> R.id.option_sort_album
|
||||||
is ByYear -> R.id.option_sort_year
|
is ByYear -> R.id.option_sort_year
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply [ascending] to the status of this sort.
|
* Apply [ascending] to the status of this sort.
|
||||||
|
|
@ -100,10 +99,10 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
fun sortSongs(songs: Collection<Song>): List<Song> {
|
fun sortSongs(songs: Collection<Song>): List<Song> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ByName -> songs.stringSort { it.name }
|
is ByName -> songs.stringSort { it.name }
|
||||||
|
else ->
|
||||||
else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
|
sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
|
||||||
album.songs.intSort(true) { it.track ?: 0 }
|
album.songs.intSort(true) { it.track ?: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,10 +116,10 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
fun sortAlbums(albums: Collection<Album>): List<Album> {
|
fun sortAlbums(albums: Collection<Album>): List<Album> {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ByName, is ByAlbum -> albums.stringSort { it.resolvedName }
|
is ByName, is ByAlbum -> albums.stringSort { it.resolvedName }
|
||||||
|
is ByArtist ->
|
||||||
is ByArtist -> sortParents(albums.groupBy { it.artist }.keys)
|
sortParents(albums.groupBy { it.artist }.keys).flatMap {
|
||||||
.flatMap { ByYear(false).sortAlbums(it.albums) }
|
ByYear(false).sortAlbums(it.albums)
|
||||||
|
}
|
||||||
is ByYear -> albums.intSort { it.year ?: 0 }
|
is ByYear -> albums.intSort { it.year ?: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,9 +157,7 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
return sortSongs(genre.songs)
|
return sortSongs(genre.songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert this sort to it's integer representation. */
|
||||||
* Convert this sort to it's integer representation.
|
|
||||||
*/
|
|
||||||
fun toInt(): Int {
|
fun toInt(): Int {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is ByName -> INT_NAME
|
is ByName -> INT_NAME
|
||||||
|
|
@ -175,15 +172,14 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
selector: (T) -> String
|
selector: (T) -> String
|
||||||
): List<T> {
|
): List<T> {
|
||||||
// Chain whatever item call with sliceArticle for correctness
|
// Chain whatever item call with sliceArticle for correctness
|
||||||
val chained: (T) -> String = {
|
val chained: (T) -> String = { selector(it).sliceArticle() }
|
||||||
selector(it).sliceArticle()
|
|
||||||
}
|
|
||||||
|
|
||||||
val comparator = if (asc) {
|
val comparator =
|
||||||
compareBy(String.CASE_INSENSITIVE_ORDER, chained)
|
if (asc) {
|
||||||
} else {
|
compareBy(String.CASE_INSENSITIVE_ORDER, chained)
|
||||||
compareByDescending(String.CASE_INSENSITIVE_ORDER, chained)
|
} else {
|
||||||
}
|
compareByDescending(String.CASE_INSENSITIVE_ORDER, chained)
|
||||||
|
}
|
||||||
|
|
||||||
return sortedWith(comparator)
|
return sortedWith(comparator)
|
||||||
}
|
}
|
||||||
|
|
@ -192,11 +188,12 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
asc: Boolean = isAscending,
|
asc: Boolean = isAscending,
|
||||||
selector: (T) -> Int,
|
selector: (T) -> Int,
|
||||||
): List<T> {
|
): List<T> {
|
||||||
val comparator = if (asc) {
|
val comparator =
|
||||||
compareBy(selector)
|
if (asc) {
|
||||||
} else {
|
compareBy(selector)
|
||||||
compareByDescending(selector)
|
} else {
|
||||||
}
|
compareByDescending(selector)
|
||||||
|
}
|
||||||
|
|
||||||
return sortedWith(comparator)
|
return sortedWith(comparator)
|
||||||
}
|
}
|
||||||
|
|
@ -227,9 +224,9 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slice a string so that any preceding articles like The/A(n) are truncated.
|
* Slice a string so that any preceding articles like The/A(n) are truncated. This is hilariously
|
||||||
* This is hilariously anglo-centric, but its mostly for MediaStore compat and hopefully
|
* anglo-centric, but its mostly for MediaStore compat and hopefully shouldn't run with other
|
||||||
* shouldn't run with other languages.
|
* languages.
|
||||||
*/
|
*/
|
||||||
fun String.sliceArticle(): String {
|
fun String.sliceArticle(): String {
|
||||||
if (length > 5 && startsWith("the ", true)) {
|
if (length > 5 && startsWith("the ", true)) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* SortHeaderViewHolder.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -53,22 +52,18 @@ abstract class BaseViewHolder<T : Item>(
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
init {
|
init {
|
||||||
// Force the layout to *actually* be the screen width
|
// Force the layout to *actually* be the screen width
|
||||||
binding.root.layoutParams = RecyclerView.LayoutParams(
|
binding.root.layoutParams =
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT
|
RecyclerView.LayoutParams(
|
||||||
)
|
RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind the viewholder with whatever [Item] instance that has been specified.
|
* Bind the viewholder with whatever [Item] instance that has been specified. Will call [onBind]
|
||||||
* Will call [onBind] on the inheriting ViewHolder.
|
* on the inheriting ViewHolder.
|
||||||
* @param data Data that the viewholder should be bound with
|
* @param data Data that the viewholder should be bound with
|
||||||
*/
|
*/
|
||||||
fun bind(data: T) {
|
fun bind(data: T) {
|
||||||
doOnClick?.let { onClick ->
|
doOnClick?.let { onClick -> binding.root.setOnClickListener { onClick(data) } }
|
||||||
binding.root.setOnClickListener {
|
|
||||||
onClick(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doOnLongClick?.let { onLongClick ->
|
doOnLongClick?.let { onLongClick ->
|
||||||
binding.root.setOnLongClickListener { view ->
|
binding.root.setOnLongClickListener { view ->
|
||||||
|
|
@ -84,16 +79,15 @@ abstract class BaseViewHolder<T : Item>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that performs binding operations unique to the inheriting viewholder.
|
* Function that performs binding operations unique to the inheriting viewholder. Add any
|
||||||
* Add any specialized code to an override of this instead of [BaseViewHolder] itself.
|
* specialized code to an override of this instead of [BaseViewHolder] itself.
|
||||||
*/
|
*/
|
||||||
protected abstract fun onBind(data: T)
|
protected abstract fun onBind(data: T)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. */
|
||||||
* The Shared ViewHolder for a [Song]. Instantiation should be done with [from].
|
class SongViewHolder
|
||||||
*/
|
private constructor(
|
||||||
class SongViewHolder private constructor(
|
|
||||||
private val binding: ItemSongBinding,
|
private val binding: ItemSongBinding,
|
||||||
doOnClick: (data: Song) -> Unit,
|
doOnClick: (data: Song) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Song) -> Unit
|
doOnLongClick: (view: View, data: Song) -> Unit
|
||||||
|
|
@ -109,26 +103,21 @@ class SongViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA000
|
const val ITEM_TYPE = 0xA000
|
||||||
|
|
||||||
/**
|
/** Create an instance of [SongViewHolder] */
|
||||||
* Create an instance of [SongViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
context: Context,
|
||||||
doOnClick: (data: Song) -> Unit,
|
doOnClick: (data: Song) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Song) -> Unit
|
doOnLongClick: (view: View, data: Song) -> Unit
|
||||||
): SongViewHolder {
|
): SongViewHolder {
|
||||||
return SongViewHolder(
|
return SongViewHolder(
|
||||||
ItemSongBinding.inflate(context.inflater),
|
ItemSongBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||||
doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for a [Album]. Instantiation should be done with [from]. */
|
||||||
* The Shared ViewHolder for a [Album]. Instantiation should be done with [from].
|
class AlbumViewHolder
|
||||||
*/
|
private constructor(
|
||||||
class AlbumViewHolder private constructor(
|
|
||||||
private val binding: ItemAlbumBinding,
|
private val binding: ItemAlbumBinding,
|
||||||
doOnClick: (data: Album) -> Unit,
|
doOnClick: (data: Album) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Album) -> Unit
|
doOnLongClick: (view: View, data: Album) -> Unit
|
||||||
|
|
@ -142,26 +131,21 @@ class AlbumViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA001
|
const val ITEM_TYPE = 0xA001
|
||||||
|
|
||||||
/**
|
/** Create an instance of [AlbumViewHolder] */
|
||||||
* Create an instance of [AlbumViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
context: Context,
|
||||||
doOnClick: (data: Album) -> Unit,
|
doOnClick: (data: Album) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Album) -> Unit
|
doOnLongClick: (view: View, data: Album) -> Unit
|
||||||
): AlbumViewHolder {
|
): AlbumViewHolder {
|
||||||
return AlbumViewHolder(
|
return AlbumViewHolder(
|
||||||
ItemAlbumBinding.inflate(context.inflater),
|
ItemAlbumBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||||
doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for a [Artist]. Instantiation should be done with [from]. */
|
||||||
* The Shared ViewHolder for a [Artist]. Instantiation should be done with [from].
|
class ArtistViewHolder
|
||||||
*/
|
private constructor(
|
||||||
class ArtistViewHolder private constructor(
|
|
||||||
private val binding: ItemArtistBinding,
|
private val binding: ItemArtistBinding,
|
||||||
doOnClick: (Artist) -> Unit,
|
doOnClick: (Artist) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
doOnLongClick: (view: View, data: Artist) -> Unit
|
||||||
|
|
@ -175,26 +159,21 @@ class ArtistViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA002
|
const val ITEM_TYPE = 0xA002
|
||||||
|
|
||||||
/**
|
/** Create an instance of [ArtistViewHolder] */
|
||||||
* Create an instance of [ArtistViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
context: Context,
|
||||||
doOnClick: (Artist) -> Unit,
|
doOnClick: (Artist) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Artist) -> Unit
|
doOnLongClick: (view: View, data: Artist) -> Unit
|
||||||
): ArtistViewHolder {
|
): ArtistViewHolder {
|
||||||
return ArtistViewHolder(
|
return ArtistViewHolder(
|
||||||
ItemArtistBinding.inflate(context.inflater),
|
ItemArtistBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||||
doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for a [Genre]. Instantiation should be done with [from]. */
|
||||||
* The Shared ViewHolder for a [Genre]. Instantiation should be done with [from].
|
class GenreViewHolder
|
||||||
*/
|
private constructor(
|
||||||
class GenreViewHolder private constructor(
|
|
||||||
private val binding: ItemGenreBinding,
|
private val binding: ItemGenreBinding,
|
||||||
doOnClick: (Genre) -> Unit,
|
doOnClick: (Genre) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
doOnLongClick: (view: View, data: Genre) -> Unit
|
||||||
|
|
@ -208,28 +187,21 @@ class GenreViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA003
|
const val ITEM_TYPE = 0xA003
|
||||||
|
|
||||||
/**
|
/** Create an instance of [GenreViewHolder] */
|
||||||
* Create an instance of [GenreViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(
|
fun from(
|
||||||
context: Context,
|
context: Context,
|
||||||
doOnClick: (Genre) -> Unit,
|
doOnClick: (Genre) -> Unit,
|
||||||
doOnLongClick: (view: View, data: Genre) -> Unit
|
doOnLongClick: (view: View, data: Genre) -> Unit
|
||||||
): GenreViewHolder {
|
): GenreViewHolder {
|
||||||
return GenreViewHolder(
|
return GenreViewHolder(
|
||||||
ItemGenreBinding.inflate(context.inflater),
|
ItemGenreBinding.inflate(context.inflater), doOnClick, doOnLongClick)
|
||||||
doOnClick, doOnLongClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */
|
||||||
* The Shared ViewHolder for a [Header]. Instantiation should be done with [from]
|
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||||
*/
|
BaseViewHolder<Header>(binding) {
|
||||||
class HeaderViewHolder private constructor(
|
|
||||||
private val binding: ItemHeaderBinding
|
|
||||||
) : BaseViewHolder<Header>(binding) {
|
|
||||||
|
|
||||||
override fun onBind(data: Header) {
|
override fun onBind(data: Header) {
|
||||||
binding.header = data
|
binding.header = data
|
||||||
|
|
@ -238,23 +210,16 @@ class HeaderViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA004
|
const val ITEM_TYPE = 0xA004
|
||||||
|
|
||||||
/**
|
/** Create an instance of [HeaderViewHolder] */
|
||||||
* Create an instance of [HeaderViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(context: Context): HeaderViewHolder {
|
fun from(context: Context): HeaderViewHolder {
|
||||||
return HeaderViewHolder(
|
return HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
||||||
ItemHeaderBinding.inflate(context.inflater)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] */
|
||||||
* The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from]
|
class ActionHeaderViewHolder private constructor(private val binding: ItemActionHeaderBinding) :
|
||||||
*/
|
BaseViewHolder<ActionHeader>(binding) {
|
||||||
class ActionHeaderViewHolder private constructor(
|
|
||||||
private val binding: ItemActionHeaderBinding
|
|
||||||
) : BaseViewHolder<ActionHeader>(binding) {
|
|
||||||
|
|
||||||
override fun onBind(data: ActionHeader) {
|
override fun onBind(data: ActionHeader) {
|
||||||
binding.header = data
|
binding.header = data
|
||||||
|
|
@ -271,9 +236,7 @@ class ActionHeaderViewHolder private constructor(
|
||||||
companion object {
|
companion object {
|
||||||
const val ITEM_TYPE = 0xA005
|
const val ITEM_TYPE = 0xA005
|
||||||
|
|
||||||
/**
|
/** Create an instance of [ActionHeaderViewHolder] */
|
||||||
* Create an instance of [ActionHeaderViewHolder]
|
|
||||||
*/
|
|
||||||
fun from(context: Context): ActionHeaderViewHolder {
|
fun from(context: Context): ActionHeaderViewHolder {
|
||||||
return ActionHeaderViewHolder(ItemActionHeaderBinding.inflate(context.inflater))
|
return ActionHeaderViewHolder(ItemActionHeaderBinding.inflate(context.inflater))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* ContextUtil.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
|
@ -39,30 +38,28 @@ import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.Px
|
import androidx.annotation.Px
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import org.oxycblt.auxio.MainActivity
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
import org.oxycblt.auxio.MainActivity
|
||||||
|
|
||||||
const val INTENT_REQUEST_CODE = 0xA0A0
|
const val INTENT_REQUEST_CODE = 0xA0A0
|
||||||
|
|
||||||
/**
|
/** Shortcut to get a [LayoutInflater] from a [Context] */
|
||||||
* Shortcut to get a [LayoutInflater] from a [Context]
|
val Context.inflater: LayoutInflater
|
||||||
*/
|
get() = LayoutInflater.from(this)
|
||||||
val Context.inflater: LayoutInflater get() = LayoutInflater.from(this)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the current UI is in night mode or not. This will work if the theme is
|
* Returns whether the current UI is in night mode or not. This will work if the theme is automatic
|
||||||
* automatic as well.
|
* as well.
|
||||||
*/
|
*/
|
||||||
val Context.isNight: Boolean get() =
|
val Context.isNight: Boolean
|
||||||
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
get() =
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
||||||
|
Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
/**
|
/** Returns if this device is in landscape. */
|
||||||
* Returns if this device is in landscape.
|
val Context.isLandscape
|
||||||
*/
|
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
val Context.isLandscape get() =
|
|
||||||
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method for getting a plural.
|
* Convenience method for getting a plural.
|
||||||
|
|
@ -117,11 +114,12 @@ fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
|
||||||
theme.resolveAttribute(attr, resolvedAttr, true)
|
theme.resolveAttribute(attr, resolvedAttr, true)
|
||||||
|
|
||||||
// Then convert it to a proper color
|
// Then convert it to a proper color
|
||||||
val color = if (resolvedAttr.resourceId != 0) {
|
val color =
|
||||||
resolvedAttr.resourceId
|
if (resolvedAttr.resourceId != 0) {
|
||||||
} else {
|
resolvedAttr.resourceId
|
||||||
resolvedAttr.data
|
} else {
|
||||||
}
|
resolvedAttr.data
|
||||||
|
}
|
||||||
|
|
||||||
return getColorSafe(color)
|
return getColorSafe(color)
|
||||||
}
|
}
|
||||||
|
|
@ -183,9 +181,8 @@ fun Context.getDimenOffsetSafe(@DimenRes dimen: Int): Int {
|
||||||
|
|
||||||
@Px
|
@Px
|
||||||
fun Context.pxOfDp(@Dimension dp: Float): Int {
|
fun Context.pxOfDp(@Dimension dp: Float): Int {
|
||||||
return TypedValue.applyDimension(
|
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
|
||||||
TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics
|
.toInt()
|
||||||
).toInt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> Context.handleResourceFailure(e: Exception, what: String, default: T): T {
|
private fun <T> Context.handleResourceFailure(e: Exception, what: String, default: T): T {
|
||||||
|
|
@ -207,46 +204,37 @@ fun <T : Any> Context.getSystemServiceSafe(serviceClass: KClass<T>): T {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create a toast using the provided string resource. */
|
||||||
* Create a toast using the provided string resource.
|
|
||||||
*/
|
|
||||||
fun Context.showToast(@StringRes str: Int) {
|
fun Context.showToast(@StringRes str: Int) {
|
||||||
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
|
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create a [PendingIntent] that leads to Auxio's [MainActivity] */
|
||||||
* Create a [PendingIntent] that leads to Auxio's [MainActivity]
|
|
||||||
*/
|
|
||||||
fun Context.newMainIntent(): PendingIntent {
|
fun Context.newMainIntent(): PendingIntent {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
this, INTENT_REQUEST_CODE, Intent(this, MainActivity::class.java),
|
this,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
INTENT_REQUEST_CODE,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
Intent(this, MainActivity::class.java),
|
||||||
else 0
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create a broadcast [PendingIntent] */
|
||||||
* Create a broadcast [PendingIntent]
|
|
||||||
*/
|
|
||||||
fun Context.newBroadcastIntent(what: String): PendingIntent {
|
fun Context.newBroadcastIntent(what: String): PendingIntent {
|
||||||
return PendingIntent.getBroadcast(
|
return PendingIntent.getBroadcast(
|
||||||
this, INTENT_REQUEST_CODE, Intent(what),
|
this,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
INTENT_REQUEST_CODE,
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
Intent(what),
|
||||||
else 0
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Hard-restarts the app. Useful for forcing the app to reload music. */
|
||||||
* Hard-restarts the app. Useful for forcing the app to reload music.
|
|
||||||
*/
|
|
||||||
fun Context.hardRestart() {
|
fun Context.hardRestart() {
|
||||||
// Instead of having to do a ton of cleanup and horrible code changes
|
// Instead of having to do a ton of cleanup and horrible code changes
|
||||||
// to restart this application non-destructively, I just restart the UI task [There is only
|
// to restart this application non-destructively, I just restart the UI task [There is only
|
||||||
// one, after all] and then kill the application using exitProcess. Works well enough.
|
// one, after all] and then kill the application using exitProcess. Works well enough.
|
||||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
val intent =
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
Intent(applicationContext, MainActivity::class.java)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
exitProcess(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* DbUtil.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
|
@ -23,15 +22,13 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for querying all items in a database and running [block] with the cursor returned.
|
* Shortcut for querying all items in a database and running [block] with the cursor returned. Will
|
||||||
* Will not run if the cursor is null.
|
* not run if the cursor is null.
|
||||||
*/
|
*/
|
||||||
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
||||||
query(tableName, null, null, null, null, null, null)?.use(block)
|
query(tableName, null, null, null, null, null, null)?.use(block)
|
||||||
|
|
||||||
/**
|
/** Assert that we are on a background thread. */
|
||||||
* Assert that we are on a background thread.
|
|
||||||
*/
|
|
||||||
fun assertBackgroundThread() {
|
fun assertBackgroundThread() {
|
||||||
check(Looper.myLooper() != Looper.getMainLooper()) {
|
check(Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
"This operation must be ran on a background thread"
|
"This operation must be ran on a background thread"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* LogUtil.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
@ -25,14 +24,16 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
// Yes, I know timber exists but this does what I need.
|
// Yes, I know timber exists but this does what I need.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut method for logging a non-string [obj] to debug. Should only be used for debug preferably.
|
* Shortcut method for logging a non-string [obj] to debug. Should only be used for debug
|
||||||
|
* preferably.
|
||||||
*/
|
*/
|
||||||
fun Any.logD(obj: Any) {
|
fun Any.logD(obj: Any) {
|
||||||
logD(obj.toString())
|
logD(obj.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous objects
|
* Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous
|
||||||
|
* objects
|
||||||
*/
|
*/
|
||||||
fun Any.logD(msg: String) {
|
fun Any.logD(msg: String) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
|
|
@ -41,16 +42,12 @@ fun Any.logD(msg: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects */
|
||||||
* Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects
|
|
||||||
*/
|
|
||||||
fun Any.logW(msg: String) {
|
fun Any.logW(msg: String) {
|
||||||
Log.w(getName(), msg)
|
Log.w(getName(), msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shortcut method for logging [msg] as an error to the console. Handles anonymous objects */
|
||||||
* Shortcut method for logging [msg] as an error to the console. Handles anonymous objects
|
|
||||||
*/
|
|
||||||
fun Any.logE(msg: String) {
|
fun Any.logE(msg: String) {
|
||||||
Log.e(getName(), msg)
|
Log.e(getName(), msg)
|
||||||
}
|
}
|
||||||
|
|
@ -77,18 +74,16 @@ private fun Any.getName(): String = "Auxio.${this::class.simpleName ?: "Anonymou
|
||||||
* I know that this will not stop you, but consider what you are doing with your life, plagiarizers.
|
* I know that this will not stop you, but consider what you are doing with your life, plagiarizers.
|
||||||
* Do you want to live a fulfilling existence on this planet? Or do you want to spend your life
|
* Do you want to live a fulfilling existence on this planet? Or do you want to spend your life
|
||||||
* taking work others did and making it objectively worse so you could arbitrage a fraction of a
|
* taking work others did and making it objectively worse so you could arbitrage a fraction of a
|
||||||
* penny on every AdMob impression you get? You could do so many great things if you simply had
|
* penny on every AdMob impression you get? You could do so many great things if you simply had the
|
||||||
* the courage to come up with an idea of your own. If you still want to go on, I guess the only
|
* courage to come up with an idea of your own. If you still want to go on, I guess the only thing I
|
||||||
* thing I can say is this: JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件
|
* can say is this: JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件
|
||||||
*/
|
*/
|
||||||
private fun basedCopyleftNotice() {
|
private fun basedCopyleftNotice() {
|
||||||
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" &&
|
||||||
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug"
|
BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") {
|
||||||
) {
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"Auxio Project",
|
"Auxio Project",
|
||||||
"Friendly reminder: Auxio is licensed under the " +
|
"Friendly reminder: Auxio is licensed under the " +
|
||||||
"GPLv3 and all modifications must be made open source!"
|
"GPLv3 and all modifications must be made open source!")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* ViewUtil.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.util
|
package org.oxycblt.auxio.util
|
||||||
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
|
|
@ -29,10 +28,9 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
|
||||||
/**
|
/** Converts this color to a single-color [ColorStateList]. */
|
||||||
* Converts this color to a single-color [ColorStateList].
|
val @receiver:ColorRes Int.stateList
|
||||||
*/
|
get() = ColorStateList.valueOf(this)
|
||||||
val @receiver:ColorRes Int.stateList get() = ColorStateList.valueOf(this)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the recommended spans for a [RecyclerView].
|
* Apply the recommended spans for a [RecyclerView].
|
||||||
|
|
@ -47,25 +45,24 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) {
|
||||||
val mgr = GridLayoutManager(context, spans)
|
val mgr = GridLayoutManager(context, spans)
|
||||||
|
|
||||||
if (shouldBeFullWidth != null) {
|
if (shouldBeFullWidth != null) {
|
||||||
mgr.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
mgr.spanSizeLookup =
|
||||||
override fun getSpanSize(position: Int): Int {
|
object : GridLayoutManager.SpanSizeLookup() {
|
||||||
return if (shouldBeFullWidth(position)) spans else 1
|
override fun getSpanSize(position: Int): Int {
|
||||||
|
return if (shouldBeFullWidth(position)) spans else 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutManager = mgr
|
layoutManager = mgr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns whether a recyclerview can scroll. */
|
||||||
* Returns whether a recyclerview can scroll.
|
|
||||||
*/
|
|
||||||
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables drop shadows on a view programmatically in a version-compatible manner.
|
* Disables drop shadows on a view programmatically in a version-compatible manner. This only works
|
||||||
* This only works on Android 9 and above. Below that version, shadows will remain visible.
|
* on Android 9 and above. Below that version, shadows will remain visible.
|
||||||
*/
|
*/
|
||||||
fun View.disableDropShadowCompat() {
|
fun View.disableDropShadowCompat() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
|
@ -77,49 +74,44 @@ fun View.disableDropShadowCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to
|
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view
|
||||||
* a view that properly follows all the frustrating changes that were made between 8-11.
|
* that properly follows all the frustrating changes that were made between 8-11.
|
||||||
*/
|
*/
|
||||||
val WindowInsets.systemBarInsetsCompat: Rect get() {
|
val WindowInsets.systemBarInsetsCompat: Rect
|
||||||
return when {
|
get() {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
return when {
|
||||||
getInsets(WindowInsets.Type.systemBars()).run {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
Rect(left, top, right, bottom)
|
getInsets(WindowInsets.Type.systemBars()).run { Rect(left, top, right, bottom) }
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Rect(
|
||||||
|
systemWindowInsetLeft,
|
||||||
|
systemWindowInsetTop,
|
||||||
|
systemWindowInsetRight,
|
||||||
|
systemWindowInsetBottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
Rect(
|
|
||||||
systemWindowInsetLeft,
|
|
||||||
systemWindowInsetTop,
|
|
||||||
systemWindowInsetRight,
|
|
||||||
systemWindowInsetBottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the system bar insets in a version-aware manner. This can be used to modify the insets
|
* Replaces the system bar insets in a version-aware manner. This can be used to modify the insets
|
||||||
* for child views in a way that follows all of the frustrating changes that were made between 8-11.
|
* for child views in a way that follows all of the frustrating changes that were made between 8-11.
|
||||||
*/
|
*/
|
||||||
fun WindowInsets.replaceSystemBarInsetsCompat(left: Int, top: Int, right: Int, bottom: Int): WindowInsets {
|
fun WindowInsets.replaceSystemBarInsetsCompat(
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int
|
||||||
|
): WindowInsets {
|
||||||
return when {
|
return when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
WindowInsets.Builder(this)
|
WindowInsets.Builder(this)
|
||||||
.setInsets(
|
.setInsets(WindowInsets.Type.systemBars(), Insets.of(left, top, right, bottom))
|
||||||
WindowInsets.Type.systemBars(),
|
|
||||||
Insets.of(left, top, right, bottom)
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION") replaceSystemWindowInsets(left, top, right, bottom)
|
||||||
replaceSystemWindowInsets(
|
|
||||||
left, top, right, bottom
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* Forms.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.widgets
|
package org.oxycblt.auxio.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -28,8 +27,8 @@ import org.oxycblt.auxio.util.newBroadcastIntent
|
||||||
import org.oxycblt.auxio.util.newMainIntent
|
import org.oxycblt.auxio.util.newMainIntent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default widget is displayed whenever there is no music playing. It just shows the
|
* The default widget is displayed whenever there is no music playing. It just shows the message "No
|
||||||
* message "No music playing".
|
* music playing".
|
||||||
*/
|
*/
|
||||||
fun createDefaultWidget(context: Context): RemoteViews {
|
fun createDefaultWidget(context: Context): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_default)
|
return createViews(context, R.layout.widget_default)
|
||||||
|
|
@ -46,9 +45,9 @@ fun createTinyWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The small widget is for 2x2 widgets and just shows the cover art and playback controls.
|
* The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is
|
||||||
* This is generally because a Medium widget is too large for this widget size and a text-only
|
* generally because a Medium widget is too large for this widget size and a text-only widget is too
|
||||||
* widget is too small for this widget size.
|
* small for this widget size.
|
||||||
*/
|
*/
|
||||||
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
|
fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_small)
|
return createViews(context, R.layout.widget_small)
|
||||||
|
|
@ -57,8 +56,8 @@ fun createSmallWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three
|
* The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three controls.
|
||||||
* controls. This is the default widget configuration.
|
* This is the default widget configuration.
|
||||||
*/
|
*/
|
||||||
fun createMediumWidget(context: Context, state: WidgetState): RemoteViews {
|
fun createMediumWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_medium)
|
return createViews(context, R.layout.widget_medium)
|
||||||
|
|
@ -66,34 +65,24 @@ fun createMediumWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
.applyBasicControls(context, state)
|
.applyBasicControls(context, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The wide widget is for Nx2 widgets and is like the small widget but with more controls. */
|
||||||
* The wide widget is for Nx2 widgets and is like the small widget but with more controls.
|
|
||||||
*/
|
|
||||||
fun createWideWidget(context: Context, state: WidgetState): RemoteViews {
|
fun createWideWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_wide)
|
return createViews(context, R.layout.widget_wide)
|
||||||
.applyCover(context, state)
|
.applyCover(context, state)
|
||||||
.applyFullControls(context, state)
|
.applyFullControls(context, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The large widget is for 3x4 widgets and shows all metadata and controls. */
|
||||||
* The large widget is for 3x4 widgets and shows all metadata and controls.
|
|
||||||
*/
|
|
||||||
fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
fun createLargeWidget(context: Context, state: WidgetState): RemoteViews {
|
||||||
return createViews(context, R.layout.widget_large)
|
return createViews(context, R.layout.widget_large)
|
||||||
.applyMeta(context, state)
|
.applyMeta(context, state)
|
||||||
.applyFullControls(context, state)
|
.applyFullControls(context, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViews(
|
private fun createViews(context: Context, @LayoutRes layout: Int): RemoteViews {
|
||||||
context: Context,
|
|
||||||
@LayoutRes layout: Int
|
|
||||||
): RemoteViews {
|
|
||||||
val views = RemoteViews(context.packageName, layout)
|
val views = RemoteViews(context.packageName, layout)
|
||||||
|
|
||||||
views.setOnClickPendingIntent(
|
views.setOnClickPendingIntent(android.R.id.background, context.newMainIntent())
|
||||||
android.R.id.background,
|
|
||||||
context.newMainIntent()
|
|
||||||
)
|
|
||||||
|
|
||||||
return views
|
return views
|
||||||
}
|
}
|
||||||
|
|
@ -112,8 +101,7 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote
|
||||||
setImageViewBitmap(R.id.widget_cover, state.albumArt)
|
setImageViewBitmap(R.id.widget_cover, state.albumArt)
|
||||||
setContentDescription(
|
setContentDescription(
|
||||||
R.id.widget_cover,
|
R.id.widget_cover,
|
||||||
context.getString(R.string.desc_album_cover, state.song.resolvedAlbumName)
|
context.getString(R.string.desc_album_cover, state.song.resolvedAlbumName))
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
setImageViewResource(R.id.widget_cover, R.drawable.ic_widget_album)
|
setImageViewResource(R.id.widget_cover, R.drawable.ic_widget_album)
|
||||||
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover))
|
||||||
|
|
@ -124,11 +112,7 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote
|
||||||
|
|
||||||
private fun RemoteViews.applyPlayControls(context: Context, state: WidgetState): RemoteViews {
|
private fun RemoteViews.applyPlayControls(context: Context, state: WidgetState): RemoteViews {
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_play_pause,
|
R.id.widget_play_pause, context.newBroadcastIntent(PlaybackService.ACTION_PLAY_PAUSE))
|
||||||
context.newBroadcastIntent(
|
|
||||||
PlaybackService.ACTION_PLAY_PAUSE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setImageViewResource(
|
setImageViewResource(
|
||||||
R.id.widget_play_pause,
|
R.id.widget_play_pause,
|
||||||
|
|
@ -136,8 +120,7 @@ private fun RemoteViews.applyPlayControls(context: Context, state: WidgetState):
|
||||||
R.drawable.ic_pause
|
R.drawable.ic_pause
|
||||||
} else {
|
} else {
|
||||||
R.drawable.ic_play
|
R.drawable.ic_play
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
@ -146,18 +129,10 @@ private fun RemoteViews.applyBasicControls(context: Context, state: WidgetState)
|
||||||
applyPlayControls(context, state)
|
applyPlayControls(context, state)
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_prev,
|
R.id.widget_skip_prev, context.newBroadcastIntent(PlaybackService.ACTION_SKIP_PREV))
|
||||||
context.newBroadcastIntent(
|
|
||||||
PlaybackService.ACTION_SKIP_PREV
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_skip_next,
|
R.id.widget_skip_next, context.newBroadcastIntent(PlaybackService.ACTION_SKIP_NEXT))
|
||||||
context.newBroadcastIntent(
|
|
||||||
PlaybackService.ACTION_SKIP_NEXT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
@ -166,31 +141,25 @@ private fun RemoteViews.applyFullControls(context: Context, state: WidgetState):
|
||||||
applyBasicControls(context, state)
|
applyBasicControls(context, state)
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_loop,
|
R.id.widget_loop, context.newBroadcastIntent(PlaybackService.ACTION_LOOP))
|
||||||
context.newBroadcastIntent(
|
|
||||||
PlaybackService.ACTION_LOOP
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickPendingIntent(
|
setOnClickPendingIntent(
|
||||||
R.id.widget_shuffle,
|
R.id.widget_shuffle, context.newBroadcastIntent(PlaybackService.ACTION_SHUFFLE))
|
||||||
context.newBroadcastIntent(
|
|
||||||
PlaybackService.ACTION_SHUFFLE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Like notifications, use the remote variants of icons since we really don't want to hack
|
// Like notifications, use the remote variants of icons since we really don't want to hack
|
||||||
// indicators.
|
// indicators.
|
||||||
val shuffleRes = when {
|
val shuffleRes =
|
||||||
state.isShuffled -> R.drawable.ic_remote_shuffle_on
|
when {
|
||||||
else -> R.drawable.ic_remote_shuffle_off
|
state.isShuffled -> R.drawable.ic_remote_shuffle_on
|
||||||
}
|
else -> R.drawable.ic_remote_shuffle_off
|
||||||
|
}
|
||||||
|
|
||||||
val loopRes = when (state.loopMode) {
|
val loopRes =
|
||||||
LoopMode.NONE -> R.drawable.ic_remote_loop_off
|
when (state.loopMode) {
|
||||||
LoopMode.ALL -> R.drawable.ic_loop_on
|
LoopMode.NONE -> R.drawable.ic_remote_loop_off
|
||||||
LoopMode.TRACK -> R.drawable.ic_loop_one
|
LoopMode.ALL -> R.drawable.ic_loop_on
|
||||||
}
|
LoopMode.TRACK -> R.drawable.ic_loop_one
|
||||||
|
}
|
||||||
|
|
||||||
setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
setImageViewResource(R.id.widget_shuffle, shuffleRes)
|
||||||
setImageViewResource(R.id.widget_loop, loopRes)
|
setImageViewResource(R.id.widget_loop, loopRes)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* WidgetController.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.widgets
|
package org.oxycblt.auxio.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
@ -28,12 +27,11 @@ import org.oxycblt.auxio.util.logD
|
||||||
/**
|
/**
|
||||||
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
|
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
|
||||||
* widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may
|
* widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may
|
||||||
* result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound
|
* result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound to
|
||||||
* to without being released.
|
* without being released.
|
||||||
*/
|
*/
|
||||||
class WidgetController(private val context: Context) :
|
class WidgetController(private val context: Context) :
|
||||||
PlaybackStateManager.Callback,
|
PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||||
SettingsManager.Callback {
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
private val widget = WidgetProvider()
|
private val widget = WidgetProvider()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* WidgetProvider.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.widgets
|
package org.oxycblt.auxio.widgets
|
||||||
|
|
||||||
import android.appwidget.AppWidgetHostView
|
import android.appwidget.AppWidgetHostView
|
||||||
|
|
@ -33,6 +32,7 @@ import androidx.core.graphics.drawable.toBitmap
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transform.RoundedCornersTransformation
|
import coil.transform.RoundedCornersTransformation
|
||||||
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.coil.SquareFrameTransform
|
import org.oxycblt.auxio.coil.SquareFrameTransform
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
@ -41,7 +41,6 @@ import org.oxycblt.auxio.util.getDimenSizeSafe
|
||||||
import org.oxycblt.auxio.util.isLandscape
|
import org.oxycblt.auxio.util.isLandscape
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
|
* Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively
|
||||||
|
|
@ -67,38 +66,36 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadWidgetBitmap(context, song) { bitmap ->
|
loadWidgetBitmap(context, song) { bitmap ->
|
||||||
val state = WidgetState(
|
val state =
|
||||||
song,
|
WidgetState(
|
||||||
bitmap,
|
song,
|
||||||
playbackManager.isPlaying,
|
bitmap,
|
||||||
playbackManager.isShuffling,
|
playbackManager.isPlaying,
|
||||||
playbackManager.loopMode
|
playbackManager.isShuffling,
|
||||||
)
|
playbackManager.loopMode)
|
||||||
|
|
||||||
// Map each widget form to the cells where it would look at least okay.
|
// Map each widget form to the cells where it would look at least okay.
|
||||||
val views = mapOf(
|
val views =
|
||||||
SizeF(180f, 100f) to createTinyWidget(context, state),
|
mapOf(
|
||||||
SizeF(180f, 152f) to createSmallWidget(context, state),
|
SizeF(180f, 100f) to createTinyWidget(context, state),
|
||||||
SizeF(272f, 152f) to createWideWidget(context, state),
|
SizeF(180f, 152f) to createSmallWidget(context, state),
|
||||||
SizeF(180f, 270f) to createMediumWidget(context, state),
|
SizeF(272f, 152f) to createWideWidget(context, state),
|
||||||
SizeF(272f, 270f) to createLargeWidget(context, state)
|
SizeF(180f, 270f) to createMediumWidget(context, state),
|
||||||
)
|
SizeF(272f, 270f) to createLargeWidget(context, state))
|
||||||
|
|
||||||
appWidgetManager.applyViewsCompat(context, views)
|
appWidgetManager.applyViewsCompat(context, views)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom function for loading bitmaps to the widget in a way that works with the
|
* Custom function for loading bitmaps to the widget in a way that works with the widget
|
||||||
* widget ImageView instances.
|
* ImageView instances.
|
||||||
*/
|
*/
|
||||||
private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) {
|
||||||
val coverRequest = ImageRequest.Builder(context)
|
val coverRequest =
|
||||||
.data(song.album)
|
ImageRequest.Builder(context)
|
||||||
.target(
|
.data(song.album)
|
||||||
onError = { onDone(null) },
|
.target(onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) })
|
||||||
onSuccess = { onDone(it.toBitmap()) }
|
|
||||||
)
|
|
||||||
|
|
||||||
// The widget has two distinct styles that we must transform the album art to accommodate:
|
// The widget has two distinct styles that we must transform the album art to accommodate:
|
||||||
// - Before Android 12, the widget has hard edges, so we don't need to round out the album
|
// - Before Android 12, the widget has hard edges, so we don't need to round out the album
|
||||||
|
|
@ -109,15 +106,17 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
// Use RoundedCornersTransformation. This is because our hack to get a 1:1 aspect
|
// Use RoundedCornersTransformation. This is because our hack to get a 1:1 aspect
|
||||||
// ratio on widget ImageViews doesn't actually result in a square ImageView, so
|
// ratio on widget ImageViews doesn't actually result in a square ImageView, so
|
||||||
// clipToOutline won't work.
|
// clipToOutline won't work.
|
||||||
val transform = RoundedCornersTransformation(
|
val transform =
|
||||||
context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius)
|
RoundedCornersTransformation(
|
||||||
.toFloat()
|
context
|
||||||
)
|
.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius)
|
||||||
|
.toFloat())
|
||||||
|
|
||||||
// The output of RoundedCornersTransformation is dimension-dependent, so scale up the
|
// The output of RoundedCornersTransformation is dimension-dependent, so scale up the
|
||||||
// image to the screen size to ensure consistent radii.
|
// image to the screen size to ensure consistent radii.
|
||||||
val metrics = context.resources.displayMetrics
|
val metrics = context.resources.displayMetrics
|
||||||
coverRequest.transformations(SquareFrameTransform(), transform)
|
coverRequest
|
||||||
|
.transformations(SquareFrameTransform(), transform)
|
||||||
.size(min(metrics.widthPixels, metrics.heightPixels))
|
.size(min(metrics.widthPixels, metrics.heightPixels))
|
||||||
} else {
|
} else {
|
||||||
coverRequest.transformations(SquareFrameTransform())
|
coverRequest.transformations(SquareFrameTransform())
|
||||||
|
|
@ -132,9 +131,8 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
fun reset(context: Context) {
|
fun reset(context: Context) {
|
||||||
logD("Resetting widget")
|
logD("Resetting widget")
|
||||||
|
|
||||||
AppWidgetManager.getInstance(context).updateAppWidget(
|
AppWidgetManager.getInstance(context)
|
||||||
ComponentName(context, this::class.java), createDefaultWidget(context)
|
.updateAppWidget(ComponentName(context, this::class.java), createDefaultWidget(context))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
@ -170,8 +168,7 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
private fun requestUpdate(context: Context) {
|
private fun requestUpdate(context: Context) {
|
||||||
logD("Sending update intent to PlaybackService")
|
logD("Sending update intent to PlaybackService")
|
||||||
|
|
||||||
val intent = Intent(ACTION_WIDGET_UPDATE)
|
val intent = Intent(ACTION_WIDGET_UPDATE).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
|
||||||
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
|
|
||||||
|
|
||||||
context.sendBroadcast(intent)
|
context.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
|
|
@ -243,9 +240,8 @@ class WidgetProvider : AppWidgetProvider() {
|
||||||
// Default to the smallest view if no layout fits
|
// Default to the smallest view if no layout fits
|
||||||
logW("No good widget layout found")
|
logW("No good widget layout found")
|
||||||
|
|
||||||
val minimum = requireNotNull(
|
val minimum =
|
||||||
views.minByOrNull { it.key.width * it.key.height }?.value
|
requireNotNull(views.minByOrNull { it.key.width * it.key.height }?.value)
|
||||||
)
|
|
||||||
|
|
||||||
updateAppWidget(id, minimum)
|
updateAppWidget(id, minimum)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 Auxio Project
|
* Copyright (c) 2021 Auxio Project
|
||||||
* WidgetState.kt is part of Auxio.
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
|
@ -15,7 +14,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.widgets
|
package org.oxycblt.auxio.widgets
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ buildscript {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||||
|
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue