mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-16 07:16:34 +03:00
Merge branch 'develop' into 'master'
Release 4.6.0 See merge request ultrasonic/ultrasonic!1052
This commit is contained in:
commit
1d17274e00
@ -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
|
||||
|
10
.gitlab/merge_request_templates/Release.md
Normal file
10
.gitlab/merge_request_templates/Release.md
Normal 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``
|
12
fastlane/metadata/android/en-US/changelogs/123.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/123.txt
Normal 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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,4 @@
|
||||
#### From Jackson
|
||||
|
||||
-keepattributes *Annotation*,EnclosingMethod,Signature
|
||||
-keepnames class com.fasterxml.jackson.** {
|
||||
*;
|
||||
|
@ -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 { *; }
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
5
ultrasonic/src/main/res/drawable/arrow_drop_down.xml
Normal file
5
ultrasonic/src/main/res/drawable/arrow_drop_down.xml
Normal 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>
|
@ -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"
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
12
ultrasonic/src/main/res/menu/search_view_menu.xml
Normal file
12
ultrasonic/src/main/res/menu/search_view_menu.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
12
ultrasonic/src/main/res/xml/network_security_config.xml
Normal file
12
ultrasonic/src/main/res/xml/network_security_config.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user