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 { companion object {
private const val serialVersionUID = -4160515427075433798L private const val serialVersionUID = -4160515427075433798L
} }
override fun toString(): String {
return name
}
} }

View File

@ -12,4 +12,6 @@ data class PodcastsChannel(
companion object { companion object {
private const val serialVersionUID = -4160515427075433798L 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 package org.moire.ultrasonic.api.subsonic
import okhttp3.ResponseBody import okhttp3.ResponseBody

View File

@ -129,13 +129,12 @@ class SubsonicAPIClient(
this.addInterceptor(loggingInterceptor) this.addInterceptor(loggingInterceptor)
} }
@Suppress("CustomX509TrustManager", "TrustAllX509TrustManager")
private fun OkHttpClient.Builder.allowSelfSignedCertificates() { private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
val trustManager = val trustManager =
@Suppress("CustomX509TrustManager")
object : X509TrustManager { object : X509TrustManager {
@Suppress("TrustAllX509TrustManager")
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {} override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
@Suppress("TrustAllX509TrustManager")
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {} override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray() 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 package org.moire.ultrasonic.api.subsonic
import okhttp3.ResponseBody 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 package org.moire.ultrasonic.api.subsonic.models
/** /**
@ -16,8 +23,7 @@ enum class AlbumListType(val typeName: String) {
SORTED_BY_ARTIST("alphabeticalByArtist"), SORTED_BY_ARTIST("alphabeticalByArtist"),
STARRED("starred"), STARRED("starred"),
BY_YEAR("byYear"), BY_YEAR("byYear"),
BY_GENRE("byGenre"), BY_GENRE("byGenre");
BY_ARTIST("albumsByArtist");
override fun toString(): String { override fun toString(): String {
return typeName return typeName
@ -36,7 +42,6 @@ enum class AlbumListType(val typeName: String) {
in STARRED.typeName -> STARRED in STARRED.typeName -> STARRED
in BY_YEAR.typeName -> BY_YEAR in BY_YEAR.typeName -> BY_YEAR
in BY_GENRE.typeName -> BY_GENRE in BY_GENRE.typeName -> BY_GENRE
in BY_ARTIST.typeName -> BY_ARTIST
else -> throw IllegalArgumentException("Unknown type: $typeName") else -> throw IllegalArgumentException("Unknown type: $typeName")
} }

View File

@ -36,6 +36,7 @@ class AlbumListTypeTest {
@Test @Test
fun `Should return type name for toString call`() { 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" ktlintGradle = "11.0.0"
detekt = "1.21.0" detekt = "1.21.0"
preferences = "1.2.0" preferences = "1.2.0"
media = "1.6.0"
media3 = "1.0.0-beta02" media3 = "1.0.0-beta02"
androidSupport = "1.5.0" androidSupport = "1.5.0"
androidLegacySupport = "1.0.0" materialDesign = "1.6.1"
androidSupportDesign = "1.6.1"
constraintLayout = "2.1.4" constraintLayout = "2.1.4"
multidex = "2.0.1" multidex = "2.0.1"
room = "2.4.3" room = "2.4.3"
@ -22,6 +20,7 @@ kotlin = "1.7.20"
kotlinxCoroutines = "1.6.4-native-mt" kotlinxCoroutines = "1.6.4-native-mt"
kotlinxGuava = "1.6.4" kotlinxGuava = "1.6.4"
viewModelKtx = "2.5.1" viewModelKtx = "2.5.1"
swipeRefresh = "1.1.0"
retrofit = "2.9.0" retrofit = "2.9.0"
jackson = "2.13.4" 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" } detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" } 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 = "materialDesign" }
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" } annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" } 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" } navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"} navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
preferences = { module = "androidx.preference:preference", version.ref = "preferences" } 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" } media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
media3session = { module = "androidx.media3:media3-session", 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" } kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }

View File

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

View File

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

View File

@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="154" line="153"
column="5"/> column="5"/>
</issue> </issue>
@ -70,138 +70,6 @@
column="10"/> column="10"/>
</issue> </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 <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.drawable.media3_notification_pause` appears to be unused" message="The resource `R.drawable.media3_notification_pause` appears to be unused"
@ -297,54 +165,10 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/res/layout/time_span_dialog.xml" file="src/main/res/layout/time_span_dialog.xml"
line="30" line="28"
column="10"/> column="10"/>
</issue> </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 <issue
id="LabelFor" id="LabelFor"
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`" 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=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/res/layout/time_span_dialog.xml" file="src/main/res/layout/time_span_dialog.xml"
line="30" line="28"
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"
column="10"/> column="10"/>
</issue> </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> public class ShareAdapter extends ArrayAdapter<Share>
{ {
private final Context context; private final Context context;
public ShareAdapter(Context context, List<Share> Shares) 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.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSettingDao import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.fragment.MainFragmentDirections
import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider 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. * 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... * onCreate/onResume/onDestroy methods...
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class NavigationActivity : AppCompatActivity() { class NavigationActivity : AppCompatActivity() {
private var videoMenuItem: MenuItem? = null
private var chatMenuItem: MenuItem? = null private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null private var bookmarksMenuItem: MenuItem? = null
private var sharesMenuItem: MenuItem? = null private var sharesMenuItem: MenuItem? = null
@ -301,6 +301,13 @@ class NavigationActivity : AppCompatActivity() {
R.id.bookmarksFragment -> { R.id.bookmarksFragment -> {
navController.navigate(NavigationGraphDirections.toBookmarks()) navController.navigate(NavigationGraphDirections.toBookmarks())
} }
R.id.trackCollectionFragment -> {
navController.navigate(
NavigationGraphDirections.toTrackCollection(
getVideos = true
)
)
}
R.id.menu_exit -> { R.id.menu_exit -> {
setResult(Constants.RESULT_CLOSE_ALL) setResult(Constants.RESULT_CLOSE_ALL)
mediaPlayerController.onDestroy() mediaPlayerController.onDestroy()
@ -319,6 +326,7 @@ class NavigationActivity : AppCompatActivity() {
podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment) podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment)
playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment) playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment)
downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment) downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment)
videoMenuItem = navigationView?.menu?.findItem(R.id.trackCollectionFragment)
selectServerButton = selectServerButton =
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server) navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
@ -348,7 +356,7 @@ class NavigationActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
val retValue = super.onCreateOptionsMenu(menu) val retValue = super.onCreateOptionsMenu(menu)
if (navigationView == null) { if (navigationView == null) {
menuInflater.inflate(R.menu.navigation, menu) menuInflater.inflate(R.menu.navigation_drawer, menu)
return true return true
} }
return retValue return retValue
@ -390,7 +398,7 @@ class NavigationActivity : AppCompatActivity() {
) )
suggestions.saveRecentQuery(query, null) suggestions.saveRecentQuery(query, null)
val action = MainFragmentDirections.toSearchFragment(query, autoPlay) val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
findNavController(R.id.nav_host_fragment).navigate(action) findNavController(R.id.nav_host_fragment).navigate(action)
} }
} }
@ -498,5 +506,6 @@ class NavigationActivity : AppCompatActivity() {
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
playlistsMenuItem?.isVisible = isOnline playlistsMenuItem?.isVisible = isOnline
downloadsMenuItem?.isVisible = isOnline downloadsMenuItem?.isVisible = isOnline
videoMenuItem?.isVisible = isOnline
} }
} }

View File

@ -7,6 +7,7 @@
package org.moire.ultrasonic.adapters package org.moire.ultrasonic.adapters
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -15,31 +16,31 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewDelegate
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
import timber.log.Timber import timber.log.Timber
/** /**
* Creates a Row in a RecyclerView which contains the details of an Album * Creates a Row in a RecyclerView which contains the details of an Album
*/ */
class AlbumRowBinder( open class AlbumRowDelegate(
val onItemClick: (Album) -> Unit, open val onItemClick: (Album) -> Unit,
val onContextMenuClick: (MenuItem, Album) -> Boolean, open val onContextMenuClick: (MenuItem, Album) -> Boolean,
private val imageLoader: ImageLoader 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 starDrawable: Int = R.drawable.ic_star_full
private val starHollowDrawable: Int = R.drawable.ic_star_hollow private val starHollowDrawable: Int = R.drawable.ic_star_hollow
// Set our layout files open var layoutType = LayoutType.LIST
val layout = R.layout.list_item_album
override fun onBindViewHolder(holder: ViewHolder, item: Album) { override fun onBindViewHolder(holder: ListViewHolder, item: Album) {
holder.album.text = item.title holder.album.text = item.title
holder.artist.text = item.artist holder.artist.text = item.artist
holder.details.setOnClickListener { onItemClick(item) } holder.details.setOnClickListener { onItemClick(item) }
@ -66,15 +67,40 @@ class AlbumRowBinder(
/** /**
* Holds the view properties of an Item row * Holds the view properties of an Item row
*/ */
class ViewHolder( open class ListViewHolder(
view: View view: View
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
var album: TextView = view.findViewById(R.id.album_title)
var artist: TextView = view.findViewById(R.id.album_artist) var album: TextView
var details: LinearLayout = view.findViewById(R.id.row_album_details) var artist: TextView
var coverArt: ImageView = view.findViewById(R.id.coverart) var details: LinearLayout
var star: ImageView = view.findViewById(R.id.album_star) var coverArt: ImageView
var star: ImageView
var coverArtId: String? = null 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() }.start()
} }
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false)) 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 section: TextView = itemView.findViewById(R.id.row_section)
var textView: TextView = itemView.findViewById(R.id.row_artist_name) var textView: TextView = itemView.findViewById(R.id.row_artist_name)
var layout: RelativeLayout = itemView.findViewById(R.id.containing_layout) 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 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.R
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import timber.log.Timber
@Suppress("LongParameterList") @Suppress("LongParameterList")
class TrackViewBinder( class TrackViewBinder(
@ -63,7 +62,7 @@ class TrackViewBinder(
diffAdapter.isSelected(item.longId) diffAdapter.isSelected(item.longId)
) )
Timber.v("Setting listeners") // Timber.v("Setting listeners")
holder.itemView.setOnLongClickListener { holder.itemView.setOnLongClickListener {
if (onContextMenuClick != null) { if (onContextMenuClick != null) {
@ -118,7 +117,7 @@ class TrackViewBinder(
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
} }
Timber.v("Setting listeners done") // Timber.v("Setting listeners done")
} }
override fun onViewRecycled(holder: TrackViewHolder) { override fun onViewRecycled(holder: TrackViewHolder) {

View File

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

View File

@ -10,24 +10,41 @@
package org.moire.ultrasonic.fragment package org.moire.ultrasonic.fragment
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R 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.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.model.AlbumListModel 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 * 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 * 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 * The central function to pass a query to the model and return a LiveData object
*/ */
override fun getLiveData( override fun getLiveData(
refresh: Boolean refresh: Boolean,
append: Boolean
): LiveData<List<Album>> { ): LiveData<List<Album>> {
fetchAlbums(refresh) fetchAlbums(refresh)
@ -63,7 +81,7 @@ class AlbumListFragment : EntryListFragment<Album>() {
listModel.viewModelScope.launch(handler) { listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true refreshListView?.isRefreshing = true
if (navArgs.type == AlbumListType.BY_ARTIST) { if (navArgs.byArtist) {
listModel.getAlbumsOfArtist( listModel.getAlbumsOfArtist(
refresh = navArgs.refresh, refresh = navArgs.refresh,
id = navArgs.id!!, id = navArgs.id!!,
@ -71,7 +89,7 @@ class AlbumListFragment : EntryListFragment<Album>() {
) )
} else { } else {
listModel.getAlbums( listModel.getAlbums(
albumListType = navArgs.type, albumListType = orderType?.mapToAlbumListType() ?: navArgs.type,
size = navArgs.size, size = navArgs.size,
offset = navArgs.offset, offset = navArgs.offset,
append = append, 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setTitle(navArgs.title) // In most cases this fragment will be hosted by a ViewPager2 in the MainFragment,
// which provides its own FilterBar.
// Attach our onScrollListener // But when we are looking at the Albums of a specific Artist this Fragment is standalone,
listView = view.findViewById<RecyclerView>(recyclerViewId).apply { // so we need to setup the FilterBar here..
val scrollListener = object : EndlessScrollListener(viewManager) { if (navArgs.byArtist) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { setTitle(navArgs.title)
// Triggered only when new data needs to be appended to the list setupFilterBar(view)
// Add whatever code is needed to append new items to the bottom of the list
fetchAlbums(append = true)
}
}
addOnScrollListener(scrollListener)
} }
viewAdapter.register( // Get a reference to the listView
AlbumRowBinder( listView = view.findViewById(recyclerViewId)
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, setLayoutType(layoutType)
imageLoaderProvider.getImageLoader()
) 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) 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) { override fun onItemClick(item: Album) {
val action = AlbumListFragmentDirections.albumListToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
item.id, item.id,
isAlbum = item.isDirectory, isAlbum = item.isDirectory,
name = item.title, name = item.title,
@ -121,4 +246,20 @@ class AlbumListFragment : EntryListFragment<Album>() {
) )
findNavController().navigate(action) 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.lifecycle.LiveData
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType 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 * 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!!) return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
} }
@ -66,15 +67,16 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
override fun onItemClick(item: ArtistOrIndex) { override fun onItemClick(item: ArtistOrIndex) {
// Check type // Check type
val action = if (item is Index) { val action = if (item is Index) {
ArtistListFragmentDirections.artistsListToTrackCollection( NavigationGraphDirections.toTrackCollection(
id = item.id, id = item.id,
name = item.name, name = item.name,
parentId = item.id, parentId = item.id,
isArtist = (item is Artist) isArtist = (item is Artist)
) )
} else { } else {
ArtistListFragmentDirections.artistsListToAlbumsList( NavigationGraphDirections.toAlbumList(
type = AlbumListType.BY_ARTIST, type = AlbumListType.SORTED_BY_NAME,
byArtist = true,
id = item.id, id = item.id,
title = item.name, title = item.name,
size = 1000, size = 1000,

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.fragment
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
/** /**
* Contains utility functions related to Fragment title handling * Contains utility functions related to Fragment title handling
@ -9,11 +10,17 @@ import androidx.fragment.app.Fragment
class FragmentTitle { class FragmentTitle {
companion object { companion object {
fun setTitle(fragment: Fragment, title: CharSequence?) { 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) { 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? { fun getTitle(fragment: Fragment): CharSequence? {

View File

@ -7,251 +7,217 @@
package org.moire.ultrasonic.fragment package org.moire.ultrasonic.fragment
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment 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.koin.core.component.KoinComponent
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.databinding.MainBinding 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.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 { class MainFragment : Fragment(), KoinComponent {
private lateinit var musicTitle: TextView private var filterButtonBar: FilterButtonBar? = null
private lateinit var artistsButton: TextView private var layoutType: LayoutType = LayoutType.COVER
private lateinit var albumsButton: TextView private var binding: View? = null
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 binding: MainBinding? = null private lateinit var musicCollectionAdapter: MusicCollectionAdapter
private lateinit var viewPager: ViewPager2
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = MainBinding.inflate(inflater, container, false) Timber.i("onCreate")
return binding!!.root binding = inflater.inflate(R.layout.primary, container, false)
return binding!!
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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() { // Load last layout from settings
super.onResume() layoutType = LayoutType.from(Settings.lastViewType)
var shouldRelayout = false
val currentId3Setting = Settings.shouldUseId3Tags
// If setting has changed... // Init ViewPager2
if (currentId3Setting != useId3) { musicCollectionAdapter = MusicCollectionAdapter(this, layoutType)
useId3 = currentId3Setting viewPager = binding!!.findViewById(R.id.pager)
shouldRelayout = true viewPager.adapter = musicCollectionAdapter
filterButtonBar = binding!!.findViewById(R.id.filter_button_bar)
musicCollectionAdapter.filterButtonBar = filterButtonBar
filterButtonBar!!.setOnLayoutTypeChangedListener {
updateLayoutTypeOnCurrentFragment(it)
} }
// then setup the list anew. filterButtonBar!!.setOnOrderChangedListener {
if (shouldRelayout) { updateSortOrderOnCurrentFragment(it)
setupItemVisibility() }
// 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() { private fun findCurrentFragment(): Fragment? {
super.onDestroyView() return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
binding = null
} }
private fun setupButtons() { private fun findFragmentAtPosition(
musicTitle = binding!!.mainMusic fragmentManager: FragmentManager,
artistsButton = binding!!.mainArtistsButton position: Int
albumsButton = binding!!.mainAlbumsButton ): Fragment? {
genresButton = binding!!.mainGenresButton // If a fragment was recently created and never shown the fragment manager might not
videosTitle = binding!!.mainVideosTitle // hold a reference to it. Fallback on the WeakMap instead.
songsTitle = binding!!.mainSongs return fragmentManager.findFragmentByTag("f$position")
randomSongsButton = binding!!.mainSongsButton ?: musicCollectionAdapter.fragmentMap[position]?.get()
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 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 * 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() return MutableLiveData()
} }

View File

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

View File

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

View File

@ -27,9 +27,10 @@ import java.util.Collections
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumHeader 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.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider 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.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities
import timber.log.Timber 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, * 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. * or using Offline mode, both in which Indexes instead of Artists are being used.
* *
* TODO: Remove more button and introduce endless scrolling
*/ */
@Suppress("TooManyFunctions") @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 albumButtons: View? = null
private var selectButton: MaterialButton? = null private var selectButton: MaterialButton? = null
@ -73,7 +77,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private var unpinButton: MaterialButton? = null private var unpinButton: MaterialButton? = null
private var downloadButton: MaterialButton? = null private var downloadButton: MaterialButton? = null
private var deleteButton: MaterialButton? = null private var deleteButton: MaterialButton? = null
private var moreButton: MaterialButton? = null
private var playAllButtonVisible = false private var playAllButtonVisible = false
private var shareButtonVisible = false private var shareButtonVisible = false
private var playAllButton: MenuItem? = null private var playAllButton: MenuItem? = null
@ -87,6 +90,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
override val listModel: TrackCollectionModel by viewModels() override val listModel: TrackCollectionModel by viewModels()
private val rxBusSubscription: CompositeDisposable = CompositeDisposable() private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
private var sortOrder = initialOrder
private var offset: Int? = null
/** /**
* The id of the main layout * The id of the main layout
*/ */
@ -138,7 +144,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
) )
viewAdapter.register( viewAdapter.register(
AlbumRowBinder( AlbumRowDelegate(
{ entry -> onItemClick(entry) }, { entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader() imageLoaderProvider.getImageLoader()
@ -161,6 +167,25 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
) { ) {
triggerButtonUpdate() 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() { internal open fun handleRefresh() {
@ -176,7 +201,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
unpinButton = view.findViewById(R.id.select_album_unpin) unpinButton = view.findViewById(R.id.select_album_unpin)
downloadButton = view.findViewById(R.id.select_album_download) downloadButton = view.findViewById(R.id.select_album_download)
deleteButton = view.findViewById(R.id.select_album_delete) deleteButton = view.findViewById(R.id.select_album_delete)
moreButton = view.findViewById(R.id.select_album_more)
selectButton?.setOnClickListener { selectButton?.setOnClickListener {
selectAllOrNone() 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 // Hide select button for video lists and singular selection lists
selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0 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 // Show a text if we have no entries
emptyView.isVisible = entryList.isEmpty() emptyView.isVisible = entryList.isEmpty()
@ -524,33 +533,6 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
Timber.i("Processed list") 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> { internal fun getSelectedSongs(): List<Track> {
// Walk through selected set and get the Entries based on the saved ids. // Walk through selected set and get the Entries based on the saved ids.
return viewAdapter.getCurrentList().mapNotNull { return viewAdapter.getCurrentList().mapNotNull {
@ -571,7 +553,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
@Suppress("LongMethod") @Suppress("LongMethod")
override fun getLiveData( override fun getLiveData(
refresh: Boolean refresh: Boolean,
append: Boolean
): LiveData<List<MusicDirectory.Child>> { ): LiveData<List<MusicDirectory.Child>> {
Timber.i("Starting gathering track collection data...") Timber.i("Starting gathering track collection data...")
val id = navArgs.id val id = navArgs.id
@ -584,11 +567,11 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
val shareName = navArgs.shareName val shareName = navArgs.shareName
val genreName = navArgs.genreName val genreName = navArgs.genreName
val getStarredTracks = navArgs.getStarred val getStarredTracks = displayStarred()
val getVideos = navArgs.getVideos val getVideos = navArgs.getVideos
val getRandomTracks = navArgs.getRandom val getRandomTracks = displayRandom()
val albumListSize = navArgs.size val size = if (navArgs.size < 0) Settings.maxSongs else navArgs.size
val albumListOffset = navArgs.offset val offset = offset ?: navArgs.offset
val refresh2 = navArgs.refresh || refresh val refresh2 = navArgs.refresh || refresh
listModel.viewModelScope.launch(handler) { listModel.viewModelScope.launch(handler) {
@ -605,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
listModel.getShare(shareId) listModel.getShare(shareId)
} else if (genreName != null) { } else if (genreName != null) {
setTitle(genreName) setTitle(genreName)
listModel.getSongsForGenre(genreName, albumListSize, albumListOffset) listModel.getSongsForGenre(genreName, size, offset, append)
} else if (getStarredTracks) { } else if (getStarredTracks) {
setTitle(getString(R.string.main_songs_starred)) setTitle(getString(R.string.main_songs_starred))
listModel.getStarred() listModel.getStarred()
@ -614,7 +597,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
listModel.getVideos(refresh2) listModel.getVideos(refresh2)
} else if (getRandomTracks) { } else if (getRandomTracks) {
setTitle(R.string.main_songs_random) setTitle(R.string.main_songs_random)
listModel.getRandom(albumListSize) listModel.getRandom(size, append)
} else { } else {
setTitle(name) setTitle(name)
if (ActiveServerProvider.isID3Enabled()) { if (ActiveServerProvider.isID3Enabled()) {
@ -633,6 +616,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
return listModel.currentList return listModel.currentList
} }
private fun displayStarred() = (sortOrder == SortOrder.STARRED) || navArgs.getStarred
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onContextMenuItemSelected( override fun onContextMenuItemSelected(
menuItem: MenuItem, menuItem: MenuItem,
@ -703,7 +690,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
override fun onItemClick(item: MusicDirectory.Child) { override fun onItemClick(item: MusicDirectory.Child) {
when { when {
item.isDirectory -> { item.isDirectory -> {
val action = TrackCollectionFragmentDirections.loadMoreTracks( val action = NavigationGraphDirections.toTrackCollection(
id = item.id, id = item.id,
isAlbum = true, isAlbum = true,
name = item.title, 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.ArrayAdapter
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.ListView import android.widget.ListView
@ -30,6 +31,7 @@ import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale import java.util.Locale
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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.LoadingTask
import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.view.PlaylistAdapter
/** /**
* Displays the playlists stored on the server * Displays the playlists stored on the server
@ -56,11 +57,14 @@ class PlaylistsFragment : Fragment() {
private var refreshPlaylistsListView: SwipeRefreshLayout? = null private var refreshPlaylistsListView: SwipeRefreshLayout? = null
private var playlistsListView: ListView? = null private var playlistsListView: ListView? = null
private var emptyTextView: View? = null private var emptyTextView: View? = null
private var playlistAdapter: PlaylistAdapter? = null private var playlistAdapter: ArrayAdapter<Playlist>? = null
private val downloadHandler = inject<DownloadHandler>( private val downloadHandler = inject<DownloadHandler>(
DownloadHandler::class.java DownloadHandler::class.java
) )
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context) applyTheme(this.context)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -83,7 +87,7 @@ class PlaylistsFragment : Fragment() {
playlistsListView!!.setOnItemClickListener { parent, _, position, _ -> playlistsListView!!.setOnItemClickListener { parent, _, position, _ ->
val (id1, name) = parent.getItemAtPosition(position) as Playlist val (id1, name) = parent.getItemAtPosition(position) as Playlist
val action = PlaylistsFragmentDirections.playlistsToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
id = id1, id = id1,
playlistId = id1, playlistId = id1,
name = name, name = name,
@ -116,7 +120,7 @@ class PlaylistsFragment : Fragment() {
override fun done(result: List<Playlist>) { override fun done(result: List<Playlist>) {
playlistsListView!!.adapter = 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 emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE
} }
} }
@ -183,7 +187,7 @@ class PlaylistsFragment : Fragment() {
) )
} }
R.id.playlist_menu_play_now -> { R.id.playlist_menu_play_now -> {
val action = PlaylistsFragmentDirections.playlistsToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
playlistId = playlist.id, playlistId = playlist.id,
playlistName = playlist.name, playlistName = playlist.name,
autoPlay = true autoPlay = true
@ -191,7 +195,7 @@ class PlaylistsFragment : Fragment() {
findNavController().navigate(action) findNavController().navigate(action)
} }
R.id.playlist_menu_play_shuffled -> { R.id.playlist_menu_play_shuffled -> {
val action = PlaylistsFragmentDirections.playlistsToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
playlistId = playlist.id, playlistId = playlist.id,
playlistName = playlist.name, playlistName = playlist.name,
autoPlay = true, autoPlay = true,

View File

@ -7,15 +7,16 @@
package org.moire.ultrasonic.fragment.legacy package org.moire.ultrasonic.fragment.legacy
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView import android.widget.ListView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.PodcastsChannel import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle 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.CancellationToken
import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.view.PodcastsChannelsAdapter
/** /**
* Displays the podcasts available on the server * Displays the podcasts available on the server
* *
* TODO: This file has been converted from Java, but not modernized yet. * TODO: This file has been converted from Java, but not modernized yet.
* TODO: Use Coroutines
*/ */
class PodcastFragment : Fragment() { class PodcastFragment : Fragment() {
private var emptyTextView: View? = null private var emptyTextView: View? = null
var channelItemsListView: ListView? = null var channelItemsListView: ListView? = null
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
private var swipeRefresh: SwipeRefreshLayout? = null private var swipeRefresh: SwipeRefreshLayout? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context) applyTheme(this.context)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -53,19 +56,19 @@ class PodcastFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken() cancellationToken = CancellationToken()
swipeRefresh = view.findViewById(R.id.podcasts_refresh) swipeRefresh = view.findViewById(R.id.podcasts_refresh)
swipeRefresh!!.setOnRefreshListener { load(view.context, true) } swipeRefresh!!.setOnRefreshListener { load(true) }
setTitle(this, R.string.podcasts_label) setTitle(this, R.string.podcasts_label)
emptyTextView = view.findViewById(R.id.select_podcasts_empty) emptyTextView = view.findViewById(R.id.select_podcasts_empty)
channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list) channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list)
channelItemsListView!!.setOnItemClickListener { parent, _, position, _ -> channelItemsListView!!.setOnItemClickListener { parent, _, position, _ ->
val (id) = parent.getItemAtPosition(position) as PodcastsChannel val (id) = parent.getItemAtPosition(position) as PodcastsChannel
val action = PodcastFragmentDirections.podcastToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
podcastChannelId = id podcastChannelId = id
) )
findNavController().navigate(action) findNavController().navigate(action)
} }
load(view.context, false) load(false)
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -73,7 +76,7 @@ class PodcastFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
} }
private fun load(context: Context, refresh: Boolean) { private fun load(refresh: Boolean) {
val task: BackgroundTask<List<PodcastsChannel>> = val task: BackgroundTask<List<PodcastsChannel>> =
object : FragmentBackgroundTask<List<PodcastsChannel>>( object : FragmentBackgroundTask<List<PodcastsChannel>>(
activity, true, swipeRefresh, cancellationToken activity, true, swipeRefresh, cancellationToken
@ -85,7 +88,8 @@ class PodcastFragment : Fragment() {
} }
override fun done(result: List<PodcastsChannel>) { 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 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.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
@ -58,13 +59,14 @@ class SelectGenreFragment : Fragment() {
refreshGenreListView = view.findViewById(R.id.select_genre_refresh) refreshGenreListView = view.findViewById(R.id.select_genre_refresh)
genreListView = view.findViewById(R.id.select_genre_list) genreListView = view.findViewById(R.id.select_genre_list)
refreshGenreListView!!.setOnRefreshListener { load(true) } refreshGenreListView!!.setOnRefreshListener { load(true) }
genreListView!!.setOnItemClickListener { parent: AdapterView<*>, genreListView!!.setOnItemClickListener { parent: AdapterView<*>,
_: View?, _: View?,
position: Int, position: Int,
_: Long -> _: Long ->
val genre = parent.getItemAtPosition(position) as Genre val genre = parent.getItemAtPosition(position) as Genre
val action = SelectGenreFragmentDirections.selectGenreToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
genreName = genre.name, genreName = genre.name,
size = maxSongs, size = maxSongs,
offset = 0 offset = 0
@ -82,6 +84,7 @@ class SelectGenreFragment : Fragment() {
super.onDestroyView() super.onDestroyView()
} }
// TODO: Migrate to Coroutines
private fun load(refresh: Boolean) { private fun load(refresh: Boolean) {
val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>( val task: BackgroundTask<List<Genre>> = object : FragmentBackgroundTask<List<Genre>>(
activity, true, refreshGenreListView, cancellationToken activity, true, refreshGenreListView, cancellationToken

View File

@ -29,6 +29,7 @@ import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale import java.util.Locale
import org.koin.java.KoinJavaComponent import org.koin.java.KoinJavaComponent
import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Share
@ -82,7 +83,7 @@ class SharesFragment : Fragment() {
AdapterView.OnItemClickListener { parent, _, position, _ -> AdapterView.OnItemClickListener { parent, _, position, _ ->
val share = parent.getItemAtPosition(position) as Share val share = parent.getItemAtPosition(position) as Share
val action = SharesFragmentDirections.sharesToTrackCollection( val action = NavigationGraphDirections.toTrackCollection(
shareId = share.id, shareId = share.id,
shareName = share.name 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 { override fun showSelectFolderHeader(): Boolean {
val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) || val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) ||
(lastType == AlbumListType.SORTED_BY_ARTIST) (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) { withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService() val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getSongsByGenre(genre, count, offset) 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) { withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService() val service = MusicServiceFactory.getMusicService()
@ -102,7 +102,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
currentListIsSortable = false currentListIsSortable = false
updateList(musicDirectory) updateList(musicDirectory, append)
} }
} }
@ -158,8 +158,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
} }
} }
private fun updateList(root: MusicDirectory) { private fun updateList(root: MusicDirectory, append: Boolean = false) {
currentList.postValue(root.getChildren()) val newList = if (append) {
currentList.value!! + root.getChildren()
} else {
root.getChildren()
}
currentList.postValue(newList)
} }
@Synchronized @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.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media.utils.MediaConstants
import androidx.media3.common.HeartRating import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
@ -19,9 +18,15 @@ import java.text.DateFormat
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.provider.AlbumArtContentProvider 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 { object MediaItemConverter {
private const val CACHE_SIZE = 250 private const val CACHE_SIZE = 250
private const val CACHE_EXPIRY_MINUTES = 10L private const val CACHE_EXPIRY_MINUTES = 10L
val mediaItemCache: LRUCache<String, TimeLimitedCache<MediaItem>> = LRUCache(CACHE_SIZE) val mediaItemCache: LRUCache<String, TimeLimitedCache<MediaItem>> = LRUCache(CACHE_SIZE)
val trackCache: LRUCache<String, TimeLimitedCache<Track>> = LRUCache(CACHE_SIZE) val trackCache: LRUCache<String, TimeLimitedCache<Track>> = LRUCache(CACHE_SIZE)
@ -233,7 +238,7 @@ fun buildMediaItem(
metadataBuilder.setExtras( metadataBuilder.setExtras(
Bundle().apply { Bundle().apply {
putString( putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
group group
) )
} }

View File

@ -19,6 +19,10 @@ import org.moire.ultrasonic.app.UApp
*/ */
object Settings { object Settings {
@JvmStatic
val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext())
@JvmStatic @JvmStatic
var theme by StringSetting( var theme by StringSetting(
getKey(R.string.setting_key_theme), getKey(R.string.setting_key_theme),
@ -241,10 +245,6 @@ object Settings {
@JvmStatic @JvmStatic
var debugLogToFile by BooleanSetting(getKey(R.string.setting_key_debug_log_to_file), false) var debugLogToFile by BooleanSetting(getKey(R.string.setting_key_debug_log_to_file), false)
@JvmStatic
val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext())
@JvmStatic @JvmStatic
val overrideLanguage by StringSetting(getKey(R.string.setting_key_override_language), "") val overrideLanguage by StringSetting(getKey(R.string.setting_key_override_language), "")
@ -267,11 +267,11 @@ object Settings {
false false
) )
// TODO: Remove in December 2022 @JvmStatic
fun migrateFeatureStorage() { var lastViewType by IntSetting(
val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE) getKey(R.string.setting_key_last_view_type),
useFiveStarRating = sp.getBoolean("FIVE_STAR_RATING", false) 0
} )
fun hasKey(key: String): Boolean { fun hasKey(key: String): Boolean {
return preferences.contains(key) 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"> android:viewportHeight="100">
<path <path
android:pathData="M0,0h100v100h-100z" android:pathData="M0,0h100v100h-100z"
android:fillColor="#1a1a1a"/> android:fillColor="?attr/colorSecondary"/>
<path <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: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:strokeWidth="0.85"
android:fillColor="#ffffff"/> android:fillColor="?attr/colorOnSecondary"/>
</vector> </vector>

View File

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

View File

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

View File

@ -1,46 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?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" <RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="vertical" xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content"> a:layout_height="wrap_content"
a:orientation="vertical">
<TextView <TextView
a:id="@+id/equalizer_frequency" a:id="@+id/equalizer_frequency"
a:textSize="12sp" a:layout_width="wrap_content"
a:textColor="#c0c0c0" a:layout_height="wrap_content"
a:layout_width="wrap_content" a:layout_alignParentLeft="true"
a:layout_height="wrap_content" a:layout_marginTop="8dp"
a:layout_marginTop="8dp" a:textColor="#c0c0c0"
a:layout_alignParentLeft="true" a:textSize="12sp" />
/>
<TextView <TextView
a:id="@+id/equalizer_level" a:id="@+id/equalizer_level"
a:text="0 dB" a:layout_width="wrap_content"
a:textSize="12sp" a:layout_height="wrap_content"
a:textColor="#c0c0c0" a:layout_alignParentRight="true"
a:gravity="right" a:layout_marginTop="8dp"
a:layout_width="wrap_content" a:layout_toEndOf="@+id/equalizer_frequency"
a:layout_height="wrap_content" a:gravity="right"
a:layout_marginTop="8dp" a:textColor="#c0c0c0"
a:layout_alignParentRight="true" a:textSize="12sp"
a:layout_toEndOf="@+id/equalizer_frequency" tools:text="0 dB" />
/>
<SeekBar <SeekBar
a:id="@+id/equalizer_bar" a:id="@+id/equalizer_bar"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:layout_below="@+id/equalizer_frequency" a:layout_below="@+id/equalizer_frequency" />
/>
</RelativeLayout> </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"> a:focusable="true">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
a:id="@+id/coverart" a:id="@+id/cover_art"
a:layout_width="64dp" a:layout_width="64dp"
a:layout_height="64dp" a:layout_height="64dp"
a:layout_gravity="center_horizontal|center_vertical" a:layout_gravity="center_horizontal|center_vertical"
@ -20,7 +20,7 @@
a:src="@drawable/unknown_album" a:src="@drawable/unknown_album"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/largeRoundedImageView" /> app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.SmallComponent" />
<LinearLayout <LinearLayout
a:id="@+id/row_album_details" a:id="@+id/row_album_details"
@ -35,8 +35,8 @@
a:paddingEnd="3dip" a:paddingEnd="3dip"
a:textAppearance="?android:attr/textAppearanceMedium" a:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintEnd_toStartOf="@+id/album_star" app:layout_constraintEnd_toStartOf="@+id/album_star"
app:layout_constraintLeft_toRightOf="@+id/coverart" app:layout_constraintLeft_toRightOf="@+id/cover_art"
app:layout_constraintStart_toEndOf="@+id/coverart" app:layout_constraintStart_toEndOf="@+id/cover_art"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<TextView <TextView
@ -61,6 +61,7 @@
<ImageView <ImageView
a:id="@+id/album_star" a:id="@+id/album_star"
a:layout_width="38dp" a:layout_width="38dp"
a:padding="4dp"
a:layout_height="38dp" a:layout_height="38dp"
a:layout_marginStart="16dp" a:layout_marginStart="16dp"
a:layout_marginTop="16dp" a:layout_marginTop="16dp"

View File

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

View File

@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:a="http://schemas.android.com/apk/res/android" <TextView xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:id="@android:id/text1" a:id="@android:id/text1"
a:drawablePadding="6dip"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content" a:layout_height="56dp"
a:textAppearance="?android:attr/textAppearanceMedium" a:drawablePadding="6dip"
a:gravity="center_vertical" a:gravity="center_vertical"
a:paddingStart="3dip" a:paddingStart="16dip"
a:paddingEnd="3dip" a:paddingTop="8dp"
a:minHeight="50dip"/> 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:layout_gravity="start"
a:fitsSystemWindows="true" a:fitsSystemWindows="true"
app:headerLayout="@layout/navigation_header" app:headerLayout="@layout/navigation_header"
app:menu="@menu/navigation"/> app:menu="@menu/navigation_drawer"/>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="match_parent" a:layout_width="match_parent"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:orientation="vertical" a:orientation="vertical"
@ -33,7 +34,8 @@
a:layout_alignParentRight="true" a:layout_alignParentRight="true"
a:layout_marginEnd="12dip" a:layout_marginEnd="12dip"
a:text="@string/util.no_time" a:text="@string/util.no_time"
a:textAppearance="?android:attr/textAppearanceSmall" /> a:textAppearance="?android:attr/textAppearanceSmall"
tools:ignore="RelativeOverlap" />
</RelativeLayout> </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"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="fill_parent" a:layout_height="fill_parent"
a:orientation="vertical" > a:orientation="vertical">
<TextView <TextView
a:id="@+id/select_genre_empty" a:id="@+id/select_genre_empty"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:padding="10dip" a:padding="16dip"
a:text="@string/select_genre.empty" 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 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/select_genre_refresh" android:id="@+id/select_genre_refresh"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dip" android:layout_height="0dip"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,14 +4,10 @@
a:checkableBehavior="none" a:checkableBehavior="none"
a:enabled="true" a:enabled="true"
a:visible="true"> a:visible="true">
<item <item
a:id="@+id/mainFragment" a:id="@+id/mainFragment"
a:checkable="true" 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:icon="@drawable/ic_menu_browse"
a:title="@string/button_bar.browse" /> a:title="@string/button_bar.browse" />
<item <item
@ -19,11 +15,6 @@
a:checkable="true" a:checkable="true"
a:icon="@drawable/ic_menu_search" a:icon="@drawable/ic_menu_search"
a:title="@string/button_bar.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 <item
a:id="@+id/downloadsFragment" a:id="@+id/downloadsFragment"
a:checkable="true" a:checkable="true"
@ -44,16 +35,21 @@
a:checkable="true" a:checkable="true"
a:icon="@drawable/ic_menu_chat" a:icon="@drawable/ic_menu_chat"
a:title="@string/button_bar.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 <item
a:id="@+id/podcastFragment" a:id="@+id/podcastFragment"
a:checkable="true" a:checkable="true"
a:icon="@drawable/ic_menu_podcasts" a:icon="@drawable/ic_menu_podcasts"
a:title="@string/button_bar.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> </group>
<item a:title="@string/menu.common"> <item a:title="@string/menu.common">
<menu> <menu>

View File

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

View File

@ -13,13 +13,11 @@
<string name="button_bar.bookmarks">Záložky</string> <string name="button_bar.bookmarks">Záložky</string>
<string name="button_bar.browse">Knihovna médií</string> <string name="button_bar.browse">Knihovna médií</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Právě hraje</string>
<string name="buttons.shuffle">Náhodně</string> <string name="buttons.shuffle">Náhodně</string>
<string name="podcasts.label">Podcasty</string> <string name="podcasts.label">Podcasty</string>
<string name="podcasts_channels.empty">Není registrován žádný podcast kanál</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.podcasts">Podcasty</string>
<string name="button_bar.playlists">Playlisty</string>
<string name="button_bar.search">Hledat</string> <string name="button_bar.search">Hledat</string>
<string name="chat.send_a_message">Poslat zprávu</string> <string name="chat.send_a_message">Poslat zprávu</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Alba</string> <string name="main.albums_title">Alba</string>
<string name="main.artists_title">Umělci</string> <string name="main.artists_title">Umělci</string>
<string name="main.genres_title">Žánry</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.offline">Bez připojení</string>
<string name="main.songs_random">Náhodné</string> <string name="main.songs_random">Náhodné</string>
<string name="main.songs_starred">Označené hvězdičkou</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="share_via">Sdílet skladby přes</string>
<string name="menu.share">Sdílení</string> <string name="menu.share">Sdílení</string>
<string name="download.menu_show_artist">Zobrazit umělce</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="common_multiple_years">Vícenásobné roky</string>
<string name="settings.debug.title">Možnosti ladění aplikace</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> <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.bookmarks">Lesezeichen</string>
<string name="button_bar.browse">Medienbibliothek</string> <string name="button_bar.browse">Medienbibliothek</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Aktuelle Wiedergabe</string>
<string name="buttons.play">Abspielen</string> <string name="buttons.play">Abspielen</string>
<string name="buttons.pause">Pause</string> <string name="buttons.pause">Pause</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Keine Podcast Kanäle registriert</string> <string name="podcasts_channels.empty">Keine Podcast Kanäle registriert</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Wiedergabeliste</string>
<string name="button_bar.search">Suche</string> <string name="button_bar.search">Suche</string>
<string name="chat.send_a_message">Nachricht senden</string> <string name="chat.send_a_message">Nachricht senden</string>
<string name="chat.send_button">Senden</string> <string name="chat.send_button">Senden</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Alben</string> <string name="main.albums_title">Alben</string>
<string name="main.artists_title">Künstler*innen</string> <string name="main.artists_title">Künstler*innen</string>
<string name="main.genres_title">Genres</string> <string name="main.genres_title">Genres</string>
<string name="main.music">Musik</string>
<string name="main.offline">Offline</string> <string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Server einrichten</string> <string name="main.setup_server">%s - Server einrichten</string>
<string name="main.songs_random">Zufällig</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.bookmarks">Marcadores</string>
<string name="button_bar.browse">Biblioteca</string> <string name="button_bar.browse">Biblioteca</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Reproduciendo ahora</string>
<string name="buttons.play">Reproducir</string> <string name="buttons.play">Reproducir</string>
<string name="buttons.pause">Pausar</string> <string name="buttons.pause">Pausar</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No hay canales de Podcasts registrados</string> <string name="podcasts_channels.empty">No hay canales de Podcasts registrados</string>
<string name="button_bar.podcasts">Podcast</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="button_bar.search">Buscar</string>
<string name="chat.send_a_message">Enviar un mensaje</string> <string name="chat.send_a_message">Enviar un mensaje</string>
<string name="chat.send_button">Enviar</string> <string name="chat.send_button">Enviar</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Álbumes</string> <string name="main.albums_title">Álbumes</string>
<string name="main.artists_title">Artistas</string> <string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Géneros</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.offline">Sin conexión</string>
<string name="main.setup_server">%s - Configurar servidor</string> <string name="main.setup_server">%s - Configurar servidor</string>
<string name="main.songs_random">Aleatorio</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.bookmarks">Signets</string>
<string name="button_bar.browse">Bibliothèque musicale</string> <string name="button_bar.browse">Bibliothèque musicale</string>
<string name="button_bar.chat">Salon de discussion</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="button_bar.now_playing">Lecture en cours</string>
<string name="buttons.play">Lecture</string> <string name="buttons.play">Lecture</string>
<string name="buttons.pause">Pause</string> <string name="buttons.pause">Pause</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Aucune chaîne de podcast enregistrée</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.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Recherche</string> <string name="button_bar.search">Recherche</string>
<string name="chat.send_a_message">Envoyer un message</string> <string name="chat.send_a_message">Envoyer un message</string>
<string name="chat.send_button">Envoyer</string> <string name="chat.send_button">Envoyer</string>
@ -117,7 +115,6 @@
<string name="main.albums_title">Albums</string> <string name="main.albums_title">Albums</string>
<string name="main.artists_title">Artistes</string> <string name="main.artists_title">Artistes</string>
<string name="main.genres_title">Genres</string> <string name="main.genres_title">Genres</string>
<string name="main.music">Musique</string>
<string name="main.offline">Hors-ligne</string> <string name="main.offline">Hors-ligne</string>
<string name="main.setup_server">%s - Configurer le serveur</string> <string name="main.setup_server">%s - Configurer le serveur</string>
<string name="main.songs_random">Aléatoire</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.bookmarks">Könyvjelzők</string>
<string name="button_bar.browse">Médiakönyvtár</string> <string name="button_bar.browse">Médiakönyvtár</string>
<string name="button_bar.chat">Csevegés (Chat)</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="button_bar.now_playing">Lejátszó</string>
<string name="buttons.play">Lejátszás</string> <string name="buttons.play">Lejátszás</string>
<string name="buttons.pause">Szünet</string> <string name="buttons.pause">Szünet</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Nincsenek podcast-csatornák regisztrálva</string> <string name="podcasts_channels.empty">Nincsenek podcast-csatornák regisztrálva</string>
<string name="button_bar.podcasts">Podcast</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="button_bar.search">Keresés</string>
<string name="chat.send_a_message">Üzenet küldése</string> <string name="chat.send_a_message">Üzenet küldése</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
@ -93,7 +91,6 @@
<string name="main.albums_title">Albumok</string> <string name="main.albums_title">Albumok</string>
<string name="main.artists_title">Előadók</string> <string name="main.artists_title">Előadók</string>
<string name="main.genres_title">Műfajok</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.offline">Kapcsolat nélküli</string>
<string name="main.songs_random">Véletlenszerű</string> <string name="main.songs_random">Véletlenszerű</string>
<string name="main.songs_starred">Csillaggal megjelölt</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.label">Podcast</string>
<string name="podcasts_channels.empty">Nessun canale podcast registrato</string> <string name="podcasts_channels.empty">Nessun canale podcast registrato</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Cerca</string> <string name="button_bar.search">Cerca</string>
<string name="chat.send_a_message">Spedisci un messaggio</string> <string name="chat.send_a_message">Spedisci un messaggio</string>
<string name="common.cancel">Annulla</string> <string name="common.cancel">Annulla</string>
@ -84,7 +83,6 @@
<string name="main.albums_title">Album</string> <string name="main.albums_title">Album</string>
<string name="main.artists_title">Artisti</string> <string name="main.artists_title">Artisti</string>
<string name="main.genres_title">Generi</string> <string name="main.genres_title">Generi</string>
<string name="main.music">Musica</string>
<string name="main.offline">Disconnesso</string> <string name="main.offline">Disconnesso</string>
<string name="main.songs_random">Casuale</string> <string name="main.songs_random">Casuale</string>
<string name="main.songs_starred">Preferiti</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.bookmarks">Bladwijzers</string>
<string name="button_bar.browse">Mediabibliotheek</string> <string name="button_bar.browse">Mediabibliotheek</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Nu aan het afspelen</string>
<string name="buttons.play">Afspelen</string> <string name="buttons.play">Afspelen</string>
<string name="buttons.pause">Pauzeren</string> <string name="buttons.pause">Pauzeren</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">Geen podcastkanalen opgegeven</string> <string name="podcasts_channels.empty">Geen podcastkanalen opgegeven</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Afspeellijsten</string>
<string name="button_bar.search">Zoeken</string> <string name="button_bar.search">Zoeken</string>
<string name="chat.send_a_message">Bericht versturen</string> <string name="chat.send_a_message">Bericht versturen</string>
<string name="chat.send_button">Versturen</string> <string name="chat.send_button">Versturen</string>
@ -119,7 +117,6 @@
<string name="main.albums_title">Albums</string> <string name="main.albums_title">Albums</string>
<string name="main.artists_title">Artiesten</string> <string name="main.artists_title">Artiesten</string>
<string name="main.genres_title">Genres</string> <string name="main.genres_title">Genres</string>
<string name="main.music">Muziek</string>
<string name="main.offline">Offline</string> <string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Server instellen</string> <string name="main.setup_server">%s - Server instellen</string>
<string name="main.songs_random">Willekeurig</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.bookmarks">Zakładki</string>
<string name="button_bar.browse">Biblioteka</string> <string name="button_bar.browse">Biblioteka</string>
<string name="button_bar.chat">Czat</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="button_bar.now_playing">Teraz gra</string>
<string name="buttons.shuffle">Wymieszaj</string> <string name="buttons.shuffle">Wymieszaj</string>
<string name="podcasts.label">Podcasty</string> <string name="podcasts.label">Podcasty</string>
<string name="podcasts_channels.empty">Brak kanałów</string> <string name="podcasts_channels.empty">Brak kanałów</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlisty</string>
<string name="button_bar.search">Szukaj</string> <string name="button_bar.search">Szukaj</string>
<string name="chat.send_a_message">Wyślij wiadomość</string> <string name="chat.send_a_message">Wyślij wiadomość</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Albumy</string> <string name="main.albums_title">Albumy</string>
<string name="main.artists_title">Artyści</string> <string name="main.artists_title">Artyści</string>
<string name="main.genres_title">Gatunki</string> <string name="main.genres_title">Gatunki</string>
<string name="main.music">Muzyka</string>
<string name="main.offline">Offline</string> <string name="main.offline">Offline</string>
<string name="main.songs_random">Losowe</string> <string name="main.songs_random">Losowe</string>
<string name="main.songs_starred">Ulubione</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.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string> <string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Tocando Agora</string>
<string name="buttons.play">Tocar</string> <string name="buttons.play">Tocar</string>
<string name="buttons.pause">Pausar</string> <string name="buttons.pause">Pausar</string>
@ -25,7 +24,6 @@
<string name="podcasts.label">Podcasts</string> <string name="podcasts.label">Podcasts</string>
<string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string> <string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string>
<string name="button_bar.podcasts">Podcasts</string> <string name="button_bar.podcasts">Podcasts</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Pesquisa</string> <string name="button_bar.search">Pesquisa</string>
<string name="chat.send_a_message">Enviar uma mensagem</string> <string name="chat.send_a_message">Enviar uma mensagem</string>
<string name="common.album">Álbum</string> <string name="common.album">Álbum</string>
@ -113,7 +111,6 @@
<string name="main.albums_title">Álbuns</string> <string name="main.albums_title">Álbuns</string>
<string name="main.artists_title">Artistas</string> <string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Gêneros</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.offline">Offline</string>
<string name="main.setup_server">%s - Configurar Servidor</string> <string name="main.setup_server">%s - Configurar Servidor</string>
<string name="main.songs_random">Aleatórias</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.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string> <string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Tocando Agora</string>
<string name="buttons.shuffle">Misturar</string> <string name="buttons.shuffle">Misturar</string>
<string name="podcasts.label">Podcasts</string> <string name="podcasts.label">Podcasts</string>
<string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string> <string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Pesquisa</string> <string name="button_bar.search">Pesquisa</string>
<string name="chat.send_a_message">Enviar uma mensagem</string> <string name="chat.send_a_message">Enviar uma mensagem</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
@ -87,7 +85,6 @@
<string name="main.albums_title">Álbuns</string> <string name="main.albums_title">Álbuns</string>
<string name="main.artists_title">Artistas</string> <string name="main.artists_title">Artistas</string>
<string name="main.genres_title">Gêneros</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.offline">Offline</string>
<string name="main.songs_random">Aleatórias</string> <string name="main.songs_random">Aleatórias</string>
<string name="main.songs_starred">Favoritas</string> <string name="main.songs_starred">Favoritas</string>

View File

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

View File

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

View File

@ -5,7 +5,6 @@
<string name="button_bar.bookmarks">書籤</string> <string name="button_bar.bookmarks">書籤</string>
<string name="button_bar.browse">媒體庫</string> <string name="button_bar.browse">媒體庫</string>
<string name="button_bar.now_playing">正在播放</string> <string name="button_bar.now_playing">正在播放</string>
<string name="button_bar.playlists">播放清單</string>
<string name="button_bar.search">搜尋</string> <string name="button_bar.search">搜尋</string>
<string name="common.cancel">取消</string> <string name="common.cancel">取消</string>
<string name="common.comment">註記</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.override_language" translatable="false">overrideLanguage</string>
<string name="setting_key.first_installed_version" translatable="false">firstInstalledVersion</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.show_confirmation_dialog" translatable="false">showConfirmationDialog</string>
<string name="setting_key.last_view_type" translatable="false">lastViewType</string>
</resources> </resources>

View File

@ -13,7 +13,6 @@
<string name="button_bar.bookmarks">Bookmarks</string> <string name="button_bar.bookmarks">Bookmarks</string>
<string name="button_bar.browse">Media Library</string> <string name="button_bar.browse">Media Library</string>
<string name="button_bar.chat">Chat</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="button_bar.now_playing">Now Playing</string>
<string name="buttons.play">Play</string> <string name="buttons.play">Play</string>
<string name="buttons.pause">Pause</string> <string name="buttons.pause">Pause</string>
@ -25,10 +24,10 @@
<string name="podcasts.label">Podcast</string> <string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No podcasts channels registered</string> <string name="podcasts_channels.empty">No podcasts channels registered</string>
<string name="button_bar.podcasts">Podcast</string> <string name="button_bar.podcasts">Podcast</string>
<string name="button_bar.playlists">Playlists</string>
<string name="button_bar.search">Search</string> <string name="button_bar.search">Search</string>
<string name="chat.send_a_message">Send a message</string> <string name="chat.send_a_message">Send a message</string>
<string name="chat.send_button">Send</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.album">Album</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
<string name="common.artist">Artist</string> <string name="common.artist">Artist</string>
@ -116,10 +115,10 @@
<string name="main.albums_random">Random</string> <string name="main.albums_random">Random</string>
<string name="main.albums_recent">Recently Played</string> <string name="main.albums_recent">Recently Played</string>
<string name="main.albums_starred">Starred</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.albums_title">Albums</string>
<string name="main.artists_title">Artists</string> <string name="main.artists_title">Artists</string>
<string name="main.genres_title">Genres</string> <string name="main.genres_title">Genres</string>
<string name="main.music">Music</string>
<string name="main.offline">Offline</string> <string name="main.offline">Offline</string>
<string name="main.setup_server">%s - Set up Server</string> <string name="main.setup_server">%s - Set up Server</string>
<string name="main.songs_random">Random</string> <string name="main.songs_random">Random</string>
@ -370,7 +369,7 @@
<string name="share_via">Share songs via</string> <string name="share_via">Share songs via</string>
<string name="menu.share">Share</string> <string name="menu.share">Share</string>
<string name="download.menu_show_artist">Show Artist</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="common_multiple_years">Multiple Years</string>
<string name="settings.show_confirmation_dialog">Show confirmation dialog</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> <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_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="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> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Ultrasonic.AllCapsLabel" parent=""> <style name="Ultrasonic.AllCapsLabel" parent="">
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
@ -15,16 +15,6 @@
<item name="android:paddingStart">16dp</item> <item name="android:paddingStart">16dp</item>
</style> </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"> <style name="Widget.AppWidget.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item> <item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item> <item name="android:padding">?attr/appWidgetPadding</item>
@ -60,6 +50,11 @@
<item name="android:adjustViewBounds">true</item> <item name="android:adjustViewBounds">true</item>
</style> </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"> <style name="Theme.AppWidget.AppWidgetContainerCropped" parent="Theme.AppWidget.AppWidgetContainer">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners --> <!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">4dp</item> <item name="appWidgetPadding">4dp</item>