Rework UI

This commit is contained in:
birdbird 2022-11-10 09:04:19 +00:00
parent af98b19050
commit 6d93a98b22
97 changed files with 1459 additions and 1555 deletions

View File

@ -14,4 +14,8 @@ data class Playlist @JvmOverloads constructor(
companion object {
private const val serialVersionUID = -4160515427075433798L
}
override fun toString(): String {
return name
}
}

View File

@ -12,4 +12,6 @@ data class PodcastsChannel(
companion object {
private const val serialVersionUID = -4160515427075433798L
}
override fun toString(): String = name.toString()
}

View File

@ -1,3 +1,10 @@
/*
* ApiVersionCheckWrapper.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.api.subsonic
import okhttp3.ResponseBody

View File

@ -129,13 +129,12 @@ class SubsonicAPIClient(
this.addInterceptor(loggingInterceptor)
}
@Suppress("CustomX509TrustManager", "TrustAllX509TrustManager")
private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
val trustManager =
@Suppress("CustomX509TrustManager")
object : X509TrustManager {
@Suppress("TrustAllX509TrustManager")
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
@Suppress("TrustAllX509TrustManager")
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}

View File

@ -1,3 +1,10 @@
/*
* SubsonicAPIDefinition.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.api.subsonic
import okhttp3.ResponseBody

View File

@ -1,3 +1,10 @@
/*
* AlbumListOrderType.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.api.subsonic.models
/**
@ -16,8 +23,7 @@ enum class AlbumListType(val typeName: String) {
SORTED_BY_ARTIST("alphabeticalByArtist"),
STARRED("starred"),
BY_YEAR("byYear"),
BY_GENRE("byGenre"),
BY_ARTIST("albumsByArtist");
BY_GENRE("byGenre");
override fun toString(): String {
return typeName
@ -36,7 +42,6 @@ enum class AlbumListType(val typeName: String) {
in STARRED.typeName -> STARRED
in BY_YEAR.typeName -> BY_YEAR
in BY_GENRE.typeName -> BY_GENRE
in BY_ARTIST.typeName -> BY_ARTIST
else -> throw IllegalArgumentException("Unknown type: $typeName")
}

View File

@ -36,6 +36,7 @@ class AlbumListTypeTest {
@Test
fun `Should return type name for toString call`() {
AlbumListType.STARRED.typeName `should be equal to` AlbumListType.STARRED.toString()
AlbumListType.STARRED.typeName `should be equal to`
AlbumListType.STARRED.toString()
}
}

View File

@ -9,12 +9,10 @@ ktlint = "0.43.2"
ktlintGradle = "11.0.0"
detekt = "1.21.0"
preferences = "1.2.0"
media = "1.6.0"
media3 = "1.0.0-beta02"
androidSupport = "1.5.0"
androidLegacySupport = "1.0.0"
androidSupportDesign = "1.6.1"
materialDesign = "1.6.1"
constraintLayout = "2.1.4"
multidex = "2.0.1"
room = "2.4.3"
@ -22,6 +20,7 @@ kotlin = "1.7.20"
kotlinxCoroutines = "1.6.4-native-mt"
kotlinxGuava = "1.6.4"
viewModelKtx = "2.5.1"
swipeRefresh = "1.1.0"
retrofit = "2.9.0"
jackson = "2.13.4"
@ -50,8 +49,7 @@ ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", ver
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
design = { module = "com.google.android.material:material", version.ref = "materialDesign" }
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
@ -66,11 +64,10 @@ navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", ve
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
media = { module = "androidx.media:media", version.ref = "media" }
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
media3session = { module = "androidx.media3:media3-session", version.ref = "media3" }
swipeRefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefresh" }
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }

View File

@ -4,6 +4,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion versions.compileSdk

View File

@ -97,7 +97,6 @@ dependencies {
}
implementation libs.core
implementation libs.support
implementation libs.design
implementation libs.multidex
implementation libs.roomRuntime
@ -105,10 +104,10 @@ dependencies {
implementation libs.viewModelKtx
implementation libs.constraintLayout
implementation libs.preferences
implementation libs.media
implementation libs.media3exoplayer
implementation libs.media3session
implementation libs.media3okhttp
implementation libs.swipeRefresh
implementation libs.navigationFragment
implementation libs.navigationUi

View File

@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="154"
line="153"
column="5"/>
</issue>
@ -70,138 +70,6 @@
column="10"/>
</issue>
<issue
id="ObsoleteLayoutParam"
message="Invalid layout param in a `LinearLayout`: `layout_above`"
errorLine1=" android:layout_above=&quot;@+id/bottom&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/time_span_dialog.xml"
line="12"
column="9"/>
</issue>
<issue
id="ObsoleteLayoutParam"
message="Invalid layout param in a `LinearLayout`: `layout_below`"
errorLine1=" android:layout_below=&quot;@+id/top&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/time_span_dialog.xml"
line="28"
column="9"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_baseline_close` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_baseline_close.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_create_new_folder` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_create_new_folder.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_drag_queue` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_drag_queue.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_folder` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_folder.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_menu_arrow.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_menu_backward` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_menu_backward.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_menu_forward` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_menu_forward.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_menu_help` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_menu_help.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_sd_storage` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_sd_storage.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_subdirectory_up` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_subdirectory_up.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
@ -297,54 +165,10 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/time_span_dialog.xml"
line="30"
line="28"
column="10"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/update_playlist.xml"
line="18"
column="4"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/update_playlist.xml"
line="40"
column="4"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/chat_item.xml"
line="7"
column="6"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/chat_item_reverse.xml"
line="64"
column="6"/>
</issue>
<issue
id="LabelFor"
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
@ -374,29 +198,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/time_span_dialog.xml"
line="30"
column="10"/>
</issue>
<issue
id="HardcodedText"
message="Hardcoded string &quot;0 dB&quot;, should use `@string` resource"
errorLine1=" a:text=&quot;0 dB&quot;"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="src/main/res/layout/equalizer_bar.xml"
line="26"
column="13"/>
</issue>
<issue
id="RelativeOverlap"
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"
errorLine1=" &lt;TextView"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/player_slider.xml"
line="29"
line="28"
column="10"/>
</issue>

View File

@ -1,56 +0,0 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Playlist;
import java.util.List;
/**
* @author Sindre Mehus
*/
public class PlaylistAdapter extends ArrayAdapter<Playlist>
{
private final Context context;
public PlaylistAdapter(Context context, List<Playlist> Playlists)
{
super(context, R.layout.playlist_list_item, Playlists);
this.context = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
Playlist entry = getItem(position);
PlaylistView view;
if (convertView instanceof PlaylistView)
{
PlaylistView currentView = (PlaylistView) convertView;
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
view = currentView;
view.setViewHolder(viewHolder);
}
else
{
view = new PlaylistView(context);
view.setLayout();
}
view.setPlaylist(entry);
return view;
}
static class ViewHolder
{
TextView name;
}
}

View File

@ -1,62 +0,0 @@
/*
This file is part of Subsonic.
Subsonic 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.
Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Playlist;
/**
* Used to display playlists in a {@code ListView}.
*
* @author Sindre Mehus
*/
public class PlaylistView extends LinearLayout
{
private final Context context;
private PlaylistAdapter.ViewHolder viewHolder;
public PlaylistView(Context context)
{
super(context);
this.context = context;
}
public void setLayout()
{
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
viewHolder = new PlaylistAdapter.ViewHolder();
viewHolder.name = findViewById(R.id.playlist_name);
setTag(viewHolder);
}
public void setViewHolder(PlaylistAdapter.ViewHolder viewHolder)
{
this.viewHolder = viewHolder;
setTag(this.viewHolder);
}
public void setPlaylist(Playlist playlist)
{
viewHolder.name.setText(playlist.getName());
}
}

View File

@ -1,62 +0,0 @@
/*
This file is part of Subsonic.
Subsonic 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.
Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.Playlist;
/**
* Used to display playlists in a {@code ListView}.
*
* @author Sindre Mehus
*/
public class PodcastChannelItemView extends LinearLayout
{
private final Context context;
private PlaylistAdapter.ViewHolder viewHolder;
public PodcastChannelItemView(Context context)
{
super(context);
this.context = context;
}
public void setLayout()
{
LayoutInflater.from(context).inflate(R.layout.playlist_list_item, this, true);
viewHolder = new PlaylistAdapter.ViewHolder();
viewHolder.name = findViewById(R.id.playlist_name);
setTag(viewHolder);
}
public void setViewHolder(PlaylistAdapter.ViewHolder viewHolder)
{
this.viewHolder = viewHolder;
setTag(this.viewHolder);
}
public void setPlaylist(Playlist playlist)
{
viewHolder.name.setText(playlist.getName());
}
}

View File

@ -1,46 +0,0 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import androidx.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.PodcastsChannel;
import java.util.List;
/**
* @author Sindre Mehus
*/
public class PodcastsChannelsAdapter extends ArrayAdapter<PodcastsChannel> {
private final LayoutInflater layoutInflater;
public PodcastsChannelsAdapter(Context context, List<PodcastsChannel> channels) {
super(context, R.layout.podcasts_channel_item, channels);
layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
PodcastsChannel entry = getItem(position);
TextView view;
if (convertView instanceof PlaylistView) {
view = (TextView) convertView;
} else {
view = (TextView) layoutInflater
.inflate(R.layout.podcasts_channel_item, parent, false);
}
view.setText(entry.getTitle());
return view;
}
}

View File

@ -16,7 +16,6 @@ import java.util.List;
*/
public class ShareAdapter extends ArrayAdapter<Share>
{
private final Context context;
public ShareAdapter(Context context, List<Share> Shares)

View File

@ -52,7 +52,6 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.fragment.MainFragmentDirections
import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
@ -72,11 +71,12 @@ import timber.log.Timber
/**
* The main (and only) Activity of Ultrasonic which loads all other screens as Fragments.
* Because this is the only Activity we have to manage the apps lifecycle through tis activitys
* Because this is the only Activity we have to manage the apps lifecycle through this activity
* onCreate/onResume/onDestroy methods...
*/
@Suppress("TooManyFunctions")
class NavigationActivity : AppCompatActivity() {
private var videoMenuItem: MenuItem? = null
private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null
private var sharesMenuItem: MenuItem? = null
@ -301,6 +301,13 @@ class NavigationActivity : AppCompatActivity() {
R.id.bookmarksFragment -> {
navController.navigate(NavigationGraphDirections.toBookmarks())
}
R.id.trackCollectionFragment -> {
navController.navigate(
NavigationGraphDirections.toTrackCollection(
getVideos = true
)
)
}
R.id.menu_exit -> {
setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.onDestroy()
@ -319,6 +326,7 @@ class NavigationActivity : AppCompatActivity() {
podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment)
downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment)
videoMenuItem = navigationView?.menu?.findItem(R.id.trackCollectionFragment)
selectServerButton =
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
@ -348,7 +356,7 @@ class NavigationActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val retValue = super.onCreateOptionsMenu(menu)
if (navigationView == null) {
menuInflater.inflate(R.menu.navigation, menu)
menuInflater.inflate(R.menu.navigation_drawer, menu)
return true
}
return retValue
@ -390,7 +398,7 @@ class NavigationActivity : AppCompatActivity() {
)
suggestions.saveRecentQuery(query, null)
val action = MainFragmentDirections.toSearchFragment(query, autoPlay)
val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(action)
}
}
@ -498,5 +506,6 @@ class NavigationActivity : AppCompatActivity() {
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
playlistsMenuItem?.isVisible = isOnline
downloadsMenuItem?.isVisible = isOnline
videoMenuItem?.isVisible = isOnline
}
}

View File

