diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
index 6e1d657a..9e639a38 100644
--- a/.gitlab/merge_request_templates/Default.md
+++ b/.gitlab/merge_request_templates/Default.md
@@ -1,4 +1,3 @@
-
----------------------------------------------------------------------------
@@ -7,8 +6,6 @@
- [ ] I ran `./gradlew -Pqc ktlintCheck`, `./gradlew -Pqc detekt` and
`./gradlew :ultrasonic:lintRelease` and no problems found. See
[CONTRIBUTING](CONTRIBUTING.md) for further information.
-- [ ] I'm using my own branch in my local copy. Ej, I want to merge
- `myuser/ultrasonic:my-new-contribution` into `develop`.
- [ ] All commits [are
signed](https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/).
- [ ] I agree to release my code and all other changes of this MR under the
diff --git a/.gitlab/merge_request_templates/Release.md b/.gitlab/merge_request_templates/Release.md
new file mode 100644
index 00000000..d6b50a2c
--- /dev/null
+++ b/.gitlab/merge_request_templates/Release.md
@@ -0,0 +1,10 @@
+#### Before merge:
+- [ ] MR is targetting the master branch
+- [ ] **Squash commits must be disabled!**
+- [ ] RoboTests (5 physical, 10 virtual) on a Release apk return no errors
+- [ ] Release notes present
+
+#### After merge
+- [ ] ``git fetch``
+- [ ] Create an annotated and signed tag: ``git tag -sa``
+- [ ] Push the tag to git:``git push --tags``
diff --git a/fastlane/metadata/android/en-US/changelogs/123.txt b/fastlane/metadata/android/en-US/changelogs/123.txt
new file mode 100644
index 00000000..490f3eb8
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/123.txt
@@ -0,0 +1,12 @@
+Features:
+- Search is accesible through a new icon on the main screen
+- Modernize Back Handling
+- Reenable R8 Code minification
+- Add a "Play Random Songs" shortcut
+
+Bug fixes:
+- Tracks buttons flash a scrollbar sometimes in Android 13
+- Fix EndlessScrolling in genre listing
+- Couldn't delete a track when shuffle was active
+- Upgrade material to 1.9.0
+
diff --git a/gradle.properties b/gradle.properties
index e2c4770f..5fc57f04 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,5 +21,5 @@ android.nonFinalResIds=true
org.gradle.unsafe.configuration-cache=true
# TODO Renable on day (check that Retrofit, Jackson, and Imageloader are working)
-android.enableR8.fullMode=false
+android.enableR8.fullMode=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 057f57a7..a18ca95a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,21 +2,21 @@
# You need to run ./gradlew wrapper after updating the version
gradle = "8.1.1"
-navigation = "2.5.3"
+navigation = "2.6.0"
gradlePlugin = "8.0.2"
androidxcore = "1.10.1"
ktlint = "0.43.2"
-ktlintGradle = "11.3.2"
+ktlintGradle = "11.4.0"
detekt = "1.23.0"
preferences = "1.2.0"
media3 = "1.0.2"
androidSupport = "1.6.0"
-materialDesign = "1.8.0"
+materialDesign = "1.9.0"
constraintLayout = "2.1.4"
multidex = "2.0.1"
room = "2.5.1"
-kotlin = "1.8.21"
+kotlin = "1.8.22"
kotlinxCoroutines = "1.7.1"
viewModelKtx = "2.6.1"
swipeRefresh = "1.1.0"
diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle
index bc68203f..a437074b 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -9,8 +9,8 @@ android {
defaultConfig {
applicationId "org.moire.ultrasonic"
- versionCode 122
- versionName "4.5.0"
+ versionCode 123
+ versionName "4.6.0"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
diff --git a/ultrasonic/minify/proguard-jackson.pro b/ultrasonic/minify/proguard-jackson.pro
index 865b1091..d154a877 100644
--- a/ultrasonic/minify/proguard-jackson.pro
+++ b/ultrasonic/minify/proguard-jackson.pro
@@ -1,5 +1,4 @@
#### From Jackson
-
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** {
*;
diff --git a/ultrasonic/minify/proguard-main.pro b/ultrasonic/minify/proguard-main.pro
index 83df2458..dda62679 100644
--- a/ultrasonic/minify/proguard-main.pro
+++ b/ultrasonic/minify/proguard-main.pro
@@ -1,8 +1,14 @@
-dontobfuscate
### Don't remove subsonic api serializers/entities
--keep class org.moire.ultrasonic.api.subsonic.response.** { *; }
--keep class org.moire.ultrasonic.api.subsonic.models.** { *; }
+-keep class org.moire.ultrasonic.api.subsonic.** { *; }
+
+## Don't remove the domain models
+-keep class org.moire.ultrasonic.domain.** { *; }
+
+## Don't remove the imageloader
+-keep class org.moire.ultrasonic.imageloader.** { *; }
+-keep class org.moire.ultrasonic.provider.AlbumArtContentProvider { *; }
## Don't remove NowPlayingFragment
-keep class org.moire.ultrasonic.fragment.NowPlayingFragment { *; }
diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml
index 266ccd79..711f13e2 100644
--- a/ultrasonic/src/main/AndroidManifest.xml
+++ b/ultrasonic/src/main/AndroidManifest.xml
@@ -32,7 +32,9 @@
android:usesCleartextTraffic="true"
android:supportsRtl="false"
android:preserveLegacyExternalStorage="true"
+ android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
+
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/SearchSuggestionProvider.java b/ultrasonic/src/main/java/org/moire/ultrasonic/provider/SearchSuggestionProvider.java
deleted file mode 100644
index 4cca8444..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/provider/SearchSuggestionProvider.java
+++ /dev/null
@@ -1,37 +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 2010 (C) Sindre Mehus
- */
-package org.moire.ultrasonic.provider;
-
-import android.content.SearchRecentSuggestionsProvider;
-
-/**
- * Provides search suggestions based on recent searches.
- *
- * @author Sindre Mehus
- */
-public class SearchSuggestionProvider extends SearchRecentSuggestionsProvider
-{
- public static final String AUTHORITY = SearchSuggestionProvider.class.getName();
- public static final int MODE = DATABASE_MODE_QUERIES;
-
- public SearchSuggestionProvider()
- {
- setupSuggestions(AUTHORITY, MODE);
- }
-}
\ No newline at end of file
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 8ed8c8bc..76df1ea5 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
@@ -18,15 +18,20 @@ import android.os.Bundle
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
import android.view.Menu
+import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
+import androidx.core.view.MenuProvider
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentContainerView
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.common.Player.STATE_BUFFERING
@@ -51,18 +56,20 @@ 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.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager
+import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
+import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LocaleHelper
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings
+import org.moire.ultrasonic.util.ShortcutUtil
import org.moire.ultrasonic.util.Storage
import org.moire.ultrasonic.util.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util
@@ -91,6 +98,10 @@ class NavigationActivity : AppCompatActivity() {
private var selectServerButton: MaterialButton? = null
private var headerBackgroundImage: ImageView? = null
+ // We store the last search string in this variable.
+ // Seems a bit like a hack, is there a better way?
+ var searchQuery: String? = null
+
private lateinit var appBarConfiguration: AppBarConfiguration
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
@@ -126,6 +137,8 @@ class NavigationActivity : AppCompatActivity() {
navigationView = findViewById(R.id.nav_view)
drawerLayout = findViewById(R.id.drawer_layout)
+ setupDrawerLayout(drawerLayout!!)
+
val toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
@@ -210,6 +223,80 @@ class NavigationActivity : AppCompatActivity() {
cachedServerCount = count ?: 0
updateNavigationHeaderForServer()
}
+
+ // Setup app shortcuts on supported devices, but not on first start, when the server
+ // is not configured yet.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
+ ShortcutUtil.registerShortcuts(this)
+ }
+
+ // Register our options menu
+ addMenuProvider(
+ searchMenuProvider,
+ this,
+ Lifecycle.State.RESUMED
+ )
+ }
+
+ private val searchMenuProvider: MenuProvider = object : MenuProvider {
+ override fun onPrepareMenu(menu: Menu) {
+ setupSearchField(menu)
+ }
+
+ override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.search_view_menu, menu)
+ }
+
+ override fun onMenuItemSelected(item: MenuItem): Boolean {
+ return false
+ }
+ }
+
+ fun setupSearchField(menu: Menu) {
+ Timber.i("Recreating search field")
+ val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+ val searchableInfo = searchManager.getSearchableInfo(this.componentName)
+ searchView.setSearchableInfo(searchableInfo)
+ searchView.setIconifiedByDefault(false)
+
+ if (searchQuery != null) {
+ Timber.e("Found existing search query")
+ searchItem.expandActionView()
+ searchView.isIconified = false
+ searchView.setQuery(searchQuery, false)
+ searchView.clearFocus()
+ // Restore search text only once!
+ searchQuery = null
+ }
+ }
+
+ private fun setupDrawerLayout(drawerLayout: DrawerLayout) {
+ // Set initial state passed on drawer state
+ closeNavigationDrawerOnBack.isEnabled = drawerLayout.isOpen
+
+ // Add the back press listener
+ onBackPressedDispatcher.addCallback(this, closeNavigationDrawerOnBack)
+
+ // Listen to changes in the drawer state and enable the back press listener accordingly.
+ drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
+ override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
+ // Nothing
+ }
+
+ override fun onDrawerOpened(drawerView: View) {
+ closeNavigationDrawerOnBack.isEnabled = true
+ }
+
+ override fun onDrawerClosed(drawerView: View) {
+ closeNavigationDrawerOnBack.isEnabled = false
+ }
+
+ override fun onDrawerStateChanged(newState: Int) {
+ // Nothing
+ }
+ })
}
override fun onResume() {
@@ -315,11 +402,18 @@ class NavigationActivity : AppCompatActivity() {
selectServerButton =
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
- selectServerButton?.setOnClickListener {
+ val dropDownButton: ImageView? =
+ navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
+
+ val onClick: (View) -> Unit = {
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
this.drawerLayout?.closeDrawer(GravityCompat.START)
navController.navigate(R.id.serverSelectorFragment)
}
+
+ selectServerButton?.setOnClickListener(onClick)
+ dropDownButton?.setOnClickListener(onClick)
+
headerBackgroundImage =
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
}
@@ -328,13 +422,9 @@ class NavigationActivity : AppCompatActivity() {
setupActionBarWithNavController(navController, appBarConfig)
}
- override fun onBackPressed() {
- if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
- this.drawerLayout?.closeDrawer(GravityCompat.START)
- } else {
- val currentFragment = host!!.childFragmentManager.fragments.last()
- if (currentFragment is OnBackPressedHandler) currentFragment.onBackPressed()
- else super.onBackPressed()
+ private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ drawerLayout?.closeDrawer(GravityCompat.START)
}
}
@@ -352,40 +442,60 @@ class NavigationActivity : AppCompatActivity() {
super.onOptionsItemSelected(item)
}
+ // TODO: Why is this needed? Shouldn't it just work by default?
override fun onSupportNavigateUp(): Boolean {
- val currentFragment = host!!.childFragmentManager.fragments.last()
- return if (currentFragment is OnBackPressedHandler) {
- currentFragment.onBackPressed()
- true
- } else {
- findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
+ return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+
+ when (intent?.action) {
+ Constants.INTENT_PLAY_RANDOM_SONGS -> {
+ playRandomSongs()
+ }
+ Intent.ACTION_MAIN -> {
+ if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
+ findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
+ }
+ }
+ Intent.ACTION_SEARCH -> {
+ searchQuery = intent.getStringExtra(SearchManager.QUERY)
+ handleSearchIntent(searchQuery, false)
+ }
+ MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH -> {
+ searchQuery = intent.getStringExtra(SearchManager.QUERY)
+ handleSearchIntent(searchQuery, true)
+ }
}
}
- // TODO Test if this works with external Intents
- // android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- if (intent == null) return
+ private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
+ val suggestions = SearchRecentSuggestions(
+ this,
+ SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
+ )
+ suggestions.saveRecentQuery(query, null)
- if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
- findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
- return
- }
+ val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
+ findNavController(R.id.nav_host_fragment).navigate(action)
+ }
- val query = intent.getStringExtra(SearchManager.QUERY)
-
- if (query != null) {
- val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
- val suggestions = SearchRecentSuggestions(
- this,
- SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
- )
- suggestions.saveRecentQuery(query, null)
-
- val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
- findNavController(R.id.nav_host_fragment).navigate(action)
- }
+ private fun playRandomSongs() {
+ val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
+ val service = MusicServiceFactory.getMusicService()
+ val musicDirectory = service.getRandomSongs(Settings.maxSongs)
+ val downloadHandler: DownloadHandler by inject()
+ downloadHandler.addTracksToMediaController(
+ songs = musicDirectory.getTracks(),
+ append = false,
+ playNext = false,
+ autoPlay = true,
+ shuffle = false,
+ fragment = currentFragment,
+ playlistName = null
+ )
+ return
}
/**
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 2efea271..4994a0b4 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt
@@ -1,5 +1,6 @@
package org.moire.ultrasonic.adapters
+import android.graphics.Color
import android.view.View
import android.widget.Checkable
import android.widget.CheckedTextView
@@ -13,6 +14,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.media3.common.HeartRating
import androidx.media3.common.StarRating
import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.color.MaterialColors
import com.google.android.material.progressindicator.CircularProgressIndicator
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
@@ -43,8 +45,13 @@ class TrackViewHolder(val view: View) :
KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.IO) {
+ companion object {
+ val COLOR_HIGHLIGHT = com.google.android.material.R.attr.colorSecondaryContainer
+ }
+
var entry: Track? = null
private set
+ var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
var check: CheckedTextView = view.findViewById(R.id.song_check)
var drag: ImageView = view.findViewById(R.id.song_drag)
var observableChecked = MutableLiveData(false)
@@ -164,17 +171,23 @@ class TrackViewHolder(val view: View) :
ContextCompat.getDrawable(view.context, R.drawable.ic_stat_play)!!
}
+ @Suppress("MagicNumber")
private fun setPlayIcon(isPlaying: Boolean) {
if (isPlaying && !isPlayingCached) {
isPlayingCached = true
title.setCompoundDrawablesWithIntrinsicBounds(
playingIcon, null, null, null
)
+ val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
+ songLayout.setBackgroundColor(color)
+ songLayout.elevation = 3F
} else if (!isPlaying && isPlayingCached) {
isPlayingCached = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
)
+ songLayout.setBackgroundColor(Color.TRANSPARENT)
+ songLayout.elevation = 0F
}
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
index 73d3a0ba..b50cfb67 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt
@@ -7,12 +7,14 @@
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.Button
import android.widget.ImageView
+import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
@@ -52,7 +54,7 @@ private const val DIALOG_PADDING = 12
/**
* Displays a form where server settings can be created / edited
*/
-class EditServerFragment : Fragment(), OnBackPressedHandler {
+class EditServerFragment : Fragment() {
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val activeServerProvider: ActiveServerProvider by inject()
@@ -82,6 +84,13 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
super.onCreate(savedInstanceState)
}
+ override fun onAttach(context: Context) {
+ requireActivity().onBackPressedDispatcher.addCallback(
+ this, confirmCloseCallback
+ )
+ super.onAttach(context)
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -189,11 +198,25 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
}
}
+ private val confirmCloseCallback = object : OnBackPressedCallback(
+ true // default to enabled
+ ) {
+ override fun handleOnBackPressed() {
+ finishActivity()
+ }
+ }
+
override fun onStop() {
Util.hideKeyboard(activity)
+ confirmCloseCallback.isEnabled = false
super.onStop()
}
+ override fun onResume() {
+ confirmCloseCallback.isEnabled = true
+ super.onResume()
+ }
+
private fun correctServerAddress() {
serverAddressEditText?.editText?.setText(
serverAddressEditText?.editText?.text?.trim(' ', '/')
@@ -206,11 +229,6 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
image?.setTint(currentColor)
serverColorImageView?.background = image
}
-
- override fun onBackPressed() {
- finishActivity()
- }
-
override fun onSaveInstanceState(savedInstanceState: Bundle) {
savedInstanceState.putString(
::serverNameEditText.name, serverNameEditText!!.editText?.text.toString()
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt
deleted file mode 100644
index c34ca2dd..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/OnBackPressedHandler.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * OnBackPressedHandler.kt
- * Copyright (C) 2009-2022 Ultrasonic developers
- *
- * Distributed under terms of the GNU GPLv3 license.
- */
-
-package org.moire.ultrasonic.fragment
-
-/**
- * Interface for fragments handling their own Back button
- */
-interface OnBackPressedHandler {
- fun onBackPressed()
-}
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 cc46a670..7f50a870 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt
@@ -51,7 +51,6 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicator
@@ -481,9 +480,7 @@ class PlayerFragment :
val index = mediaPlayerManager.currentMediaItemIndex
if (index != -1) {
- val smoothScroller = LinearSmoothScroller(context)
- smoothScroller.targetPosition = index
- viewManager.startSmoothScroll(smoothScroller)
+ viewManager.scrollToPosition(index)
}
}
@@ -930,7 +927,8 @@ class PlayerFragment :
// Swipe to delete from playlist
@SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- val pos = viewHolder.bindingAdapterPosition
+ val viewPos = viewHolder.bindingAdapterPosition
+ val pos = mediaPlayerManager.getUnshuffledIndexOf(viewPos)
val item = mediaPlayerManager.getMediaItemAt(pos)
// Remove the item from the list quickly
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 bd70f431..cd4d1104 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt
@@ -7,19 +7,11 @@
package org.moire.ultrasonic.fragment
-import android.app.SearchManager
-import android.content.Context
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
-import androidx.appcompat.widget.SearchView
-import androidx.core.view.MenuHost
-import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -54,16 +46,14 @@ import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast
-import timber.log.Timber
/**
* Initiates a search on the media library and displays the results
- * TODO: Switch to material3 class
+
*/
class SearchFragment : MultiListFragment(), KoinComponent {
private var searchResult: SearchResult? = null
private var searchRefresh: SwipeRefreshLayout? = null
- private var searchView: SearchView? = null
private val mediaPlayerManager: MediaPlayerManager by inject()
@@ -83,13 +73,6 @@ class SearchFragment : MultiListFragment(), KoinComponent {
cancellationToken = CancellationToken()
setTitle(this, R.string.search_title)
- // Register our options menu
- (requireActivity() as MenuHost).addMenuProvider(
- menuProvider,
- viewLifecycleOwner,
- Lifecycle.State.RESUMED
- )
-
listModel.searchResult.observe(
viewLifecycleOwner
) {
@@ -148,73 +131,6 @@ class SearchFragment : MultiListFragment(), KoinComponent {
}
}
- /**
- * This provide creates the search bar above the recycler view
- */
- private val menuProvider: MenuProvider = object : MenuProvider {
- override fun onPrepareMenu(menu: Menu) {
- setupOptionsMenu(menu)
- }
-
- override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
- menuInflater.inflate(R.menu.search, menu)
- }
-
- override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
- return true
- }
- }
- fun setupOptionsMenu(menu: Menu) {
- val activity = activity ?: return
- val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager
- val searchItem = menu.findItem(R.id.search_item)
- searchView = searchItem.actionView as SearchView
- val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName)
- searchView!!.setSearchableInfo(searchableInfo)
-
- val autoPlay = navArgs.autoplay
- val query = navArgs.query
-
- // If started with a query, enter it to the searchView
- if (query != null) {
- searchView!!.setQuery(query, false)
- searchView!!.clearFocus()
- }
-
- searchView!!.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
- override fun onSuggestionSelect(position: Int): Boolean {
- return true
- }
-
- override fun onSuggestionClick(position: Int): Boolean {
- Timber.d("onSuggestionClick: %d", position)
- val cursor = searchView!!.suggestionsAdapter.cursor
- cursor.moveToPosition(position)
-
- // 2 is the index of col containing suggestion name.
- val suggestion = cursor.getString(2)
- searchView!!.setQuery(suggestion, true)
- return true
- }
- })
-
- searchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- Timber.d("onQueryTextSubmit: %s", query)
- searchView!!.clearFocus()
- search(query, autoPlay)
- return true
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- return true
- }
- })
-
- searchView!!.setIconifiedByDefault(false)
- searchItem.expandActionView()
- }
-
override fun onDestroyView() {
Util.hideKeyboard(activity)
cancellationToken?.cancel()
@@ -313,7 +229,6 @@ class SearchFragment : MultiListFragment(), KoinComponent {
}
private fun onAlbumSelected(album: Album, autoplay: Boolean) {
-
val action = SearchFragmentDirections.searchToTrackCollection(
id = album.id,
name = album.title,
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 5491bca0..cd71ee7c 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt
@@ -36,7 +36,6 @@ import org.moire.ultrasonic.adapters.AlbumHeader
import org.moire.ultrasonic.adapters.AlbumRowDelegate
import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder
-import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
@@ -64,7 +63,6 @@ import timber.log.Timber
* In most cases the data should be just a list of Entries, but there are some cases
* 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.
- *
*/
@Suppress("TooManyFunctions")
open class TrackCollectionFragment(
@@ -93,7 +91,6 @@ open class TrackCollectionFragment(
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
private var sortOrder = initialOrder
- private var offset: Int? = null
/**
* The id of the main layout
@@ -190,13 +187,12 @@ open class TrackCollectionFragment(
private fun loadMoreTracks() {
if (displayRandom() || navArgs.genreName != null) {
- offset = navArgs.offset + navArgs.size
- getLiveData(refresh = true, append = true)
+ getLiveData(append = true)
}
}
internal open fun handleRefresh() {
- getLiveData(true)
+ getLiveData(refresh = true)
}
internal open fun setupButtons(view: View) {
@@ -268,6 +264,9 @@ open class TrackCollectionFragment(
private val menuProvider: MenuProvider = object : MenuProvider {
override fun onPrepareMenu(menu: Menu) {
+ // Hide search button (from xml)
+ menu.findItem(R.id.action_search).isVisible = false
+
playAllButton = menu.findItem(R.id.select_album_play_all)
if (playAllButton != null) {
@@ -282,7 +281,7 @@ open class TrackCollectionFragment(
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.select_album, menu)
+ inflater.inflate(R.menu.track_collection_menu, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
@@ -552,7 +551,7 @@ open class TrackCollectionFragment(
val getVideos = navArgs.getVideos
val getRandomTracks = displayRandom()
val size = if (navArgs.size < 0) Settings.maxSongs else navArgs.size
- val offset = offset ?: navArgs.offset
+ val offset = navArgs.offset
val refresh2 = navArgs.refresh || refresh
listModel.viewModelScope.launch(handler) {
@@ -584,12 +583,8 @@ open class TrackCollectionFragment(
} else {
setTitle(name)
- if (ActiveServerProvider.shouldUseId3Tags()) {
- if (isAlbum) {
- listModel.getAlbum(refresh2, id, name)
- } else {
- throw IllegalAccessException("Use AlbumFragment instead!")
- }
+ if (isAlbum) {
+ listModel.getAlbum(refresh2, id, name)
} else {
listModel.getMusicDirectory(refresh2, id, name)
}
@@ -688,7 +683,7 @@ open class TrackCollectionFragment(
override fun setOrderType(newOrder: SortOrder) {
sortOrder = newOrder
- getLiveData(true)
+ getLiveData(refresh = true)
}
override var viewCapabilities: ViewCapabilities = ViewCapabilities(
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 df0cb4aa..22a92869 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt
@@ -27,6 +27,7 @@ import org.moire.ultrasonic.util.Util
class TrackCollectionModel(application: Application) : GenericListModel(application) {
val currentList: MutableLiveData> = MutableLiveData()
+ private var loadedUntil: Int = 0
/*
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
@@ -37,7 +38,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
name: String?
) {
withContext(Dispatchers.IO) {
-
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getMusicDirectory(id, name, refresh)
currentListIsSortable = true
@@ -57,11 +57,19 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getSongsForGenre(genre: String, count: Int, offset: Int, append: Boolean) {
+ // Handle the logic for endless scrolling:
+ // If appending the existing list, set the offset from where to load
+ var newOffset = offset
+ if (append) newOffset += (count + loadedUntil)
+
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
- val musicDirectory = service.getSongsByGenre(genre, count, offset)
+ val musicDirectory = service.getSongsByGenre(genre, count, newOffset)
currentListIsSortable = false
updateList(musicDirectory, append)
+
+ // Update current offset
+ loadedUntil = newOffset
}
}
@@ -96,7 +104,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getRandom(size: Int, append: Boolean) {
-
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(size)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
index 6dd656a3..7965558e 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt
@@ -340,6 +340,7 @@ class PlaybackService :
// needed starting Android 12 (S = 31)
flags = flags or FLAG_IMMUTABLE
}
+ intent.action = Intent.ACTION_MAIN
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
return PendingIntent.getActivity(this, 0, intent, flags)
}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/SearchSuggestionProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/SearchSuggestionProvider.kt
new file mode 100644
index 00000000..06803c17
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/SearchSuggestionProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * SearchSuggestionProvider.kt
+ * Copyright (C) 2009-2023 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+package org.moire.ultrasonic.provider
+
+import android.content.SearchRecentSuggestionsProvider
+import org.moire.ultrasonic.BuildConfig
+
+/**
+ * Provides search suggestions based on recent searches.
+ */
+class SearchSuggestionProvider : SearchRecentSuggestionsProvider() {
+ init {
+ setupSuggestions(AUTHORITY, MODE)
+ }
+
+ companion object {
+ val AUTHORITY = "${BuildConfig.APPLICATION_ID}.provider.SearchSuggestionProvider"
+ const val MODE = DATABASE_MODE_QUERIES
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt
index 5902d772..3a422a2b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.kt
@@ -219,8 +219,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
NavigationActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (playerActive) intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
- intent.action = "android.intent.action.MAIN"
- intent.addCategory("android.intent.category.LAUNCHER")
+ intent.action = Intent.ACTION_MAIN
+ intent.addCategory(Intent.CATEGORY_LAUNCHER)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt
index a580b3d3..dba110ec 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerManager.kt
@@ -720,16 +720,25 @@ class MediaPlayerManager(
/**
* Loops over the timeline windows to find the entry which matches the given closure.
*
+ * @param returnWindow Determines which of the two indexes of a match to return:
+ * True for the position in the playlist, False for position in the play order.
* @param searchClosure Determines the condition which the searched for window needs to match.
* @return the index of the window that satisfies the search condition,
* or [C.INDEX_UNSET] if not found.
*/
- private fun getWindowIndexWhere(searchClosure: (Int, Int) -> Boolean): Int {
+ @Suppress("KotlinConstantConditions")
+ private fun getWindowIndexWhere(
+ returnWindow: Boolean,
+ searchClosure: (Int, Int) -> Boolean
+ ): Int {
val timeline = controller?.currentTimeline!!
var windowIndex = timeline.getFirstWindowIndex(true)
var count = 0
+
while (windowIndex != C.INDEX_UNSET) {
- if (searchClosure(count, windowIndex)) return count
+ val match = searchClosure(count, windowIndex)
+ if (match && returnWindow) return windowIndex
+ if (match && !returnWindow) return count
count++
windowIndex = timeline.getNextWindowIndex(
windowIndex, REPEAT_MODE_OFF, true
@@ -748,7 +757,10 @@ class MediaPlayerManager(
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
*/
fun getShuffledIndexOf(searchPosition: Int): Int {
- return getWindowIndexWhere { _, windowIndex -> windowIndex == searchPosition }
+ return getWindowIndexWhere(false) {
+ _, windowIndex ->
+ windowIndex == searchPosition
+ }
}
/**
@@ -760,7 +772,10 @@ class MediaPlayerManager(
* @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found.
*/
fun getUnshuffledIndexOf(shufflePosition: Int): Int {
- return getWindowIndexWhere { count, _ -> count == shufflePosition }
+ return getWindowIndexWhere(true) {
+ count, _ ->
+ count == shufflePosition
+ }
}
val mediaItemCount: Int
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
index 351de91c..fceee797 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Constants.kt
@@ -25,11 +25,11 @@ object Constants {
const val CMD_PREVIOUS = "org.moire.ultrasonic.CMD_PREVIOUS"
const val CMD_NEXT = "org.moire.ultrasonic.CMD_NEXT"
const val INTENT_SHOW_PLAYER = "org.moire.ultrasonic.SHOW_PLAYER"
+ const val INTENT_PLAY_RANDOM_SONGS = "org.moire.ultrasonic.CMD_RANDOM_SONGS"
// Legacy Preferences keys
// Warning: Don't add any new here!
// Use setting_keys.xml
- const val PREFERENCES_KEY_USE_FIVE_STAR_RATING = "use_five_star_rating"
const val PREFERENCE_VALUE_ALL = 0
const val PREFERENCE_VALUE_A2DP = 1
const val PREFERENCE_VALUE_DISABLED = 2
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShortcutUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShortcutUtil.kt
new file mode 100644
index 00000000..7034d75b
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ShortcutUtil.kt
@@ -0,0 +1,37 @@
+/*
+ * ShortcutUtil.kt
+ * Copyright (C) 2009-2023 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
+package org.moire.ultrasonic.util
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Build
+import androidx.annotation.RequiresApi
+import org.moire.ultrasonic.R
+import org.moire.ultrasonic.activity.NavigationActivity
+
+object ShortcutUtil {
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ fun registerShortcuts(activity: Activity) {
+ val shortcutIntent = Intent(activity, NavigationActivity::class.java).apply {
+ action = Constants.INTENT_PLAY_RANDOM_SONGS
+ }
+
+ val shortcut = ShortcutInfo.Builder(activity, "shortcut_play_random_songs")
+ .setShortLabel(activity.getString(R.string.shortcut_play_random_songs_short))
+ .setLongLabel(activity.getString(R.string.shortcut_play_random_songs_long))
+ .setIcon(Icon.createWithResource(activity, R.drawable.media_shuffle))
+ .setIntent(shortcutIntent)
+ .build()
+
+ val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
+ shortcutManager?.dynamicShortcuts = listOf(shortcut)
+ }
+}
diff --git a/ultrasonic/src/main/res/drawable/arrow_drop_down.xml b/ultrasonic/src/main/res/drawable/arrow_drop_down.xml
new file mode 100644
index 00000000..0a9fd3a6
--- /dev/null
+++ b/ultrasonic/src/main/res/drawable/arrow_drop_down.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/ultrasonic/src/main/res/layout/navigation_activity.xml b/ultrasonic/src/main/res/layout/navigation_activity.xml
index 867f1f6b..ef421eac 100644
--- a/ultrasonic/src/main/res/layout/navigation_activity.xml
+++ b/ultrasonic/src/main/res/layout/navigation_activity.xml
@@ -42,6 +42,7 @@
-
+ app:layout_constraintTop_toTopOf="parent" />
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/track_buttons.xml b/ultrasonic/src/main/res/layout/track_buttons.xml
index 04708fcc..b0688f86 100644
--- a/ultrasonic/src/main/res/layout/track_buttons.xml
+++ b/ultrasonic/src/main/res/layout/track_buttons.xml
@@ -7,7 +7,7 @@
android:orientation="horizontal"
android:padding="6dp" >
-
-
+ app:iconSize="26dp"
+ android:scrollbars="none" />
-
+ app:iconSize="26dp"
+ android:scrollbars="none" />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
-
+ app:iconSize="26dp"
+ android:scrollbars="none"
+ />
diff --git a/ultrasonic/src/main/res/menu/search.xml b/ultrasonic/src/main/res/menu/search.xml
deleted file mode 100644
index 1fba16c6..00000000
--- a/ultrasonic/src/main/res/menu/search.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/menu/search_view_menu.xml b/ultrasonic/src/main/res/menu/search_view_menu.xml
new file mode 100644
index 00000000..9b39a8b2
--- /dev/null
+++ b/ultrasonic/src/main/res/menu/search_view_menu.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/menu/select_album.xml b/ultrasonic/src/main/res/menu/track_collection_menu.xml
similarity index 100%
rename from ultrasonic/src/main/res/menu/select_album.xml
rename to ultrasonic/src/main/res/menu/track_collection_menu.xml
diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml
index c06ae0b5..2bcf7604 100644
--- a/ultrasonic/src/main/res/values-cs/strings.xml
+++ b/ultrasonic/src/main/res/values-cs/strings.xml
@@ -104,7 +104,6 @@
Chyba aktualizace informací playlistu %sAlbaUmělci
- VyhledáváníZobrazit víceNenalezeno, zkuste znovuSkladby
diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml
index 728688b9..b60c265f 100644
--- a/ultrasonic/src/main/res/values-de/strings.xml
+++ b/ultrasonic/src/main/res/values-de/strings.xml
@@ -139,7 +139,6 @@
Aktualisierung der Wiedergabeliste %s ist fehlgeschlagenAlbenKünstler*innen
- SucheZeige mehrKeine Treffer, bitte erneut versuchenTitel
diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml
index 39adfa95..0f66c28f 100644
--- a/ultrasonic/src/main/res/values-es/strings.xml
+++ b/ultrasonic/src/main/res/values-es/strings.xml
@@ -141,7 +141,6 @@
Fallo al actualizar la información de la lista de reproducción para %sÁlbumesArtistas
- BuscarMostrar masSin resultados, por favor inténtalo de nuevoCanciones
@@ -453,4 +452,6 @@
No se puede reanudar la reproducciónPresione el botón de reproducción en la notificación de medios si aún está presente; de lo contrario, abra la aplicación para iniciar la reproducción y vuelva a conectar la sesión al controladorTasa de bits máxima: al fijar una canción de forma permanente
+ Canciones aleatorias
+ Reproducir las canciones aleatoriamente
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml
index 735457b5..73226555 100644
--- a/ultrasonic/src/main/res/values-fr/strings.xml
+++ b/ultrasonic/src/main/res/values-fr/strings.xml
@@ -137,7 +137,6 @@
Échec de la mise à jour des informations de la liste de lecture pour %sAlbumsArtistes
- RechercheAfficher plusAucun résultat, veuillez essayer à nouveauTitres
@@ -392,7 +391,7 @@
Utiliser les étoiles pour noter les morceauxChronologique
- Reprise
+ Couverture1000 morceaux100 morceaux500 morceaux
diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml
index f85bf6b3..853d1e7f 100644
--- a/ultrasonic/src/main/res/values-hu/strings.xml
+++ b/ultrasonic/src/main/res/values-hu/strings.xml
@@ -110,7 +110,6 @@
Lejátszási lista módosítása sikertelen %sAlbumokElőadók
- KeresésTovábbiakNincs találat, próbálja újra!Dalok
diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml
index 0b778491..b0f2faa9 100644
--- a/ultrasonic/src/main/res/values-it/strings.xml
+++ b/ultrasonic/src/main/res/values-it/strings.xml
@@ -100,7 +100,6 @@
Impossibile aggiornare informazioni playlist per %sAlbumArtisti
- CercaMostra di piùNessun risultato, riprova per favoreCanzoni
diff --git a/ultrasonic/src/main/res/values-ja/strings.xml b/ultrasonic/src/main/res/values-ja/strings.xml
index b1ee02f1..d09880f5 100644
--- a/ultrasonic/src/main/res/values-ja/strings.xml
+++ b/ultrasonic/src/main/res/values-ja/strings.xml
@@ -106,7 +106,6 @@
%s のプレイリスト情報をアップデートできませんアルバムアーティスト
- 検索もっと表示一致するものはありません、やり直してください曲
@@ -446,4 +445,5 @@
ハードウェア再生を使用する (実験的)端末のメディアデコーダーチップを使用してメディアを再生するよう試行します。これにより、バッテリー使用量を改善できます。このオプションを有効化することで、再生の不具合が起こる場合も報告されています!メディア通知の再生ボタンがある場合はそれをタップします。ない場合はアプリを開いて再生を開始し、セッションをコントローラーに再接続します
+ 最高ビットレート - 永続的に固定された曲の場合
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/values-nb-rNO/strings.xml b/ultrasonic/src/main/res/values-nb-rNO/strings.xml
index 5d530829..86e58df5 100644
--- a/ultrasonic/src/main/res/values-nb-rNO/strings.xml
+++ b/ultrasonic/src/main/res/values-nb-rNO/strings.xml
@@ -186,7 +186,6 @@
Oppdater infoAlbumArtister
- SøkVis merSporSøk
diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml
index 882ba982..2fea5e44 100644
--- a/ultrasonic/src/main/res/values-nl/strings.xml
+++ b/ultrasonic/src/main/res/values-nl/strings.xml
@@ -142,7 +142,6 @@
Kan afspeellijstinformatie voor %s niet bijwerkenAlbumsArtiesten
- ZoekenMeer tonenGeen overeenkomsten; probeer het opnieuwNummers
diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml
index c5f5cef2..2e87a7ad 100644
--- a/ultrasonic/src/main/res/values-pl/strings.xml
+++ b/ultrasonic/src/main/res/values-pl/strings.xml
@@ -1,10 +1,10 @@
Ładowanie…
- Wystąpił błąd sieci. Proszę sprawdzić adres serwera i spróbować później.
- Server api v%1$s does not support this function.
- Ta aplikacja wymaga dostępu do sieci. Proszę włączyć wi-fi lub dane komórkowe.
- Nie znaleziono zasobów. Proszę sprawdzić adres serwera.
+ Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później.
+ API serwera w wersji v%1$s nie wspiera tej funkcjonalności.
+ Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.
+ Nie udało się odnaleźć zasobu. Proszę sprawdzić adres serwera.Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera.Błąd certyfikatu HTTPS: %1$s.Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.
@@ -15,8 +15,8 @@
Teraz graWymieszajPodcasty
- Brak kanałów
- Podcast
+ Nie zarejestrowano żadnych kanałów podcastowych
+ PodcastySzukajWyślij wiadomośćUltrasonic
@@ -34,10 +34,10 @@
Odtwórz następneOdtwórz terazOdtwórz losowo
- Publicznie
+ PublicznaZapiszOdepnij
- Różni artyści
+ Różni wykonawcyCzy chcesz usunąć %1$sZakładka usunięta.Zakładka ustawiona na %s.
@@ -54,7 +54,7 @@
Zapisz playlistęEkran wyłączonyEkran włączony
- Wyświetl album
+ Przejdź do albumuWymieszajOdtwarzanie losowePlaylista została zapisana.
@@ -71,7 +71,7 @@
BłądJukebox domyślnieBrak tekstu utworu
- wg artystów
+ wg wykonawcówwg tytułuNajczęściej odtwarzaneNajlepiej oceniane
@@ -80,7 +80,7 @@
Ostatnio odtwarzaneUlubioneAlbumy
- Artyści
+ WykonawcyGatunkiOfflineLosowe
@@ -98,12 +98,11 @@
Biblioteka mediówMedia offlinePlaylisty
- Aktualizacja informacji
+ Zmień informacjeZaktualizowano informacje dla playlisty %s
- Błąc podczas aktualizacji playlisty %s
+ Błąd podczas aktualizacji playlisty %sAlbumy
- Artyści
- Wyszukaj
+ WykonawcyWyświetl więcejBrak wyników, proszę spróbować ponownieUtwory
@@ -150,12 +149,12 @@
9 GBNieograniczonaPamięć podręczna muzyki
- Okres odświeżania czatu
- Czyszczenie zakładek
- Czyść zakładkę po zakończeniu odtwarzania utworu
+ Odświeżaj czat co
+ Usuwanie zakładek
+ Usuwaj zakładkę po zakończeniu odtwarzania utworuWyczyść historię wyszukiwaniaDomyślna ilość wyników - albumy
- Domyślna ilość wyników - artyści
+ Domyślna ilość wyników - wykonawcyDomyślna ilość wyników - utworyOkres przechowywania pamięci podręcznej kataloguWyłączone
@@ -165,15 +164,15 @@
30 minut5 minut1 godzina
- Sortuj utwory wg dysku
- Sortuje listę utworów wg numeru dysku i numeru utworu
- Dołącza bitrate i typ pliku do nazwy artysty
+ Sortuj utwory wg numeru płyty
+ Sortuj listę utworów wg numeru płyty i numeru utworu
+ Dołącz bitrate i rozszerzenie pliku obok nazwy wykonawcyUkrywa pliki muzyczne przed innymi aplikacjami.Ukryj plikiEfekt widoczny będzie po następnym skanowaniu muzyki przez system Android.Proszę wprowadzić prawidłowy URL.Maksymalna ilość wyników - albumy
- Maksymalna ilość wyników - artyści
+ Maksymalna ilość wyników - wykonawcy112 Kbps128 Kbps160 Kbps
@@ -186,9 +185,9 @@
96 KbpsMaksymalny bitrate dla połączenia komórkowegoBez limitu
- Maksymalny bitrate dla połączenia Wi-fi
+ Maksymalny bitrate dla połączenia Wi-FiMaksymalna ilość wyników - utwory
- Reaguje na przyciski telefonu, słuchawek i Bluetooth
+ Reaguj na przyciski multimedialne telefonu, słuchawek i urządzeń BluetoothPrzyciskiPrzekroczenie limitu czasu sieci105 sekund
@@ -208,7 +207,7 @@
Wszystkie urządzenia BluetoothTylko urządzenia audio (A2DP)Wyłączone
- Wznawiaj po podłączeniu słuchawek
+ Wznów po podłączeniu słuchawekAplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek.Ilość wstępnie ładowanych utworów1 utwór
@@ -249,8 +248,8 @@
MotywZezwalaj na własne certyfikaty HTTPSWymuś uwierzytelnianie zwykłym hasłem
- Używaj folderów jako nazw artystów
- Zakłada, że folder najwyższego poziomu jest nazwą artysty albumu
+ Używaj folderów jako nazw wykonawców
+ Zakłada, że folder najwyższego poziomu jest nazwą wykonawcy albumuPrzeglądaj używając tagów ID3Używa metod z tagów ID3 zamiast metod opartych na systemie plikówWideo
@@ -276,7 +275,7 @@
Usuń zakładkęUlubioneWyczyść playlistę
- Udostępnienia
+ UdostępnioneBrak udostępnień na serwerzeUsunięto udostępnienie %sNieudane usunięcie udostępnienia %s
@@ -294,7 +293,7 @@
Sprawdź muzykę, którą udostępniam na %sUdostępnij utwory za pomocąUdostępnianie
- Wyświetlaj artystę
+ Przejdź do wykonawcyZ różnych latSkonfigurowane serweryCzy na pewno chcesz usunąć ten serwer\?
@@ -326,7 +325,7 @@
Brak zgodności wersji. Uaktualnij aplikację Ultrasonic na Androida.Brak zgodności wersji. Uaktualnij serwer Subsonic.
- Użyj pięciu gwiazdek dla utworów
+ 5-gwiazdkowy system ocen utworówPokaż okno potwierdzające usunięcie lub odpięcie utworówAngielskiPamiętaj o ustawieniu nazwy użytkownika i hasła do usługi Scrobble na serwerze
@@ -336,7 +335,7 @@
ZatrzymajFrancuskiCzy na pewno chcesz odpiąć zaznaczone pozycje\?
- Użyj niestandardowej lokacji pamięci podręcznej
+ Niestandardowa lokalizacja pamięci podręcznejWybierz wszystkoWłączony tryb losowyNastępne
@@ -352,7 +351,7 @@
1000 piosenekWspierane funkcjePolski
- Artysta
+ WykonawcaHolenderskiWęgierskiZapisanych jest %1$s plików z logami, które zajmują ~%2$s MB miejsca w katalogu %3$s. Czy chcesz je zachować\?
@@ -366,7 +365,7 @@
PowtarzajNic nie jest pobieraneRosyjski
- Byforowanie…
+ Buforowanie…%s - Ustaw serwer50 piosenekChiński (Chiny)
@@ -383,13 +382,13 @@
PortugalskiKolor serweraPauza
- Pokaż obraz wykonawcy na liście
+ Wyświetlaj obraz wykonawcy w liścieTytułCzy na pewno chcesz usunąć zaznaczone pozycje\?Okładka albumuAlbum500 piosenek
- Udostępnianie spowoduje utworzenie go na serwerze i udostępnienie jego adresu URL. Jeśli ta opcja jest wyłączona, udostępniane są tylko szczegóły utworu
+ Udostępnianie umożliwi dostęp do utworu na serwerze i wygeneruje do niego adres URL. Jeśli ta opcja jest wyłączona, udostępniane będą tylko informacje o utworzePokaż Obecnie odtwarzane po kliknięciu przycisku OdtwarzajHiszpańskiWymagane jest ponowne uruchomienie aplikacji po zmianie języka
@@ -398,7 +397,7 @@
Chiński (Tajwan)Dzień i nocCzarny
- Zmusza to aplikację do wysyłania hasła w postaci niezaszyfrowanej. Przydatne, jeśli serwer Subsonic nie obsługuje nowego interfejsu API uwierzytelniania dla użytkowników.
+ Zmusza aplikację do przesyłania hasła w postaci niezaszyfrowanej. Przydaje się, gdy serwer Subsonic nie obsługuje nowego API uwierzytelniania użytkowników.Pokaż więcej informacji o utworze w sekcji Obecnie odtwarzane (gatunek, rok, przepustowość)Pokaż szczegóły w sekcji Obecnie odtwarzanePobieraj tylko przez Wi-Fi
@@ -423,7 +422,7 @@
Odpięto %d utworówOdpięto %d utworów
- Użyj odtwarzania sprzętowwego (eksperymentalne)
+ Użyj odtwarzania sprzętowego (eksperymentalne)JukeboxUwaga: Brak dostępnych sieci do użycia.
\n Jeżeli używasz danych mobilnych, potrzebne może być włączenie płatnych połączeń w ustawieniach.
@@ -434,7 +433,7 @@
Scrobbluj moje odtworzeniaJeśli włączysz to ustawienie, będzie ono wyświetlać tylko muzykę pobraną za pomocą Ultrasonic w wersji 4.0 lub nowszej. Wcześniejsze pobrane pliki nie zawierają wymaganych metadanych. Możesz przełączać się między trybami Przypinania i Zapisywania, aby wyzwolić pobieranie brakujących metadanych.Wyświetla obraz wykonawcy na liście wykonawców, jeśli jest dostępny
- Pobieraj tylko podczas połączeń niepłatnuch
+ Pobieraj tylko podczas połączeń niepłatnychUdostępnij obecnie odtwarzaną piosenkęPokaż okno potwierdzająceOpcje debugowania
@@ -443,7 +442,7 @@
Jedna lub więcej funkcji zostało wyłączonych ponieważ serwer ich nie obsługiwał.
\nMożesz uruchomić ten test ponownie kiedykolwiek.Serwer demonstracyjny
- Użyj systemu pięciu gwiazdek do oceniania utworów zamiast po prostu dodawać lub usuwać utwory z ulubionych.
+ Używaj 5-gwiazdkowego systemu ocen utworów zamiast zwykłego oznaczania/odznaczania utworów gwiazdką.ListaOkładkaSpróbuj odtworzyć pliki multimedialne za pomocą układu dekodującego w telefonie. Może to zmniejszyć zużycie baterii!
@@ -463,10 +462,11 @@
\n
\nDzięki Ultrasonic możesz łatwo przesyłać strumieniowo lub pobierać muzykę z komputera domowego na telefon za pomocą serwera multimediów kompatybilnego z Subsonic. Oprogramowanie serwera Subsonic wymaga oddzielnej konfiguracji od Ultrasonic.
\n
-\nDomyślnie Ultrasonic nie jest skonfigurowane. Po skonfigurowaniu własnego serwera zmień ustawienia serwera, aby połączyć się z komputerem.
+\nDomyślnie Ultrasonic jest nieskonfigurowany. Po skonfigurowaniu własnego serwera zmień ustawienia serwera, aby połączyć się z komputerem.
Aby używać Ultrasonic z własną muzyką, potrzebujesz własnego serwera.
\n
\n➤ Jeśli chcesz najpierw wypróbować aplikację, możesz teraz dodać serwer demonstracyjny.
\n
\n➤ W przeciwnym razie możesz skonfigurować serwer w ustawieniach.
+ Maksymalny bitrate - Przy trwałym przypięciu utworu
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
index b61c8b15..96691401 100644
--- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml
+++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
@@ -140,7 +140,6 @@
Falha ao atualizar a informação da playlist para %sÁlbunsArtistas
- PesquisarMostrar MaisNada coincide, tente novamenteMúsicas
diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml
index 682ecc02..e0cf7285 100644
--- a/ultrasonic/src/main/res/values-pt/strings.xml
+++ b/ultrasonic/src/main/res/values-pt/strings.xml
@@ -103,7 +103,6 @@
Falha ao atualizar a informação da playlist para %sÁlbunsArtistas
- PesquisarMostrar MaisNada coincide, tente novamenteMúsicas
diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml
index 93eb988a..0d9e5ff4 100644
--- a/ultrasonic/src/main/res/values-ru/strings.xml
+++ b/ultrasonic/src/main/res/values-ru/strings.xml
@@ -128,7 +128,6 @@
Не удалось обновить информацию о плейлисте для %sАльбомыИсполнители
- ПоискПоказать ещеНет совпадений, пожалуйста попробуйте еще разПесни
diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml
index 769c22ae..f704d2a1 100644
--- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml
+++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml
@@ -128,7 +128,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 0610cc46..ced7be3b 100644
--- a/ultrasonic/src/main/res/values-zh-rTW/strings.xml
+++ b/ultrasonic/src/main/res/values-zh-rTW/strings.xml
@@ -67,7 +67,6 @@
播放清單專輯歌手
- 搜尋搜尋無符合類型已停用
diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml
index 4f888cbb..f8d91b62 100644
--- a/ultrasonic/src/main/res/values/strings.xml
+++ b/ultrasonic/src/main/res/values/strings.xml
@@ -142,7 +142,6 @@
Failed to update playlist information for %sAlbumsArtists
- SearchShow MoreNo matches, please try againSongs
@@ -456,6 +455,8 @@
Press on the play button on the media notification if it
is still present, otherwise please open the app to start the playback and re-connect the session
to the controller
+ Random songs
+ Play random songs
diff --git a/ultrasonic/src/main/res/xml/network_security_config.xml b/ultrasonic/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..e0690d2f
--- /dev/null
+++ b/ultrasonic/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+