widgets: revert most widget changes

Revert the introduction of the thin/tiny widgets, but keep the new
cover layout I created while working on them.

There is simply no way I can cram controls and metadata within the
size bucket that the thin widget occupies. I have decided to give up
and revert the widget to it's old form.

I understand why the thin widget is not appealing. However, the sizing
at which a widget can properly accomodate a taller widget is just too
precise and not really large enough to justify it's existance.
This commit is contained in:
OxygenCobalt 2022-05-25 11:03:59 -06:00
parent 4c74879cf1
commit 444e4299d6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
26 changed files with 201 additions and 231 deletions

View file

@ -12,7 +12,7 @@
#### What's Improved #### What's Improved
- Re-enabled theme customization on Android 12 - Re-enabled theme customization on Android 12
- The tab selector now hides itself when there is only one tab - The tab selector now hides itself when there is only one tab
- Added more buttons to the smallest widget form - Made the cover on the thin widget larger
#### What's Fixed #### What's Fixed
- Fixed incorrect ellipsizing on song items - Fixed incorrect ellipsizing on song items

View file

@ -24,15 +24,15 @@ android {
// ExoPlayer needs Java 8 to compile. // ExoPlayer needs Java 8 to compile.
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
freeCompilerArgs += "-Xjvm-default=all" freeCompilerArgs += "-Xjvm-default=all"
} }
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes { buildTypes {
debug { debug {
debuggable true debuggable true
@ -94,8 +94,7 @@ dependencies {
// Exoplayer // Exoplayer
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION. // WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION.
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. // IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
def exoplayerVersion = "2.17.1" implementation "com.google.android.exoplayer:exoplayer-core:2.17.1"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayerVersion"
implementation fileTree(dir: "libs", include: ["extension-*.aar"]) implementation fileTree(dir: "libs", include: ["extension-*.aar"])
// Image loading // Image loading

View file

@ -145,7 +145,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
} }
} }
override fun onLibraryChanged() { override fun onLibrarySettingsChanged() {
tabs = visibleTabs tabs = visibleTabs
_shouldRecreateTabs.value = true _shouldRecreateTabs.value = true
} }

View file

@ -66,8 +66,6 @@ class PlaybackPanelFragment :
) { ) {
// --- UI SETUP --- // --- UI SETUP ---
logD(binding.root.paddingBottom)
binding.root.setOnApplyWindowInsetsListener { _, insets -> binding.root.setOnApplyWindowInsetsListener { _, insets ->
val bars = insets.systemBarInsetsCompat val bars = insets.systemBarInsetsCompat
val gestures = insets.systemGestureInsetsCompat val gestures = insets.systemGestureInsetsCompat

View file

@ -31,8 +31,9 @@ enum class ReplayGainMode {
DYNAMIC; DYNAMIC;
companion object { companion object {
fun fromIntCode(value: Int): ReplayGainMode? { /** Convert an int [code] into an instance, or null if it isn't valid. */
return when (value) { fun fromIntCode(code: Int): ReplayGainMode? {
return when (code) {
IntegerTable.REPLAY_GAIN_MODE_OFF -> OFF IntegerTable.REPLAY_GAIN_MODE_OFF -> OFF
IntegerTable.REPLAY_GAIN_MODE_TRACK -> TRACK IntegerTable.REPLAY_GAIN_MODE_TRACK -> TRACK
IntegerTable.REPLAY_GAIN_MODE_ALBUM -> ALBUM IntegerTable.REPLAY_GAIN_MODE_ALBUM -> ALBUM
@ -42,11 +43,3 @@ enum class ReplayGainMode {
} }
} }
} }
/** Represents the ReplayGain pre-amp values. */
data class ReplayGainPreAmp(
/** The value to use when ReplayGain tags are present. */
val with: Float,
/** The value to use when ReplayGain tags are not present. */
val without: Float,
)

View file

@ -0,0 +1,26 @@
/*
* 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.replaygain
/** Represents the ReplayGain pre-amp values. */
data class ReplayGainPreAmp(
/** The value to use when ReplayGain tags are present. */
val with: Float,
/** The value to use when ReplayGain tags are not present. */
val without: Float,
)

View file

@ -59,9 +59,9 @@ enum class RepeatMode {
} }
companion object { companion object {
/** Convert an int [constant] into a LoopMode, or null if it isn't valid. */ /** Convert an int [code] into an instance, or null if it isn't valid. */
fun fromIntCode(constant: Int): RepeatMode? { fun fromIntCode(code: Int): RepeatMode? {
return when (constant) { return when (code) {
IntegerTable.REPEAT_MODE_NONE -> NONE IntegerTable.REPEAT_MODE_NONE -> NONE
IntegerTable.REPEAT_MODE_ALL -> ALL IntegerTable.REPEAT_MODE_ALL -> ALL
IntegerTable.REPEAT_MODE_TRACK -> TRACK IntegerTable.REPEAT_MODE_TRACK -> TRACK

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
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.unlikelyToBeNull
/** /**
*/ */
@ -106,10 +107,21 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
.putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist) .putText(MediaMetadataCompat.METADATA_KEY_COMPOSER, artist)
.putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist) .putText(MediaMetadataCompat.METADATA_KEY_WRITER, artist)
.putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context)) .putText(MediaMetadataCompat.METADATA_KEY_GENRE, song.genre.resolveName(context))
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track?.toLong() ?: 0L)
.putText(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year?.toString())
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.durationMs)
if (song.track != null) {
metadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, song.track.toLong())
}
if (song.disc != null) {
metadata.putLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER, song.disc.toLong())
}
if (song.album.year != null) {
metadata.putString(
MediaMetadataCompat.METADATA_KEY_DATE, unlikelyToBeNull(song.album.year).toString())
}
// Normally, android expects one to provide a URI to the metadata instance instead of // Normally, android expects one to provide a URI to the metadata instance instead of
// a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot // a full blown bitmap. In practice, this is not ideal in the slightest, as we cannot
// provide any user customization or quality of life improvements with a flat URI. // provide any user customization or quality of life improvements with a flat URI.

View file

@ -66,7 +66,7 @@ fun handleAccentCompat(prefs: SharedPreferences): Accent {
} }
} }
return Accent(prefs.getInt(SettingsManager.KEY_ACCENT, 5)) return Accent.from(prefs.getInt(SettingsManager.KEY_ACCENT, 5))
} }
/** Cache of the old keys used in Auxio. */ /** Cache of the old keys used in Auxio. */

