diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt index fa91d9b9..e0ef67fc 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt @@ -14,4 +14,8 @@ data class Playlist @JvmOverloads constructor( companion object { private const val serialVersionUID = -4160515427075433798L } + + override fun toString(): String { + return name + } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt index a589877e..39dcec3a 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt @@ -12,4 +12,6 @@ data class PodcastsChannel( companion object { private const val serialVersionUID = -4160515427075433798L } + + override fun toString(): String = name.toString() } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt index d2da7070..732de75d 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/ApiVersionCheckWrapper.kt @@ -1,3 +1,10 @@ +/* + * ApiVersionCheckWrapper.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.api.subsonic import okhttp3.ResponseBody diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index addafac5..18aeb0d4 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -129,13 +129,12 @@ class SubsonicAPIClient( this.addInterceptor(loggingInterceptor) } + @Suppress("CustomX509TrustManager", "TrustAllX509TrustManager") private fun OkHttpClient.Builder.allowSelfSignedCertificates() { val trustManager = - @Suppress("CustomX509TrustManager") + object : X509TrustManager { - @Suppress("TrustAllX509TrustManager") override fun checkClientTrusted(p0: Array?, p1: String?) {} - @Suppress("TrustAllX509TrustManager") override fun checkServerTrusted(p0: Array?, p1: String?) {} override fun getAcceptedIssuers(): Array = emptyArray() } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt index ad9b9d19..858632e2 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt @@ -1,3 +1,10 @@ +/* + * SubsonicAPIDefinition.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.api.subsonic import okhttp3.ResponseBody diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt index 8d399c2a..f30f389b 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListType.kt @@ -1,3 +1,10 @@ +/* + * AlbumListOrderType.kt + * Copyright (C) 2009-2022 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + package org.moire.ultrasonic.api.subsonic.models /** @@ -16,8 +23,7 @@ enum class AlbumListType(val typeName: String) { SORTED_BY_ARTIST("alphabeticalByArtist"), STARRED("starred"), BY_YEAR("byYear"), - BY_GENRE("byGenre"), - BY_ARTIST("albumsByArtist"); + BY_GENRE("byGenre"); override fun toString(): String { return typeName @@ -36,7 +42,6 @@ enum class AlbumListType(val typeName: String) { in STARRED.typeName -> STARRED in BY_YEAR.typeName -> BY_YEAR in BY_GENRE.typeName -> BY_GENRE - in BY_ARTIST.typeName -> BY_ARTIST else -> throw IllegalArgumentException("Unknown type: $typeName") } diff --git a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt index 932ee62f..93052b73 100644 --- a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt +++ b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt @@ -36,6 +36,7 @@ class AlbumListTypeTest { @Test fun `Should return type name for toString call`() { - AlbumListType.STARRED.typeName `should be equal to` AlbumListType.STARRED.toString() + AlbumListType.STARRED.typeName `should be equal to` + AlbumListType.STARRED.toString() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c04994f..ad4bc221 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,12 +9,10 @@ ktlint = "0.43.2" ktlintGradle = "11.0.0" detekt = "1.21.0" preferences = "1.2.0" -media = "1.6.0" media3 = "1.0.0-beta02" androidSupport = "1.5.0" -androidLegacySupport = "1.0.0" -androidSupportDesign = "1.6.1" +materialDesign = "1.6.1" constraintLayout = "2.1.4" multidex = "2.0.1" room = "2.4.3" @@ -22,6 +20,7 @@ kotlin = "1.7.20" kotlinxCoroutines = "1.6.4-native-mt" kotlinxGuava = "1.6.4" viewModelKtx = "2.5.1" +swipeRefresh = "1.1.0" retrofit = "2.9.0" jackson = "2.13.4" @@ -50,8 +49,7 @@ ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", ver detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" } -support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" } -design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" } +design = { module = "com.google.android.material:material", version.ref = "materialDesign" } annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" } multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" } @@ -66,11 +64,10 @@ navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", ve navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" } navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"} preferences = { module = "androidx.preference:preference", version.ref = "preferences" } -media = { module = "androidx.media:media", version.ref = "media" } media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" } media3session = { module = "androidx.media3:media3-session", version.ref = "media3" } - +swipeRefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefresh" } kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index 70a8a479..93919145 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -4,6 +4,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" +apply plugin: 'kotlin-kapt' android { compileSdkVersion versions.compileSdk diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 336a12c0..b873263a 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -97,7 +97,6 @@ dependencies { } implementation libs.core - implementation libs.support implementation libs.design implementation libs.multidex implementation libs.roomRuntime @@ -105,10 +104,10 @@ dependencies { implementation libs.viewModelKtx implementation libs.constraintLayout implementation libs.preferences - implementation libs.media implementation libs.media3exoplayer implementation libs.media3session implementation libs.media3okhttp + implementation libs.swipeRefresh implementation libs.navigationFragment implementation libs.navigationUi diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index 3bd04935..0f0ec704 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -8,7 +8,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -70,138 +70,6 @@ column="10"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistAdapter.java deleted file mode 100644 index bfa4c65e..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistAdapter.java +++ /dev/null @@ -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 -{ - - private final Context context; - - public PlaylistAdapter(Context context, List 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; - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java deleted file mode 100644 index 16c396ad..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PlaylistView.java +++ /dev/null @@ -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 . - - 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()); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java deleted file mode 100644 index 367d01f4..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastChannelItemView.java +++ /dev/null @@ -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 . - - 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()); - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastsChannelsAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastsChannelsAdapter.java deleted file mode 100644 index b821a197..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/PodcastsChannelsAdapter.java +++ /dev/null @@ -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 { - private final LayoutInflater layoutInflater; - - public PodcastsChannelsAdapter(Context context, List 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; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java index 497d8ce0..2df142a7 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/ShareAdapter.java @@ -16,7 +16,6 @@ import java.util.List; */ public class ShareAdapter extends ArrayAdapter { - private final Context context; public ShareAdapter(Context context, List Shares) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 9073d545..53e80438 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -52,7 +52,6 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSettingDao -import org.moire.ultrasonic.fragment.MainFragmentDirections import org.moire.ultrasonic.fragment.OnBackPressedHandler import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.provider.SearchSuggestionProvider @@ -72,11 +71,12 @@ import timber.log.Timber /** * The main (and only) Activity of Ultrasonic which loads all other screens as Fragments. - * Because this is the only Activity we have to manage the apps lifecycle through tis activitys + * Because this is the only Activity we have to manage the apps lifecycle through this activity * onCreate/onResume/onDestroy methods... */ @Suppress("TooManyFunctions") class NavigationActivity : AppCompatActivity() { + private var videoMenuItem: MenuItem? = null private var chatMenuItem: MenuItem? = null private var bookmarksMenuItem: MenuItem? = null private var sharesMenuItem: MenuItem? = null @@ -301,6 +301,13 @@ class NavigationActivity : AppCompatActivity() { R.id.bookmarksFragment -> { navController.navigate(NavigationGraphDirections.toBookmarks()) } + R.id.trackCollectionFragment -> { + navController.navigate( + NavigationGraphDirections.toTrackCollection( + getVideos = true + ) + ) + } R.id.menu_exit -> { setResult(Constants.RESULT_CLOSE_ALL) mediaPlayerController.onDestroy() @@ -319,6 +326,7 @@ class NavigationActivity : AppCompatActivity() { podcastsMenuItem = navigationView?.menu?.findItem(R.id.podcastFragment) playlistsMenuItem = navigationView?.menu?.findItem(R.id.playlistsFragment) downloadsMenuItem = navigationView?.menu?.findItem(R.id.downloadsFragment) + videoMenuItem = navigationView?.menu?.findItem(R.id.trackCollectionFragment) selectServerButton = navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server) @@ -348,7 +356,7 @@ class NavigationActivity : AppCompatActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { val retValue = super.onCreateOptionsMenu(menu) if (navigationView == null) { - menuInflater.inflate(R.menu.navigation, menu) + menuInflater.inflate(R.menu.navigation_drawer, menu) return true } return retValue @@ -390,7 +398,7 @@ class NavigationActivity : AppCompatActivity() { ) suggestions.saveRecentQuery(query, null) - val action = MainFragmentDirections.toSearchFragment(query, autoPlay) + val action = NavigationGraphDirections.toSearchFragment(query, autoPlay) findNavController(R.id.nav_host_fragment).navigate(action) } } @@ -498,5 +506,6 @@ class NavigationActivity : AppCompatActivity() { podcastsMenuItem?.isVisible = activeServer.podcastSupport != false playlistsMenuItem?.isVisible = isOnline downloadsMenuItem?.isVisible = isOnline + videoMenuItem?.isVisible = isOnline } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt similarity index 58% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt index e6769211..2f3fad98 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt @@ -7,6 +7,7 @@ package org.moire.ultrasonic.adapters +import android.content.Context import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -15,31 +16,31 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.drakeet.multitype.ItemViewBinder +import com.drakeet.multitype.ItemViewDelegate import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService +import org.moire.ultrasonic.util.LayoutType import org.moire.ultrasonic.util.Settings.shouldUseId3Tags import timber.log.Timber /** * Creates a Row in a RecyclerView which contains the details of an Album */ -class AlbumRowBinder( - val onItemClick: (Album) -> Unit, - val onContextMenuClick: (MenuItem, Album) -> Boolean, +open class AlbumRowDelegate( + open val onItemClick: (Album) -> Unit, + open val onContextMenuClick: (MenuItem, Album) -> Boolean, private val imageLoader: ImageLoader -) : ItemViewBinder(), KoinComponent { +) : ItemViewDelegate(), KoinComponent { private val starDrawable: Int = R.drawable.ic_star_full private val starHollowDrawable: Int = R.drawable.ic_star_hollow - // Set our layout files - val layout = R.layout.list_item_album + open var layoutType = LayoutType.LIST - override fun onBindViewHolder(holder: ViewHolder, item: Album) { + override fun onBindViewHolder(holder: ListViewHolder, item: Album) { holder.album.text = item.title holder.artist.text = item.artist holder.details.setOnClickListener { onItemClick(item) } @@ -66,15 +67,40 @@ class AlbumRowBinder( /** * Holds the view properties of an Item row */ - class ViewHolder( + open class ListViewHolder( view: View ) : RecyclerView.ViewHolder(view) { - var album: TextView = view.findViewById(R.id.album_title) - var artist: TextView = view.findViewById(R.id.album_artist) - var details: LinearLayout = view.findViewById(R.id.row_album_details) - var coverArt: ImageView = view.findViewById(R.id.coverart) - var star: ImageView = view.findViewById(R.id.album_star) + + var album: TextView + var artist: TextView + var details: LinearLayout + var coverArt: ImageView + var star: ImageView var coverArtId: String? = null + + constructor(parent: ViewGroup, inflater: LayoutInflater) : this( + inflater.inflate(R.layout.list_item_album, parent, false) + ) + + init { + album = view.findViewById(R.id.album_title) + artist = view.findViewById(R.id.album_artist) + details = view.findViewById(R.id.row_album_details) + coverArt = view.findViewById(R.id.cover_art) + star = view.findViewById(R.id.album_star) + coverArtId = null + } + } + + /** + * Holds the view properties of an Item row + */ + class CoverViewHolder( + view: View + ) : ListViewHolder(view) { + constructor(parent: ViewGroup, inflater: LayoutInflater) : this( + inflater.inflate(R.layout.grid_item_album, parent, false) + ) } /** @@ -106,7 +132,24 @@ class AlbumRowBinder( }.start() } - override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder { - return ViewHolder(inflater.inflate(layout, parent, false)) + override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder { + return when (layoutType) { + LayoutType.LIST -> ListViewHolder( + parent, + LayoutInflater.from(context) + ) + LayoutType.COVER -> CoverViewHolder( + parent, + LayoutInflater.from(context) + ) + } } } + +class AlbumGridDelegate( + onItemClick: (Album) -> Unit, + onContextMenuClick: (MenuItem, Album) -> Boolean, + imageLoader: ImageLoader +) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoader) { + override var layoutType = LayoutType.COVER +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index b85308f7..a0b77260 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -123,7 +123,7 @@ class ArtistRowBinder( var section: TextView = itemView.findViewById(R.id.row_section) var textView: TextView = itemView.findViewById(R.id.row_artist_name) var layout: RelativeLayout = itemView.findViewById(R.id.containing_layout) - var coverArt: ImageView = itemView.findViewById(R.id.coverart) + var coverArt: ImageView = itemView.findViewById(R.id.cover_art) var coverArtId: String? = null } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index f9c96edd..62612082 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -13,7 +13,6 @@ import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Track -import timber.log.Timber @Suppress("LongParameterList") class TrackViewBinder( @@ -63,7 +62,7 @@ class TrackViewBinder( diffAdapter.isSelected(item.longId) ) - Timber.v("Setting listeners") + // Timber.v("Setting listeners") holder.itemView.setOnLongClickListener { if (onContextMenuClick != null) { @@ -118,7 +117,7 @@ class TrackViewBinder( if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus } - Timber.v("Setting listeners done") + // Timber.v("Setting listeners done") } override fun onViewRecycled(holder: TrackViewHolder) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index a89f1390..630c00cf 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -81,7 +81,7 @@ class TrackViewHolder(val view: View) : draggable: Boolean, isSelected: Boolean = false ) { - Timber.v("Setting song") + // Timber.v("Setting song") val useFiveStarRating = Settings.useFiveStarRating entry = song @@ -139,7 +139,7 @@ class TrackViewHolder(val view: View) : updateStatus(it.state, it.progress) } - Timber.v("Setting song done") + // Timber.v("Setting song done") } // This is called when the Holder is recycled and receives a new Song diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index ea918f33..1cae1c5a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -10,24 +10,41 @@ package org.moire.ultrasonic.fragment import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R -import org.moire.ultrasonic.adapters.AlbumRowBinder +import org.moire.ultrasonic.adapters.AlbumGridDelegate +import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.api.subsonic.models.AlbumListType +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.model.AlbumListModel +import org.moire.ultrasonic.util.LayoutType +import org.moire.ultrasonic.util.Settings +import org.moire.ultrasonic.view.FilterButtonBar +import org.moire.ultrasonic.view.SortOrder +import org.moire.ultrasonic.view.ViewCapabilities /** * Displays a list of Albums from the media library */ -class AlbumListFragment : EntryListFragment() { +class AlbumListFragment( + private var layoutType: LayoutType = LayoutType.LIST, + private var orderType: SortOrder? = null +) : FilterableFragment, EntryListFragment() { + + private var filterButtonBar: FilterButtonBar? = null /** * The ViewModel to use to get the data @@ -50,7 +67,8 @@ class AlbumListFragment : EntryListFragment() { * The central function to pass a query to the model and return a LiveData object */ override fun getLiveData( - refresh: Boolean + refresh: Boolean, + append: Boolean ): LiveData> { fetchAlbums(refresh) @@ -63,7 +81,7 @@ class AlbumListFragment : EntryListFragment() { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true - if (navArgs.type == AlbumListType.BY_ARTIST) { + if (navArgs.byArtist) { listModel.getAlbumsOfArtist( refresh = navArgs.refresh, id = navArgs.id!!, @@ -71,7 +89,7 @@ class AlbumListFragment : EntryListFragment() { ) } else { listModel.getAlbums( - albumListType = navArgs.type, + albumListType = orderType?.mapToAlbumListType() ?: navArgs.type, size = navArgs.size, offset = navArgs.offset, append = append, @@ -82,38 +100,145 @@ class AlbumListFragment : EntryListFragment() { } } - // 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 { + val useId3 = Settings.shouldUseId3Tags + val useId3Offline = Settings.useId3TagsOffline + val isOnline = !ActiveServerProvider.isOffline() + + val supported = mutableListOf() + + if (isOnline || useId3Offline) { + supported.add(SortOrder.NEWEST) + } + if (isOnline) { + supported.add(SortOrder.RECENT) + } + if (isOnline) { + supported.add(SortOrder.FREQUENT) + } + if (isOnline && !useId3) { + supported.add(SortOrder.HIGHEST) + } + if (isOnline) { + supported.add(SortOrder.RANDOM) + } + if (isOnline) { + supported.add(SortOrder.STARRED) + } + if (isOnline || useId3Offline) { + supported.add(SortOrder.BY_NAME) + } + if (isOnline || useId3Offline) { + supported.add(SortOrder.BY_ARTIST) + } + + return supported + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val layout = if (navArgs.byArtist) R.layout.list_layout_filterable else mainLayout + return inflater.inflate(layout, container, false) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setTitle(navArgs.title) - - // Attach our onScrollListener - listView = view.findViewById(recyclerViewId).apply { - val scrollListener = object : EndlessScrollListener(viewManager) { - override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { - // Triggered only when new data needs to be appended to the list - // Add whatever code is needed to append new items to the bottom of the list - fetchAlbums(append = true) - } - } - addOnScrollListener(scrollListener) + // In most cases this fragment will be hosted by a ViewPager2 in the MainFragment, + // which provides its own FilterBar. + // But when we are looking at the Albums of a specific Artist this Fragment is standalone, + // so we need to setup the FilterBar here.. + if (navArgs.byArtist) { + setTitle(navArgs.title) + setupFilterBar(view) } - viewAdapter.register( - AlbumRowBinder( - { entry -> onItemClick(entry) }, - { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider.getImageLoader() - ) - ) + // Get a reference to the listView + listView = view.findViewById(recyclerViewId) + + setLayoutType(layoutType) + + val imageLoader = imageLoaderProvider.getImageLoader() + + // Magic to switch between different view layouts: + // We register two delegates, one which layouts grid items and one which layouts row items + // Based on the current status of the ViewType, the right delegate is picked. + viewAdapter.register(Album::class).to( + AlbumRowDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader), + AlbumGridDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader) + ).withKotlinClassLinker { _, _ -> + when (layoutType) { + LayoutType.COVER -> AlbumGridDelegate::class + LayoutType.LIST -> AlbumRowDelegate::class + } + } emptyTextView.setText(R.string.select_album_empty) } + private fun setupFilterBar(view: View) { + // Load last layout from settings + layoutType = LayoutType.from(Settings.lastViewType) + filterButtonBar = view.findViewById(R.id.filter_button_bar) + filterButtonBar!!.setOnLayoutTypeChangedListener(::setLayoutType) + filterButtonBar!!.setOnOrderChangedListener(::setOrderType) + filterButtonBar!!.configureWithCapabilities( + ViewCapabilities( + supportsGrid = true, + supportedSortOrders = listOf( + SortOrder.BY_NAME, + SortOrder.BY_YEAR + ) + ) + ) + + // Set layout toggle Chip to correct state + filterButtonBar!!.setLayoutType(layoutType) + } + override fun onItemClick(item: Album) { - val action = AlbumListFragmentDirections.albumListToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( item.id, isAlbum = item.isDirectory, name = item.title, @@ -121,4 +246,20 @@ class AlbumListFragment : EntryListFragment() { ) 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 + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index eef035d9..afd9dd82 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType @@ -41,7 +42,7 @@ class ArtistListFragment : EntryListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(refresh: Boolean): LiveData> { + override fun getLiveData(refresh: Boolean, append: Boolean): LiveData> { return listModel.getItems(navArgs.refresh || refresh, refreshListView!!) } @@ -66,15 +67,16 @@ class ArtistListFragment : EntryListFragment() { override fun onItemClick(item: ArtistOrIndex) { // Check type val action = if (item is Index) { - ArtistListFragmentDirections.artistsListToTrackCollection( + NavigationGraphDirections.toTrackCollection( id = item.id, name = item.name, parentId = item.id, isArtist = (item is Artist) ) } else { - ArtistListFragmentDirections.artistsListToAlbumsList( - type = AlbumListType.BY_ARTIST, + NavigationGraphDirections.toAlbumList( + type = AlbumListType.SORTED_BY_NAME, + byArtist = true, id = item.id, title = item.name, size = 1000, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt index 8cbf1a74..ed644e72 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/BookmarksFragment.kt @@ -37,7 +37,8 @@ class BookmarksFragment : TrackCollectionFragment() { } override fun getLiveData( - refresh: Boolean + refresh: Boolean, + append: Boolean ): LiveData> { listModel.viewModelScope.launch(handler) { refreshListView?.isRefreshing = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index 2f7a0832..76983914 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -40,7 +40,7 @@ class DownloadsFragment : MultiListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(refresh: Boolean): LiveData> { + override fun getLiveData(refresh: Boolean, append: Boolean): LiveData> { return listModel.getList() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt index 4b8664c4..e391f70b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt @@ -89,6 +89,7 @@ abstract class EndlessScrollListener : RecyclerView.OnScrollListener { loading = true } } + // If it’s still loading, we check to see if the dataset count has // changed, if so we conclude it has finished loading and update the current page // number and total item count. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt index 231b06cf..01386265 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/FragmentTitle.kt @@ -2,6 +2,7 @@ package org.moire.ultrasonic.fragment import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment /** * Contains utility functions related to Fragment title handling @@ -9,11 +10,17 @@ import androidx.fragment.app.Fragment class FragmentTitle { companion object { fun setTitle(fragment: Fragment, title: CharSequence?) { - (fragment.activity as AppCompatActivity).supportActionBar?.title = title + // Only set the title if our fragment is a direct child of the NavHostFragment... + if (fragment.parentFragment is NavHostFragment) { + (fragment.activity as AppCompatActivity).supportActionBar?.title = title + } } fun setTitle(fragment: Fragment, id: Int) { - (fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id) + // Only set the title if our fragment is a direct child of the NavHostFragment... + if (fragment.parentFragment is NavHostFragment) { + (fragment.activity as AppCompatActivity).supportActionBar?.setTitle(id) + } } fun getTitle(fragment: Fragment): CharSequence? { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt index 75f07df5..ee0ce451 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt @@ -7,251 +7,217 @@ package org.moire.ultrasonic.fragment +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController +import androidx.fragment.app.FragmentManager +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import java.lang.ref.SoftReference import org.koin.core.component.KoinComponent +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline -import org.moire.ultrasonic.databinding.MainBinding +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.fragment.legacy.PlaylistsFragment +import org.moire.ultrasonic.fragment.legacy.SelectGenreFragment +import org.moire.ultrasonic.util.LayoutType import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.view.EMPTY_CAPABILITIES +import org.moire.ultrasonic.view.FilterButtonBar +import org.moire.ultrasonic.view.SortOrder +import org.moire.ultrasonic.view.ViewCapabilities +import timber.log.Timber -/** - * Displays the Main screen of Ultrasonic, where the music library can be browsed - */ class MainFragment : Fragment(), KoinComponent { - private lateinit var musicTitle: TextView - private lateinit var artistsButton: TextView - private lateinit var albumsButton: TextView - private lateinit var genresButton: TextView - private lateinit var videosTitle: TextView - private lateinit var songsTitle: TextView - private lateinit var randomSongsButton: TextView - private lateinit var songsStarredButton: TextView - private lateinit var albumsTitle: TextView - private lateinit var albumsNewestButton: TextView - private lateinit var albumsRandomButton: TextView - private lateinit var albumsHighestButton: TextView - private lateinit var albumsStarredButton: TextView - private lateinit var albumsRecentButton: TextView - private lateinit var albumsFrequentButton: TextView - private lateinit var albumsAlphaByNameButton: TextView - private lateinit var albumsAlphaByArtistButton: TextView - private lateinit var videosButton: TextView + private var filterButtonBar: FilterButtonBar? = null + private var layoutType: LayoutType = LayoutType.COVER + private var binding: View? = null - private var binding: MainBinding? = null - - override fun onCreate(savedInstanceState: Bundle?) { - Util.applyTheme(this.context) - super.onCreate(savedInstanceState) - } + private lateinit var musicCollectionAdapter: MusicCollectionAdapter + private lateinit var viewPager: ViewPager2 override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = MainBinding.inflate(inflater, container, false) - return binding!!.root + Timber.i("onCreate") + binding = inflater.inflate(R.layout.primary, container, false) + return binding!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - setupButtons() - setupClickListener() - setupItemVisibility() - super.onViewCreated(view, savedInstanceState) - } + FragmentTitle.setTitle(this, R.string.music_library_label) - override fun onResume() { - super.onResume() - var shouldRelayout = false - val currentId3Setting = Settings.shouldUseId3Tags + // Load last layout from settings + layoutType = LayoutType.from(Settings.lastViewType) - // If setting has changed... - if (currentId3Setting != useId3) { - useId3 = currentId3Setting - shouldRelayout = true + // Init ViewPager2 + musicCollectionAdapter = MusicCollectionAdapter(this, layoutType) + viewPager = binding!!.findViewById(R.id.pager) + viewPager.adapter = musicCollectionAdapter + + filterButtonBar = binding!!.findViewById(R.id.filter_button_bar) + musicCollectionAdapter.filterButtonBar = filterButtonBar + + filterButtonBar!!.setOnLayoutTypeChangedListener { + updateLayoutTypeOnCurrentFragment(it) } - // then setup the list anew. - if (shouldRelayout) { - setupItemVisibility() + filterButtonBar!!.setOnOrderChangedListener { + updateSortOrderOnCurrentFragment(it) + } + + // Set layout toggle Chip to correct state + filterButtonBar!!.setLayoutType(layoutType) + + // Listen to changes in the current page (=fragment) + viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + + Timber.i("On Page changed $position") + + // This is a bit tricky. We need to configure the FilterButtonBar based on the + // fragments capabilities. But this function can be called before the fragment has + // been created, and the ViewPager might create the fragments in arbitrary order. + // Therefore we store a flag in the Adapter, to signal that the next created + // fragment of the given position should propagate its capabilities + val frag = findFragmentAtPosition(childFragmentManager, position) + if (frag != null) { + filterButtonBar!!.configureWithCapabilitiesFromFragment(frag) + } else { + musicCollectionAdapter.propagateCapabilitiesMatcher = position + } + } + }) + + // The TabLayoutMediator manages the names of the Tabs (Albums, Artists, etc) + val tabLayout: TabLayout = binding!!.findViewById(R.id.tab_layout) + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + tab.text = musicCollectionAdapter.getTitleForFragment(position, requireContext()) + }.attach() + } + + private fun updateLayoutTypeOnCurrentFragment(it: LayoutType) { + val curFrag = findCurrentFragment() + + if (curFrag is FilterableFragment) { + curFrag.setLayoutType(it) + } + + Settings.lastViewType = layoutType.value + } + + private fun updateSortOrderOnCurrentFragment(it: SortOrder) { + val curFrag = findCurrentFragment() + + if (curFrag is FilterableFragment) { + curFrag.setOrderType(it) } } - override fun onDestroyView() { - super.onDestroyView() - binding = null + private fun findCurrentFragment(): Fragment? { + return findFragmentAtPosition(childFragmentManager, viewPager.currentItem) } - private fun setupButtons() { - musicTitle = binding!!.mainMusic - artistsButton = binding!!.mainArtistsButton - albumsButton = binding!!.mainAlbumsButton - genresButton = binding!!.mainGenresButton - videosTitle = binding!!.mainVideosTitle - songsTitle = binding!!.mainSongs - randomSongsButton = binding!!.mainSongsButton - songsStarredButton = binding!!.mainSongsStarred - albumsTitle = binding!!.mainAlbums - albumsNewestButton = binding!!.mainAlbumsNewest - albumsRandomButton = binding!!.mainAlbumsRandom - albumsHighestButton = binding!!.mainAlbumsHighest - albumsStarredButton = binding!!.mainAlbumsStarred - albumsRecentButton = binding!!.mainAlbumsRecent - albumsFrequentButton = binding!!.mainAlbumsFrequent - albumsAlphaByNameButton = binding!!.mainAlbumsAlphaByName - albumsAlphaByArtistButton = binding!!.mainAlbumsAlphaByArtist - videosButton = binding!!.mainVideos - } - - private fun setupItemVisibility() { - // Cache some values - useId3 = Settings.shouldUseId3Tags - useId3Offline = Settings.useId3TagsOffline - - val isOnline = !isOffline() - - // Music - musicTitle.isVisible = true - artistsButton.isVisible = true - albumsButton.isVisible = isOnline || useId3Offline - genresButton.isVisible = isOnline - - // Songs - songsTitle.isVisible = true - randomSongsButton.isVisible = true - songsStarredButton.isVisible = isOnline - - // Albums - albumsTitle.isVisible = isOnline || useId3Offline - albumsNewestButton.isVisible = isOnline || useId3Offline - albumsRecentButton.isVisible = isOnline - albumsFrequentButton.isVisible = isOnline - albumsHighestButton.isVisible = isOnline && !useId3 - albumsRandomButton.isVisible = isOnline - albumsStarredButton.isVisible = isOnline - albumsAlphaByNameButton.isVisible = isOnline || useId3Offline - albumsAlphaByArtistButton.isVisible = isOnline || useId3Offline - - // Videos - videosTitle.isVisible = isOnline - videosButton.isVisible = isOnline - } - - private fun setupClickListener() { - albumsNewestButton.setOnClickListener { - showAlbumList(AlbumListType.NEWEST, R.string.main_albums_newest) - } - - albumsRandomButton.setOnClickListener { - showAlbumList(AlbumListType.RANDOM, R.string.main_albums_random) - } - - albumsHighestButton.setOnClickListener { - showAlbumList(AlbumListType.HIGHEST, R.string.main_albums_highest) - } - - albumsRecentButton.setOnClickListener { - showAlbumList(AlbumListType.RECENT, R.string.main_albums_recent) - } - - albumsFrequentButton.setOnClickListener { - showAlbumList(AlbumListType.FREQUENT, R.string.main_albums_frequent) - } - - albumsStarredButton.setOnClickListener { - showAlbumList(AlbumListType.STARRED, R.string.main_albums_starred) - } - - albumsAlphaByNameButton.setOnClickListener { - showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_alphaByName) - } - - albumsAlphaByArtistButton.setOnClickListener { - showAlbumList(AlbumListType.SORTED_BY_ARTIST, R.string.main_albums_alphaByArtist) - } - - songsStarredButton.setOnClickListener { - showStarredSongs() - } - - artistsButton.setOnClickListener { - showArtists() - } - - albumsButton.setOnClickListener { - showAlbumList(AlbumListType.SORTED_BY_NAME, R.string.main_albums_title) - } - - randomSongsButton.setOnClickListener { - showRandomSongs() - } - - genresButton.setOnClickListener { - showGenres() - } - - videosButton.setOnClickListener { - showVideos() - } - } - - private fun showStarredSongs() { - val action = MainFragmentDirections.mainToTrackCollection( - getStarred = true, - ) - findNavController().navigate(action) - } - - private fun showRandomSongs() { - val action = MainFragmentDirections.mainToTrackCollection( - getRandom = true, - size = Settings.maxSongs - ) - findNavController().navigate(action) - } - - private fun showArtists() { - val action = MainFragmentDirections.mainToArtistList( - title = requireContext().resources.getString(R.string.main_artists_title) - ) - findNavController().navigate(action) - } - - private fun showAlbumList(type: AlbumListType, titleIndex: Int) { - val title = requireContext().resources.getString(titleIndex, "") - val action = MainFragmentDirections.mainToAlbumList( - type = type, - title = title, - size = Settings.maxAlbums, - offset = 0 - ) - findNavController().navigate(action) - } - - private fun showGenres() { - findNavController().navigate(R.id.mainToSelectGenre) - } - - private fun showVideos() { - val action = MainFragmentDirections.mainToTrackCollection( - getVideos = true, - ) - findNavController().navigate(action) - } - - companion object { - private var useId3 = false - private var useId3Offline = false + private fun findFragmentAtPosition( + fragmentManager: FragmentManager, + position: Int + ): Fragment? { + // If a fragment was recently created and never shown the fragment manager might not + // hold a reference to it. Fallback on the WeakMap instead. + return fragmentManager.findFragmentByTag("f$position") + ?: musicCollectionAdapter.fragmentMap[position]?.get() } } + +private fun FilterButtonBar.configureWithCapabilitiesFromFragment(frag: Fragment?) { + if (frag is FilterableFragment) { + Timber.w("Setting kapas: ${frag.viewCapabilities}") + this.configureWithCapabilities(frag.viewCapabilities) + } else { + Timber.w("Setting kapas: $EMPTY_CAPABILITIES") + this.configureWithCapabilities(EMPTY_CAPABILITIES) + } +} + +@Suppress("MagicNumber") +class MusicCollectionAdapter(fragment: Fragment, initialType: LayoutType = LayoutType.LIST) : + FragmentStateAdapter(fragment) { + + var filterButtonBar: FilterButtonBar? = null + private var layoutType: LayoutType = initialType + + var propagateCapabilitiesMatcher: Int? = null + + // viewPager.findFragmentAtPosition(childFragmentManager, position) is sometimes delayed.. + var fragmentMap: HashMap> = 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 +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt index e1900806..6cc94b1d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MultiListFragment.kt @@ -75,7 +75,7 @@ abstract class MultiListFragment : Fragment() { /** * The central function to pass a query to the model and return a LiveData object */ - open fun getLiveData(refresh: Boolean = false): LiveData> { + open fun getLiveData(refresh: Boolean = false, append: Boolean = false): LiveData> { return MutableLiveData() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 6a79f080..3ef77e13 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -578,7 +578,8 @@ class PlayerFragment : if (Settings.shouldUseId3Tags) { val action = PlayerFragmentDirections.playerToAlbumsList( - type = AlbumListType.BY_ARTIST, + type = AlbumListType.SORTED_BY_NAME, + byArtist = true, id = track.artistId, title = track.artist, offset = 0, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index b4b5d2f7..a5f415f1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R -import org.moire.ultrasonic.adapters.AlbumRowBinder +import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.MoreButtonBinder @@ -109,7 +109,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) viewAdapter.register( - AlbumRowBinder( + AlbumRowDelegate( onItemClick = ::onItemClick, onContextMenuClick = ::onContextMenuItemSelected, imageLoader = imageLoaderProvider.getImageLoader() @@ -280,7 +280,8 @@ class SearchFragment : MultiListFragment(), KoinComponent { ) } else { SearchFragmentDirections.searchToAlbumsList( - type = AlbumListType.BY_ARTIST, + type = AlbumListType.SORTED_BY_NAME, + byArtist = true, id = item.id, title = item.name, size = 1000, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 433cec6a..e8e6f477 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -27,9 +27,10 @@ import java.util.Collections import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumHeader -import org.moire.ultrasonic.adapters.AlbumRowBinder +import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.data.ActiveServerProvider @@ -50,6 +51,8 @@ import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util +import org.moire.ultrasonic.view.SortOrder +import org.moire.ultrasonic.view.ViewCapabilities import timber.log.Timber /** @@ -59,10 +62,11 @@ import timber.log.Timber * where the list can contain Albums as well. This happens especially when having ID3 tags disabled, * or using Offline mode, both in which Indexes instead of Artists are being used. * - * TODO: Remove more button and introduce endless scrolling */ @Suppress("TooManyFunctions") -open class TrackCollectionFragment : MultiListFragment() { +open class TrackCollectionFragment( + initialOrder: SortOrder? = null +) : MultiListFragment(), FilterableFragment { private var albumButtons: View? = null private var selectButton: MaterialButton? = null @@ -73,7 +77,6 @@ open class TrackCollectionFragment : MultiListFragment() { private var unpinButton: MaterialButton? = null private var downloadButton: MaterialButton? = null private var deleteButton: MaterialButton? = null - private var moreButton: MaterialButton? = null private var playAllButtonVisible = false private var shareButtonVisible = false private var playAllButton: MenuItem? = null @@ -87,6 +90,9 @@ open class TrackCollectionFragment : MultiListFragment() { override val listModel: TrackCollectionModel by viewModels() private val rxBusSubscription: CompositeDisposable = CompositeDisposable() + private var sortOrder = initialOrder + private var offset: Int? = null + /** * The id of the main layout */ @@ -138,7 +144,7 @@ open class TrackCollectionFragment : MultiListFragment() { ) viewAdapter.register( - AlbumRowBinder( + AlbumRowDelegate( { entry -> onItemClick(entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, imageLoaderProvider.getImageLoader() @@ -161,6 +167,25 @@ open class TrackCollectionFragment : MultiListFragment() { ) { triggerButtonUpdate() } + + // Attach our onScrollListener + val scrollListener = object : EndlessScrollListener(viewManager) { + override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) { + Timber.w("LOAD MORE") + // Triggered only when new data needs to be appended to the list + // Add whatever code is needed to append new items to the bottom of the list + loadMoreTracks() + } + } + + listView!!.addOnScrollListener(scrollListener) + } + + private fun loadMoreTracks() { + if (displayRandom() || navArgs.genreName != null) { + offset = navArgs.offset + navArgs.size + getLiveData(refresh = true, append = true) + } } internal open fun handleRefresh() { @@ -176,7 +201,6 @@ open class TrackCollectionFragment : MultiListFragment() { unpinButton = view.findViewById(R.id.select_album_unpin) downloadButton = view.findViewById(R.id.select_album_download) deleteButton = view.findViewById(R.id.select_album_delete) - moreButton = view.findViewById(R.id.select_album_more) selectButton?.setOnClickListener { selectAllOrNone() @@ -469,24 +493,9 @@ open class TrackCollectionFragment : MultiListFragment() { } } - val listSize = navArgs.size - // Hide select button for video lists and singular selection lists selectButton!!.isVisible = !allVideos && viewAdapter.hasMultipleSelection() && songCount > 0 - if (songCount > 0) { - if (listSize == 0 || songCount < listSize) { - moreButton!!.visibility = View.GONE - } else { - moreButton!!.visibility = View.VISIBLE - if (navArgs.getRandom) { - moreRandomTracks() - } else if (navArgs.genreName != null) { - moreSongsForGenre() - } - } - } - // Show a text if we have no entries emptyView.isVisible = entryList.isEmpty() @@ -524,33 +533,6 @@ open class TrackCollectionFragment : MultiListFragment() { 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 { // Walk through selected set and get the Entries based on the saved ids. return viewAdapter.getCurrentList().mapNotNull { @@ -571,7 +553,8 @@ open class TrackCollectionFragment : MultiListFragment() { @Suppress("LongMethod") override fun getLiveData( - refresh: Boolean + refresh: Boolean, + append: Boolean ): LiveData> { Timber.i("Starting gathering track collection data...") val id = navArgs.id @@ -584,11 +567,11 @@ open class TrackCollectionFragment : MultiListFragment() { val shareName = navArgs.shareName val genreName = navArgs.genreName - val getStarredTracks = navArgs.getStarred + val getStarredTracks = displayStarred() val getVideos = navArgs.getVideos - val getRandomTracks = navArgs.getRandom - val albumListSize = navArgs.size - val albumListOffset = navArgs.offset + val getRandomTracks = displayRandom() + val size = if (navArgs.size < 0) Settings.maxSongs else navArgs.size + val offset = offset ?: navArgs.offset val refresh2 = navArgs.refresh || refresh listModel.viewModelScope.launch(handler) { @@ -605,7 +588,7 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.getShare(shareId) } else if (genreName != null) { setTitle(genreName) - listModel.getSongsForGenre(genreName, albumListSize, albumListOffset) + listModel.getSongsForGenre(genreName, size, offset, append) } else if (getStarredTracks) { setTitle(getString(R.string.main_songs_starred)) listModel.getStarred() @@ -614,7 +597,7 @@ open class TrackCollectionFragment : MultiListFragment() { listModel.getVideos(refresh2) } else if (getRandomTracks) { setTitle(R.string.main_songs_random) - listModel.getRandom(albumListSize) + listModel.getRandom(size, append) } else { setTitle(name) if (ActiveServerProvider.isID3Enabled()) { @@ -633,6 +616,10 @@ open class TrackCollectionFragment : MultiListFragment() { return listModel.currentList } + private fun displayStarred() = (sortOrder == SortOrder.STARRED) || navArgs.getStarred + + private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom + @Suppress("LongMethod") override fun onContextMenuItemSelected( menuItem: MenuItem, @@ -703,7 +690,7 @@ open class TrackCollectionFragment : MultiListFragment() { override fun onItemClick(item: MusicDirectory.Child) { when { item.isDirectory -> { - val action = TrackCollectionFragmentDirections.loadMoreTracks( + val action = NavigationGraphDirections.toTrackCollection( id = item.id, isAlbum = true, name = item.title, @@ -719,4 +706,24 @@ open class TrackCollectionFragment : MultiListFragment() { } } } + + override fun setOrderType(newOrder: SortOrder) { + sortOrder = newOrder + getLiveData(true) + } + + override var viewCapabilities: ViewCapabilities = ViewCapabilities( + supportsGrid = false, + supportedSortOrders = getListOfSortOrders() + ) + + private fun getListOfSortOrders(): List { + val isOnline = !isOffline() + val supported = mutableListOf(SortOrder.RANDOM) + + if (isOnline) { + supported.add(SortOrder.STARRED) + } + return supported + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index 0bd9e4cd..8db3d469 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -21,6 +21,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.AdapterView.AdapterContextMenuInfo +import android.widget.ArrayAdapter import android.widget.CheckBox import android.widget.EditText import android.widget.ListView @@ -30,6 +31,7 @@ import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline @@ -45,7 +47,6 @@ import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.LoadingTask import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.toast -import org.moire.ultrasonic.view.PlaylistAdapter /** * Displays the playlists stored on the server @@ -56,11 +57,14 @@ class PlaylistsFragment : Fragment() { private var refreshPlaylistsListView: SwipeRefreshLayout? = null private var playlistsListView: ListView? = null private var emptyTextView: View? = null - private var playlistAdapter: PlaylistAdapter? = null + private var playlistAdapter: ArrayAdapter? = null + private val downloadHandler = inject( DownloadHandler::class.java ) + private var cancellationToken: CancellationToken? = null + override fun onCreate(savedInstanceState: Bundle?) { applyTheme(this.context) super.onCreate(savedInstanceState) @@ -83,7 +87,7 @@ class PlaylistsFragment : Fragment() { playlistsListView!!.setOnItemClickListener { parent, _, position, _ -> val (id1, name) = parent.getItemAtPosition(position) as Playlist - val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( id = id1, playlistId = id1, name = name, @@ -116,7 +120,7 @@ class PlaylistsFragment : Fragment() { override fun done(result: List) { playlistsListView!!.adapter = - PlaylistAdapter(context, result).also { playlistAdapter = it } + ArrayAdapter(requireContext(), R.layout.list_item_generic, result) emptyTextView!!.visibility = if (result.isEmpty()) View.VISIBLE else View.GONE } } @@ -183,7 +187,7 @@ class PlaylistsFragment : Fragment() { ) } R.id.playlist_menu_play_now -> { - val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( playlistId = playlist.id, playlistName = playlist.name, autoPlay = true @@ -191,7 +195,7 @@ class PlaylistsFragment : Fragment() { findNavController().navigate(action) } R.id.playlist_menu_play_shuffled -> { - val action = PlaylistsFragmentDirections.playlistsToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( playlistId = playlist.id, playlistName = playlist.name, autoPlay = true, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt index b8fec24b..2ab59b00 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PodcastFragment.kt @@ -7,15 +7,16 @@ package org.moire.ultrasonic.fragment.legacy -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter import android.widget.ListView import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.PodcastsChannel import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -24,18 +25,20 @@ import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.Util.applyTheme -import org.moire.ultrasonic.view.PodcastsChannelsAdapter /** * Displays the podcasts available on the server * * TODO: This file has been converted from Java, but not modernized yet. + * TODO: Use Coroutines */ class PodcastFragment : Fragment() { + private var emptyTextView: View? = null var channelItemsListView: ListView? = null private var cancellationToken: CancellationToken? = null private var swipeRefresh: SwipeRefreshLayout? = null + override fun onCreate(savedInstanceState: Bundle?) { applyTheme(this.context) super.onCreate(savedInstanceState) @@ -53,19 +56,19 @@ class PodcastFragment : Fragment() { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() swipeRefresh = view.findViewById(R.id.podcasts_refresh) - swipeRefresh!!.setOnRefreshListener { load(view.context, true) } + swipeRefresh!!.setOnRefreshListener { load(true) } setTitle(this, R.string.podcasts_label) emptyTextView = view.findViewById(R.id.select_podcasts_empty) channelItemsListView = view.findViewById(R.id.podcasts_channels_items_list) channelItemsListView!!.setOnItemClickListener { parent, _, position, _ -> val (id) = parent.getItemAtPosition(position) as PodcastsChannel - val action = PodcastFragmentDirections.podcastToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( podcastChannelId = id ) findNavController().navigate(action) } - load(view.context, false) + load(false) } override fun onDestroyView() { @@ -73,7 +76,7 @@ class PodcastFragment : Fragment() { super.onDestroyView() } - private fun load(context: Context, refresh: Boolean) { + private fun load(refresh: Boolean) { val task: BackgroundTask> = object : FragmentBackgroundTask>( activity, true, swipeRefresh, cancellationToken @@ -85,7 +88,8 @@ class PodcastFragment : Fragment() { } override fun done(result: List) { - 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 } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt index 9f038b30..31790ef3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SelectGenreFragment.kt @@ -17,6 +17,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle @@ -58,13 +59,14 @@ class SelectGenreFragment : Fragment() { refreshGenreListView = view.findViewById(R.id.select_genre_refresh) genreListView = view.findViewById(R.id.select_genre_list) refreshGenreListView!!.setOnRefreshListener { load(true) } + genreListView!!.setOnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long -> val genre = parent.getItemAtPosition(position) as Genre - val action = SelectGenreFragmentDirections.selectGenreToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( genreName = genre.name, size = maxSongs, offset = 0 @@ -82,6 +84,7 @@ class SelectGenreFragment : Fragment() { super.onDestroyView() } + // TODO: Migrate to Coroutines private fun load(refresh: Boolean) { val task: BackgroundTask> = object : FragmentBackgroundTask>( activity, true, refreshGenreListView, cancellationToken diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt index 4657ead2..eae31252 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/SharesFragment.kt @@ -29,6 +29,7 @@ import androidx.navigation.fragment.findNavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import java.util.Locale import org.koin.java.KoinJavaComponent +import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.domain.Share @@ -82,7 +83,7 @@ class SharesFragment : Fragment() { AdapterView.OnItemClickListener { parent, _, position, _ -> val share = parent.getItemAtPosition(position) as Share - val action = SharesFragmentDirections.sharesToTrackCollection( + val action = NavigationGraphDirections.toTrackCollection( shareId = share.id, shareName = share.name ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index ae08cb55..56f82ab3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -96,6 +96,25 @@ class AlbumListModel(application: Application) : GenericListModel(application) { } } + fun sortListByOrder(order: AlbumListType) { + val newList = when (order) { + AlbumListType.BY_YEAR -> { + list.value?.sortedBy { + it.year + } + } + else -> { + list.value?.sortedBy { + it.name + } + } + } + + newList?.let { + list.postValue(it) + } + } + override fun showSelectFolderHeader(): Boolean { val isAlphabetical = (lastType == AlbumListType.SORTED_BY_NAME) || (lastType == AlbumListType.SORTED_BY_ARTIST) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index eaf40924..140d9a65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -56,11 +56,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - suspend fun getSongsForGenre(genre: String, count: Int, offset: Int) { + suspend fun getSongsForGenre(genre: String, count: Int, offset: Int, append: Boolean) { withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() val musicDirectory = service.getSongsByGenre(genre, count, offset) - updateList(musicDirectory) + updateList(musicDirectory, append) } } @@ -94,7 +94,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - suspend fun getRandom(size: Int) { + suspend fun getRandom(size: Int, append: Boolean) { withContext(Dispatchers.IO) { val service = MusicServiceFactory.getMusicService() @@ -102,7 +102,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat currentListIsSortable = false - updateList(musicDirectory) + updateList(musicDirectory, append) } } @@ -158,8 +158,14 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat } } - private fun updateList(root: MusicDirectory) { - currentList.postValue(root.getChildren()) + private fun updateList(root: MusicDirectory, append: Boolean = false) { + val newList = if (append) { + currentList.value!! + root.getChildren() + } else { + root.getChildren() + } + + currentList.postValue(newList) } @Synchronized diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/LayoutType.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/LayoutType.kt new file mode 100644 index 00000000..46fa11c7 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/LayoutType.kt @@ -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 + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt index 58045c67..771d8410 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt @@ -10,7 +10,6 @@ package org.moire.ultrasonic.util import android.net.Uri import android.os.Bundle import androidx.core.net.toUri -import androidx.media.utils.MediaConstants import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata @@ -19,9 +18,15 @@ import java.text.DateFormat import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.provider.AlbumArtContentProvider +// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependecy +// for a single string value +private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT" + object MediaItemConverter { private const val CACHE_SIZE = 250 private const val CACHE_EXPIRY_MINUTES = 10L + val mediaItemCache: LRUCache> = LRUCache(CACHE_SIZE) val trackCache: LRUCache> = LRUCache(CACHE_SIZE) @@ -233,7 +238,7 @@ fun buildMediaItem( metadataBuilder.setExtras( Bundle().apply { putString( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, group ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index a1c81eb7..e6c0ca23 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -19,6 +19,10 @@ import org.moire.ultrasonic.app.UApp */ object Settings { + @JvmStatic + val preferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext()) + @JvmStatic var theme by StringSetting( getKey(R.string.setting_key_theme), @@ -241,10 +245,6 @@ object Settings { @JvmStatic var debugLogToFile by BooleanSetting(getKey(R.string.setting_key_debug_log_to_file), false) - @JvmStatic - val preferences: SharedPreferences - get() = PreferenceManager.getDefaultSharedPreferences(Util.appContext()) - @JvmStatic val overrideLanguage by StringSetting(getKey(R.string.setting_key_override_language), "") @@ -267,11 +267,11 @@ object Settings { false ) - // TODO: Remove in December 2022 - fun migrateFeatureStorage() { - val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE) - useFiveStarRating = sp.getBoolean("FIVE_STAR_RATING", false) - } + @JvmStatic + var lastViewType by IntSetting( + getKey(R.string.setting_key_last_view_type), + 0 + ) fun hasKey(key: String): Boolean { return preferences.contains(key) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/FilterButtonBar.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/FilterButtonBar.kt new file mode 100644 index 00000000..28865abb --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/FilterButtonBar.kt @@ -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? = 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( + context, + com.google.android.material.R.layout.mtrl_auto_complete_simple_item, + mutableListOf() + ) + + 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 +) + +val EMPTY_CAPABILITIES = ViewCapabilities( + supportsGrid = false, + supportedSortOrders = listOf() +) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SortOrder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SortOrder.kt new file mode 100644 index 00000000..5c372198 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SortOrder.kt @@ -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"); +} diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_close.xml b/ultrasonic/src/main/res/drawable/ic_baseline_close.xml deleted file mode 100644 index 361e6899..00000000 --- a/ultrasonic/src/main/res/drawable/ic_baseline_close.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_videos.xml b/ultrasonic/src/main/res/drawable/ic_baseline_videos.xml new file mode 100644 index 00000000..27a88fc2 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_videos.xml @@ -0,0 +1,11 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_view_grid.xml b/ultrasonic/src/main/res/drawable/ic_baseline_view_grid.xml new file mode 100644 index 00000000..ad246785 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_view_grid.xml @@ -0,0 +1,11 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_baseline_view_list.xml b/ultrasonic/src/main/res/drawable/ic_baseline_view_list.xml new file mode 100644 index 00000000..7f5c0a73 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/ic_baseline_view_list.xml @@ -0,0 +1,11 @@ + + + diff --git a/ultrasonic/src/main/res/drawable/ic_create_new_folder.xml b/ultrasonic/src/main/res/drawable/ic_create_new_folder.xml deleted file mode 100644 index fc38ba91..00000000 --- a/ultrasonic/src/main/res/drawable/ic_create_new_folder.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_drag_queue.xml b/ultrasonic/src/main/res/drawable/ic_drag_queue.xml deleted file mode 100644 index 9ccb33b0..00000000 --- a/ultrasonic/src/main/res/drawable/ic_drag_queue.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_folder.xml b/ultrasonic/src/main/res/drawable/ic_folder.xml deleted file mode 100644 index a601f5fc..00000000 --- a/ultrasonic/src/main/res/drawable/ic_folder.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_arrow.xml b/ultrasonic/src/main/res/drawable/ic_menu_arrow.xml deleted file mode 100644 index 64ff6cff..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_arrow.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_backward.xml b/ultrasonic/src/main/res/drawable/ic_menu_backward.xml deleted file mode 100644 index 04a5e7fa..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_backward.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_forward.xml b/ultrasonic/src/main/res/drawable/ic_menu_forward.xml deleted file mode 100644 index bfa8055f..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_forward.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_help.xml b/ultrasonic/src/main/res/drawable/ic_menu_help.xml deleted file mode 100644 index c63537e8..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_help.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_menu_home.xml b/ultrasonic/src/main/res/drawable/ic_menu_home.xml deleted file mode 100644 index b01fabad..00000000 --- a/ultrasonic/src/main/res/drawable/ic_menu_home.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_sd_storage.xml b/ultrasonic/src/main/res/drawable/ic_sd_storage.xml deleted file mode 100644 index 20d7315e..00000000 --- a/ultrasonic/src/main/res/drawable/ic_sd_storage.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/ic_subdirectory_up.xml b/ultrasonic/src/main/res/drawable/ic_subdirectory_up.xml deleted file mode 100644 index b91962e3..00000000 --- a/ultrasonic/src/main/res/drawable/ic_subdirectory_up.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ultrasonic/src/main/res/drawable/unknown_album.xml b/ultrasonic/src/main/res/drawable/unknown_album.xml index 54f39529..192f0500 100644 --- a/ultrasonic/src/main/res/drawable/unknown_album.xml +++ b/ultrasonic/src/main/res/drawable/unknown_album.xml @@ -5,9 +5,9 @@ android:viewportHeight="100"> + android:fillColor="?attr/colorSecondary"/> + android:fillColor="?attr/colorOnSecondary"/> diff --git a/ultrasonic/src/main/res/layout/chat_item.xml b/ultrasonic/src/main/res/layout/chat_item.xml index 8497995d..337b7fed 100644 --- a/ultrasonic/src/main/res/layout/chat_item.xml +++ b/ultrasonic/src/main/res/layout/chat_item.xml @@ -1,53 +1,54 @@ + a:layout_width="fill_parent" + a:layout_height="wrap_content" + a:orientation="horizontal"> + a:contentDescription="@string/chat.user_avatar" /> + a:gravity="center_vertical" + a:orientation="vertical"> + a:textIsSelectable="true" + a:textStyle="bold" /> + a:orientation="horizontal"> + a:textIsSelectable="true" /> + a:textIsSelectable="true" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/chat_item_reverse.xml b/ultrasonic/src/main/res/layout/chat_item_reverse.xml index a7684a43..1cc126d7 100644 --- a/ultrasonic/src/main/res/layout/chat_item_reverse.xml +++ b/ultrasonic/src/main/res/layout/chat_item_reverse.xml @@ -1,71 +1,72 @@ + a:layout_width="fill_parent" + a:layout_height="wrap_content" + a:orientation="horizontal"> + a:gravity="center_vertical|right" + a:orientation="vertical"> + a:textIsSelectable="true" + a:textStyle="bold" /> + a:layout_marginTop="2dip" + a:gravity="center_vertical|right" + a:orientation="horizontal"> + a:textIsSelectable="true" /> + a:textIsSelectable="true" /> + a:layout_toStartOf="@id/chat_avatar" + a:contentDescription="@string/chat.user_avatar" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/equalizer_bar.xml b/ultrasonic/src/main/res/layout/equalizer_bar.xml index 6e00cf83..b4e82b51 100644 --- a/ultrasonic/src/main/res/layout/equalizer_bar.xml +++ b/ultrasonic/src/main/res/layout/equalizer_bar.xml @@ -1,46 +1,36 @@ - - + xmlns:tools="http://schemas.android.com/tools" + a:layout_width="fill_parent" + a:layout_height="wrap_content" + a:orientation="vertical"> + a:id="@+id/equalizer_frequency" + a:layout_width="wrap_content" + a:layout_height="wrap_content" + a:layout_alignParentLeft="true" + a:layout_marginTop="8dp" + a:textColor="#c0c0c0" + a:textSize="12sp" /> + a:id="@+id/equalizer_level" + a:layout_width="wrap_content" + a:layout_height="wrap_content" + a:layout_alignParentRight="true" + a:layout_marginTop="8dp" + a:layout_toEndOf="@+id/equalizer_frequency" + a:gravity="right" + a:textColor="#c0c0c0" + a:textSize="12sp" + tools:text="0 dB" /> - + a:id="@+id/equalizer_bar" + a:layout_width="fill_parent" + a:layout_height="wrap_content" + a:layout_below="@+id/equalizer_frequency" /> diff --git a/ultrasonic/src/main/res/layout/filter_button_bar.xml b/ultrasonic/src/main/res/layout/filter_button_bar.xml new file mode 100644 index 00000000..2a20c8c6 --- /dev/null +++ b/ultrasonic/src/main/res/layout/filter_button_bar.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/grid_item_album.xml b/ultrasonic/src/main/res/layout/grid_item_album.xml new file mode 100644 index 00000000..59b061d5 --- /dev/null +++ b/ultrasonic/src/main/res/layout/grid_item_album.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/list_item_album.xml b/ultrasonic/src/main/res/layout/list_item_album.xml index 5c18f21d..9d5e0265 100644 --- a/ultrasonic/src/main/res/layout/list_item_album.xml +++ b/ultrasonic/src/main/res/layout/list_item_album.xml @@ -10,7 +10,7 @@ a:focusable="true"> + app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.SmallComponent" /> + app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.MediumComponent" /> \ No newline at end of file + a:paddingStart="16dip" + a:paddingTop="8dp" + a:paddingEnd="24dip" + a:singleLine="true" + a:ellipsize="marquee" + a:paddingBottom="8dp" + a:textAppearance="@style/TextAppearance.Material3.BodyLarge" + tools:text="Item text" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/list_layout_filterable.xml b/ultrasonic/src/main/res/layout/list_layout_filterable.xml new file mode 100644 index 00000000..f6803427 --- /dev/null +++ b/ultrasonic/src/main/res/layout/list_layout_filterable.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/ultrasonic/src/main/res/layout/main.xml b/ultrasonic/src/main/res/layout/main.xml deleted file mode 100644 index c0813368..00000000 --- a/ultrasonic/src/main/res/layout/main.xml +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/navigation_activity.xml b/ultrasonic/src/main/res/layout/navigation_activity.xml index d8446433..867f1f6b 100644 --- a/ultrasonic/src/main/res/layout/navigation_activity.xml +++ b/ultrasonic/src/main/res/layout/navigation_activity.xml @@ -46,5 +46,5 @@ a:layout_gravity="start" a:fitsSystemWindows="true" app:headerLayout="@layout/navigation_header" - app:menu="@menu/navigation"/> + app:menu="@menu/navigation_drawer"/> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/player_slider.xml b/ultrasonic/src/main/res/layout/player_slider.xml index 9e6408c2..5131c521 100644 --- a/ultrasonic/src/main/res/layout/player_slider.xml +++ b/ultrasonic/src/main/res/layout/player_slider.xml @@ -1,5 +1,6 @@ + a:textAppearance="?android:attr/textAppearanceSmall" + tools:ignore="RelativeOverlap" /> diff --git a/ultrasonic/src/main/res/layout/playlist_list_item.xml b/ultrasonic/src/main/res/layout/playlist_list_item.xml deleted file mode 100644 index 410837ad..00000000 --- a/ultrasonic/src/main/res/layout/playlist_list_item.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/podcasts_channel_item.xml b/ultrasonic/src/main/res/layout/podcasts_channel_item.xml deleted file mode 100644 index bfa54b5f..00000000 --- a/ultrasonic/src/main/res/layout/podcasts_channel_item.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/ultrasonic/src/main/res/layout/primary.xml b/ultrasonic/src/main/res/layout/primary.xml new file mode 100644 index 00000000..69dc85a3 --- /dev/null +++ b/ultrasonic/src/main/res/layout/primary.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/select_genre.xml b/ultrasonic/src/main/res/layout/select_genre.xml index bd7a4d72..ca5083f6 100644 --- a/ultrasonic/src/main/res/layout/select_genre.xml +++ b/ultrasonic/src/main/res/layout/select_genre.xml @@ -1,19 +1,21 @@ + a:orientation="vertical"> + a:textAppearance="@style/TextAppearance.Material3.BodyLarge" + a:visibility="gone" + tools:visibility="visible" /> - + a:orientation="vertical"> + a:textAppearance="@style/TextAppearance.Material3.BodyLarge" + a:visibility="gone" + tools:visibility="visible" /> - @@ -8,13 +9,14 @@ a:id="@+id/select_share_empty" a:layout_width="fill_parent" a:layout_height="wrap_content" - a:padding="10dip" + a:padding="16dip" a:text="@string/select_share.empty" - a:visibility="gone" /> + a:textAppearance="@style/TextAppearance.Material3.BodyLarge" + a:visibility="gone" + tools:visibility="visible" /> - + xmlns:tools="http://schemas.android.com/tools" + a:layout_width="0dp" + a:layout_height="56dp" + a:layout_gravity="center_vertical" + a:layout_weight="1" + a:minHeight="44dip" + a:orientation="vertical" + a:paddingStart="16dip" + a:paddingEnd="16dip" + tools:layout_width="match_parent"> + a:textAppearance="?android:attr/textAppearanceMedium" + tools:text="Url" /> @@ -38,9 +41,9 @@ a:layout_gravity="left|center_vertical" a:layout_weight="1" a:ellipsize="middle" - a:paddingStart="4dip" a:singleLine="true" - a:textAppearance="?android:attr/textAppearanceSmall"/> + a:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Description" /> diff --git a/ultrasonic/src/main/res/layout/time_span_dialog.xml b/ultrasonic/src/main/res/layout/time_span_dialog.xml index 920a87b6..65b95425 100644 --- a/ultrasonic/src/main/res/layout/time_span_dialog.xml +++ b/ultrasonic/src/main/res/layout/time_span_dialog.xml @@ -1,50 +1,48 @@ + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_height="match_parent"> + android:layout_alignParentTop="true" + android:text="@string/time_span_disable" /> + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + android:gravity="left" + android:inputType="numberSigned" /> + android:layout_toEndOf="@+id/timeSpanEditText" /> \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/update_playlist.xml b/ultrasonic/src/main/res/layout/update_playlist.xml index cc33e81e..afb09d4b 100644 --- a/ultrasonic/src/main/res/layout/update_playlist.xml +++ b/ultrasonic/src/main/res/layout/update_playlist.xml @@ -1,70 +1,76 @@ - - + android:layout_height="wrap_content" + android:orientation="vertical"> - - - - - + - - - + - + + - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/menu/navigation.xml b/ultrasonic/src/main/res/menu/navigation_drawer.xml similarity index 87% rename from ultrasonic/src/main/res/menu/navigation.xml rename to ultrasonic/src/main/res/menu/navigation_drawer.xml index 91ba4d91..6726e227 100644 --- a/ultrasonic/src/main/res/menu/navigation.xml +++ b/ultrasonic/src/main/res/menu/navigation_drawer.xml @@ -4,14 +4,10 @@ a:checkableBehavior="none" a:enabled="true" a:visible="true"> + - - - + + diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index 33bd249e..906d8a6e 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -1,90 +1,77 @@ - - - + + + + + + + + - - - - - + android:label="@string/common.appname"> + - - - + android:label="@string/music_library.label" /> - - + android:label="@string/button_bar.now_playing" /> - - + android:name="org.moire.ultrasonic.fragment.ArtistListFragment"> + android:defaultValue="false" + app:argType="boolean" /> + app:argType="string" + app:nullable="true" /> + android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment"> + app:argType="string" + app:nullable="true" /> + android:defaultValue="false" + app:argType="boolean" /> + android:defaultValue="false" + app:argType="boolean" /> - - + - + - + - + - + - + - + + app:argType="string" + app:nullable="true" /> - - - - - - - + android:defaultValue="-1" + app:argType="integer" /> + android:defaultValue="0" + app:argType="integer" /> + + + + + + + + app:argType="string" + app:nullable="true" /> + android:name="org.moire.ultrasonic.fragment.EntryListFragment"> + android:name="org.moire.ultrasonic.fragment.SearchFragment"> @@ -214,30 +208,24 @@ app:destination="@id/albumListFragment" /> + app:nullable="true" /> + android:defaultValue="false" + app:argType="boolean" /> - + android:name="org.moire.ultrasonic.fragment.legacy.PlaylistsFragment"> - + android:name="org.moire.ultrasonic.fragment.legacy.SharesFragment"> - + android:name="org.moire.ultrasonic.fragment.legacy.PodcastFragment"> - + android:name="org.moire.ultrasonic.fragment.SettingsFragment" /> - + android:name="org.moire.ultrasonic.fragment.PlayerFragment"> @@ -284,7 +265,7 @@ + android:name="org.moire.ultrasonic.fragment.legacy.LyricsFragment"> @@ -297,20 +278,17 @@ android:name="org.moire.ultrasonic.fragment.EqualizerFragment" /> + android:name="org.moire.ultrasonic.fragment.ServerSelectorFragment"> + android:name="org.moire.ultrasonic.fragment.EditServerFragment"> + android:defaultValue="-1" + app:argType="integer" /> - \ No newline at end of file diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 9602c7e8..840e01ca 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -13,13 +13,11 @@ Záložky Knihovna médií Chat - Ultrasonic Menu Právě hraje Náhodně Podcasty Není registrován žádný podcast kanál Podcasty - Playlisty Hledat Poslat zprávu Ultrasonic @@ -87,7 +85,6 @@ Alba Umělci Žánry - Hudba Bez připojení Náhodné Označené hvězdičkou @@ -307,7 +304,6 @@ Sdílet skladby přes Sdílení Zobrazit umělce - albumArt Vícenásobné roky Možnosti ladění aplikace Zapisovat logy ladění do souboru diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index bc61c313..48da8f82 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -13,7 +13,6 @@ Lesezeichen Medienbibliothek Chat - Ultrasonic Hauptseite Aktuelle Wiedergabe Abspielen Pause @@ -25,7 +24,6 @@ Podcast Keine Podcast Kanäle registriert Podcast - Wiedergabeliste Suche Nachricht senden Senden @@ -119,7 +117,6 @@ Alben Künstler*innen Genres - Musik Offline %s - Server einrichten Zufällig diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index c7f528a8..4453972d 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -13,7 +13,6 @@ Marcadores Biblioteca Chat - Inicio de Ultrasonic Reproduciendo ahora Reproducir Pausar @@ -25,7 +24,6 @@ Podcast No hay canales de Podcasts registrados Podcast - Listas de reproducción Buscar Enviar un mensaje Enviar @@ -119,7 +117,6 @@ Álbumes Artistas Géneros - Música Sin conexión %s - Configurar servidor Aleatorio diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index fb707f88..a519b2df 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -13,7 +13,6 @@ Signets Bibliothèque musicale Salon de discussion - Accueil Ultrasonic Lecture en cours Lecture Pause @@ -25,7 +24,6 @@ Podcast Aucune chaîne de podcast enregistrée Podcast - Playlists Recherche Envoyer un message Envoyer @@ -117,7 +115,6 @@ Albums Artistes Genres - Musique Hors-ligne %s - Configurer le serveur Aléatoire diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 669a2413..94dcdae1 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -13,7 +13,6 @@ Könyvjelzők Médiakönyvtár Csevegés (Chat) - Ultrasonic főoldal Lejátszó Lejátszás Szünet @@ -25,7 +24,6 @@ Podcast Nincsenek podcast-csatornák regisztrálva Podcast - Lejátszási listák Keresés Üzenet küldése Ultrasonic @@ -93,7 +91,6 @@ Albumok Előadók Műfajok - Zenék Kapcsolat nélküli Véletlenszerű Csillaggal megjelölt diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index a9896b3d..c21a3ba7 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -17,7 +17,6 @@ Podcast Nessun canale podcast registrato Podcast - Playlists Cerca Spedisci un messaggio Annulla @@ -84,7 +83,6 @@ Album Artisti Generi - Musica Disconnesso Casuale Preferiti diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 992a7d96..5011ddf5 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -13,7 +13,6 @@ Bladwijzers Mediabibliotheek Chat - Ultrasonic Main Nu aan het afspelen Afspelen Pauzeren @@ -25,7 +24,6 @@ Podcast Geen podcastkanalen opgegeven Podcast - Afspeellijsten Zoeken Bericht versturen Versturen @@ -119,7 +117,6 @@ Albums Artiesten Genres - Muziek Offline %s - Server instellen Willekeurig diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 99679e4d..cb87fb66 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -13,13 +13,11 @@ Zakładki Biblioteka Czat - Ultrasonic Teraz gra Wymieszaj Podcasty Brak kanałów Podcast - Playlisty Szukaj Wyślij wiadomość Ultrasonic @@ -87,7 +85,6 @@ Albumy Artyści Gatunki - Muzyka Offline Losowe Ulubione diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index f4d5b814..1d69242c 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -13,7 +13,6 @@ Favoritos Biblioteca de Mídia Chat - Menu Principal Tocando Agora Tocar Pausar @@ -25,7 +24,6 @@ Podcasts Nenhum canal de podcasts registrado Podcasts - Playlists Pesquisa Enviar uma mensagem Álbum @@ -113,7 +111,6 @@ Álbuns Artistas Gêneros - Música Offline %s - Configurar Servidor Aleatórias diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index c0781b0c..e9e72d0b 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -13,13 +13,11 @@ Favoritos Biblioteca de Mídia Chat - Menu Principal Tocando Agora Misturar Podcasts Nenhum canal de podcasts registrado Podcast - Playlists Pesquisa Enviar uma mensagem Ultrasonic @@ -87,7 +85,6 @@ Álbuns Artistas Gêneros - Música Offline Aleatórias Favoritas diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index eb4ac301..9755e10f 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -13,7 +13,6 @@ Закладки Медиа библиотека Чат - UltraSonic Главная Сейчас играет Воспроизведение Пауза @@ -25,7 +24,6 @@ Подкаст Подкасты не зарегистрированы Подкаст - Плейлист Поиск Отправить сообщение Альбом @@ -108,7 +106,6 @@ Альбомы Исполнители Жанры - Музыка Не в сети Случайный Отмеченные diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 565d1e9e..b28bad83 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -13,7 +13,6 @@ 书签 媒体库 聊天 - Ultrasonic 主页 正在播放 播放 暂停 @@ -25,7 +24,6 @@ 播客 没有已注册的播客频道 播客 - 播放列表 搜索 发送消息 发送 @@ -104,7 +102,6 @@ 专辑 艺术家 流派 - 音乐 离线 %s - 已设置服务器 随机 diff --git a/ultrasonic/src/main/res/values-zh-rTW/strings.xml b/ultrasonic/src/main/res/values-zh-rTW/strings.xml index 64c5bc1b..94374d4c 100644 --- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml @@ -5,7 +5,6 @@ 書籤 媒體庫 正在播放 - 播放清單 搜尋 取消 註記 diff --git a/ultrasonic/src/main/res/values/setting_keys.xml b/ultrasonic/src/main/res/values/setting_keys.xml index 5052a082..c18a9a92 100644 --- a/ultrasonic/src/main/res/values/setting_keys.xml +++ b/ultrasonic/src/main/res/values/setting_keys.xml @@ -55,4 +55,5 @@ overrideLanguage firstInstalledVersion showConfirmationDialog + lastViewType \ No newline at end of file diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 3a32cfae..c1f88922 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -13,7 +13,6 @@ Bookmarks Media Library Chat - Ultrasonic Main Now Playing Play Pause @@ -25,10 +24,10 @@ Podcast No podcasts channels registered Podcast - Playlists Search Send a message Send + Avatar image Album Ultrasonic Artist @@ -116,10 +115,10 @@ Random Recently Played Starred + Chronological Albums Artists Genres - Music Offline %s - Set up Server Random @@ -370,7 +369,7 @@ Share songs via Share Show Artist - albumArt + Album artwork Multiple Years Show confirmation dialog Displays a confirmation dialog before deleting or unpinning songs @@ -451,5 +450,8 @@ Use hardware playback (experimental) Try to play the media using the media decoder chip on your phone. This can improve battery usage. + List + Cover + diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index 9f280d39..35d1b309 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -1,5 +1,5 @@ - + - - - - + +