Merge branch 'develop' into 'master'

Release 4.6.0

See merge request ultrasonic/ultrasonic!1052
This commit is contained in:
birdbird 2023-06-16 18:02:50 +00:00
commit 1d17274e00
48 changed files with 462 additions and 314 deletions

View File

@ -1,4 +1,3 @@
<!-- Please describe your changes here -->
----------------------------------------------------------------------------
@ -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

View File

@ -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``

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -1,5 +1,4 @@
#### From Jackson
-keepattributes *Annotation*,EnclosingMethod,Signature
-keepnames class com.fasterxml.jackson.** {
*;

View File

@ -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 { *; }

View File

@ -32,7 +32,9 @@
android:usesCleartextTraffic="true"
android:supportsRtl="false"
android:preserveLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
<!-- Add for API 34 android:enableOnBackInvokedCallBack="true" -->
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>

View File

@ -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 <http://www.gnu.org/licenses/>.
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);
}
}

View File

@ -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<Toolbar>(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
}
/**

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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

View File

@ -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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), 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<Identifiable>(), KoinComponent {
}
private fun onAlbumSelected(album: Album, autoplay: Boolean) {
val action = SearchFragmentDirections.searchToTrackCollection(
id = album.id,
name = album.title,

View File

@ -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(

View File

@ -27,6 +27,7 @@ import org.moire.ultrasonic.util.Util
class TrackCollectionModel(application: Application) : GenericListModel(application) {
val currentList: MutableLiveData<List<MusicDirectory.Child>> = 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)

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
</vector>

View File

@ -42,6 +42,7 @@
<com.google.android.material.navigation.NavigationView
a:id="@+id/nav_view"
a:layout_width="wrap_content"
a:maxWidth="300dp"
a:layout_height="match_parent"
a:layout_gravity="start"
a:fitsSystemWindows="true"

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:a="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/view_container"
@ -13,19 +12,18 @@
a:id="@+id/img_header_bg"
a:layout_width="match_parent"
a:layout_height="0dp"
a:importantForAccessibility="no"
a:scaleType="fitXY"
a:src="@drawable/ic_header_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
a:importantForAccessibility="no" />
app:layout_constraintTop_toTopOf="parent" />
<Button
a:id="@+id/header_select_server"
style="@style/Widget.Material3.Button.TextButton.Icon"
a:layout_width="match_parent"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_marginStart="3dp"
a:layout_marginTop="24dp"
a:layout_marginTop="48dp"
a:background="@drawable/default_ripple"
a:gravity="center_vertical"
a:paddingHorizontal="22dp"
@ -39,9 +37,27 @@
app:iconPadding="12dp"
app:iconSize="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/edit_server_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:iconTint="@color/selected_menu_dark"
tools:textColor="@color/selected_menu_dark" />
<ImageView
a:id="@+id/edit_server_button"
style="@style/Widget.Material3.Button.TextButton.Icon"
a:layout_width="wrap_content"
a:layout_height="0dp"
a:layout_marginTop="6dp"
a:layout_marginBottom="6dp"
a:contentDescription="@string/server_menu.edit"
a:maxHeight="32dp"
a:src="@drawable/arrow_drop_down"
a:text="@null"
app:layout_constraintBottom_toBottomOf="@id/header_select_server"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/header_select_server"
app:layout_constraintTop_toTopOf="@id/header_select_server"
app:tint="@color/selected_menu_dark" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -7,7 +7,7 @@
android:orientation="horizontal"
android:padding="6dp" >
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_select"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -20,7 +20,7 @@
app:iconGravity="textEnd"
app:iconSize="26dp" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_now"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -31,9 +31,10 @@
android:contentDescription="@string/common.play_now"
app:icon="@drawable/media_start"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_next"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -44,9 +45,10 @@
android:contentDescription="@string/common.play_next"
app:icon="@drawable/ic_play_next"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_play_last"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -57,9 +59,11 @@
android:contentDescription="@string/common.play_last"
app:icon="@drawable/ic_play_last"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_pin"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -70,9 +74,11 @@
android:contentDescription="@string/common.pin"
app:icon="@drawable/ic_menu_pin"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_unpin"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -83,9 +89,11 @@
android:contentDescription="@string/common.unpin"
app:icon="@drawable/ic_menu_unpin"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_download"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -96,9 +104,11 @@
android:contentDescription="@string/common.download"
app:icon="@drawable/ic_menu_download"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_delete"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -109,9 +119,11 @@
android:contentDescription="@string/common.delete"
app:icon="@drawable/ic_menu_close"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/select_album_more"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="0dp"
@ -122,6 +134,8 @@
android:contentDescription="@string/search.more"
app:icon="@drawable/media_forward"
app:iconGravity="textEnd"
app:iconSize="26dp" />
app:iconSize="26dp"
android:scrollbars="none"
/>
</LinearLayout>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:a="http://schemas.android.com/apk/res/android">
<item a:id="@+id/search_item"
a:title="@string/search.label"
a:icon="@drawable/ic_menu_search"
app:showAsAction="always"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
a:id="@+id/action_search"
a:icon="@drawable/ic_menu_search"
a:title="@string/button_bar.search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
</menu>

View File

@ -104,7 +104,6 @@
<string name="playlist.updated_info_error">Chyba aktualizace informací playlistu %s</string>
<string name="search.albums">Alba</string>
<string name="search.artists">Umělci</string>
<string name="search.label">Vyhledávání</string>
<string name="search.more">Zobrazit více</string>
<string name="search.no_match">Nenalezeno, zkuste znovu</string>
<string name="search.songs">Skladby</string>

View File

@ -139,7 +139,6 @@
<string name="playlist.updated_info_error">Aktualisierung der Wiedergabeliste %s ist fehlgeschlagen</string>
<string name="search.albums">Alben</string>
<string name="search.artists">Künstler*innen</string>
<string name="search.label">Suche</string>
<string name="search.more">Zeige mehr</string>
<string name="search.no_match">Keine Treffer, bitte erneut versuchen</string>
<string name="search.songs">Titel</string>

View File

@ -141,7 +141,6 @@
<string name="playlist.updated_info_error">Fallo al actualizar la información de la lista de reproducción para %s</string>
<string name="search.albums">Álbumes</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Buscar</string>
<string name="search.more">Mostrar mas</string>
<string name="search.no_match">Sin resultados, por favor inténtalo de nuevo</string>
<string name="search.songs">Canciones</string>
@ -453,4 +452,6 @@
<string name="foreground_exception_title">No se puede reanudar la reproducción</string>
<string name="foreground_exception_text">Presione 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 controlador</string>
<string name="settings.max_bitrate_pinning">Tasa de bits máxima: al fijar una canción de forma permanente</string>
<string name="shortcut_play_random_songs_short">Canciones aleatorias</string>
<string name="shortcut_play_random_songs_long">Reproducir las canciones aleatoriamente</string>
</resources>

View File

@ -137,7 +137,6 @@
<string name="playlist.updated_info_error">Échec de la mise à jour des informations de la liste de lecture pour %s</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artistes</string>
<string name="search.label">Recherche</string>
<string name="search.more">Afficher plus</string>
<string name="search.no_match">Aucun résultat, veuillez essayer à nouveau</string>
<string name="search.songs">Titres</string>
@ -392,7 +391,7 @@
<!-- Subsonic features -->
<string name="settings.five_star_rating_title">Utiliser les étoiles pour noter les morceaux</string>
<string name="main.albums_by_year">Chronologique</string>
<string name="grid_view">Reprise</string>
<string name="grid_view">Couverture</string>
<string name="settings.preload_1000">1000 morceaux</string>
<string name="settings.preload_100">100 morceaux</string>
<string name="settings.preload_500">500 morceaux</string>

View File

@ -110,7 +110,6 @@
<string name="playlist.updated_info_error">Lejátszási lista módosítása sikertelen %s</string>
<string name="search.albums">Albumok</string>
<string name="search.artists">Előadók</string>
<string name="search.label">Keresés</string>
<string name="search.more">Továbbiak</string>
<string name="search.no_match">Nincs találat, próbálja újra!</string>
<string name="search.songs">Dalok</string>

View File

@ -100,7 +100,6 @@
<string name="playlist.updated_info_error">Impossibile aggiornare informazioni playlist per %s</string>
<string name="search.albums">Album</string>
<string name="search.artists">Artisti</string>
<string name="search.label">Cerca</string>
<string name="search.more">Mostra di più</string>
<string name="search.no_match">Nessun risultato, riprova per favore</string>
<string name="search.songs">Canzoni</string>

View File

@ -106,7 +106,6 @@
<string name="playlist.updated_info_error">%s のプレイリスト情報をアップデートできません</string>
<string name="search.albums">アルバム</string>
<string name="search.artists">アーティスト</string>
<string name="search.label">検索</string>
<string name="search.more">もっと表示</string>
<string name="search.no_match">一致するものはありません、やり直してください</string>
<string name="search.songs"></string>
@ -446,4 +445,5 @@
<string name="settings.use_hw_offload_title">ハードウェア再生を使用する (実験的)</string>
<string name="settings.use_hw_offload_description">端末のメディアデコーダーチップを使用してメディアを再生するよう試行します。これにより、バッテリー使用量を改善できます。このオプションを有効化することで、再生の不具合が起こる場合も報告されています!</string>
<string name="foreground_exception_text">メディア通知の再生ボタンがある場合はそれをタップします。ない場合はアプリを開いて再生を開始し、セッションをコントローラーに再接続します</string>
<string name="settings.max_bitrate_pinning">最高ビットレート - 永続的に固定された曲の場合</string>
</resources>

View File

@ -186,7 +186,6 @@
<string name="playlist.update_info">Oppdater info</string>
<string name="search.albums">Album</string>
<string name="search.artists">Artister</string>
<string name="search.label">Søk</string>
<string name="search.more">Vis mer</string>
<string name="search.songs">Spor</string>
<string name="search.title">Søk</string>

View File

@ -142,7 +142,6 @@
<string name="playlist.updated_info_error">Kan afspeellijstinformatie voor %s niet bijwerken</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artiesten</string>
<string name="search.label">Zoeken</string>
<string name="search.more">Meer tonen</string>
<string name="search.no_match">Geen overeenkomsten; probeer het opnieuw</string>
<string name="search.songs">Nummers</string>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Ładowanie…</string>
<string name="background_task.network_error">Wystąpił błąd sieci. Proszę sprawdzić adres serwera i spróbować później.</string>
<string name="background_task.unsupported_api">Server api v%1$s does not support this function.</string>
<string name="background_task.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć wi-fi lub dane komórkowe.</string>
<string name="background_task.not_found">Nie znaleziono zasobów. Proszę sprawdzić adres serwera.</string>
<string name="background_task.network_error">Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później.</string>
<string name="background_task.unsupported_api">API serwera w wersji v%1$s nie wspiera tej funkcjonalności.</string>
<string name="background_task.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.</string>
<string name="background_task.not_found">Nie udało się odnaleźć zasobu. Proszę sprawdzić adres serwera.</string>
<string name="background_task.parse_error">Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera.</string>
<string name="background_task.ssl_cert_error">Błąd certyfikatu HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.</string>
@ -15,8 +15,8 @@
<string name="button_bar.now_playing">Teraz gra</string>
<string name="buttons.shuffle">Wymieszaj</string>
<string name="podcasts.label">Podcasty</string>
<string name="podcasts_channels.empty">Brak kanałów</string>
<string name="button_bar.podcasts">Podcast</string>
<string name="podcasts_channels.empty">Nie zarejestrowano żadnych kanałów podcastowych</string>
<string name="button_bar.podcasts">Podcasty</string>
<string name="button_bar.search">Szukaj</string>
<string name="chat.send_a_message">Wyślij wiadomość</string>
<string name="common.appname">Ultrasonic</string>
@ -34,10 +34,10 @@
<string name="common.play_next">Odtwórz następne</string>
<string name="common.play_now">Odtwórz teraz</string>
<string name="common.play_shuffled">Odtwórz losowo</string>
<string name="common.public">Publicznie</string>
<string name="common.public">Publiczna</string>
<string name="common.save">Zapisz</string>
<string name="common.unpin">Odepnij</string>
<string name="common.various_artists">Różni artyści</string>
<string name="common.various_artists">Różni wykonawcy</string>
<string name="delete_playlist">Czy chcesz usunąć %1$s</string>
<string name="download.bookmark_removed" formatted="false">Zakładka usunięta.</string>
<string name="download.bookmark_set_at_position" formatted="false">Zakładka ustawiona na %s.</string>
@ -54,7 +54,7 @@
<string name="download.menu_save">Zapisz playlistę</string>
<string name="download.menu_screen_off">Ekran wyłączony</string>
<string name="download.menu_screen_on">Ekran włączony</string>
<string name="download.menu_show_album">Wyświetl album</string>
<string name="download.menu_show_album">Przejdź do albumu</string>
<string name="download.menu_shuffle">Wymieszaj</string>
<string name="download.playerstate_playing_shuffle">Odtwarzanie losowe</string>
<string name="download.playlist_done">Playlista została zapisana.</string>
@ -71,7 +71,7 @@
<string name="error.label">Błąd</string>
<string name="jukebox.is_default">Jukebox domyślnie</string>
<string name="lyrics.nomatch">Brak tekstu utworu</string>
<string name="main.albums_alphaByArtist">wg artystów</string>
<string name="main.albums_alphaByArtist">wg wykonawców</string>
<string name="main.albums_alphaByName">wg tytułu</string>
<string name="main.albums_frequent">Najczęściej odtwarzane</string>
<string name="main.albums_highest">Najlepiej oceniane</string>
@ -80,7 +80,7 @@
<string name="main.albums_recent">Ostatnio odtwarzane</string>
<string name="main.albums_starred">Ulubione</string>
<string name="main.albums_title">Albumy</string>
<string name="main.artists_title">Artyści</string>
<string name="main.artists_title">Wykonawcy</string>
<string name="main.genres_title">Gatunki</string>
<string name="main.offline">Offline</string>
<string name="main.songs_random">Losowe</string>
@ -98,12 +98,11 @@
<string name="music_library.label">Biblioteka mediów</string>
<string name="music_library.label_offline">Media offline</string>
<string name="playlist.label">Playlisty</string>
<string name="playlist.update_info">Aktualizacja informacji</string>
<string name="playlist.update_info">Zmień informacje</string>
<string name="playlist.updated_info">Zaktualizowano informacje dla playlisty %s</string>
<string name="playlist.updated_info_error">Błąc podczas aktualizacji playlisty %s</string>
<string name="playlist.updated_info_error">Błąd podczas aktualizacji playlisty %s</string>
<string name="search.albums">Albumy</string>
<string name="search.artists">Artyści</string>
<string name="search.label">Wyszukaj</string>
<string name="search.artists">Wykonawcy</string>
<string name="search.more">Wyświetl więcej</string>
<string name="search.no_match">Brak wyników, proszę spróbować ponownie</string>
<string name="search.songs">Utwory</string>
@ -150,12 +149,12 @@
<string name="settings.cache_size_9000">9 GB</string>
<string name="settings.cache_size_unlimited">Nieograniczona</string>
<string name="settings.cache_title">Pamięć podręczna muzyki</string>
<string name="settings.chat_refresh">Okres odświeżania czatu</string>
<string name="settings.clear_bookmark">Czyszczenie zakładek</string>
<string name="settings.clear_bookmark_summary">Czyść zakładkę po zakończeniu odtwarzania utworu</string>
<string name="settings.chat_refresh">Odświeżaj czat co</string>
<string name="settings.clear_bookmark">Usuwanie zakładek</string>
<string name="settings.clear_bookmark_summary">Usuwaj zakładkę po zakończeniu odtwarzania utworu</string>
<string name="settings.clear_search_history">Wyczyść historię wyszukiwania</string>
<string name="settings.default_albums">Domyślna ilość wyników - albumy</string>
<string name="settings.default_artists">Domyślna ilość wyników - artyści</string>
<string name="settings.default_artists">Domyślna ilość wyników - wykonawcy</string>
<string name="settings.default_songs">Domyślna ilość wyników - utwory</string>
<string name="settings.directory_cache_time">Okres przechowywania pamięci podręcznej katalogu</string>
<string name="settings.directory_cache_time_0">Wyłączone</string>
@ -165,15 +164,15 @@
<string name="settings.directory_cache_time_30">30 minut</string>
<string name="settings.directory_cache_time_5">5 minut</string>
<string name="settings.directory_cache_time_60">1 godzina</string>
<string name="settings.disc_sort">Sortuj utwory wg dysku</string>
<string name="settings.disc_sort_summary">Sortuje listę utworów wg numeru dysku i numeru utworu</string>
<string name="settings.display_bitrate_summary">Dołącza bitrate i typ pliku do nazwy artysty</string>
<string name="settings.disc_sort">Sortuj utwory wg numeru płyty</string>
<string name="settings.disc_sort_summary">Sortuj listę utworów wg numeru płyty i numeru utworu</string>
<string name="settings.display_bitrate_summary">Dołącz bitrate i rozszerzenie pliku obok nazwy wykonawcy</string>
<string name="settings.hide_media_summary">Ukrywa pliki muzyczne przed innymi aplikacjami.</string>
<string name="settings.hide_media_title">Ukryj pliki</string>
<string name="settings.hide_media_toast">Efekt widoczny będzie po następnym skanowaniu muzyki przez system Android.</string>
<string name="settings.invalid_url">Proszę wprowadzić prawidłowy URL.</string>
<string name="settings.max_albums">Maksymalna ilość wyników - albumy</string>
<string name="settings.max_artists">Maksymalna ilość wyników - artyści</string>
<string name="settings.max_artists">Maksymalna ilość wyników - wykonawcy</string>
<string name="settings.max_bitrate_112">112 Kbps</string>
<string name="settings.max_bitrate_128">128 Kbps</string>
<string name="settings.max_bitrate_160">160 Kbps</string>
@ -186,9 +185,9 @@
<string name="settings.max_bitrate_96">96 Kbps</string>
<string name="settings.max_bitrate_mobile">Maksymalny bitrate dla połączenia komórkowego</string>
<string name="settings.max_bitrate_unlimited">Bez limitu</string>
<string name="settings.max_bitrate_wifi">Maksymalny bitrate dla połączenia Wi-fi</string>
<string name="settings.max_bitrate_wifi">Maksymalny bitrate dla połączenia Wi-Fi</string>
<string name="settings.max_songs">Maksymalna ilość wyników - utwory</string>
<string name="settings.media_button_summary">Reaguje na przyciski telefonu, słuchawek i Bluetooth</string>
<string name="settings.media_button_summary">Reaguj na przyciski multimedialne telefonu, słuchawek i urządzeń Bluetooth</string>
<string name="settings.media_button_title">Przyciski</string>
<string name="settings.network_timeout">Przekroczenie limitu czasu sieci</string>
<string name="settings.network_timeout_105000">105 sekund</string>
@ -208,7 +207,7 @@
<string name="settings.playback.bluetooth_all">Wszystkie urządzenia Bluetooth</string>
<string name="settings.playback.bluetooth_a2dp">Tylko urządzenia audio (A2DP)</string>
<string name="settings.playback.bluetooth_disabled">Wyłączone</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Wznawiaj po podłączeniu słuchawek</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Wznów po podłączeniu słuchawek</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">Aplikacja wznowi zatrzymane odtwarzanie po podpięciu słuchawek.</string>
<string name="settings.preload">Ilość wstępnie ładowanych utworów</string>
<string name="settings.preload_1">1 utwór</string>
@ -249,8 +248,8 @@
<string name="settings.theme_title">Motyw</string>
<string name="settings.title.allow_self_signed_certificate">Zezwalaj na własne certyfikaty HTTPS</string>
<string name="settings.title.force_plain_text_password">Wymuś uwierzytelnianie zwykłym hasłem</string>
<string name="settings.use_folder_for_album_artist">Używaj folderów jako nazw artystów</string>
<string name="settings.use_folder_for_album_artist_summary">Zakłada, że folder najwyższego poziomu jest nazwą artysty albumu</string>
<string name="settings.use_folder_for_album_artist">Używaj folderów jako nazw wykonawców</string>
<string name="settings.use_folder_for_album_artist_summary">Zakłada, że folder najwyższego poziomu jest nazwą wykonawcy albumu</string>
<string name="settings.use_id3">Przeglądaj używając tagów ID3</string>
<string name="settings.use_id3_summary">Używa metod z tagów ID3 zamiast metod opartych na systemie plików</string>
<string name="main.video" tools:ignore="UnusedResources">Wideo</string>
@ -276,7 +275,7 @@
<string name="download.bookmark_delete">Usuń zakładkę</string>
<string name="download.menu_star">Ulubione</string>
<string name="download.menu_clear_playlist">Wyczyść playlistę</string>
<string name="button_bar.shares">Udostępnienia</string>
<string name="button_bar.shares">Udostępnione</string>
<string name="select_share.empty">Brak udostępnień na serwerze</string>
<string name="menu_deleted_share">Usunięto udostępnienie %s</string>
<string name="menu_deleted_share_error">Nieudane usunięcie udostępnienia %s</string>
@ -294,7 +293,7 @@
<string name="share_default_greeting">Sprawdź muzykę, którą udostępniam na %s</string>
<string name="share_via">Udostępnij utwory za pomocą</string>
<string name="menu.share">Udostępnianie</string>
<string name="download.menu_show_artist">Wyświetlaj artystę</string>
<string name="download.menu_show_artist">Przejdź do wykonawcy</string>
<string name="common_multiple_years">Z różnych lat</string>
<string name="server_selector.label">Skonfigurowane serwery</string>
<string name="server_selector.delete_confirmation">Czy na pewno chcesz usunąć ten serwer\?</string>
@ -326,7 +325,7 @@
<string name="api.subsonic.upgrade_client">Brak zgodności wersji. Uaktualnij aplikację Ultrasonic na Androida.</string>
<string name="api.subsonic.upgrade_server">Brak zgodności wersji. Uaktualnij serwer Subsonic.</string>
<!-- Subsonic features -->
<string name="settings.five_star_rating_title">Użyj pięciu gwiazdek dla utworów</string>
<string name="settings.five_star_rating_title">5-gwiazdkowy system ocen utworów</string>
<string name="settings.show_confirmation_dialog_summary">Pokaż okno potwierdzające usunięcie lub odpięcie utworów</string>
<string name="language.en">Angielski</string>
<string name="settings.scrobble_summary">Pamiętaj o ustawieniu nazwy użytkownika i hasła do usługi Scrobble na serwerze</string>
@ -336,7 +335,7 @@
<string name="buttons.stop">Zatrzymaj</string>
<string name="language.fr">Francuski</string>
<string name="common.unpin_selection_confirmation">Czy na pewno chcesz odpiąć zaznaczone pozycje\?</string>
<string name="settings.custom_cache_location">Użyj niestandardowej lokacji pamięci podręcznej</string>
<string name="settings.custom_cache_location">Niestandardowa lokalizacja pamięci podręcznej</string>
<string name="common.select_all">Wybierz wszystko</string>
<string name="download.menu_shuffle_on">Włączony tryb losowy</string>
<string name="buttons.next">Następne</string>
@ -352,7 +351,7 @@
<string name="settings.preload_1000">1000 piosenek</string>
<string name="supported_server_features">Wspierane funkcje</string>
<string name="language.pl">Polski</string>
<string name="common.artist">Artysta</string>
<string name="common.artist">Wykonawca</string>
<string name="language.nl">Holenderski</string>
<string name="language.hu">Węgierski</string>
<string name="settings.debug.log_summary">Zapisanych jest %1$s plików z logami, które zajmują ~%2$s MB miejsca w katalogu %3$s. Czy chcesz je zachować\?</string>
@ -366,7 +365,7 @@
<string name="buttons.repeat">Powtarzaj</string>
<string name="download.empty">Nic nie jest pobierane</string>
<string name="language.ru">Rosyjski</string>
<string name="download.playerstate_loading">Byforowanie…</string>
<string name="download.playerstate_loading">Buforowanie…</string>
<string name="main.setup_server">%s - Ustaw serwer</string>
<string name="settings.preload_50">50 piosenek</string>
<string name="language.zh_CN">Chiński (Chiny)</string>
@ -383,13 +382,13 @@
<string name="language.pt">Portugalski</string>
<string name="settings.server_color">Kolor serwera</string>
<string name="buttons.pause">Pauza</string>
<string name="settings.show_artist_picture">Pokaż obraz wykonawcy na liście</string>
<string name="settings.show_artist_picture">Wyświetlaj obraz wykonawcy w liście</string>
<string name="common.title">Tytuł</string>
<string name="common.delete_selection_confirmation">Czy na pewno chcesz usunąć zaznaczone pozycje\?</string>
<string name="albumArt">Okładka albumu</string>
<string name="common.album">Album</string>
<string name="settings.preload_500">500 piosenek</string>
<string name="settings.share_on_server_summary">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</string>
<string name="settings.share_on_server_summary">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 utworze</string>
<string name="settings.download_transition">Pokaż Obecnie odtwarzane po kliknięciu przycisku Odtwarzaj</string>
<string name="language.es">Hiszpański</string>
<string name="settings.override_language_summary">Wymagane jest ponowne uruchomienie aplikacji po zmianie języka</string>
@ -398,7 +397,7 @@
<string name="language.zh_TW">Chiński (Tajwan)</string>
<string name="settings.theme_day_night">Dzień i noc</string>
<string name="settings.theme_black">Czarny</string>
<string name="settings.summary.force_plain_text_password">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.</string>
<string name="settings.summary.force_plain_text_password">Zmusza aplikację do przesyłania hasła w postaci niezaszyfrowanej. Przydaje się, gdy serwer Subsonic nie obsługuje nowego API uwierzytelniania użytkowników.</string>
<string name="settings.show_now_playing_details_summary">Pokaż więcej informacji o utworze w sekcji Obecnie odtwarzane (gatunek, rok, przepustowość)</string>
<string name="settings.show_now_playing_details">Pokaż szczegóły w sekcji Obecnie odtwarzane</string>
<string name="settings.wifi_required_title">Pobieraj tylko przez Wi-Fi</string>
@ -423,7 +422,7 @@
<item quantity="many">Odpięto %d utworów</item>
<item quantity="other">Odpięto %d utworów</item>
</plurals>
<string name="settings.use_hw_offload_title">Użyj odtwarzania sprzętowwego (eksperymentalne)</string>
<string name="settings.use_hw_offload_title">Użyj odtwarzania sprzętowego (eksperymentalne)</string>
<string name="jukebox">Jukebox</string>
<string name="select_album.no_network">Uwaga: 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.</string>
@ -434,7 +433,7 @@
<string name="settings.scrobble_title">Scrobbluj moje odtworzenia</string>
<string name="settings.use_id3_offline_summary">Jeś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.</string>
<string name="settings.show_artist_picture_summary">Wyświetla obraz wykonawcy na liście wykonawców, jeśli jest dostępny</string>
<string name="settings.wifi_required_summary">Pobieraj tylko podczas połączeń niepłatnuch</string>
<string name="settings.wifi_required_summary">Pobieraj tylko podczas połączeń niepłatnych</string>
<string name="download.share_song">Udostępnij obecnie odtwarzaną piosenkę</string>
<string name="settings.show_confirmation_dialog">Pokaż okno potwierdzające</string>
<string name="settings.debug.title">Opcje debugowania</string>
@ -443,7 +442,7 @@
<string name="server_editor.disabled_feature">Jedna lub więcej funkcji zostało wyłączonych ponieważ serwer ich nie obsługiwał.
\nMożesz uruchomić ten test ponownie kiedykolwiek.</string>
<string name="server_menu.demo">Serwer demonstracyjny</string>
<string name="settings.five_star_rating_description">Użyj systemu pięciu gwiazdek do oceniania utworów zamiast po prostu dodawać lub usuwać utwory z ulubionych.</string>
<string name="settings.five_star_rating_description">Używaj 5-gwiazdkowego systemu ocen utworów zamiast zwykłego oznaczania/odznaczania utworów gwiazdką.</string>
<string name="list_view">Lista</string>
<string name="grid_view">Okładka</string>
<string name="settings.use_hw_offload_description">Spróbuj odtworzyć pliki multimedialne za pomocą układu dekodującego w telefonie. Może to zmniejszyć zużycie baterii!</string>
@ -463,10 +462,11 @@
\n
\nDzięki <b>Ultrasonic</b> 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.</string>
\nDomyślnie Ultrasonic jest nieskonfigurowany. Po skonfigurowaniu własnego serwera zmień ustawienia serwera, aby połączyć się z komputerem.</string>
<string name="main.welcome_text_demo">Aby używać Ultrasonic z własną muzyką, potrzebujesz <b>własnego serwera</b>.
\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 <b>ustawieniach</b>.</string>
<string name="settings.max_bitrate_pinning">Maksymalny bitrate - Przy trwałym przypięciu utworu</string>
</resources>

View File

@ -140,7 +140,6 @@
<string name="playlist.updated_info_error">Falha ao atualizar a informação da playlist para %s</string>
<string name="search.albums">Álbuns</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Pesquisar</string>
<string name="search.more">Mostrar Mais</string>
<string name="search.no_match">Nada coincide, tente novamente</string>
<string name="search.songs">Músicas</string>

View File

@ -103,7 +103,6 @@
<string name="playlist.updated_info_error">Falha ao atualizar a informação da playlist para %s</string>
<string name="search.albums">Álbuns</string>
<string name="search.artists">Artistas</string>
<string name="search.label">Pesquisar</string>
<string name="search.more">Mostrar Mais</string>
<string name="search.no_match">Nada coincide, tente novamente</string>
<string name="search.songs">Músicas</string>

View File

@ -128,7 +128,6 @@
<string name="playlist.updated_info_error">Не удалось обновить информацию о плейлисте для %s</string>
<string name="search.albums">Альбомы</string>
<string name="search.artists">Исполнители</string>
<string name="search.label">Поиск</string>
<string name="search.more">Показать еще</string>
<string name="search.no_match">Нет совпадений, пожалуйста попробуйте еще раз</string>
<string name="search.songs">Песни</string>

View File

@ -128,7 +128,6 @@
<string name="playlist.updated_info_error">更新播放列表信息失败 - %s</string>
<string name="search.albums">专辑</string>
<string name="search.artists">艺人</string>
<string name="search.label">搜索</string>
<string name="search.more">显示更多</string>
<string name="search.no_match">没有匹配的结果,请重试</string>
<string name="search.songs">歌曲</string>

View File

@ -67,7 +67,6 @@
<string name="playlist.label">播放清單</string>
<string name="search.albums">專輯</string>
<string name="search.artists">歌手</string>
<string name="search.label">搜尋</string>
<string name="search.title">搜尋</string>
<string name="select_genre.empty">無符合類型</string>
<string name="settings.increment_time_0">已停用</string>

View File

@ -142,7 +142,6 @@
<string name="playlist.updated_info_error">Failed to update playlist information for %s</string>
<string name="search.albums">Albums</string>
<string name="search.artists">Artists</string>
<string name="search.label">Search</string>
<string name="search.more">Show More</string>
<string name="search.no_match">No matches, please try again</string>
<string name="search.songs">Songs</string>
@ -456,6 +455,8 @@
<string name="foreground_exception_text">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</string>
<string name="shortcut_play_random_songs_short">Random songs</string>
<string name="shortcut_play_random_songs_long">Play random songs</string>
</resources>

View File

@ -0,0 +1,12 @@
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config>
<trust-anchors>
<!-- Allow system CAs -->
<certificates src="system" />
<!-- Allow user CAs -->
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>