View file

@ -65,7 +65,7 @@ class SettingsManager private constructor(context: Context) :
} }
/** /**
* Whether to display the LoopMode or the shuffle status on the notification. False if loop, * Whether to display the RepeatMode or the shuffle status on the notification. False if repeat,
* true if shuffle. * true if shuffle.
*/ */
val useAltNotifAction: Boolean val useAltNotifAction: Boolean
@ -256,7 +256,7 @@ class SettingsManager private constructor(context: Context) :
when (key) { when (key) {
KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { it.onNotifSettingsChanged() } KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { it.onNotifSettingsChanged() }
KEY_SHOW_COVERS, KEY_QUALITY_COVERS -> callbacks.forEach { it.onCoverSettingsChanged() } KEY_SHOW_COVERS, KEY_QUALITY_COVERS -> callbacks.forEach { it.onCoverSettingsChanged() }
KEY_LIB_TABS -> callbacks.forEach { it.onLibraryChanged() } KEY_LIB_TABS -> callbacks.forEach { it.onLibrarySettingsChanged() }
KEY_REPLAY_GAIN, KEY_PRE_AMP_WITH, KEY_PRE_AMP_WITHOUT -> KEY_REPLAY_GAIN, KEY_PRE_AMP_WITH, KEY_PRE_AMP_WITHOUT ->
callbacks.forEach { it.onReplayGainSettingsChanged() } callbacks.forEach { it.onReplayGainSettingsChanged() }
} }
@ -268,7 +268,7 @@ class SettingsManager private constructor(context: Context) :
* context. * context.
*/ */
interface Callback { interface Callback {
fun onLibraryChanged() {} fun onLibrarySettingsChanged() {}
fun onNotifSettingsChanged() {} fun onNotifSettingsChanged() {}
fun onCoverSettingsChanged() {} fun onCoverSettingsChanged() {}
fun onReplayGainSettingsChanged() {} fun onReplayGainSettingsChanged() {}

View file

@ -77,7 +77,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// TODO: Scale this drawable based on available space after padding // FIXME: Scale this drawable based on available space after padding
imageMatrix = imageMatrix =
centerMatrix.apply { centerMatrix.apply {

View file

@ -63,6 +63,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
styledAttrs.recycle() styledAttrs.recycle()
// Use clipToOutline and a background drawable to crop images. While Coil's transformation
// could theoretically be used to round corners, the corner radius is dependent on the
// dimensions of the image, which will result in inconsistent corners across different
// album covers unless we resize all covers to be the same size. clipToOutline is both
// cheaper and more elegant. As a side-note, this also allows us to re-use the same
// background for both the tonal background color and the corner rounding.
clipToOutline = true clipToOutline = true
background = background =
MaterialShapeDrawable().apply { MaterialShapeDrawable().apply {
@ -79,11 +85,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
// Use clipToOutline and a background drawable to crop images. While Coil's transformation
// could theoretically be used to round corners, the corner radius is dependent on the
// dimensions of the image, which will result in inconsistent corners across different
// album covers unless we resize all covers to be the same size. clipToOutline is both
// cheaper and more elegant.
if (!isInEditMode) { if (!isInEditMode) {
val settingsManager = SettingsManager.getInstance() val settingsManager = SettingsManager.getInstance()
if (settingsManager.roundCovers) { if (settingsManager.roundCovers) {

View file

@ -17,7 +17,6 @@
package org.oxycblt.auxio.ui package org.oxycblt.auxio.ui
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -35,17 +34,39 @@ import org.oxycblt.auxio.util.logD
abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() { abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
private var _binding: T? = null private var _binding: T? = null
/**
* Inflate the binding from the given [inflater]. This should usually be done by the binding
* implementation's inflate function.
*/
protected abstract fun onCreateBinding(inflater: LayoutInflater): T protected abstract fun onCreateBinding(inflater: LayoutInflater): T
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
protected open fun onDestroyBinding(binding: T) {} /** Called during [onCreateDialog]. Dialog elements should be configured here. */
protected open fun onConfigDialog(builder: AlertDialog.Builder) {} protected open fun onConfigDialog(builder: AlertDialog.Builder) {}
/**
* Called during [onViewCreated] when the binding was successfully inflated and set as the view.
* This is where view setup should occur.
*/
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
/**
* Called during [onDestroyView] when the binding should be destroyed and all callbacks or
* leaking elements be released.
*/
protected open fun onDestroyBinding(binding: T) {}
/** Maybe get the binding. This will be null outside of the fragment view lifecycle. */
protected val binding: T? protected val binding: T?
get() = _binding get() = _binding
/**
* Get the binding under the assumption that the fragment has a view at this state in the
* lifecycle. This will throw an exception if the fragment is not in a valid lifecycle.
*/
protected fun requireBinding(): T { protected fun requireBinding(): T {
return requireNotNull(_binding) { return requireNotNull(_binding) {
"ViewBinding was not available, as the fragment was not in a valid state" "ViewBinding was available. Fragment should be a valid state " +
"right now, but instead it was ${lifecycle.currentState}"
} }
} }
@ -55,12 +76,11 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View = onCreateBinding(inflater).also { _binding = it }.root ): View = onCreateBinding(inflater).also { _binding = it }.root
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?) =
return MaterialAlertDialogBuilder(requireActivity(), theme).run { MaterialAlertDialogBuilder(requireActivity(), theme).run {
onConfigDialog(this) onConfigDialog(this)
create() create()
} }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -73,5 +93,6 @@ abstract class ViewBindingDialogFragment<T : ViewBinding> : DialogFragment() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(requireBinding()) onDestroyBinding(requireBinding())
_binding = null _binding = null
logD("Fragment destroyed")
} }
} }

View file

@ -32,16 +32,36 @@ import org.oxycblt.auxio.util.logD
abstract class ViewBindingFragment<T : ViewBinding> : Fragment() { abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
private var _binding: T? = null private var _binding: T? = null
/**
* Inflate the binding from the given [inflater]. This should usually be done by the binding
* implementation's inflate function.
*/
protected abstract fun onCreateBinding(inflater: LayoutInflater): T protected abstract fun onCreateBinding(inflater: LayoutInflater): T
/**
* Called during [onViewCreated] when the binding was successfully inflated and set as the view.
* This is where view setup should occur.
*/
protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {} protected open fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {}
/**
* Called during [onDestroyView] when the binding should be destroyed and all callbacks or
* leaking elements be released.
*/
protected open fun onDestroyBinding(binding: T) {} protected open fun onDestroyBinding(binding: T) {}
/** Maybe get the binding. This will be null outside of the fragment view lifecycle. */
protected val binding: T? protected val binding: T?
get() = _binding get() = _binding
/**
* Get the binding under the assumption that the fragment has a view at this state in the
* lifecycle. This will throw an exception if the fragment is not in a valid lifecycle.
*/
protected fun requireBinding(): T { protected fun requireBinding(): T {
return requireNotNull(_binding) { return requireNotNull(_binding) {
"ViewBinding was not available, as the fragment was not in a valid state" "ViewBinding was available. Fragment should be a valid state " +
"right now, but instead it was ${lifecycle.currentState}"
} }
} }
@ -61,5 +81,6 @@ abstract class ViewBindingFragment<T : ViewBinding> : Fragment() {
super.onDestroyView() super.onDestroyView()
onDestroyBinding(requireBinding()) onDestroyBinding(requireBinding())
_binding = null _binding = null
logD("Fragment destroyed")
} }
} }

View file

@ -19,9 +19,7 @@ package org.oxycblt.auxio.ui.accent
import android.os.Build import android.os.Build
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.logW
val ACCENT_COUNT: Int
get() = ACCENT_NAMES.size
private val ACCENT_NAMES = private val ACCENT_NAMES =
intArrayOf( intArrayOf(
@ -116,7 +114,7 @@ private val ACCENT_PRIMARY_COLORS =
* @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) { class Accent private constructor(val index: Int) {
val name: Int val name: Int
get() = ACCENT_NAMES[index] get() = ACCENT_NAMES[index]
val theme: Int val theme: Int
@ -126,7 +124,24 @@ data class Accent(val index: Int) {
val primary: Int val primary: Int
get() = ACCENT_PRIMARY_COLORS[index] get() = ACCENT_PRIMARY_COLORS[index]
override fun equals(other: Any?) = other is Accent && index == other.index
override fun hashCode() = index.hashCode()
companion object { companion object {
fun from(index: Int): Accent {
if (index > (MAX - 1)) {
logW("Account outside of bounds [idx: $index, max: $MAX")
return Accent(5)
}
return Accent(index)
}
/**
* The maximum amount of accents that are valid. This excludes the dynamic accent on
* versions that do not support it.
*/
val MAX = val MAX =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ACCENT_THEMES.size ACCENT_THEMES.size

View file

@ -30,7 +30,10 @@ import org.oxycblt.auxio.util.getColorSafe
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
/** An adapter that displays the accent palette. */ /**
* An adapter that displays the accent palette.
* @author OxygenCobalt
*/
class AccentAdapter(listener: Listener) : class AccentAdapter(listener: Listener) :
MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) { MonoAdapter<Accent, AccentAdapter.Listener, AccentViewHolder>(listener) {
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
@ -64,7 +67,7 @@ class AccentAdapter(listener: Listener) :
} }
class AccentData : BackingData<Accent>() { class AccentData : BackingData<Accent>() {
override fun getItem(position: Int) = Accent(position) override fun getItem(position: Int) = Accent.from(position)
override fun getItemCount() = Accent.MAX override fun getItemCount() = Accent.MAX
} }
} }
@ -84,15 +87,15 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin
} }
fun setSelected(isSelected: Boolean) { fun setSelected(isSelected: Boolean) {
val context = binding.accent.context binding.accent.apply {
isEnabled = !isSelected
binding.accent.isEnabled = !isSelected imageTintList =
binding.accent.imageTintList = if (isSelected) {
if (isSelected) { context.getAttrColorSafe(R.attr.colorSurface).stateList
context.getAttrColorSafe(R.attr.colorSurface).stateList } else {
} else { context.getColorSafe(android.R.color.transparent).stateList
context.getColorSafe(android.R.color.transparent).stateList }
} }
} }
companion object { companion object {

View file

@ -55,17 +55,17 @@ class AccentCustomizeDialog :
} }
override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: DialogAccentBinding, savedInstanceState: Bundle?) {
// --- UI SETUP ---
binding.accentRecycler.adapter = accentAdapter
accentAdapter.setSelectedAccent( accentAdapter.setSelectedAccent(
if (savedInstanceState != null) { if (savedInstanceState != null) {
Accent(savedInstanceState.getInt(KEY_PENDING_ACCENT)) Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else { } else {
settingsManager.accent settingsManager.accent
}, },
binding.accentRecycler) binding.accentRecycler)
// --- UI SETUP ---
binding.accentRecycler.adapter = accentAdapter
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View file

@ -222,22 +222,20 @@ fun Context.showToast(@StringRes str: Int) {
} }
/** 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( PendingIntent.getActivity(
this, this,
IntegerTable.REQUEST_CODE, IntegerTable.REQUEST_CODE,
Intent(this, MainActivity::class.java), Intent(this, MainActivity::class.java),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE 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( PendingIntent.getBroadcast(
this, this,
IntegerTable.REQUEST_CODE, IntegerTable.REQUEST_CODE,
Intent(what), Intent(what).setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE 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() {

View file

@ -33,20 +33,13 @@ import org.oxycblt.auxio.util.newMainIntent
fun createDefaultWidget(context: Context) = createViews(context, R.layout.widget_default) fun createDefaultWidget(context: Context) = createViews(context, R.layout.widget_default)
/** /**
* The tiny widget like a small or medium widget, but for landscape or exceptionally small screens. * The thin widget is a weird outlier widget intended to work well on strange launchers or
*/ * landscape grid launchers that allow really thin widget sizing.
fun createTinyWidget(context: Context, state: WidgetComponent.WidgetState) =
createViews(context, R.layout.widget_tiny)
.applyCover(context, state)
.applyBasicControls(context, state)
/**
* The thin widget is like a wide or large widget, but for landscape or exceptionally small screens.
*/ */
fun createThinWidget(context: Context, state: WidgetComponent.WidgetState) = fun createThinWidget(context: Context, state: WidgetComponent.WidgetState) =
createViews(context, R.layout.widget_thin) createViews(context, R.layout.widget_thin)
.applyCover(context, state) .applyMeta(context, state)
.applyFullControls(context, state) .applyBasicControls(context, state)
/** /**
* The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is * The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is

View file

@ -58,8 +58,7 @@ class WidgetProvider : AppWidgetProvider() {
// 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 = val views =
mapOf( mapOf(
SizeF(180f, 100f) to createTinyWidget(context, state), SizeF(180f, 100f) to createThinWidget(context, state),
SizeF(372f, 100f) to createThinWidget(context, state),
SizeF(180f, 152f) to createSmallWidget(context, state), SizeF(180f, 152f) to createSmallWidget(context, state),
SizeF(272f, 152f) to createWideWidget(context, state), SizeF(272f, 152f) to createWideWidget(context, state),
SizeF(180f, 270f) to createMediumWidget(context, state), SizeF(180f, 270f) to createMediumWidget(context, state),

View file

@ -8,7 +8,7 @@
android:theme="@style/Theme.Widget"> android:theme="@style/Theme.Widget">
<!-- <!--
See widget_small.xml for an explanation for the ImageView setup See widget_small.xml for an explanation for the ImageView setup.
--> -->
<android.widget.ImageView <android.widget.ImageView

View file

@ -8,7 +8,7 @@
android:theme="@style/Theme.Widget"> android:theme="@style/Theme.Widget">
<!-- <!--
See widget_small.xml for an explanation for the ImageView setup See widget_small.xml for an explanation for the ImageView setup.
--> -->
<android.widget.ImageView <android.widget.ImageView

View file

@ -8,7 +8,7 @@
android:theme="@style/Theme.Widget"> android:theme="@style/Theme.Widget">
<!-- <!--
For this widget form to work, we need to scale the ImageView across a 1:1 aspect ratio. For most widget forms to work, we need to scale the ImageView across a 1:1 aspect ratio.
However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout However, since we are working with a RemoteViews instance, we can't just use ConstraintLayout
to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with to achieve this. We can use RelativeLayout, but there's no way to force an aspect ratio with
that layout. However, if we create an invisible ImageView that contains a massive fixed-size that layout. However, if we create an invisible ImageView that contains a massive fixed-size

View file

@ -5,22 +5,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget" android:baselineAligned="false"
android:orientation="horizontal" android:orientation="horizontal"
android:baselineAligned="false"> android:theme="@style/Theme.Widget">
<!-- <!--
Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily
without squishing the controls. without squishing the controls.
--> -->
<RelativeLayout <android.widget.RelativeLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1"> android:layout_weight="1">
<!-- <!--
See widget_small.xml for an explanation for the ImageView setup See widget_small.xml for an explanation for the ImageView setup.
--> -->
<android.widget.ImageView <android.widget.ImageView
@ -52,63 +52,53 @@
android:src="@drawable/ic_remote_default_cover" android:src="@drawable/ic_remote_default_cover"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</RelativeLayout> </android.widget.RelativeLayout>
<android.widget.LinearLayout <android.widget.LinearLayout
android:id="@+id/widget_panel" android:id="@+id/widget_panel"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_gravity="center" android:layout_gravity="center"
android:orientation="horizontal"
android:layout_marginTop="@dimen/spacing_medium" android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium" android:layout_marginBottom="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"> android:layout_weight="2"
android:orientation="horizontal">
<android.widget.ImageButton <android.widget.LinearLayout
android:id="@+id/widget_repeat"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:contentDescription="@string/desc_change_repeat" android:layout_marginEnd="@dimen/spacing_small"
android:src="@drawable/ic_repeat" /> android:orientation="vertical">
<android.widget.ImageButton <android.widget.TextView
android:id="@+id/widget_skip_prev" android:id="@+id/widget_song"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:text="@string/def_widget_song" />
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton <android.widget.TextView
android:id="@+id/widget_play_pause" android:id="@+id/widget_artist"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:text="@string/def_widget_artist" />
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" />
<android.widget.ImageButton </android.widget.LinearLayout>
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget" <android.widget.ImageButton
android:layout_width="wrap_content" android:id="@+id/widget_play_pause"
android:layout_height="wrap_content" style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_weight="1" android:layout_width="wrap_content"
android:contentDescription="@string/desc_skip_next" android:layout_height="wrap_content"
android:src="@drawable/ic_skip_next" /> android:minWidth="@dimen/size_btn_small"
android:minHeight="@dimen/size_btn_small"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play" />
<android.widget.ImageButton
android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_shuffle"
android:src="@drawable/ic_shuffle_state" />
</android.widget.LinearLayout> </android.widget.LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -1,99 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:theme="@style/Theme.Widget"
android:orientation="horizontal"
android:baselineAligned="false">
<!--
Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily
without squishing the controls.
-->
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<!--
See widget_small.xml for an explanation for the ImageView setup
-->
<android.widget.ImageView
android:id="@+id/widget_aspect_ratio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/ui_remote_aspect_ratio"
android:visibility="invisible"
tools:ignore="ContentDescription" />
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignStart="@id/widget_aspect_ratio"
android:layout_alignTop="@id/widget_aspect_ratio"
android:layout_alignEnd="@id/widget_aspect_ratio"
android:layout_alignBottom="@id/widget_aspect_ratio"
android:src="@drawable/ic_remote_default_cover"
tools:ignore="ContentDescription" />
</RelativeLayout>
<android.widget.LinearLayout
android:id="@+id/widget_panel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:layout_gravity="center"
android:orientation="horizontal"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/sel_playing_state" />
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next" />
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -8,7 +8,7 @@
android:theme="@style/Theme.Widget"> android:theme="@style/Theme.Widget">
<!-- <!--
See widget_small.xml for an explanation for the ImageView setup See widget_small.xml for an explanation for the ImageView setup.
--> -->
<android.widget.ImageView <android.widget.ImageView