@ -7,6 +7,7 @@
package org.moire.ultrasonic.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
@ -15,31 +16,31 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder
import com.drakeet.multitype.ItemViewDelegate
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
import timber.log.Timber
/**
* Creates a Row in a RecyclerView which contains the details of an Album
*/
class AlbumRowBinder(
val onItemClick: (Album) -> Unit,
val onContextMenuClick: (MenuItem, Album) -> Boolean,
open class AlbumRowDelegate(
open val onItemClick: (Album) -> Unit,
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
private val imageLoader: ImageLoader
) : ItemViewBinder<Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
private val starDrawable: Int = R.drawable.ic_star_full
private val starHollowDrawable: Int = R.drawable.ic_star_hollow
// Set our layout files
val layout = R.layout.list_item_album
open var layoutType = LayoutType.LIST
override fun onBindViewHolder(holder: ViewHolder, item: Album) {
override fun onBindViewHolder(holder: ListViewHolder, item: Album) {
holder.album.text = item.title
holder.artist.text = item.artist
holder.details.setOnClickListener { onItemClick(item) }
@ -66,15 +67,40 @@ class AlbumRowBinder(
/**
* Holds the view properties of an Item row
*/
class ViewHolder(
open class ListViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
var album: TextView = view.findViewById(R.id.album_title)
var artist: TextView = view.findViewById(R.id.album_artist)
var details: LinearLayout = view.findViewById(R.id.row_album_details)
var coverArt: ImageView = view.findViewById(R.id.coverart)
var star: ImageView = view.findViewById(R.id.album_star)
var album: TextView
var artist: TextView
var details: LinearLayout
var coverArt: ImageView
var star: ImageView
var coverArtId: String? = null
constructor(parent: ViewGroup, inflater: LayoutInflater) : this(
inflater.inflate(R.layout.list_item_album, parent, false)
)
init {
album = view.findViewById(R.id.album_title)
artist = view.findViewById(R.id.album_artist)
details = view.findViewById(R.id.row_album_details)
coverArt = view.findViewById(R.id.cover_art)
star = view.findViewById(R.id.album_star)
coverArtId = null
}
}
/**
* Holds the view properties of an Item row
*/
class CoverViewHolder(
view: View
) : ListViewHolder(view) {
constructor(parent: ViewGroup, inflater: LayoutInflater) : this(
inflater.inflate(R.layout.grid_item_album, parent, false)
)
}
/**
@ -106,7 +132,24 @@ class AlbumRowBinder(
}.start()
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder {
return when (layoutType) {
LayoutType.LIST -> ListViewHolder(
parent,
LayoutInflater.from(context)
)
LayoutType.COVER -> CoverViewHolder(
parent,
LayoutInflater.from(context)
)
}
}
}
class AlbumGridDelegate(
onItemClick: (Album) -> Unit,
onContextMenuClick: (MenuItem, Album) -> Boolean,
imageLoader: ImageLoader
) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoader) {
override var layoutType = LayoutType.COVER
}

View File

@ -123,7 +123,7 @@ class ArtistRowBinder(
var section: TextView = itemView.findViewById(R.id.row_section)
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
var layout: RelativeLayout = itemView.findViewById(R.id.containing_layout)
var coverArt: ImageView = itemView.findViewById(R.id.coverart)
var coverArt: ImageView = itemView.findViewById(R.id.cover_art)
var coverArtId: String? = null
}

View File

@ -13,7 +13,6 @@ import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.Track
import timber.log.Timber
@Suppress("LongParameterList")
class TrackViewBinder(
@ -63,7 +62,7 @@ class TrackViewBinder(
diffAdapter.isSelected(item.longId)
)
Timber.v("Setting listeners")
// Timber.v("Setting listeners")
holder.itemView.setOnLongClickListener {
if (onContextMenuClick != null) {
@ -118,7 +117,7 @@ class TrackViewBinder(
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
}
Timber.v("Setting listeners done")
// Timber.v("Setting listeners done")
}
override fun onViewRecycled(holder: TrackViewHolder) {

View File

@ -81,7 +81,7 @@ class TrackViewHolder(val view: View) :
draggable: Boolean,
isSelected: Boolean = false
) {
Timber.v("Setting song")
// Timber.v("Setting song")
val useFiveStarRating = Settings.useFiveStarRating
entry = song
@ -139,7 +139,7 @@ class TrackViewHolder(val view: View) :
updateStatus(it.state, it.progress)
}
Timber.v("Setting song done")
// Timber.v("Setting song done")
}
// This is called when the Holder is recycled and receives a new Song

View File

@ -10,24 +10,41 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.AlbumGridDelegate
import org.moire.ultrasonic.adapters.AlbumRowDelegate
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.model.AlbumListModel
import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.view.FilterButtonBar
import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities
/**
* Displays a list of Albums from the media library
*/
class AlbumListFragment : EntryListFragment<Album>() {
class AlbumListFragment(
private var layoutType: LayoutType = LayoutType.LIST,
private var orderType: SortOrder? = null
) : FilterableFragment, EntryListFragment<Album>() {
private var filterButtonBar: FilterButtonBar? = null
/**
* The ViewModel to use to get the data
@ -50,7 +67,8 @@ class AlbumListFragment : EntryListFragment<Album>() {
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(
refresh: Boolean
refresh: Boolean,
append: Boolean
): LiveData<List<Album>> {
fetchAlbums(refresh)
@ -63,7 +81,7 @@ class AlbumListFragment : EntryListFragment<Album>() {
listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true
if (navArgs.type == AlbumListType.BY_ARTIST) {
if (navArgs.byArtist) {
listModel.getAlbumsOfArtist(
refresh = navArgs.refresh,
id = navArgs.id!!,
@ -71,7 +89,7 @@ class AlbumListFragment : EntryListFragment<Album>() {
)
} else {
listModel.getAlbums(
albumListType = navArgs.type,
albumListType = orderType?.mapToAlbumListType() ?: navArgs.type,
size = navArgs.size,
offset = navArgs.offset,
append = append,
@ -82,38 +100,145 @@ class AlbumListFragment : EntryListFragment<Album>() {
}
}
// TODO: Make generic
override fun setLayoutType(newType: LayoutType) {
layoutType = newType
viewManager = if (layoutType == LayoutType.LIST) {
LinearLayoutManager(this.context)
} else {
GridLayoutManager(this.context, ROWS)
}
listView!!.layoutManager = viewManager
// Attach our onScrollListener
val scrollListener = object : EndlessScrollListener(viewManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
// Triggered only when new data needs to be appended to the list
// Add whatever code is needed to append new items to the bottom of the list
fetchAlbums(append = true)
}
}
listView!!.addOnScrollListener(scrollListener)
}
override fun setOrderType(newOrder: SortOrder) {
orderType = newOrder
// If we are on an Artist page we just need to reorder the list. Otherwise refetch
if (navArgs.byArtist) {
listModel.sortListByOrder(newOrder.mapToAlbumListType())
} else {
fetchAlbums(refresh = true, append = false)
}
}
override var viewCapabilities: ViewCapabilities = ViewCapabilities(
supportsGrid = true,
supportedSortOrders = getListOfSortOrders()
)
private fun getListOfSortOrders(): List<SortOrder> {
val useId3 = Settings.shouldUseId3Tags
val useId3Offline = Settings.useId3TagsOffline
val isOnline = !ActiveServerProvider.isOffline()
val supported = mutableListOf<SortOrder>()
if (isOnline || useId3Offline) {
supported.add(SortOrder.NEWEST)
}
if (isOnline) {
supported.add(SortOrder.RECENT)
}
if (isOnline) {
supported.add(SortOrder.FREQUENT)
}
if (isOnline && !useId3) {
supported.add(SortOrder.HIGHEST)
}
if (isOnline) {
supported.add(SortOrder.RANDOM)
}
if (isOnline) {
supported.add(SortOrder.STARRED)
}
if (isOnline || useId3Offline) {
supported.add(SortOrder.BY_NAME)
}
if (isOnline || useId3Offline) {
supported.add(SortOrder.BY_ARTIST)
}
return supported
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val layout = if (navArgs.byArtist) R.layout.list_layout_filterable else mainLayout
return inflater.inflate(layout, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(navArgs.title)
// Attach our onScrollListener
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
val scrollListener = object : EndlessScrollListener(viewManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
// Triggered only when new data needs to be appended to the list
// Add whatever code is needed to append new items to the bottom of the list
fetchAlbums(append = true)
}
}
addOnScrollListener(scrollListener)
// In most cases this fragment will be hosted by a ViewPager2 in the MainFragment,
// which provides its own FilterBar.
// But when we are looking at the Albums of a specific Artist this Fragment is standalone,
// so we need to setup the FilterBar here..
if (navArgs.byArtist) {
setTitle(navArgs.title)
setupFilterBar(view)
}
viewAdapter.register(
AlbumRowBinder(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader()
)
)
// Get a reference to the listView
listView = view.findViewById(recyclerViewId)
setLayoutType(layoutType)
val imageLoader = imageLoaderProvider.getImageLoader()
// Magic to switch between different view layouts:
// We register two delegates, one which layouts grid items and one which layouts row items
// Based on the current status of the ViewType, the right delegate is picked.
viewAdapter.register(Album::class).to(
AlbumRowDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader),
AlbumGridDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader)
).withKotlinClassLinker { _, _ ->
when (layoutType) {
LayoutType.COVER -> AlbumGridDelegate::class
LayoutType.LIST -> AlbumRowDelegate::class
}
}
emptyTextView.setText(R.string.select_album_empty)
}
private fun setupFilterBar(view: View) {
// Load last layout from settings
layoutType = LayoutType.from(Settings.lastViewType)
filterButtonBar = view.findViewById(R.id.filter_button_bar)
filterButtonBar!!.setOnLayoutTypeChangedListener(::setLayoutType)
filterButtonBar!!.setOnOrderChangedListener(::setOrderType)
filterButtonBar!!.configureWithCapabilities(
ViewCapabilities(
supportsGrid = true,
supportedSortOrders = listOf(
SortOrder.BY_NAME,
SortOrder.BY_YEAR
)
)
)
// Set layout toggle Chip to correct state
filterButtonBar!!.setLayoutType(layoutType)
}
override fun onItemClick(item: Album) {
val action = AlbumListFragmentDirections.albumListToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
item.id,
isAlbum = item.isDirectory,
name = item.title,
@ -121,4 +246,20 @@ class AlbumListFragment : EntryListFragment<Album>() {
)
findNavController().navigate(action)
}
private fun SortOrder.mapToAlbumListType(): AlbumListType = when (this) {
SortOrder.RANDOM -> AlbumListType.RANDOM
SortOrder.NEWEST -> AlbumListType.NEWEST
SortOrder.HIGHEST -> AlbumListType.HIGHEST
SortOrder.FREQUENT -> AlbumListType.FREQUENT
SortOrder.RECENT -> AlbumListType.RECENT
SortOrder.BY_NAME -> AlbumListType.SORTED_BY_NAME
SortOrder.BY_ARTIST -> AlbumListType.SORTED_BY_ARTIST
SortOrder.STARRED -> AlbumListType.STARRED
SortOrder.BY_YEAR -> AlbumListType.BY_YEAR
}
companion object {
private const val ROWS = 3
}
}

View File

@ -13,6 +13,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
@ -41,7 +42,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(refresh: Boolean): LiveData<List<ArtistOrIndex>> {
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<ArtistOrIndex>> {
return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
}
@ -66,15 +67,16 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
override fun onItemClick(item: ArtistOrIndex) {
// Check type
val action = if (item is Index) {
ArtistListFragmentDirections.artistsListToTrackCollection(
NavigationGraphDirections.toTrackCollection(
id = item.id,
name = item.name,
parentId = item.id,
isArtist = (item is Artist)
)
} else {
ArtistListFragmentDirections.artistsListToAlbumsList(
type = AlbumListType.BY_ARTIST,
NavigationGraphDirections.toAlbumList(
type = AlbumListType.SORTED_BY_NAME,
byArtist = true,
id = item.id,
title = item.name,
size = 1000,

View File

@ -37,7 +37,8 @@ class BookmarksFragment : TrackCollectionFragment() {
}
override fun getLiveData(
refresh: Boolean
refresh: Boolean,
append: Boolean
): LiveData<List<MusicDirectory.Child>> {
listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true

View File

@ -40,7 +40,7 @@ class DownloadsFragment : MultiListFragment<Track>() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(refresh: Boolean): LiveData<List<Track>> {
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Track>> {
return listModel.getList()
}

View File

@ -89,6 +89,7 @@ abstract class EndlessScrollListener : RecyclerView.OnScrollListener {
loading = true
}
}
// If its still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.fragment
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
/**
* Contains utility functions related to Fragment title handling
@ -9,11 +10,17 @@ import androidx.fragment.app.Fragment
class FragmentTitle {
companion object {
fun setTitle(fragment: Fragment, title: CharSequence?) {
(fragment.activity as AppCompatActivity).supportActionBar?.title = title
// Only set the title if our fragment is a direct child of the NavHostFragment...
if (fragment.parentFragment is NavHostFragment) {
(fragment.activity as AppCompatActivity).supportActionBar?.title = title
}
}
fun setTitle(fragment: Fragment, id: Int) {
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id)
// Only set the title if our fragment is a direct child of the NavHostFragment...
if (fragment.parentFragment is NavHostFragment) {
(fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id)
}
}
fun getTitle(fragment: Fragment): CharSequence? {

View File

@ -7,251 +7,217 @@
package org.moire.ultrasonic.fragment
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.fragment.app.FragmentManager
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import java.lang.ref.SoftReference
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.databinding.MainBinding
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.fragment.legacy.PlaylistsFragment
import org.moire.ultrasonic.fragment.legacy.SelectGenreFragment
import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.EMPTY_CAPABILITIES
import org.moire.ultrasonic.view.FilterButtonBar
import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities
import timber.log.Timber
/**
* Displays the Main screen of Ultrasonic, where the music library can be browsed
*/
class MainFragment : Fragment(), KoinComponent {
private lateinit var musicTitle: TextView
private lateinit var artistsButton: TextView
private lateinit var albumsButton: TextView
private lateinit var genresButton: TextView
private lateinit var videosTitle: TextView
private lateinit var songsTitle: TextView
private lateinit var randomSongsButton: TextView
private lateinit var songsStarredButton: TextView
private lateinit var albumsTitle: TextView
private lateinit var albumsNewestButton: TextView
private lateinit var albumsRandomButton: TextView
private lateinit var albumsHighestButton: TextView
private lateinit var albumsStarredButton: TextView
private lateinit var albumsRecentButton: TextView
private lateinit var albumsFrequentButton: TextView
private lateinit var albumsAlphaByNameButton: TextView
private lateinit var albumsAlphaByArtistButton: TextView
private lateinit var videosButton: TextView
private var filterButtonBar: FilterButtonBar? = null
private var layoutType: LayoutType = LayoutType.COVER
private var binding: View? = null
private var binding: MainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
private lateinit var musicCollectionAdapter: MusicCollectionAdapter
private lateinit var viewPager: ViewPager2
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = MainBinding.inflate(inflater, container, false)
return binding!!.root
Timber.i("onCreate")
binding = inflater.inflate(R.layout.primary, container, false)
return binding!!
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupButtons()
setupClickListener()
setupItemVisibility()
super.onViewCreated(view, savedInstanceState)
}
FragmentTitle.setTitle(this, R.string.music_library_label)
override fun onResume() {
super.onResume()
var shouldRelayout = false
val currentId3Setting = Settings.shouldUseId3Tags
// Load last layout from settings
layoutType = LayoutType.from(Settings.lastViewType)
// If setting has changed...
if (currentId3Setting != useId3) {
useId3 = currentId3Setting
shouldRelayout = true
// Init ViewPager2
musicCollectionAdapter = MusicCollectionAdapter(this, layoutType)
viewPager = binding!!.findViewById(R.id.pager)
viewPager.adapter = musicCollectionAdapter
filterButtonBar = binding!!.findViewById(R.id.filter_button_bar)
musicCollectionAdapter.filterButtonBar = filterButtonBar
filterButtonBar!!.setOnLayoutTypeChangedListener {
updateLayoutTypeOnCurrentFragment(it)
}
// then setup the list anew.
if (shouldRelayout) {
setupItemVisibility()
filterButtonBar!!.setOnOrderChangedListener {
updateSortOrderOnCurrentFragment(it)
}
// Set layout toggle Chip to correct state
filterButtonBar!!.setLayoutType(layoutType)
// Listen to changes in the current page (=fragment)
viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
Timber.i("On Page changed $position")
// This is a bit tricky. We need to configure the FilterButtonBar based on the
// fragments capabilities. But this function can be called before the fragment has
// been created, and the ViewPager might create the fragments in arbitrary order.
// Therefore we store a flag in the Adapter, to signal that the next created
// fragment of the given position should propagate its capabilities
val frag = findFragmentAtPosition(childFragmentManager, position)
if (frag != null) {
filterButtonBar!!.configureWithCapabilitiesFromFragment(frag)
} else {
musicCollectionAdapter.propagateCapabilitiesMatcher = position
}
}
})
// The TabLayoutMediator manages the names of the Tabs (Albums, Artists, etc)
val tabLayout: TabLayout = binding!!.findViewById(R.id.tab_layout)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = musicCollectionAdapter.getTitleForFragment(position, requireContext())
}.attach()
}
private fun updateLayoutTypeOnCurrentFragment(it: LayoutType) {
val curFrag = findCurrentFragment()
if (curFrag is FilterableFragment) {
curFrag.setLayoutType(it)
}
Settings.lastViewType = layoutType.value
}
private fun updateSortOrderOnCurrentFragment(it: SortOrder) {
val curFrag = findCurrentFragment()
if (curFrag is FilterableFragment) {
curFrag.setOrderType(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
private fun findCurrentFragment(): Fragment? {
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
}
private fun setupButtons() {
musicTitle = binding!!.mainMusic
artistsButton = binding!!.mainArtistsButton
albumsButton = binding!!.mainAlbumsButton
genresButton = binding!!.mainGenresButton
videosTitle = binding!!.mainVideosTitle
songsTitle = binding!!.mainSongs
randomSongsButton = binding!!.mainSongsButton
songsStarredButton = binding!!.mainSongsStarred
albumsTitle = binding!!.mainAlbums
albumsNewestButton = binding!!.mainAlbumsNewest
albumsRandomButton = binding!!.mainAlbumsRandom
albumsHighestButton = binding!!.mainAlbumsHighest
albumsStarredButton = binding!!.mainAlbumsStarred
albumsRecentButton = binding!!.mainAlbumsRecent
albumsFrequentButton = binding!!.mainAlbumsFrequent
albumsAlphaByNameButton = binding!!.mainAlbumsAlphaByName
albumsAlphaByArtistButton = binding!!.mainAlbumsAlphaByArtist
videosButton = binding!!.mainVideos
}
private fun setupItemVisibility() {
// Cache some values
useId3 = Settings.shouldUseId3Tags
useId3Offline = Settings.useId3TagsOffline
val isOnline = !isOffline()
// Music
musicTitle.isVisible = true
artistsButton.isVisible = true
albumsButton.isVisible = isOnline || useId3Offline
genresButton.isVisible = isOnline
// Songs
songsTitle.isVisible = true
randomSongsButton.isVisible = true
songsStarredButton.isVisible = isOnline
// Albums
albumsTitle.isVisible = isOnline || useId3Offline
albumsNewestButton.isVisible = isOnline || useId3Offline
albumsRecentButton.isVisible = isOnline
albumsFrequentButton.isVisible = isOnline
albumsHighestButton.isVisible = isOnline && !useId3
albumsRandomButton.isVisible = isOnline
albumsStarredButton.isVisible = isOnline
albumsAlphaByNameButton.isVisible = isOnline || useId3Offline
albumsAlphaByArtistButton.isVisible = isOnline || useId3Offline
// Videos
videosTitle.isVisible = isOnline
videosButton.isVisible = isOnline
}
private fun setupClickListener() {
albumsNewestButton.setOnClickListener {
showAlbumList(AlbumListType.NEWEST, R.string.main_albums_newest)
}
albumsRandomButton.setOnClickListener {
showAlbumList(AlbumListType.RANDOM, R.string.main_albums_random)
}
albumsHighestButton.setOnClickListener {
showAlbumList(AlbumListType.HIGHEST, R.string.main_albums_highest)
}
albumsRecentButton.setOnClickListener {
showAlbumList(AlbumListType.RECENT, R.string.main_albums_recent)
}
albumsFrequentButton.setOnClickListener {
showAlbumList(AlbumListType.FREQUENT, R.string.main_albums_frequent)
}
albumsStarredButton.setOnClickListener {
showAlbumList(AlbumListType.STARRED, R.string.main_albums_starred)
}
albumsAlphaByNameButton.setOnClickListener {
showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_alphaByName)
}
albumsAlphaByArtistButton.setOnClickListener {
showAlbumList(AlbumListType.SORTED_BY_ARTIST, R.string.main_albums_alphaByArtist)
}
songsStarredButton.setOnClickListener {
showStarredSongs()
}
artistsButton.setOnClickListener {
showArtists()
}
albumsButton.setOnClickListener {
showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_title)
}
randomSongsButton.setOnClickListener {
showRandomSongs()
}
genresButton.setOnClickListener {
showGenres()
}
videosButton.setOnClickListener {
showVideos()
}
}
private fun showStarredSongs() {
val action = MainFragmentDirections.mainToTrackCollection(
getStarred = true,
)
findNavController().navigate(action)
}
private fun showRandomSongs() {
val action = MainFragmentDirections.mainToTrackCollection(
getRandom = true,
size = Settings.maxSongs
)
findNavController().navigate(action)
}
private fun showArtists() {
val action = MainFragmentDirections.mainToArtistList(
title = requireContext().resources.getString(R.string.main_artists_title)
)
findNavController().navigate(action)
}
private fun showAlbumList(type: AlbumListType, titleIndex: Int) {
val title = requireContext().resources.getString(titleIndex, "")
val action = MainFragmentDirections.mainToAlbumList(
type = type,
title = title,
size = Settings.maxAlbums,
offset = 0
)
findNavController().navigate(action)
}
private fun showGenres() {
findNavController().navigate(R.id.mainToSelectGenre)
}
private fun showVideos() {
val action = MainFragmentDirections.mainToTrackCollection(
getVideos = true,
)
findNavController().navigate(action)
}
companion object {
private var useId3 = false
private var useId3Offline = false
private fun findFragmentAtPosition(
fragmentManager: FragmentManager,
position: Int
): Fragment? {
// If a fragment was recently created and never shown the fragment manager might not
// hold a reference to it. Fallback on the WeakMap instead.
return fragmentManager.findFragmentByTag("f$position")
?: musicCollectionAdapter.fragmentMap[position]?.get()
}
}
private fun FilterButtonBar.configureWithCapabilitiesFromFragment(frag: Fragment?) {
if (frag is FilterableFragment) {
Timber.w("Setting kapas: ${frag.viewCapabilities}")
this.configureWithCapabilities(frag.viewCapabilities)
} else {
Timber.w("Setting kapas: $EMPTY_CAPABILITIES")
this.configureWithCapabilities(EMPTY_CAPABILITIES)
}
}
@Suppress("MagicNumber")
class MusicCollectionAdapter(fragment: Fragment, initialType: LayoutType = LayoutType.LIST) :
FragmentStateAdapter(fragment) {
var filterButtonBar: FilterButtonBar? = null
private var layoutType: LayoutType = initialType
var propagateCapabilitiesMatcher: Int? = null
// viewPager.findFragmentAtPosition(childFragmentManager, position) is sometimes delayed..
var fragmentMap: HashMap<Int, SoftReference<Fragment>> = hashMapOf()
override fun getItemCount(): Int {
// Hide Genre tab when offline
return if (ActiveServerProvider.isOffline()) 4 else 5
}
override fun createFragment(position: Int): Fragment {
Timber.i("Creating new fragment at position: $position")
val action = when (position) {
0 -> NavigationGraphDirections.toArtistList()
1 -> NavigationGraphDirections.toAlbumList(
AlbumListType.NEWEST,
size = Settings.maxAlbums
)
2 -> NavigationGraphDirections.toPlaylistFragment()
3 -> NavigationGraphDirections.toTrackCollection()
else -> NavigationGraphDirections.toGenreList()
}
val fragment = when (position) {
0 -> ArtistListFragment()
1 -> AlbumListFragment(layoutType)
2 -> PlaylistsFragment()
3 -> TrackCollectionFragment(SortOrder.RANDOM)
else -> SelectGenreFragment()
}
fragmentMap[position] = SoftReference(fragment)
fragment.arguments = action.arguments
// See comment in onPageSelected
if (propagateCapabilitiesMatcher == position) {
Timber.w("Setting capacities while creating, $position")
propagateCapabilitiesMatcher = null
filterButtonBar!!.configureWithCapabilitiesFromFragment(fragment)
}
return fragment
}
fun getTitleForFragment(pos: Int, context: Context): String {
return when (pos) {
0 -> context.getString(R.string.main_artists_title)
1 -> context.getString(R.string.main_albums_title)
2 -> context.getString(R.string.playlist_label)
3 -> context.getString(R.string.main_songs_title)
4 -> context.getString(R.string.main_genres_title)
else -> "Unknown"
}
}
}
interface FilterableFragment {
fun setLayoutType(newType: LayoutType) {}
fun setOrderType(newOrder: SortOrder)
var viewCapabilities: ViewCapabilities
}

View File

@ -75,7 +75,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
/**
* The central function to pass a query to the model and return a LiveData object
*/
open fun getLiveData(refresh: Boolean = false): LiveData<List<T>> {
open fun getLiveData(refresh: Boolean = false, append: Boolean = false): LiveData<List<T>> {
return MutableLiveData()
}

View File

@ -578,7 +578,8 @@ class PlayerFragment :
if (Settings.shouldUseId3Tags) {
val action = PlayerFragmentDirections.playerToAlbumsList(
type = AlbumListType.BY_ARTIST,
type = AlbumListType.SORTED_BY_NAME,
byArtist = true,
id = track.artistId,
title = track.artist,
offset = 0,

View File

@ -25,7 +25,7 @@ import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.AlbumRowDelegate
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.adapters.DividerBinder
import org.moire.ultrasonic.adapters.MoreButtonBinder
@ -109,7 +109,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
)
viewAdapter.register(
AlbumRowBinder(
AlbumRowDelegate(
onItemClick = ::onItemClick,
onContextMenuClick = ::onContextMenuItemSelected,
imageLoader = imageLoaderProvider.getImageLoader()
@ -280,7 +280,8 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
)
} else {
SearchFragmentDirections.searchToAlbumsList(
type = AlbumListType.BY_ARTIST,
type = AlbumListType.SORTED_BY_NAME,
byArtist = true,
id = item.id,
title = item.name,
size = 1000,

View File

@ -27,9 +27,10 @@ import java.util.Collections
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumHeader
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.AlbumRowDelegate
import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider
@ -50,6 +51,8 @@ import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities
import timber.log.Timber
/**
@ -59,10 +62,11 @@ import timber.log.Timber
* where the list can contain Albums as well. This happens especially when having ID3 tags disabled,
* or using Offline mode, both in which Indexes instead of Artists are being used.
*
* TODO: Remove more button and introduce endless scrolling
*/
@Suppress("TooManyFunctions")
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
open class TrackCollectionFragment(
initialOrder: SortOrder? = null
) : MultiListFragment<MusicDirectory.Child>(), FilterableFragment {
private var albumButtons: View? = null
private var selectButton: MaterialButton? = null
@ -73,7 +77,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private var unpinButton: MaterialButton? = null
private var downloadButton: MaterialButton? = null
private var deleteButton: MaterialButton? = null
private var moreButton: MaterialButton? = null
private var playAllButtonVisible = false
private var shareButtonVisible = false
private var playAllButton: MenuItem? = null
@ -87,6 +90,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
override val listModel: TrackCollectionModel by viewModels()
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
private var sortOrder = initialOrder
private var offset: Int? = null
/**
* The id of the main layout
*/
@ -138,7 +144,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
)
viewAdapter.register(
AlbumRowBinder(
AlbumRowDelegate(
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader()
@ -161,6 +167,25 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
) {
triggerButtonUpdate()
}
// Attach our onScrollListener
val scrollListener = object : EndlessScrollListener(viewManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
Timber.w("LOAD MORE")
// Triggered only when new data needs to be appended to the list
// Add whatever code is needed to append new items to the bottom of the list
loadMoreTracks()
}
}
listView!!.addOnScrollListener(scrollListener)
}
private fun loadMoreTracks() {
if (displayRandom() || navArgs.genreName != null) {
offset = navArgs.offset + navArgs.size
getLiveData(refresh = true, append = true)
}
}
internal open fun handleRefresh() {
@ -176,7 +201,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
unpinButton = view.findViewById(R.id.select_album_unpin)
downloadButton = view.findViewById(R.id.select_album_download)
deleteButton = view.findViewById(R.id.select_album_delete)
moreButton = view.findViewById(R.id.select_album_more)
selectButton?.setOnClickListener {
selectAllOrNone()
@ -469,24 +493,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
}
}
val listSize = navArgs.size
// Hide select button for video lists and singular selection lists
selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0
if (songCount > 0) {
if (listSize == 0 || songCount < listSize) {
moreButton!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
if (navArgs.getRandom) {
moreRandomTracks()
} else if (navArgs.genreName != null) {
moreSongsForGenre()
}
}
}
// Show a text if we have no entries
emptyView.isVisible = entryList.isEmpty()
@ -524,33 +533,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
Timber.i("Processed list")
}
private fun moreSongsForGenre() {
moreButton!!.setOnClickListener {
val action = TrackCollectionFragmentDirections.loadMoreTracks(
genreName = navArgs.genreName,
size = navArgs.size,
offset = navArgs.offset + navArgs.size
)
findNavController().navigate(action)
}
}
private fun moreRandomTracks() {
val listSize = navArgs.size
moreButton!!.setOnClickListener {
val offset = navArgs.offset + listSize
val action = TrackCollectionFragmentDirections.loadMoreTracks(
getRandom = true,
size = listSize,
offset = offset
)
findNavController().navigate(action)
}
}
internal fun getSelectedSongs(): List<Track> {
// Walk through selected set and get the Entries based on the saved ids.
return viewAdapter.getCurrentList().mapNotNull {
@ -571,7 +553,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
@Suppress("LongMethod")
override fun getLiveData(
refresh: Boolean
refresh: Boolean,
append: Boolean
): LiveData<List<MusicDirectory.Child>> {
Timber.i("Starting gathering track collection data...")
val id = navArgs.id
@ -584,11 +567,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
val shareName = navArgs.shareName
val genreName = navArgs.genreName
val getStarredTracks = navArgs.getStarred
val getStarredTracks = displayStarred()
val getVideos = navArgs.getVideos
val getRandomTracks = navArgs.getRandom
val albumListSize = navArgs.size
val albumListOffset = navArgs.offset
val getRandomTracks = displayRandom()
val size = if (navArgs.size < 0) Settings.maxSongs else navArgs.size
val offset = offset ?: navArgs.offset
val refresh2 = navArgs.refresh || refresh
listModel.viewModelScope.launch(handler) {
@ -605,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
listModel.getShare(shareId)
} else if (genreName != null) {
setTitle(genreName)
listModel.getSongsForGenre(genreName, albumListSize, albumListOffset)
listModel.getSongsForGenre(genreName, size, offset, append)
} else if (getStarredTracks) {
setTitle(getString(R.string.main_songs_starred))
listModel.getStarred()
@ -614,7 +597,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
listModel.getVideos(refresh2)
} else if (getRandomTracks) {
setTitle(R.string.main_songs_random)
listModel.getRandom(albumListSize)
listModel.getRandom(size, append)
} else {
setTitle(name)
if (ActiveServerProvider.isID3Enabled()) {
@ -633,6 +616,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
return listModel.currentList
}
private fun displayStarred() = (sortOrder == SortOrder.STARRED) || navArgs.getStarred
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
@Suppress("LongMethod")
override fun onContextMenuItemSelected(
menuItem: MenuItem,
@ -703,7 +690,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
override fun onItemClick(item: MusicDirectory.Child) {
when {
item.isDirectory -> {
val action = TrackCollectionFragmentDirections.loadMoreTracks(
val action = NavigationGraphDirections.toTrackCollection(
id = item.id,
isAlbum = true,
name = item.title,
@ -719,4 +706,24 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
}
}
}
override fun setOrderType(newOrder: SortOrder) {
sortOrder = newOrder
getLiveData(true)
}
override var viewCapabilities: ViewCapabilities = ViewCapabilities(
supportsGrid = false,
supportedSortOrders = getListOfSortOrders()
)
private fun getListOfSortOrders(): List<SortOrder> {
val isOnline = !isOffline()
val supported = mutableListOf(SortOrder.RANDOM)
if (isOnline) {
supported.add(SortOrder.STARRED)
}
return supported
}
}

View File

@ -21,6 +21,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.ListView
@ -30,6 +31,7 @@ import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
@ -45,7 +47,6 @@ import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.LoadingTask
import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.view.PlaylistAdapter
/**
* Displays the playlists stored on the server
@ -56,11 +57,14 @@ class PlaylistsFragment : Fragment() {
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
private var playlistsListView: ListView? = null
private var emptyTextView: View? = null
private var playlistAdapter: PlaylistAdapter? = null
private var playlistAdapter: ArrayAdapter<Playlist>? = null
private val downloadHandler = inject<DownloadHandler>(
DownloadHandler::class.java
)
private var cancellationToken: CancellationToken? = null
override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context)
super.onCreate(savedInstanceState)
@ -83,7 +87,7 @@ class PlaylistsFragment : Fragment() {
playlistsListView!!.setOnItemClickListener { parent, _, position, _ ->
val (id1, name) = parent.getItemAtPosition(position) as Playlist
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
id = id1,
playlistId = id1,
name = name,
@ -116,7 +120,7 @@ class PlaylistsFragment : Fragment() {
override fun done(result: List<Playlist>) {
playlistsListView!!.adapter =
PlaylistAdapter(context, result).also { playlistAdapter = it }
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
}
}
@ -183,7 +187,7 @@ class PlaylistsFragment : Fragment() {
)
}
R.id.playlist_menu_play_now -> {
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
playlistId = playlist.id,
playlistName = playlist.name,
autoPlay = true
@ -191,7 +195,7 @@ class PlaylistsFragment : Fragment() {
findNavController().navigate(action)
}
R.id.playlist_menu_play_shuffled -> {
val action = PlaylistsFragmentDirections.playlistsToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
playlistId = playlist.id,
playlistName = playlist.name,
autoPlay = true,

View File

@ -7,15 +7,16 @@
package org.moire.ultrasonic.fragment.legacy
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
@ -24,18 +25,20 @@ import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.view.PodcastsChannelsAdapter
/**
* Displays the podcasts available on the server
*
* TODO: This file has been converted from Java, but not modernized yet.
* TODO: Use Coroutines
*/
class PodcastFragment : Fragment() {
private var emptyTextView: View? = null
var channelItemsListView: ListView? = null
private var cancellationToken: CancellationToken? = null
private var swipeRefresh: SwipeRefreshLayout? = null
override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context)
super.onCreate(savedInstanceState)
@ -53,19 +56,19 @@ class PodcastFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken()
swipeRefresh = view.findViewById(R.id.podcasts_refresh)
swipeRefresh!!.setOnRefreshListener { load(view.context, true) }
swipeRefresh!!.setOnRefreshListener { load(true) }
setTitle(this, R.string.podcasts_label)
emptyTextView = view.findViewById(R.id.select_podcasts_empty)
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list)
channelItemsListView!!.setOnItemClickListener { parent, _, position, _ ->
val (id) = parent.getItemAtPosition(position) as PodcastsChannel
val action = PodcastFragmentDirections.podcastToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
podcastChannelId = id
)
findNavController().navigate(action)
}
load(view.context, false)
load(false)
}
override fun onDestroyView() {
@ -73,7 +76,7 @@ class PodcastFragment : Fragment() {
super.onDestroyView()
}
private fun load(context: Context, refresh: Boolean) {
private fun load(refresh: Boolean) {
val task: BackgroundTask<List<PodcastsChannel>> =
object : FragmentBackgroundTask<List<PodcastsChannel>>(
activity, true, swipeRefresh, cancellationToken
@ -85,7 +88,8 @@ class PodcastFragment : Fragment() {
}
override fun done(result: List<PodcastsChannel>) {
channelItemsListView!!.adapter = PodcastsChannelsAdapter(context, result)
channelItemsListView!!.adapter =
ArrayAdapter(requireContext(), R.layout.list_item_generic, result)
emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
}
}

View File

@ -17,6 +17,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
@ -58,13 +59,14 @@ class SelectGenreFragment : Fragment() {
refreshGenreListView = view.findViewById(R.id.select_genre_refresh)
genreListView = view.findViewById(R.id.select_genre_list)
refreshGenreListView!!.setOnRefreshListener { load(true) }
genreListView!!.setOnItemClickListener { parent: AdapterView<*>,
_: View?,
position: Int,
_: Long ->
val genre = parent.getItemAtPosition(position) as Genre
val action = SelectGenreFragmentDirections.selectGenreToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
genreName = genre.name,
size = maxSongs,
offset = 0
@ -82,6 +84,7 @@ class SelectGenreFragment : Fragment() {
super.onDestroyView()
}
// TODO: Migrate to Coroutines
private fun load(refresh: Boolean) {
val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>(
activity, true, refreshGenreListView, cancellationToken

View File

@ -29,6 +29,7 @@ import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale
import org.koin.java.KoinJavaComponent
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.domain.Share
@ -82,7 +83,7 @@ class SharesFragment : Fragment() {
AdapterView.OnItemClickListener { parent, _, position, _ ->
val share = parent.getItemAtPosition(position) as Share
val action = SharesFragmentDirections.sharesToTrackCollection(
val action = NavigationGraphDirections.toTrackCollection(
shareId = share.id,
shareName = share.name
)

View File

@ -96,6 +96,25 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
}
}
fun sortListByOrder(order: AlbumListType) {
val newList = when (order) {
AlbumListType.BY_YEAR -> {
list.value?.sortedBy {
it.year
}
}
else -> {
list.value?.sortedBy {
it.name
}
}
}
newList?.let {
list.postValue(it)
}
}
override fun showSelectFolderHeader(): Boolean {
val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) ||
(lastType == AlbumListType.SORTED_BY_ARTIST)

View File

@ -56,11 +56,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
suspend fun getSongsForGenre(genre: String, count: Int, offset: Int) {
suspend fun getSongsForGenre(genre: String, count: Int, offset: Int, append: Boolean) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getSongsByGenre(genre, count, offset)
updateList(musicDirectory)
updateList(musicDirectory, append)
}
}
@ -94,7 +94,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
suspend fun getRandom(size: Int) {
suspend fun getRandom(size: Int, append: Boolean) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
@ -102,7 +102,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
currentListIsSortable = false
updateList(musicDirectory)
updateList(musicDirectory, append)
}
}
@ -158,8 +158,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
private fun updateList(root: MusicDirectory) {
currentList.postValue(root.getChildren())
private fun updateList(root: MusicDirectory, append: Boolean = false) {
val newList = if (append) {
currentList.value!! + root.getChildren()
} else {
root.getChildren()
}
currentList.postValue(newList)
}
@Synchronized

View File

@ -0,0 +1,21 @@
/*
* ViewType.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
enum class LayoutType(val value: Int) {
LIST(0),
COVER(1);
companion object {
private val map = values().associateBy { it.value }
fun from(value: Int): LayoutType {
// Default to list if unmappable
return map[value] ?: LIST
}
}
}

View File

@ -10,7 +10,6 @@ package org.moire.ultrasonic.util
import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import androidx.media.utils.MediaConstants
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
@ -19,9 +18,15 @@ import java.text.DateFormat
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.provider.AlbumArtContentProvider
// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependecy
// for a single string value
private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE =
"android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"
object MediaItemConverter {
private const val CACHE_SIZE = 250
private const val CACHE_EXPIRY_MINUTES = 10L
val mediaItemCache: LRUCache<String, TimeLimitedCache<MediaItem>> = LRUCache(CACHE_SIZE)
val trackCache: LRUCache<String, TimeLimitedCache<Track>> = LRUCache(CACHE_SIZE)
@ -233,7 +238,7 @@ fun buildMediaItem(
metadataBuilder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
group
)
}

View File

@ -19,6 +19,10 @@ import org.moire.ultrasonic.app.UApp
*/
object Settings {
@JvmStatic
val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext())
@JvmStatic
var theme by StringSetting(
getKey(R.string.setting_key_theme),
@ -241,10 +245,6 @@ object Settings {
@JvmStatic
var debugLogToFile by BooleanSetting(getKey(R.string.setting_key_debug_log_to_file), false)
@JvmStatic
val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext())
@JvmStatic
val overrideLanguage by StringSetting(getKey(R.string.setting_key_override_language), "")
@ -267,11 +267,11 @@ object Settings {
false
)
// TODO: Remove in December 2022
fun migrateFeatureStorage() {
val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE)
useFiveStarRating = sp.getBoolean("FIVE_STAR_RATING", false)
}
@JvmStatic
var lastViewType by IntSetting(
getKey(R.string.setting_key_last_view_type),
0
)
fun hasKey(key: String): Boolean {
return preferences.contains(key)

View File

@ -0,0 +1,219 @@
/*
* FilterButtonBar.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.view
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ArrayAdapter
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout
import org.moire.ultrasonic.R
import org.moire.ultrasonic.util.LayoutType
import timber.log.Timber
/**
* This Widget provides a FilterBar, which allow to set the layout options
* or sort order of a linked fragment
*/
class FilterButtonBar : ConstraintLayout {
private var adapter: ArrayAdapter<TranslatedSortOrder>? = null
private var orderChangedListener: ((SortOrder) -> Unit)? = null
private var layoutTypeChangedListener: ((LayoutType) -> Unit)? = null
private var layoutType: LayoutType = LayoutType.LIST
private var viewTypeToggle: Chip? = null
private var sortOrderMenu: TextInputLayout? = null
private var sortOrderOptions: AppCompatAutoCompleteTextView? = null
constructor(context: Context) : super(context) {
setup()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.filter_button_bar, this)
setup()
}
/**
* Set which capabilities are supported by
* the linked fragment and configure the UI accordingly
*
* @param caps
*/
fun configureWithCapabilities(caps: ViewCapabilities) {
viewTypeToggle!!.isVisible = caps.supportsGrid
sortOrderMenu!!.isVisible = caps.supportedSortOrders.isNotEmpty()
if (caps.supportedSortOrders.isNotEmpty()) {
Timber.i("Calculating order")
val translatedOrders = caps.supportedSortOrders.map {
TranslatedSortOrder(
sortOrder = it,
string = context.getString(getStringForSortOrder(it))
)
}
// Fill the visible with the first available option
sortOrderOptions!!.setText(getStringForSortOrder(translatedOrders.first().sortOrder))
adapter!!.clear()
// Next line addresses a bug in Android components:
// https://github.com/material-components/material-components-android/issues/1464
adapter!!.filter.filter("")
adapter!!.addAll(translatedOrders)
}
}
/**
* This listener is called when the user has changed the layout type.
* Register a callback from the linked fragment here, to trigger a relayout
*
* @param callback
* @receiver
*/
fun setOnLayoutTypeChangedListener(callback: (LayoutType) -> Unit) {
layoutTypeChangedListener = callback
}
/**
* This listener is called when the user has changed the sort order.
* Register a callback from the linked fragment here, to trigger a resort
*
* @param callback
* @receiver
*/
fun setOnOrderChangedListener(callback: (SortOrder) -> Unit) {
orderChangedListener = callback
}
/**
* Setup the necessary bindings
*/
fun setup() {
// Link layout toggle Chip
viewTypeToggle = findViewById(R.id.chip_view_toggle)
sortOrderMenu = findViewById(R.id.sort_order_menu)
sortOrderOptions = findViewById(R.id.sort_order_menu_options)
viewTypeToggle!!.setOnClickListener {
val newType = setLayoutType()
layoutTypeChangedListener?.let { it(newType) }
}
@SuppressLint("PrivateResource")
adapter = ArrayAdapter<TranslatedSortOrder>(
context,
com.google.android.material.R.layout.mtrl_auto_complete_simple_item,
mutableListOf<TranslatedSortOrder>()
)
sortOrderOptions!!.setAdapter(adapter)
sortOrderOptions!!.setOnItemClickListener { _, _, position, _ ->
val item = adapter!!.getItem(position)
item?.let { setOrderType(it.sortOrder) }
}
}
/**
* This function can be called from externally to set the layout type programmatically
*
* @param newType
*/
fun setLayoutType(newType: LayoutType = toggleLayoutType()): LayoutType {
layoutType = newType
updateToggleChipState(newType)
return newType
}
private fun toggleLayoutType(): LayoutType {
return when (layoutType) {
LayoutType.LIST -> {
LayoutType.COVER
}
LayoutType.COVER -> {
LayoutType.LIST
}
}
}
/**
* This function can be called from externally to set the layout type programmatically
*
* @param order
*/
fun setOrderType(order: SortOrder) {
orderChangedListener?.let { it(order) }
}
private fun updateToggleChipState(layoutType: LayoutType) {
when (layoutType) {
LayoutType.COVER -> {
viewTypeToggle!!.chipIcon = AppCompatResources.getDrawable(
context,
R.drawable.ic_baseline_view_grid
)
viewTypeToggle!!.text = context.getString(R.string.grid_view)
}
LayoutType.LIST -> {
viewTypeToggle!!.chipIcon = AppCompatResources.getDrawable(
context,
R.drawable.ic_baseline_view_list
)
viewTypeToggle!!.text = context.getString(R.string.list_view)
}
}
}
private fun getStringForSortOrder(sortOrder: SortOrder): Int {
return when (sortOrder) {
SortOrder.RANDOM -> R.string.main_albums_random
SortOrder.NEWEST -> R.string.main_albums_newest
SortOrder.HIGHEST -> R.string.main_albums_highest
SortOrder.FREQUENT -> R.string.main_albums_frequent
SortOrder.RECENT -> R.string.main_albums_recent
SortOrder.BY_NAME -> R.string.main_albums_alphaByName
SortOrder.BY_ARTIST -> R.string.main_albums_alphaByArtist
SortOrder.STARRED -> R.string.main_albums_starred
SortOrder.BY_YEAR -> R.string.main_albums_by_year
}
}
}
/**
* Wrapper which contains one sort order and the translated UI string for it
*
* @property sortOrder
* @property string
*/
data class TranslatedSortOrder(
val sortOrder: SortOrder,
val string: String
) {
override fun toString() = string
}
/**
* ViewCapabilities defines which layout and sort orders a view can support
*
* @property supportsGrid
* @property supportedSortOrders
*/
data class ViewCapabilities(
val supportsGrid: Boolean = false,
val supportedSortOrders: List<SortOrder>
)
val EMPTY_CAPABILITIES = ViewCapabilities(
supportsGrid = false,
supportedSortOrders = listOf()
)

View File

@ -0,0 +1,23 @@
/*
* kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.view
/*
* This enum is very similar to AlbumListType, but not completely the same.
*/
enum class SortOrder(val typeName: String) {
RANDOM("random"),
NEWEST("newest"),
HIGHEST("highest"),
FREQUENT("frequent"),
RECENT("recent"),
BY_NAME("alphabeticalByName"),
BY_ARTIST("alphabeticalByArtist"),
STARRED("starred"),
BY_YEAR("byYear");
}

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,11h5L9,5L4,5v6zM4,18h5v-6L4,12v6zM10,18h5v-6h-5v6zM16,18h5v-6h-5v6zM10,11h5L15,5h-5v6zM16,5v6h5L21,5h-5z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,14h4v-4L4,10v4zM4,19h4v-4L4,15v4zM4,9h4L8,5L4,5v4zM9,14h12v-4L9,10v4zM9,19h12v-4L9,15v4zM9,5v4h12L21,5L9,5z" />
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

View File

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12 L24,0v24z"
android:strokeWidth="0.396875"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z"/>
</vector>

View File

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

View File

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFF"
android:pathData="M 16,11 14.58,12.42 11,8.83 V 18 h 10 v 2 H 9 V 8.83 L 5.42,12.42 4,11 10,5 Z"/>
</vector>

View File

@ -5,9 +5,9 @@
android:viewportHeight="100">
<path
android:pathData="M0,0h100v100h-100z"
android:fillColor="#1a1a1a"/>
android:fillColor="?attr/colorSecondary"/>
<path
android:pathData="M42.415,84.787C49.463,84.53 55.166,77.789 54.75,70.704 55.614,58.35 56.479,45.979 57.342,33.633c0.513,-0.518 1.407,0.72 1.903,0.815 5.393,3.785 9.987,9.62 9.845,16.528 -0.003,5.402 -1.991,10.554 -4.162,15.413C71.552,59.26 74.281,48.374 70.666,39.149 68.858,33.816 64.197,30.278 61.795,25.279A26.452,26.452 0,0 1,58.5 14.397c-0.343,-0.816 -1.323,-0.945 -2.094,-0.999l-0.017,-0.001c-2.434,-0.17 -2.216,1.472 -2.331,3.117l-3.274,46.814c0,0 -1.255,-0.207 -2.188,-0.272 -7.098,-0.965 -15.202,3.666 -16.437,11.095 -1.246,5.877 4.638,11.171 10.257,10.635z"
android:strokeWidth="0.85"
android:fillColor="#ffffff"/>
android:fillColor="?attr/colorOnSecondary"/>
</vector>

View File

@ -1,53 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="horizontal">
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="horizontal">
<ImageView
a:id="@+id/chat_avatar"
a:layout_width="50dip"
a:layout_height="50dip"
a:id="@+id/chat_avatar"/>
a:contentDescription="@string/chat.user_avatar" />
<LinearLayout
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="vertical"
a:layout_toEndOf="@id/chat_avatar"
a:gravity="center_vertical">
a:gravity="center_vertical"
a:orientation="vertical">
<TextView
a:id="@+id/chat_username"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="left"
a:layout_marginStart="6dip"
a:layout_marginEnd="6dip"
a:ellipsize="marquee"
a:gravity="center_vertical|left"
a:singleLine="true"
a:textIsSelectable="true"
a:textAppearance="?android:attr/textAppearanceMedium"
a:textStyle="bold"
a:layout_gravity="left"
a:gravity="center_vertical|left"/>
a:textIsSelectable="true"
a:textStyle="bold" />
<LinearLayout
a:id="@+id/chat_message_layout"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="left"
a:layout_marginTop="2dip"
a:orientation="horizontal"
a:gravity="center_vertical|left"
a:layout_gravity="left">
a:orientation="horizontal">
<TextView
a:id="@+id/chat_time"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginStart="6dip"
a:gravity="left"
a:singleLine="true"
a:textIsSelectable="true"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="left"/>
a:textIsSelectable="true" />
<TextView
a:id="@+id/chat_message"
@ -55,12 +56,12 @@
a:layout_height="wrap_content"
a:layout_marginStart="6dip"
a:layout_marginEnd="6dip"
a:textIsSelectable="true"
a:autoLink="all"
a:gravity="left"
a:linksClickable="true"
a:singleLine="false"
a:autoLink="all"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="left"/>
a:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -1,71 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="horizontal">
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="horizontal">
<LinearLayout
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="vertical"
a:layout_gravity="right"
a:layout_alignParentEnd="false"
a:layout_gravity="right"
a:layout_toStartOf="@id/chat_avatar"
a:gravity="center_vertical|right">
a:gravity="center_vertical|right"
a:orientation="vertical">
<TextView
a:id="@+id/chat_username"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginEnd="6dip"
a:gravity="center_vertical|right"
a:layout_gravity="right"
a:layout_marginEnd="6dip"
a:ellipsize="marquee"
a:gravity="center_vertical|right"
a:singleLine="true"
a:textIsSelectable="true"
a:textAppearance="?android:attr/textAppearanceMedium"
a:textStyle="bold"/>
a:textIsSelectable="true"
a:textStyle="bold" />
<LinearLayout
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginTop="2dip"
a:orientation="horizontal"
a:layout_gravity="right|end"
a:gravity="center_vertical|right">
a:layout_marginTop="2dip"
a:gravity="center_vertical|right"
a:orientation="horizontal">
<TextView
a:id="@+id/chat_time"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="right"
a:layout_marginStart="6dip"
a:singleLine="true"
a:gravity="center_vertical|right"
a:textIsSelectable="true"
a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceMedium"
a:layout_gravity="right"/>
a:textIsSelectable="true" />
<TextView
a:id="@+id/chat_message"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="right"
a:layout_marginStart="6dip"
a:layout_marginEnd="6dip"
a:autoLink="all"
a:gravity="center_vertical|right"
a:linksClickable="true"
a:singleLine="false"
a:autoLink="all"
a:textIsSelectable="true"
a:textAppearance="?android:attr/textAppearanceMedium"
a:gravity="center_vertical|right"
a:layout_gravity="right"/>
a:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>
<ImageView
a:id="@+id/chat_avatar"
a:layout_width="50dip"
a:layout_height="50dip"
a:id="@+id/chat_avatar"
a:layout_alignParentEnd="true"
a:layout_toStartOf="@id/chat_avatar"/>
a:layout_toStartOf="@id/chat_avatar"
a:contentDescription="@string/chat.user_avatar" />
</RelativeLayout>

View File

@ -1,46 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ equalizer_bar.xml
~ Copyright (C) 2009-2022 Ultrasonic developers
~
~ Distributed under terms of the GNU GPLv3 license.
-->
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="vertical"
a:layout_width="fill_parent"
a:layout_height="wrap_content">
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:orientation="vertical">
<TextView
a:id="@+id/equalizer_frequency"
a:textSize="12sp"
a:textColor="#c0c0c0"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginTop="8dp"
a:layout_alignParentLeft="true"
/>
a:id="@+id/equalizer_frequency"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_alignParentLeft="true"
a:layout_marginTop="8dp"
a:textColor="#c0c0c0"
a:textSize="12sp" />
<TextView
a:id="@+id/equalizer_level"
a:text="0 dB"
a:textSize="12sp"
a:textColor="#c0c0c0"
a:gravity="right"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginTop="8dp"
a:layout_alignParentRight="true"
a:layout_toEndOf="@+id/equalizer_frequency"
/>
a:id="@+id/equalizer_level"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_alignParentRight="true"
a:layout_marginTop="8dp"
a:layout_toEndOf="@+id/equalizer_frequency"
a:gravity="right"
a:textColor="#c0c0c0"
a:textSize="12sp"
tools:text="0 dB" />
<SeekBar
a:id="@+id/equalizer_bar"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_below="@+id/equalizer_frequency"
/>
a:id="@+id/equalizer_bar"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_below="@+id/equalizer_frequency" />
</RelativeLayout>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/filter_layout"
a:layout_width="match_parent"
a:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/sort_order_menu"
app:layout_constraintTop_toTopOf="@+id/sort_order_menu"
tools:layout_height="match_parent">
<com.google.android.material.chip.Chip
a:id="@+id/chip_view_toggle"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_marginStart="8dp"
a:text="@string/list_view"
app:chipIcon="@drawable/ic_baseline_view_list"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sort_order_menu"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
a:id="@+id/sort_order_menu"
style="@style/Widget.ChipDropdown"
a:layout_width="wrap_content"
a:layout_height="48dp"
a:layout_marginStart="8dp"
a:layout_marginTop="4dp"
a:layout_marginEnd="8dp"
a:background="@null"
a:baselineAligned="false"
a:ellipsize="marquee"
a:text="@string/main.albums_newest"
a:textAppearance="@style/TextAppearance.Material3.LabelLarge"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toEndOf="@+id/chip_view_toggle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
a:id="@+id/sort_order_menu_options"
a:layout_width="wrap_content"
a:layout_height="32dp"
a:ellipsize="end"
a:focusable="false"
a:inputType="none"
a:paddingStart="16dp"
a:paddingTop="0dp"
a:paddingEnd="16dp"
a:paddingBottom="0dp"
a:singleLine="true"
a:text="@string/main.albums_newest"
a:textAppearance="@style/TextAppearance.Material3.LabelLarge"
a:textColor="?attr/colorOnSurfaceVariant"
tools:ignore="LabelFor,TouchTargetSizeCheck,TouchTargetSizeCheck" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/card"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:layout_margin="8dp"
style="?attr/materialCardViewFilledStyle">
<androidx.constraintlayout.widget.ConstraintLayout
a:id="@+id/containing_layout"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:background="?android:attr/selectableItemBackground"
a:clickable="true"
a:focusable="true">
<ImageView
a:id="@+id/cover_art"
a:layout_width="match_parent"
a:layout_height="0dp"
a:layout_gravity="center_horizontal|center_vertical"
a:adjustViewBounds="true"
a:scaleType="fitCenter"
a:src="@drawable/unknown_album"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
a:contentDescription="@string/albumArt" />
<LinearLayout
a:id="@+id/row_album_details"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:gravity="center_vertical"
a:orientation="vertical"
a:padding="11dp"
a:paddingTop="0dp"
app:layout_constraintEnd_toEndOf="@+id/cover_art"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cover_art">
<TextView
a:id="@+id/album_title"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:ellipsize="marquee"
a:singleLine="true"
a:textAppearance="@style/TextAppearance.Material3.LabelLarge"
tools:text="Title" />
<TextView
a:id="@+id/album_artist"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:singleLine="true"
a:textAppearance="@style/TextAppearance.Material3.LabelSmall"
tools:text="Artist" />
</LinearLayout>
<ImageView
a:id="@+id/album_star"
a:layout_width="38dp"
a:layout_height="38dp"
a:background="@android:color/transparent"
a:contentDescription="@string/download.menu_star"
a:gravity="center_horizontal"
a:padding="4dp"
a:src="@drawable/ic_star_hollow"
app:layout_constraintBottom_toBottomOf="@+id/cover_art"
app:layout_constraintEnd_toEndOf="@+id/cover_art" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -10,7 +10,7 @@
a:focusable="true">
<com.google.android.material.imageview.ShapeableImageView
a:id="@+id/coverart"
a:id="@+id/cover_art"
a:layout_width="64dp"
a:layout_height="64dp"
a:layout_gravity="center_horizontal|center_vertical"
@ -20,7 +20,7 @@
a:src="@drawable/unknown_album"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/largeRoundedImageView" />
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.SmallComponent" />
<LinearLayout
a:id="@+id/row_album_details"
@ -35,8 +35,8 @@
a:paddingEnd="3dip"
a:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintEnd_toStartOf="@+id/album_star"
app:layout_constraintLeft_toRightOf="@+id/coverart"
app:layout_constraintStart_toEndOf="@+id/coverart"
app:layout_constraintLeft_toRightOf="@+id/cover_art"
app:layout_constraintStart_toEndOf="@+id/cover_art"
app:layout_constraintTop_toTopOf="parent">
<TextView
@ -61,6 +61,7 @@
<ImageView
a:id="@+id/album_star"
a:layout_width="38dp"
a:padding="4dp"
a:layout_height="38dp"
a:layout_marginStart="16dp"
a:layout_marginTop="16dp"

View File

@ -23,7 +23,7 @@
tools:text="A" />
<com.google.android.material.imageview.ShapeableImageView
a:id="@+id/coverart"
a:id="@+id/cover_art"
a:layout_width="40dp"
a:layout_height="40dp"
a:layout_gravity="center_horizontal|center_vertical"
@ -33,14 +33,14 @@
a:layout_toEndOf="@+id/row_section"
a:scaleType="fitCenter"
a:src="@drawable/ic_contact_picture"
app:shapeAppearanceOverlay="@style/roundedImageView" />
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.MediumComponent" />
<TextView
a:id="@+id/row_artist_name"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:layout_marginEnd="12dp"
a:layout_toEndOf="@+id/coverart"
a:layout_toEndOf="@+id/cover_art"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:minHeight="56dip"

View File

@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:id="@android:id/text1"
a:drawablePadding="6dip"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceMedium"
a:layout_height="56dp"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:paddingStart="3dip"
a:paddingEnd="3dip"
a:minHeight="50dip"/>
a:paddingStart="16dip"
a:paddingTop="8dp"
a:paddingEnd="24dip"
a:singleLine="true"
a:ellipsize="marquee"
a:paddingBottom="8dp"
a:textAppearance="@style/TextAppearance.Material3.BodyLarge"
tools:text="Item text" />

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical">
<org.moire.ultrasonic.view.FilterButtonBar
a:id="@+id/filter_button_bar"
a:layout_width="match_parent"
a:layout_height="wrap_content" />
<include layout="@layout/list_parts_empty_view" />
<include layout="@layout/list_parts_recycler" />
</LinearLayout>

View File

@ -1,205 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/main_list">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/main_music"
android:theme="@style/Ultrasonic.AllCapsLabel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:text="@string/main.music"
/>
<TextView
android:id="@+id/main_artists_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.artists_title"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_title"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_genres_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.genres_title"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_songs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:text="@string/main.songs_title"
android:theme="@style/Ultrasonic.AllCapsLabel" />
<TextView
android:id="@+id/main_songs_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.songs_random"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_songs_starred"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.songs_starred"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums"
android:theme="@style/Ultrasonic.AllCapsLabel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:text="@string/main.albums_title" />
<TextView
android:id="@+id/main_albums_newest"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_newest"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_recent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_recent"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_frequent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_frequent"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_highest"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_highest"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_random"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_random"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_starred"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_starred"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_alphaByName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_alphaByName"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_albums_alphaByArtist"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.albums_alphaByArtist"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/main_videos_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:text="@string/main.videos"
android:theme="@style/Ultrasonic.AllCapsLabel" />
<TextView
android:id="@+id/main_videos"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="40dip"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:text="@string/main.videos"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -46,5 +46,5 @@
a:layout_gravity="start"
a:fitsSystemWindows="true"
app:headerLayout="@layout/navigation_header"
app:menu="@menu/navigation"/>
app:menu="@menu/navigation_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:orientation="vertical"
@ -33,7 +34,8 @@
a:layout_alignParentRight="true"
a:layout_marginEnd="12dip"
a:text="@string/util.no_time"
a:textAppearance="?android:attr/textAppearanceSmall" />
a:textAppearance="?android:attr/textAppearanceSmall"
tools:ignore="RelativeOverlap" />
</RelativeLayout>

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/playlist_name"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:gravity="left|center_vertical"
android:paddingStart="6dip"
android:paddingEnd="6dip"
android:minHeight="50dip"/>
</LinearLayout>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/podcast_channel_item"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:visibility="visible" />

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="match_parent"
a:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical">
<com.google.android.material.tabs.TabLayout
a:id="@+id/tab_layout"
app:tabMode="scrollable"
a:layout_width="match_parent"
a:layout_height="wrap_content" />
<org.moire.ultrasonic.view.FilterButtonBar
a:id="@+id/filter_button_bar"
a:layout_width="match_parent"
a:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
a:id="@+id/pager"
a:layout_width="match_parent"
a:layout_height="match_parent" />
</LinearLayout>

View File

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical" >
a:orientation="vertical">
<TextView
a:id="@+id/select_genre_empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:padding="16dip"
a:text="@string/select_genre.empty"
a:visibility="gone" />
a:textAppearance="@style/TextAppearance.Material3.BodyLarge"
a:visibility="gone"
tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/select_genre_refresh"
android:layout_width="fill_parent"
android:layout_height="0dip"

View File

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical" >
a:orientation="vertical">
<TextView
a:id="@+id/select_playlist_empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:padding="16dip"
a:text="@string/select_playlist.empty"
a:visibility="gone" />
a:textAppearance="@style/TextAppearance.Material3.BodyLarge"
a:visibility="gone"
tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/select_playlist_refresh"
android:layout_width="fill_parent"
android:layout_height="0dip"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:orientation="vertical">
@ -8,13 +9,14 @@
a:id="@+id/select_share_empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"
a:padding="16dip"
a:text="@string/select_share.empty"
a:visibility="gone" />
a:textAppearance="@style/TextAppearance.Material3.BodyLarge"
a:visibility="gone"
tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/select_share_refresh"
android:layout_width="fill_parent"
android:layout_height="0dip"

View File

@ -1,13 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:paddingStart="6dip"
a:paddingEnd="6dip"
a:minHeight="44dip"
a:orientation="vertical">
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="0dp"
a:layout_height="56dp"
a:layout_gravity="center_vertical"
a:layout_weight="1"
a:minHeight="44dip"
a:orientation="vertical"
a:paddingStart="16dip"
a:paddingEnd="16dip"
tools:layout_width="match_parent">
<LinearLayout
a:layout_width="fill_parent"
@ -20,7 +22,8 @@
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="left|center_vertical"
a:textAppearance="?android:attr/textAppearanceMedium"/>
a:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Url" />
</LinearLayout>
@ -38,9 +41,9 @@
a:layout_gravity="left|center_vertical"
a:layout_weight="1"
a:ellipsize="middle"
a:paddingStart="4dip"
a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceSmall"/>
a:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Description" />
</LinearLayout>

View File

@ -1,50 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/top"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/bottom">
android:layout_height="match_parent">
<CheckBox
android:id="@+id/timeSpanDisableCheckBox"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/timeSpanDisableCheckBox"
android:text="@string/time_span_disable"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"/>
android:layout_alignParentTop="true"
android:text="@string/time_span_disable" />
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bottom"
android:layout_below="@+id/top">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/timeSpanEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:id="@+id/timeSpanEditText"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:gravity="left"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="false"
android:ems="5"
android:layout_alignParentEnd="false"/>
android:gravity="left"
android:inputType="numberSigned" />
<Spinner
android:id="@+id/timeSpanSpinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/timeSpanSpinner"
android:layout_alignBottom="@+id/timeSpanEditText"
android:layout_toEndOf="@+id/timeSpanEditText"/>
android:layout_toEndOf="@+id/timeSpanEditText" />
</RelativeLayout>
</LinearLayout>

View File

@ -1,70 +1,76 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/get_playlist_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textSize="20sp"
android:text="@string/common.name" />
<EditText
android:id="@+id/get_playlist_name"
android:inputType="text"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="@string/common.name" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/get_playlist_comment_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textSize="20sp"
android:text="@string/common.comment" />
<EditText
android:id="@+id/get_playlist_comment"
android:inputType="text"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="@string/common.comment" />
</LinearLayout>
<TextView
android:id="@+id/get_playlist_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:labelFor="@+id/get_playlist_name"
android:text="@string/common.name"
android:textSize="20sp" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/get_playlist_name"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text" />
</LinearLayout>
<TextView
android:id="@+id/get_playlist_public_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textSize="20sp"
android:text="@string/common.public" />
<CheckBox
android:id="@+id/get_playlist_public"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:checked="false"/>
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/get_playlist_comment_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:labelFor="@+id/get_playlist_comment"
android:text="@string/common.comment"
android:textSize="20sp" />
<EditText
android:id="@+id/get_playlist_comment"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:importantForAutofill="no"
android:inputType="text" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/get_playlist_public_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:labelFor="@+id/get_playlist_public"
android:text="@string/common.public"
android:textSize="20sp" />
<CheckBox
android:id="@+id/get_playlist_public"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:checked="false" />
</LinearLayout>
</LinearLayout>

View File

@ -4,14 +4,10 @@
a:checkableBehavior="none"
a:enabled="true"
a:visible="true">
<item
a:id="@+id/mainFragment"
a:checkable="true"
a:icon="@drawable/ic_menu_home"
a:title="@string/button_bar.home" />
<item
a:id="@+id/mediaLibraryFragment"
a:checkable="true"
a:icon="@drawable/ic_menu_browse"
a:title="@string/button_bar.browse" />
<item
@ -19,11 +15,6 @@
a:checkable="true"
a:icon="@drawable/ic_menu_search"
a:title="@string/button_bar.search" />
<item
a:id="@+id/playlistsFragment"
a:checkable="true"
a:icon="@drawable/ic_menu_playlists"
a:title="@string/button_bar.playlists" />
<item
a:id="@+id/downloadsFragment"
a:checkable="true"
@ -44,16 +35,21 @@
a:checkable="true"
a:icon="@drawable/ic_menu_chat"
a:title="@string/button_bar.chat" />
<item
a:id="@+id/playerFragment"
a:checkable="true"
a:icon="@drawable/media_start"
a:title="@string/button_bar.now_playing" />
<item
a:id="@+id/podcastFragment"
a:checkable="true"
a:icon="@drawable/ic_menu_podcasts"
a:title="@string/button_bar.podcasts" />
<item
a:id="@+id/trackCollectionFragment"
a:checkable="true"
a:icon="@drawable/ic_baseline_videos"
a:title="@string/main.videos" />
<item
a:id="@+id/playerFragment"
a:checkable="true"
a:icon="@drawable/media_start"
a:title="@string/button_bar.now_playing" />
</group>
<item a:title="@string/menu.common">
<menu>

View File

@ -1,90 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/navigation_graph"
app:startDestination="@id/mainFragment">
<action android:id="@+id/toTrackCollection"
app:destination="@id/trackCollectionFragment" />
<action android:id="@+id/toBookmarks"
app:destination="@id/bookmarksFragment" />
<action android:id="@+id/toMediaLibrary"
app:destination="@id/mediaLibraryFragment" />
<action
android:id="@+id/toTrackCollection"
app:destination="@id/trackCollectionFragment" />
<action
android:id="@+id/toBookmarks"
app:destination="@id/bookmarksFragment" />
<action
android:id="@+id/toMediaLibrary"
app:destination="@id/mediaLibraryFragment" />
<action
android:id="@+id/toAlbumList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/toArtistList"
app:destination="@id/artistListFragment" />
<action
android:id="@+id/toGenreList"
app:destination="@id/selectGenreFragment" />
<action
android:id="@+id/toSearchFragment"
app:destination="@id/searchFragment" />
<action
android:id="@+id/toPlaylistFragment"
app:destination="@id/playlistsFragment" />
<fragment
android:id="@+id/mainFragment"
android:name="org.moire.ultrasonic.fragment.MainFragment"
android:label="@string/common.appname" >
<action
android:id="@+id/mainToTrackCollection"
app:destination="@id/trackCollectionFragment" />
<action
android:id="@+id/mainToAlbumList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/mainToArtistList"
app:destination="@id/artistListFragment" />
<action
android:id="@+id/mainToSelectGenre"
app:destination="@id/selectGenreFragment" />
<action
android:id="@+id/toTrackCollection"
app:destination="@id/trackCollectionFragment" />
android:label="@string/common.appname">
</fragment>
<fragment
android:id="@+id/mediaLibraryFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment"
android:label="@string/music_library.label" >
<action
android:id="@+id/artistsListToAlbumsList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/artistsListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
android:label="@string/music_library.label" />
<fragment
android:id="@+id/nowPlayingFragment"
android:name="org.moire.ultrasonic.fragment.NowPlayingFragment"
android:label="@string/button_bar.now_playing">
<action
android:id="@+id/toTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
android:label="@string/button_bar.now_playing" />
<fragment
android:id="@+id/artistListFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
<action
android:id="@+id/artistsListToAlbumsList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/artistsListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
android:name="org.moire.ultrasonic.fragment.ArtistListFragment">
<argument
android:name="refresh"
app:argType="boolean"
android:defaultValue="false" />
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="title"
app:argType="string"
app:nullable="true"
android:defaultValue="@null"
/>
app:argType="string"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/trackCollectionFragment"
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment">
<argument
android:name="id"
app:nullable="true"
android:defaultValue="@null"
app:argType="string" />
app:argType="string"
app:nullable="true" />
<argument
android:name="isAlbum"
app:argType="boolean"
android:defaultValue="false"/>
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="isArtist"
app:argType="boolean"
android:defaultValue="false"/>
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="getRandom"
android:defaultValue="false"
@ -109,76 +96,83 @@
android:name="refresh"
android:defaultValue="true"
app:argType="boolean" />
<argument android:name="name"
app:argType="string"
<argument
android:name="name"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="parentId"
app:argType="string"
app:nullable="true" />
<argument
android:name="parentId"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="genreName"
app:argType="string"
app:nullable="true" />
<argument
android:name="genreName"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="shareId"
app:argType="string"
app:nullable="true" />
<argument
android:name="shareId"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="playlistId"
app:argType="string"
app:nullable="true" />
<argument
android:name="playlistId"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="playlistName"
app:argType="string"
app:nullable="true" />
<argument
android:name="playlistName"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="shareName"
app:argType="string"
app:nullable="true" />
<argument
android:name="shareName"
android:defaultValue="@null"
app:nullable="true"/>
<argument android:name="podcastChannelId"
app:argType="string"
app:nullable="true" />
<argument
android:name="podcastChannelId"
android:defaultValue="@null"
app:nullable="true"/>
app:argType="string"
app:nullable="true" />
<argument
android:name="albumListType"
app:argType="string"
app:nullable="true"
android:defaultValue="@null"/>
<argument
android:name="size"
app:argType="integer"
android:defaultValue="0"
/>
<argument
android:name="offset"
app:argType="integer"
android:defaultValue="0"
/>
<action
android:id="@+id/loadMoreTracks"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/albumListFragment"
android:name="org.moire.ultrasonic.fragment.AlbumListFragment" >
<argument
android:name="type"
app:argType="org.moire.ultrasonic.api.subsonic.models.AlbumListType"
/>
<argument
android:name="title"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="size"
app:argType="integer"
/>
android:defaultValue="-1"
app:argType="integer" />
<argument
android:name="offset"
app:argType="integer"
/>
android:defaultValue="0"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/albumListFragment"
android:name="org.moire.ultrasonic.fragment.AlbumListFragment">
<argument
android:name="type"
app:argType="org.moire.ultrasonic.api.subsonic.models.AlbumListType" />
<argument
android:name="byArtist"
android:defaultValue="false"
app:argType="boolean" />
<argument
android:name="title"
app:argType="string"
android:defaultValue="@null"
app:nullable="true" />
<argument
android:name="size"
android:defaultValue="20"
app:argType="integer" />
<argument
android:name="offset"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="append"
android:defaultValue="false"
@ -190,22 +184,22 @@
<argument
android:name="id"
android:defaultValue="@null"
app:nullable="true"
app:argType="string" />
app:argType="string"
app:nullable="true" />
<action
android:id="@+id/albumListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/entryListFragment"
android:name="org.moire.ultrasonic.fragment.EntryListFragment" >
android:name="org.moire.ultrasonic.fragment.EntryListFragment">
<action
android:id="@+id/entryListToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="org.moire.ultrasonic.fragment.SearchFragment" >
android:name="org.moire.ultrasonic.fragment.SearchFragment">
<action
android:id="@+id/searchToTrackCollection"
app:destination="@id/trackCollectionFragment" />
@ -214,30 +208,24 @@
app:destination="@id/albumListFragment" />
<argument
android:name="query"
android:defaultValue="@null"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
app:nullable="true" />
<argument
android:name="autoplay"
app:argType="boolean"
android:defaultValue="false" />
android:defaultValue="false"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/playlistsFragment"
android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment" >
<action
android:id="@+id/playlistsToTrackCollection"
app:destination="@id/trackCollectionFragment" />
android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment">
</fragment>
<fragment
android:id="@+id/downloadsFragment"
android:name="org.moire.ultrasonic.fragment.DownloadsFragment" />
<fragment
android:id="@+id/sharesFragment"
android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment" >
<action
android:id="@+id/sharesToTrackCollection"
app:destination="@id/trackCollectionFragment" />
android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment">
</fragment>
<fragment
android:id="@+id/bookmarksFragment"
@ -247,28 +235,21 @@
android:name="org.moire.ultrasonic.fragment.ChatFragment" />
<fragment
android:id="@+id/podcastFragment"
android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment" >
<action
android:id="@+id/podcastToTrackCollection"
app:destination="@id/trackCollectionFragment" />
android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment">
</fragment>
<fragment
android:id="@+id/settingsFragment"
android:name="org.moire.ultrasonic.fragment.SettingsFragment" >
</fragment>
android:name="org.moire.ultrasonic.fragment.SettingsFragment" />
<fragment
android:id="@+id/aboutFragment"
android:name="org.moire.ultrasonic.fragment.AboutFragment" />
<fragment
android:id="@+id/selectGenreFragment"
android:name="org.moire.ultrasonic.fragment.legacy.SelectGenreFragment">
<action
android:id="@+id/selectGenreToTrackCollection"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/playerFragment"
android:name="org.moire.ultrasonic.fragment.PlayerFragment" >
android:name="org.moire.ultrasonic.fragment.PlayerFragment">
<action
android:id="@+id/playerToSelectAlbum"
app:destination="@id/trackCollectionFragment" />
@ -284,7 +265,7 @@
</fragment>
<fragment
android:id="@+id/lyricsFragment"
android:name="org.moire.ultrasonic.fragment.legacy.LyricsFragment" >
android:name="org.moire.ultrasonic.fragment.legacy.LyricsFragment">
<argument
android:name="artist"
app:argType="string" />
@ -297,20 +278,17 @@
android:name="org.moire.ultrasonic.fragment.EqualizerFragment" />
<fragment
android:id="@+id/serverSelectorFragment"
android:name="org.moire.ultrasonic.fragment.ServerSelectorFragment" >
android:name="org.moire.ultrasonic.fragment.ServerSelectorFragment">
<action
android:id="@+id/toEditServer"
app:destination="@id/editServerFragment" />
</fragment>
<fragment
android:id="@+id/editServerFragment"
android:name="org.moire.ultrasonic.fragment.EditServerFragment" >
android:name="org.moire.ultrasonic.fragment.EditServerFragment">
<argument
android:name="index"
app:argType="integer"
android:defaultValue="-1" />
android:defaultValue="-1"
app:argType="integer" />
</fragment>
<action
android:id="@+id/toSearchFragment"
app:destination="@id/searchFragment" />
</navigation>

View File

@ -13,13 +13,11 @@
<string name="button_bar.bookmarks">Záložky</string>
<string name="button_bar.browse">Knihovna médií</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Ultrasonic Menu</string>
<string name="button_bar.now_playing">Právě hraje</string>
<string name="buttons.shuffle">Náhodně</string>
<string name="podcasts.label">Podcasty</string>
<string name="podcasts_channels.empty">Není registrován žádný podcast kanál</string>
<string name="button_bar.podcasts">Podcasty</string>
<string name="button_bar.playlists">Playlisty</string>
<string name="button_bar.search">Hledat</string>
<string name="chat.send_a_message">Poslat zprávu</string>
<string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Alba</string>
<string name="main.artists_title">Umělci</string>
<string name="main.genres_title">Žánry</string>
<string name="main.music">Hudba</string>
<string name="main.offline">Bez připojení</string>
<string name="main.songs_random">Náhodné</string>
<string name="main.songs_starred">Označené hvězdičkou</string>
@ -307,7 +304,6 @@
<string name="share_via">Sdílet skladby přes</string>
<string name="menu.share">Sdílení</string>
<string name="download.menu_show_artist">Zobrazit umělce</string>
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Vícenásobné roky</string>
<string name="settings.debug.title">Možnosti ladění aplikace</string>
<string name="settings.debug.log_to_file">Zapisovat logy ladění do souboru</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Lesezeichen</string>
<string name="button_bar.browse">Medienbibliothek</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Ultrasonic Hauptseite</string>
<string name="button_bar.now_playing">Aktuelle Wiedergabe</string>
<string name="buttons.play">Abspielen</string>
<string name="buttons.pause">Pause</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Keine Podcast Kanäle registriert</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Wiedergabeliste</string>
<string name="button_bar.search">Suche</string>
<string name="chat.send_a_message">Nachricht senden</string>
<string name="chat.send_button">Senden</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Alben</string>
<string name="main.artists_title">Künstler*innen</string>
<string name="main.genres_title">Genres</string>
<string name="main.music">Musik</string>
<string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Server einrichten</string>
<string name="main.songs_random">Zufällig</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Marcadores</string>
<string name="button_bar.browse">Biblioteca</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Inicio de Ultrasonic</string>
<string name="button_bar.now_playing">Reproduciendo ahora</string>
<string name="buttons.play">Reproducir</string>
<string name="buttons.pause">Pausar</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No hay canales de Podcasts registrados</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Listas de reproducción</string>
<string name="button_bar.search">Buscar</string>
<string name="chat.send_a_message">Enviar un mensaje</string>
<string name="chat.send_button">Enviar</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Álbumes</string>
<string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Géneros</string>
<string name="main.music">Música</string>
<string name="main.offline">Sin conexión</string>
<string name="main.setup_server">%s - Configurar servidor</string>
<string name="main.songs_random">Aleatorio</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Signets</string>
<string name="button_bar.browse">Bibliothèque musicale</string>
<string name="button_bar.chat">Salon de discussion</string>
<string name="button_bar.home">Accueil Ultrasonic</string>
<string name="button_bar.now_playing">Lecture en cours</string>
<string name="buttons.play">Lecture</string>
<string name="buttons.pause">Pause</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Aucune chaîne de podcast enregistrée</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Recherche</string>
<string name="chat.send_a_message">Envoyer un message</string>
<string name="chat.send_button">Envoyer</string>
@ -117,7 +115,6 @@
<string name="main.albums_title">Albums</string>
<string name="main.artists_title">Artistes</string>
<string name="main.genres_title">Genres</string>
<string name="main.music">Musique</string>
<string name="main.offline">Hors-ligne</string>
<string name="main.setup_server">%s - Configurer le serveur</string>
<string name="main.songs_random">Aléatoire</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Könyvjelzők</string>
<string name="button_bar.browse">Médiakönyvtár</string>
<string name="button_bar.chat">Csevegés (Chat)</string>
<string name="button_bar.home">Ultrasonic főoldal</string>
<string name="button_bar.now_playing">Lejátszó</string>
<string name="buttons.play">Lejátszás</string>
<string name="buttons.pause">Szünet</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Nincsenek podcast-csatornák regisztrálva</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Lejátszási listák</string>
<string name="button_bar.search">Keresés</string>
<string name="chat.send_a_message">Üzenet küldése</string>
<string name="common.appname">Ultrasonic</string>
@ -93,7 +91,6 @@
<string name="main.albums_title">Albumok</string>
<string name="main.artists_title">Előadók</string>
<string name="main.genres_title">Műfajok</string>
<string name="main.music">Zenék</string>
<string name="main.offline">Kapcsolat nélküli</string>
<string name="main.songs_random">Véletlenszerű</string>
<string name="main.songs_starred">Csillaggal megjelölt</string>

View File

@ -17,7 +17,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Nessun canale podcast registrato</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Cerca</string>
<string name="chat.send_a_message">Spedisci un messaggio</string>
<string name="common.cancel">Annulla</string>
@ -84,7 +83,6 @@
<string name="main.albums_title">Album</string>
<string name="main.artists_title">Artisti</string>
<string name="main.genres_title">Generi</string>
<string name="main.music">Musica</string>
<string name="main.offline">Disconnesso</string>
<string name="main.songs_random">Casuale</string>
<string name="main.songs_starred">Preferiti</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Bladwijzers</string>
<string name="button_bar.browse">Mediabibliotheek</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Ultrasonic Main</string>
<string name="button_bar.now_playing">Nu aan het afspelen</string>
<string name="buttons.play">Afspelen</string>
<string name="buttons.pause">Pauzeren</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Geen podcastkanalen opgegeven</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Afspeellijsten</string>
<string name="button_bar.search">Zoeken</string>
<string name="chat.send_a_message">Bericht versturen</string>
<string name="chat.send_button">Versturen</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Albums</string>
<string name="main.artists_title">Artiesten</string>
<string name="main.genres_title">Genres</string>
<string name="main.music">Muziek</string>
<string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Server instellen</string>
<string name="main.songs_random">Willekeurig</string>

View File

@ -13,13 +13,11 @@
<string name="button_bar.bookmarks">Zakładki</string>
<string name="button_bar.browse">Biblioteka</string>
<string name="button_bar.chat">Czat</string>
<string name="button_bar.home">Ultrasonic</string>
<string name="button_bar.now_playing">Teraz gra</string>
<string name="buttons.shuffle">Wymieszaj</string>
<string name="podcasts.label">Podcasty</string>
<string name="podcasts_channels.empty">Brak kanałów</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlisty</string>
<string name="button_bar.search">Szukaj</string>
<string name="chat.send_a_message">Wyślij wiadomość</string>
<string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Albumy</string>
<string name="main.artists_title">Artyści</string>
<string name="main.genres_title">Gatunki</string>
<string name="main.music">Muzyka</string>
<string name="main.offline">Offline</string>
<string name="main.songs_random">Losowe</string>
<string name="main.songs_starred">Ulubione</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Menu Principal</string>
<string name="button_bar.now_playing">Tocando Agora</string>
<string name="buttons.play">Tocar</string>
<string name="buttons.pause">Pausar</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcasts</string>
<string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string>
<string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Pesquisa</string>
<string name="chat.send_a_message">Enviar uma mensagem</string>
<string name="common.album">Álbum</string>
@ -113,7 +111,6 @@
<string name="main.albums_title">Álbuns</string>
<string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Gêneros</string>
<string name="main.music">Música</string>
<string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Configurar Servidor</string>
<string name="main.songs_random">Aleatórias</string>

View File

@ -13,13 +13,11 @@
<string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Menu Principal</string>
<string name="button_bar.now_playing">Tocando Agora</string>
<string name="buttons.shuffle">Misturar</string>
<string name="podcasts.label">Podcasts</string>
<string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Pesquisa</string>
<string name="chat.send_a_message">Enviar uma mensagem</string>
<string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Álbuns</string>
<string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Gêneros</string>
<string name="main.music">Música</string>
<string name="main.offline">Offline</string>
<string name="main.songs_random">Aleatórias</string>
<string name="main.songs_starred">Favoritas</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Закладки</string>
<string name="button_bar.browse">Медиа библиотека</string>
<string name="button_bar.chat">Чат</string>
<string name="button_bar.home">UltraSonic Главная</string>
<string name="button_bar.now_playing">Сейчас играет</string>
<string name="buttons.play">Воспроизведение</string>
<string name="buttons.pause">Пауза</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Подкаст</string>
<string name="podcasts_channels.empty">Подкасты не зарегистрированы</string>
<string name="button_bar.podcasts">Подкаст</string>
<string name="button_bar.playlists">Плейлист</string>
<string name="button_bar.search">Поиск</string>
<string name="chat.send_a_message">Отправить сообщение</string>
<string name="common.album">Альбом</string>
@ -108,7 +106,6 @@
<string name="main.albums_title">Альбомы</string>
<string name="main.artists_title">Исполнители</string>
<string name="main.genres_title">Жанры</string>
<string name="main.music">Музыка</string>
<string name="main.offline">Не в сети</string>
<string name="main.songs_random">Случайный</string>
<string name="main.songs_starred">Отмеченные</string>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">书签</string>
<string name="button_bar.browse">媒体库</string>
<string name="button_bar.chat">聊天</string>
<string name="button_bar.home">Ultrasonic 主页</string>
<string name="button_bar.now_playing">正在播放</string>
<string name="buttons.play">播放</string>
<string name="buttons.pause">暂停</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">播客</string>
<string name="podcasts_channels.empty">没有已注册的播客频道</string>
<string name="button_bar.podcasts">播客</string>
<string name="button_bar.playlists">播放列表</string>
<string name="button_bar.search">搜索</string>
<string name="chat.send_a_message">发送消息</string>
<string name="chat.send_button">发送</string>
@ -104,7 +102,6 @@
<string name="main.albums_title">专辑</string>
<string name="main.artists_title">艺术家</string>
<string name="main.genres_title">流派</string>
<string name="main.music">音乐</string>
<string name="main.offline">离线</string>
<string name="main.setup_server">%s - 已设置服务器</string>
<string name="main.songs_random">随机</string>

View File

@ -5,7 +5,6 @@
<string name="button_bar.bookmarks">書籤</string>
<string name="button_bar.browse">媒體庫</string>
<string name="button_bar.now_playing">正在播放</string>
<string name="button_bar.playlists">播放清單</string>
<string name="button_bar.search">搜尋</string>
<string name="common.cancel">取消</string>
<string name="common.comment">註記</string>

View File

@ -55,4 +55,5 @@
<string name="setting_key.override_language" translatable="false">overrideLanguage</string>
<string name="setting_key.first_installed_version" translatable="false">firstInstalledVersion</string>
<string name="setting_key.show_confirmation_dialog" translatable="false">showConfirmationDialog</string>
<string name="setting_key.last_view_type" translatable="false">lastViewType</string>
</resources>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Bookmarks</string>
<string name="button_bar.browse">Media Library</string>
<string name="button_bar.chat">Chat</string>
<string name="button_bar.home">Ultrasonic Main</string>
<string name="button_bar.now_playing">Now Playing</string>
<string name="buttons.play">Play</string>
<string name="buttons.pause">Pause</string>
@ -25,10 +24,10 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No podcasts channels registered</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Search</string>
<string name="chat.send_a_message">Send a message</string>
<string name="chat.send_button">Send</string>
<string name="chat.user_avatar">Avatar image</string>
<string name="common.album">Album</string>
<string name="common.appname">Ultrasonic</string>
<string name="common.artist">Artist</string>
@ -116,10 +115,10 @@
<string name="main.albums_random">Random</string>
<string name="main.albums_recent">Recently Played</string>
<string name="main.albums_starred">Starred</string>
<string name="main.albums_by_year">Chronological</string>
<string name="main.albums_title">Albums</string>
<string name="main.artists_title">Artists</string>
<string name="main.genres_title">Genres</string>
<string name="main.music">Music</string>
<string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Set up Server</string>
<string name="main.songs_random">Random</string>
@ -370,7 +369,7 @@
<string name="share_via">Share songs via</string>
<string name="menu.share">Share</string>
<string name="download.menu_show_artist">Show Artist</string>
<string name="albumArt">albumArt</string>
<string name="albumArt">Album artwork</string>
<string name="common_multiple_years">Multiple Years</string>
<string name="settings.show_confirmation_dialog">Show confirmation dialog</string>
<string name="settings.show_confirmation_dialog_summary">Displays a confirmation dialog before deleting or unpinning songs</string>
@ -451,5 +450,8 @@
<string name="settings.use_hw_offload_title">Use hardware playback (experimental)</string>
<string name="settings.use_hw_offload_description">Try to play the media using the media decoder chip on your phone. This can improve battery usage.</string>
<string name="list_view">List</string>
<string name="grid_view">Cover</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Ultrasonic.AllCapsLabel" parent="">
<item name="android:textStyle">bold</item>
@ -15,16 +15,6 @@
<item name="android:paddingStart">16dp</item>
</style>
<style name="roundedImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
</style>
<style name="largeRoundedImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">2dp</item>
</style>
<style name="Widget.AppWidget.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
@ -60,6 +50,11 @@
<item name="android:adjustViewBounds">true</item>
</style>
<style tools:ignore="PrivateResource" name="Widget.ChipDropdown" parent="Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu">
<item name="shapeAppearance">?attr/shapeAppearanceSmallComponent</item>
<item name="boxStrokeColor">@color/m3_textfield_stroke_color</item>
</style>
<style name="Theme.AppWidget.AppWidgetContainerCropped" parent="Theme.AppWidget.AppWidgetContainer">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">4dp</item>