Merge branch 'refactorScopes' into 'develop'

Refactor Koin, Scopes & Lifecycles

See merge request ultrasonic/ultrasonic!1130
This commit is contained in:
birdbird 2023-10-14 19:09:26 +00:00
commit de523a6451
74 changed files with 1082 additions and 1219 deletions

View File

@ -1,11 +1,6 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">

View File

@ -70,50 +70,6 @@
column="1"/> column="1"/>
</issue> </issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/chat.xml"
line="33"
column="10"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/save_playlist.xml"
line="9"
column="6"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/share_details.xml"
line="29"
column="10"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/time_span_dialog.xml"
line="28"
column="10"/>
</issue>
<issue <issue
id="LabelFor" id="LabelFor"
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`" message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"

View File

@ -72,7 +72,7 @@
</service> </service>
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md --> <!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
<service android:name=".playback.PlaybackService" <service android:name=".service.PlaybackService"
android:label="@string/common.appname" android:label="@string/common.appname"
android:foregroundServiceType="mediaPlayback" android:foregroundServiceType="mediaPlayback"
android:exported="true" android:exported="true"
@ -107,6 +107,12 @@
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/> <action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".provider.UltrasonicAppWidgetProvider" android:name=".provider.UltrasonicAppWidgetProvider"
android:label="Ultrasonic" android:label="Ultrasonic"
@ -119,12 +125,6 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info"/> android:resource="@xml/appwidget_info"/>
</receiver> </receiver>
<receiver android:name=".receiver.MediaButtonIntentReceiver"
android:exported="true">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
<provider <provider
android:name=".provider.SearchSuggestionProvider" android:name=".provider.SearchSuggestionProvider"
android:authorities="${applicationId}.provider.SearchSuggestionProvider" android:authorities="${applicationId}.provider.SearchSuggestionProvider"

View File

@ -23,7 +23,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -39,6 +38,7 @@ import androidx.media3.common.Player.STATE_READY
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.onNavDestinationSelected import androidx.navigation.ui.onNavDestinationSelected
@ -50,6 +50,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.scope.ScopeActivity
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -63,7 +64,6 @@ import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LocaleHelper import org.moire.ultrasonic.util.LocaleHelper
@ -81,7 +81,7 @@ import timber.log.Timber
* onCreate/onResume/onDestroy methods... * onCreate/onResume/onDestroy methods...
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class NavigationActivity : AppCompatActivity() { class NavigationActivity : ScopeActivity() {
private var videoMenuItem: MenuItem? = null private var videoMenuItem: MenuItem? = null
private var chatMenuItem: MenuItem? = null private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null private var bookmarksMenuItem: MenuItem? = null
@ -485,15 +485,19 @@ class NavigationActivity : AppCompatActivity() {
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
val service = MusicServiceFactory.getMusicService() val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(Settings.maxSongs) val musicDirectory = service.getRandomSongs(Settings.maxSongs)
val downloadHandler: DownloadHandler by inject()
downloadHandler.addTracksToMediaController( mediaPlayerManager.addToPlaylist(
songs = musicDirectory.getTracks(), songs = musicDirectory.getTracks(),
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
autoPlay = true, autoPlay = true,
shuffle = false, shuffle = false,
fragment = currentFragment, insertionMode = MediaPlayerManager.InsertionMode.CLEAR
playlistName = null
) )
if (Settings.shouldTransitionOnPlayback) {
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
currentFragment.findNavController().navigate(R.id.playerFragment)
}
return return
} }

View File

@ -17,6 +17,10 @@ import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -63,16 +67,21 @@ class ArtistRowBinder(
if (showArtistPicture()) { if (showArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(item.name, false) CoroutineScope(Dispatchers.IO).launch {
imageLoaderProvider.executeOn { val key = FileUtil.getArtistArtKey(item.name, false)
it.loadImage(
view = holder.coverArt, withContext(Dispatchers.Main) {
id = holder.coverArtId, imageLoaderProvider.executeOn {
key = key, it.loadImage(
large = false, view = holder.coverArt,
size = 0, id = holder.coverArtId,
defaultResourceId = R.drawable.ic_contact_picture key = key,
) large = false,
size = 0,
defaultResourceId = R.drawable.ic_contact_picture
)
}
}
} }
} else { } else {
holder.coverArt.visibility = View.GONE holder.coverArt.visibility = View.GONE

View File

@ -31,7 +31,6 @@ import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber
const val INDICATOR_THICKNESS_INDEFINITE = 5 const val INDICATOR_THICKNESS_INDEFINITE = 5
const val INDICATOR_THICKNESS_DEFINITE = 10 const val INDICATOR_THICKNESS_DEFINITE = 10
@ -79,10 +78,6 @@ class TrackViewHolder(val view: View) :
private var rxBusSubscription: CompositeDisposable? = null private var rxBusSubscription: CompositeDisposable? = null
init {
Timber.v("New ViewHolder created")
}
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
fun setSong( fun setSong(
song: Track, song: Track,

View File

@ -7,8 +7,6 @@
package org.moire.ultrasonic.data package org.moire.ultrasonic.data
import android.os.Handler
import android.os.Looper
import androidx.room.Room import androidx.room.Room
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -106,6 +104,30 @@ class ActiveServerProvider(
} }
} }
/**
* Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/
fun setActiveServerById(serverId: Int) {
val oldServerId = Settings.activeServer
if (oldServerId == serverId) return
// Notify components about the change before actually resetting the MusicService
// so they can react by e.g. stopping playback on the old server
RxBus.activeServerChangingPublisher.onNext(oldServerId)
// Use a coroutine to post the server change to the end of the message queue
launch {
withContext(Dispatchers.Main) {
resetMusicService()
Settings.activeServer = serverId
RxBus.activeServerChangedPublisher.onNext(getActiveServer(serverId))
Timber.i("setActiveServerById done, new id: %s", serverId)
}
}
}
@Synchronized @Synchronized
fun getActiveMetaDatabase(): MetaDatabase { fun getActiveMetaDatabase(): MetaDatabase {
val activeServer = getActiveServerId() val activeServer = getActiveServerId()
@ -234,29 +256,6 @@ class ActiveServerProvider(
return Settings.activeServer return Settings.activeServer
} }
/**
* Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/
fun setActiveServerById(serverId: Int) {
val oldServerId = Settings.activeServer
if (oldServerId == serverId) return
// Notify components about the change before actually resetting the MusicService
// so they can react by e.g. stopping playback on the old server
RxBus.activeServerChangingPublisher.onNext(oldServerId)
// Post the server change to the end of the message queue,
// so the cleanup have time to finish
Handler(Looper.getMainLooper()).post {
resetMusicService()
Settings.activeServer = serverId
RxBus.activeServerChangedPublisher.onNext(serverId)
Timber.i("setActiveServerById done, new id: %s", serverId)
}
}
/** /**
* Queries if Scrobbling is enabled * Queries if Scrobbling is enabled
*/ */

View File

@ -1,11 +1,11 @@
/* /*
* CachedDataSource.kt * CachedDataSource.kt
* Copyright (C) 2009-2022 Ultrasonic developers * Copyright (C) 2009-2023 Ultrasonic developers
* *
* Distributed under terms of the GNU GPLv3 license. * Distributed under terms of the GNU GPLv3 license.
*/ */
package org.moire.ultrasonic.playback package org.moire.ultrasonic.data
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri

View File

@ -1,14 +1,15 @@
package org.moire.ultrasonic.di package org.moire.ultrasonic.di
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CacheCleaner
/** /**
* This Koin module contains the registration of general classes needed for Ultrasonic * This Koin module contains the registration of general classes needed for Ultrasonic
*/ */
val applicationModule = module { val applicationModule = module {
single { ActiveServerProvider(get()) } single { ActiveServerProvider(get()) }
single { ImageLoaderProvider(androidContext()) } single { ImageLoaderProvider() }
single { CacheCleaner() }
} }

View File

@ -1,19 +1,27 @@
package org.moire.ultrasonic.di package org.moire.ultrasonic.di
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.service.ExternalStorageMonitor import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.PlaybackStateSerializer import org.moire.ultrasonic.service.PlaybackStateSerializer
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
/** /**
* This Koin module contains the registration of classes related to the media player * This Koin module contains the registration of classes related to the media player
*/ */
val mediaPlayerModule = module { val mediaPlayerModule = module {
single { MediaPlayerLifecycleSupport() }
// These are dependency-free
single { PlaybackStateSerializer() } single { PlaybackStateSerializer() }
single { ExternalStorageMonitor() } single { ExternalStorageMonitor() }
single { NetworkAndStorageChecker() }
single { ShareHandler() }
// TODO Ideally this can be cleaned up when all circular references are removed. scope<NavigationActivity> {
single { MediaPlayerManager(get(), get(), get()) } scoped { MediaPlayerManager(get(), get()) }
scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
}
} }

View File

@ -3,7 +3,6 @@ package org.moire.ultrasonic.di
import kotlin.math.abs import kotlin.math.abs
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.BuildConfig
@ -16,9 +15,6 @@ import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
/** /**
@ -68,8 +64,4 @@ val musicServiceModule = module {
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) { single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
OfflineMusicService() OfflineMusicService()
} }
single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) }
single { ShareHandler(androidContext()) }
} }

View File

@ -17,7 +17,7 @@ import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.PlaybackState import org.moire.ultrasonic.service.MediaPlayerManager
/** /**
* Lists the Bookmarks available on the server * Lists the Bookmarks available on the server
@ -61,22 +61,21 @@ class BookmarksFragment : TrackCollectionFragment() {
} }
/** /**
* Custom playback function which uses the restore functionality. A bit of a hack.. * Play the selected tracks at the bookmarked position
*/ */
private fun playNow(songs: List<Track>) { private fun playNow(songs: List<Track>) {
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
val state = PlaybackState( mediaPlayerManager.addToPlaylist(
songs = songs, songs = songs,
currentPlayingIndex = 0, autoPlay = false,
currentPlayingPosition = songs[0].bookmarkPosition shuffle = false,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
) )
mediaPlayerManager.restore( mediaPlayerManager.seekTo(0, songs[0].bookmarkPosition)
state = state, mediaPlayerManager.prepare()
autoPlay = true, mediaPlayerManager.play()
newPlaylist = true
)
} }
} }
} }

View File

@ -11,10 +11,10 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.moire.ultrasonic.R import org.koin.core.component.KoinScopeComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.adapters.FolderSelectorBinder
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Artist
@ -23,17 +23,17 @@ import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.DownloadAction import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
import org.moire.ultrasonic.subsonic.DownloadHandler
/** /**
* An extension of the MultiListFragment, with a few helper functions geared * An extension of the MultiListFragment, with a few helper functions geared
* towards the display of MusicDirectory.Entries. * towards the display of MusicDirectory.Entries.
* @param T: The type of data which will be used (must extend GenericEntry) * @param T: The type of data which will be used (must extend GenericEntry)
*/ */
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() { abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), KoinScopeComponent {
private var rxBusSubscription: CompositeDisposable = CompositeDisposable() private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
private val mediaPlayerManager: MediaPlayerManager by inject()
/** /**
* Whether to show the folder selector * Whether to show the folder selector
@ -46,7 +46,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
val isArtist = (item is Artist) val isArtist = (item is Artist)
return handleContextMenu(menuItem, item, isArtist, downloadHandler, this) return handleContextMenu(menuItem, item, isArtist, mediaPlayerManager, this)
} }
override fun onItemClick(item: T) { override fun onItemClick(item: T) {
@ -119,65 +119,4 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
header header
} }
companion object {
@Suppress("LongMethod")
internal fun handleContextMenu(
menuItem: MenuItem,
item: Identifiable,
isArtist: Boolean,
downloadHandler: DownloadHandler,
fragment: Fragment
): Boolean {
when (menuItem.itemId) {
R.id.menu_play_now ->
downloadHandler.fetchTracksAndAddToController(
fragment,
item.id,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
autoPlay = true,
isArtist = isArtist
)
R.id.menu_play_next ->
downloadHandler.fetchTracksAndAddToController(
fragment,
item.id,
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
autoPlay = true,
isArtist = isArtist
)
R.id.menu_play_last ->
downloadHandler.fetchTracksAndAddToController(
fragment,
item.id,
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
autoPlay = false,
isArtist = isArtist
)
R.id.menu_pin ->
downloadHandler.justDownload(
action = DownloadAction.PIN,
fragment,
item.id,
isArtist = isArtist
)
R.id.menu_unpin ->
downloadHandler.justDownload(
action = DownloadAction.UNPIN,
fragment,
item.id,
isArtist = isArtist
)
R.id.menu_download ->
downloadHandler.justDownload(
action = DownloadAction.DOWNLOAD,
fragment,
item.id,
isArtist = isArtist
)
else -> return false
}
return true
}
}
} }

View File

@ -20,7 +20,11 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import java.lang.ref.SoftReference import java.lang.ref.SoftReference
import org.koin.core.component.KoinComponent import kotlin.collections.HashMap
import kotlin.collections.hashMapOf
import kotlin.collections.set
import org.koin.androidx.scope.ScopeFragment
import org.koin.core.component.KoinScopeComponent
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.AlbumListType
@ -34,7 +38,7 @@ import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities import org.moire.ultrasonic.view.ViewCapabilities
import timber.log.Timber import timber.log.Timber
class MainFragment : Fragment(), KoinComponent { class MainFragment : ScopeFragment(), KoinScopeComponent {
private var filterButtonBar: FilterButtonBar? = null private var filterButtonBar: FilterButtonBar? = null
private var layoutType: LayoutType = LayoutType.COVER private var layoutType: LayoutType = LayoutType.COVER

View File

@ -17,7 +17,6 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -26,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.scope.ScopeFragment
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.BaseAdapter
@ -33,7 +33,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.model.GenericListModel
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -42,11 +41,10 @@ import org.moire.ultrasonic.util.Util
* An abstract Model, which can be extended to display a list of items of type T from the API * An abstract Model, which can be extended to display a list of items of type T from the API
* @param T: The type of data which will be used (must extend GenericEntry) * @param T: The type of data which will be used (must extend GenericEntry)
*/ */
abstract class MultiListFragment<T : Identifiable> : Fragment() { abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
internal val activeServerProvider: ActiveServerProvider by inject() internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel() internal val serverSettingsModel: ServerSettingsModel by viewModel()
internal val imageLoaderProvider: ImageLoaderProvider by inject() internal val imageLoaderProvider: ImageLoaderProvider by inject()
protected val downloadHandler: DownloadHandler by inject()
protected var refreshListView: SwipeRefreshLayout? = null protected var refreshListView: SwipeRefreshLayout? = null
internal var listView: RecyclerView? = null internal var listView: RecyclerView? = null
internal lateinit var viewManager: LinearLayoutManager internal lateinit var viewManager: LinearLayoutManager

View File

@ -15,7 +15,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -23,6 +22,7 @@ import io.reactivex.rxjava3.disposables.Disposable
import java.lang.Exception import java.lang.Exception
import kotlin.math.abs import kotlin.math.abs
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.scope.ScopeFragment
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
@ -37,7 +37,7 @@ import timber.log.Timber
/** /**
* Contains the mini-now playing information box displayed at the bottom of the screen * Contains the mini-now playing information box displayed at the bottom of the screen
*/ */
class NowPlayingFragment : Fragment() { class NowPlayingFragment : ScopeFragment() {
private var downX = 0f private var downX = 0f
private var downY = 0f private var downY = 0f

View File

@ -45,7 +45,6 @@ import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuHost import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.media3.common.HeartRating import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -79,12 +78,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent import org.koin.androidx.scope.ScopeFragment
import org.koin.core.component.KoinScopeComponent
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.BaseAdapter import org.moire.ultrasonic.adapters.BaseAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
@ -106,6 +105,7 @@ import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.util.toTrack import org.moire.ultrasonic.util.toTrack
import org.moire.ultrasonic.view.AutoRepeatButton import org.moire.ultrasonic.view.AutoRepeatButton
import timber.log.Timber import timber.log.Timber
@ -116,9 +116,9 @@ import timber.log.Timber
*/ */
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber") @Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
class PlayerFragment : class PlayerFragment :
Fragment(), ScopeFragment(),
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
KoinComponent, KoinScopeComponent,
CoroutineScope by CoroutineScope(Dispatchers.Main) { CoroutineScope by CoroutineScope(Dispatchers.Main) {
// Settings // Settings
@ -356,14 +356,14 @@ class PlayerFragment :
onPlaylistChanged() onPlaylistChanged()
when (newRepeat) { when (newRepeat) {
0 -> Util.toast( 0 -> toast(
context, R.string.download_repeat_off R.string.download_repeat_off
) )
1 -> Util.toast( 1 -> toast(
context, R.string.download_repeat_single R.string.download_repeat_single
) )
2 -> Util.toast( 2 -> toast(
context, R.string.download_repeat_all R.string.download_repeat_all
) )
else -> { else -> {
} }
@ -410,7 +410,7 @@ class PlayerFragment :
// Query the Jukebox state in an IO Context // Query the Jukebox state in an IO Context
ioScope.launch(CommunicationError.getHandler(context)) { ioScope.launch(CommunicationError.getHandler(context)) {
try { try {
jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable jukeboxAvailable = getMusicService().isJukeboxAvailable()
} catch (all: Exception) { } catch (all: Exception) {
Timber.e(all) Timber.e(all)
} }
@ -457,9 +457,9 @@ class PlayerFragment :
val isEnabled = mediaPlayerManager.toggleShuffle() val isEnabled = mediaPlayerManager.toggleShuffle()
if (isEnabled) { if (isEnabled) {
Util.toast(activity, R.string.download_menu_shuffle_on) toast(R.string.download_menu_shuffle_on)
} else { } else {
Util.toast(activity, R.string.download_menu_shuffle_off) toast(R.string.download_menu_shuffle_off)
} }
updateShuffleButtonState(isEnabled) updateShuffleButtonState(isEnabled)
@ -579,8 +579,7 @@ class PlayerFragment :
equalizerMenuItem.isVisible = isEqualizerAvailable equalizerMenuItem.isVisible = isEqualizerAvailable
} }
val mediaPlayerController = mediaPlayerManager val track = mediaPlayerManager.currentMediaItem?.toTrack()
val track = mediaPlayerController.currentMediaItem?.toTrack()
if (track != null) { if (track != null) {
currentSong = track currentSong = track
@ -600,7 +599,7 @@ class PlayerFragment :
goToArtist.isVisible = false goToArtist.isVisible = false
} }
if (mediaPlayerController.keepScreenOn) { if (mediaPlayerManager.keepScreenOn) {
screenOption?.setTitle(R.string.download_menu_screen_off) screenOption?.setTitle(R.string.download_menu_screen_off)
} else { } else {
screenOption?.setTitle(R.string.download_menu_screen_on) screenOption?.setTitle(R.string.download_menu_screen_on)
@ -609,7 +608,7 @@ class PlayerFragment :
if (jukeboxOption != null) { if (jukeboxOption != null) {
jukeboxOption.isEnabled = jukeboxAvailable jukeboxOption.isEnabled = jukeboxAvailable
jukeboxOption.isVisible = jukeboxAvailable jukeboxOption.isVisible = jukeboxAvailable
if (mediaPlayerController.isJukeboxEnabled) { if (mediaPlayerManager.isJukeboxEnabled) {
jukeboxOption.setTitle(R.string.download_menu_jukebox_off) jukeboxOption.setTitle(R.string.download_menu_jukebox_off)
} else { } else {
jukeboxOption.setTitle(R.string.download_menu_jukebox_on) jukeboxOption.setTitle(R.string.download_menu_jukebox_on)
@ -707,8 +706,7 @@ class PlayerFragment :
R.id.menu_item_jukebox -> { R.id.menu_item_jukebox -> {
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
Util.toast( toast(
context,
if (jukeboxEnabled) R.string.download_jukebox_on if (jukeboxEnabled) R.string.download_jukebox_on
else R.string.download_jukebox_off, else R.string.download_jukebox_off,
false false
@ -760,7 +758,7 @@ class PlayerFragment :
R.string.download_bookmark_set_at_position, R.string.download_bookmark_set_at_position,
bookmarkTime bookmarkTime
) )
Util.toast(context, msg) toast(msg)
return true return true
} }
R.id.menu_item_bookmark_delete -> { R.id.menu_item_bookmark_delete -> {
@ -776,13 +774,12 @@ class PlayerFragment :
Timber.e(all) Timber.e(all)
} }
}.start() }.start()
Util.toast(context, R.string.download_bookmark_removed) toast(R.string.download_bookmark_removed)
return true return true
} }
R.id.menu_item_share -> { R.id.menu_item_share -> {
val mediaPlayerController = mediaPlayerManager
val tracks: MutableList<Track?> = ArrayList() val tracks: MutableList<Track?> = ArrayList()
val playlist = mediaPlayerController.playlist val playlist = mediaPlayerManager.playlist
for (item in playlist) { for (item in playlist) {
val playlistEntry = item.toTrack() val playlistEntry = item.toTrack()
tracks.add(playlistEntry) tracks.add(playlistEntry)
@ -790,8 +787,6 @@ class PlayerFragment :
shareHandler.createShare( shareHandler.createShare(
this, this,
tracks = tracks, tracks = tracks,
swipe = null,
cancellationToken = cancellationToken,
) )
return true return true
} }
@ -804,8 +799,6 @@ class PlayerFragment :
shareHandler.createShare( shareHandler.createShare(
this, this,
tracks, tracks,
swipe = null,
cancellationToken = cancellationToken
) )
return true return true
} }
@ -822,7 +815,7 @@ class PlayerFragment :
} }
private fun savePlaylistInBackground(playlistName: String) { private fun savePlaylistInBackground(playlistName: String) {
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) toast(resources.getString(R.string.download_playlist_saving, playlistName))
mediaPlayerManager.suggestedPlaylistName = playlistName mediaPlayerManager.suggestedPlaylistName = playlistName
// The playlist can be acquired only from the main thread // The playlist can be acquired only from the main thread
@ -835,7 +828,7 @@ class PlayerFragment :
musicService.createPlaylist(null, playlistName, entries) musicService.createPlaylist(null, playlistName, entries)
}.invokeOnCompletion { }.invokeOnCompletion {
if (it == null || it is CancellationException) { if (it == null || it is CancellationException) {
Util.toast(UApp.applicationContext(), R.string.download_playlist_done) toast(R.string.download_playlist_done)
} else { } else {
Timber.e(it, "Exception has occurred in savePlaylistInBackground") Timber.e(it, "Exception has occurred in savePlaylistInBackground")
val msg = String.format( val msg = String.format(
@ -844,7 +837,7 @@ class PlayerFragment :
resources.getString(R.string.download_playlist_error), resources.getString(R.string.download_playlist_error),
CommunicationError.getErrorMessage(it) CommunicationError.getErrorMessage(it)
) )
Util.toast(UApp.applicationContext(), msg) toast(msg)
} }
} }
} }
@ -958,7 +951,7 @@ class PlayerFragment :
item?.mediaMetadata?.title item?.mediaMetadata?.title
) )
Util.toast(context, songRemoved) toast(songRemoved)
// Remove the item from the playlist // Remove the item from the playlist
mediaPlayerManager.removeFromPlaylist(pos) mediaPlayerManager.removeFromPlaylist(pos)
@ -1059,15 +1052,14 @@ class PlayerFragment :
} }
private fun onPlaylistChanged() { private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerManager
// Try to display playlist in play order // Try to display playlist in play order
val list = mediaPlayerController.playlistInPlayOrder val list = mediaPlayerManager.playlistInPlayOrder
emptyTextView.setText(R.string.playlist_empty) emptyTextView.setText(R.string.playlist_empty)
viewAdapter.submitList(list.map(MediaItem::toTrack)) viewAdapter.submitList(list.map(MediaItem::toTrack))
progressIndicator.isVisible = false progressIndicator.isVisible = false
emptyView.isVisible = list.isEmpty() emptyView.isVisible = list.isEmpty()
updateRepeatButtonState(mediaPlayerController.repeatMode) updateRepeatButtonState(mediaPlayerManager.repeatMode)
} }
private fun onTrackChanged() { private fun onTrackChanged() {

View File

@ -17,7 +17,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinScopeComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.AlbumRowDelegate
@ -36,13 +36,12 @@ import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.model.SearchListModel
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.util.Util.toast
@ -51,15 +50,12 @@ import org.moire.ultrasonic.util.Util.toast
* Initiates a search on the media library and displays the results * Initiates a search on the media library and displays the results
*/ */
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent { class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
private var searchResult: SearchResult? = null private var searchResult: SearchResult? = null
private var searchRefresh: SwipeRefreshLayout? = null private var searchRefresh: SwipeRefreshLayout? = null
private val mediaPlayerManager: MediaPlayerManager by inject() private val mediaPlayerManager: MediaPlayerManager by inject()
private val shareHandler: ShareHandler by inject()
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
private val navArgs by navArgs<SearchFragmentArgs>() private val navArgs by navArgs<SearchFragmentArgs>()
@ -137,18 +133,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
super.onDestroyView() super.onDestroyView()
} }
private fun downloadBackground(save: Boolean, songs: List<Track?>) {
val onValid = Runnable {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
DownloadService.download(
songs.filterNotNull(),
save = save,
updateSaveFlag = true
)
}
onValid.run()
}
private fun search(query: String, autoplay: Boolean) { private fun search(query: String, autoplay: Boolean) {
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) { listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
refreshListView?.isRefreshing = true refreshListView?.isRefreshing = true
@ -253,7 +237,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
insertionMode = MediaPlayerManager.InsertionMode.APPEND insertionMode = MediaPlayerManager.InsertionMode.APPEND
) )
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1) mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
toast(context, resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1)) toast(resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1))
} }
private fun onVideoSelected(track: Track) { private fun onVideoSelected(track: Track) {
@ -288,103 +272,23 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
val isArtist = (item is Artist) // Here the Item could be a track or an album or an artist
if (item is Track) {
val found = EntryListFragment.handleContextMenu( return handleContextMenuTracks(
menuItem, menuItem = menuItem,
item, tracks = listOf(item),
isArtist, mediaPlayerManager = mediaPlayerManager,
downloadHandler, fragment = this
this )
) } else {
return handleContextMenu(
if (found || item !is Track) return true menuItem = menuItem,
item = item,
val songs = mutableListOf<Track>() isArtist = item is Artist,
mediaPlayerManager = mediaPlayerManager,
when (menuItem.itemId) { fragment = this
R.id.song_menu_play_now -> { )
songs.add(item)
downloadHandler.addTracksToMediaController(
songs = songs,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
autoPlay = true,
shuffle = false,
fragment = this,
playlistName = null
)
}
R.id.song_menu_play_next -> {
songs.add(item)
downloadHandler.addTracksToMediaController(
songs = songs,
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
autoPlay = false,
shuffle = false,
fragment = this,
playlistName = null
)
}
R.id.song_menu_play_last -> {
songs.add(item)
downloadHandler.addTracksToMediaController(
songs = songs,
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
autoPlay = false,
shuffle = false,
fragment = this,
playlistName = null
)
}
R.id.song_menu_pin -> {
songs.add(item)
toast(
context,
resources.getQuantityString(
R.plurals.n_songs_pinned,
songs.size,
songs.size
)
)
downloadBackground(true, songs)
}
R.id.song_menu_download -> {
songs.add(item)
toast(
context,
resources.getQuantityString(
R.plurals.n_songs_to_be_downloaded,
songs.size,
songs.size
)
)
downloadBackground(false, songs)
}
R.id.song_menu_unpin -> {
songs.add(item)
toast(
context,
resources.getQuantityString(
R.plurals.n_songs_unpinned,
songs.size,
songs.size
)
)
DownloadService.unpin(songs)
}
R.id.song_menu_share -> {
songs.add(item)
shareHandler.createShare(
fragment = this,
tracks = songs,
swipe = searchRefresh,
cancellationToken = cancellationToken!!,
additionalId = null
)
}
} }
return true
} }
companion object { companion object {

View File

@ -64,7 +64,7 @@ class ServerSelectorFragment : Fragment() {
listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ -> listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
val server = parent.getItemAtPosition(position) as ServerSetting val server = parent.getItemAtPosition(position) as ServerSetting
ActiveServerProvider.setActiveServerById(server.id) activeServerProvider.setActiveServerById(server.id)
findNavController().popBackStack(R.id.mainFragment, false) findNavController().popBackStack(R.id.mainFragment, false)
} }
@ -99,7 +99,7 @@ class ServerSelectorFragment : Fragment() {
val activeServerId = ActiveServerProvider.getActiveServerId() val activeServerId = ActiveServerProvider.getActiveServerId()
// If the currently active server is deleted, go offline // If the currently active server is deleted, go offline
if (id == activeServerId) ActiveServerProvider.setActiveServerById(OFFLINE_DB_ID) if (id == activeServerId) activeServerProvider.setActiveServerById(OFFLINE_DB_ID)
serverSettingsModel.deleteItemById(id) serverSettingsModel.deleteItemById(id)

View File

@ -300,7 +300,7 @@ class SettingsFragment :
SearchSuggestionProvider.MODE SearchSuggestionProvider.MODE
) )
suggestions.clearHistory() suggestions.clearHistory()
toast(activity, R.string.settings_search_history_cleared) toast(R.string.settings_search_history_cleared)
false false
} }
} }
@ -332,7 +332,7 @@ class SettingsFragment :
Timber.w("Failed to delete %s", nomediaDir) Timber.w("Failed to delete %s", nomediaDir)
} }
} }
toast(activity, R.string.settings_hide_media_toast, false) toast(R.string.settings_hide_media_toast, false)
} }
private fun setCacheLocation(path: String) { private fun setCacheLocation(path: String) {

View File

@ -46,14 +46,16 @@ import org.moire.ultrasonic.model.TrackCollectionModel
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.ContextMenuUtil
import org.moire.ultrasonic.util.DownloadAction
import org.moire.ultrasonic.util.DownloadUtil
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.navigateToCurrent
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities import org.moire.ultrasonic.view.ViewCapabilities
import timber.log.Timber import timber.log.Timber
@ -86,7 +88,6 @@ open class TrackCollectionFragment(
internal val mediaPlayerManager: MediaPlayerManager by inject() internal val mediaPlayerManager: MediaPlayerManager by inject()
private val shareHandler: ShareHandler by inject() private val shareHandler: ShareHandler by inject()
internal var cancellationToken: CancellationToken? = null
override val listModel: TrackCollectionModel by viewModels() override val listModel: TrackCollectionModel by viewModels()
private val rxBusSubscription: CompositeDisposable = CompositeDisposable() private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
@ -102,7 +103,6 @@ open class TrackCollectionFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken()
albumButtons = view.findViewById(R.id.menu_album) albumButtons = view.findViewById(R.id.menu_album)
@ -211,19 +211,23 @@ open class TrackCollectionFragment(
} }
playNowButton?.setOnClickListener { playNowButton?.setOnClickListener {
playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true) playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.CLEAR)
} }
playNextButton?.setOnClickListener { playNextButton?.setOnClickListener {
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true) playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.AFTER_CURRENT)
} }
playLastButton!!.setOnClickListener { playLastButton!!.setOnClickListener {
playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true) playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.APPEND)
} }
pinButton?.setOnClickListener { pinButton?.setOnClickListener {
downloadBackground(true) downloadSelectedOrAllTracks(true)
}
downloadButton?.setOnClickListener {
downloadSelectedOrAllTracks(false)
} }
unpinButton?.setOnClickListener { unpinButton?.setOnClickListener {
@ -231,26 +235,22 @@ open class TrackCollectionFragment(
ConfirmationDialog.Builder(requireContext()) ConfirmationDialog.Builder(requireContext())
.setMessage(R.string.common_unpin_selection_confirmation) .setMessage(R.string.common_unpin_selection_confirmation)
.setPositiveButton(R.string.common_unpin) { _, _ -> .setPositiveButton(R.string.common_unpin) { _, _ ->
unpin() unpinSelectedTracks()
}.show() }.show()
} else { } else {
unpin() unpinSelectedTracks()
} }
} }
downloadButton?.setOnClickListener {
downloadBackground(false)
}
deleteButton?.setOnClickListener { deleteButton?.setOnClickListener {
if (Settings.showConfirmationDialog) { if (Settings.showConfirmationDialog) {
ConfirmationDialog.Builder(requireContext()) ConfirmationDialog.Builder(requireContext())
.setMessage(R.string.common_delete_selection_confirmation) .setMessage(R.string.common_delete_selection_confirmation)
.setPositiveButton(R.string.common_delete) { _, _ -> .setPositiveButton(R.string.common_delete) { _, _ ->
delete() deleteSelectedTracks()
}.show() }.show()
} else { } else {
delete() deleteSelectedTracks()
} }
} }
} }
@ -283,9 +283,9 @@ open class TrackCollectionFragment(
return true return true
} else if (item.itemId == R.id.menu_item_share) { } else if (item.itemId == R.id.menu_item_share) {
shareHandler.createShare( shareHandler.createShare(
this@TrackCollectionFragment, getSelectedTracks(), fragment = this@TrackCollectionFragment,
refreshListView, cancellationToken!!, tracks = getSelectedTracks(),
navArgs.id additionalId = navArgs.id
) )
return true return true
} }
@ -294,46 +294,10 @@ open class TrackCollectionFragment(
} }
override fun onDestroyView() { override fun onDestroyView() {
cancellationToken!!.cancel()
rxBusSubscription.dispose() rxBusSubscription.dispose()
super.onDestroyView() super.onDestroyView()
} }
private fun playNow(
insertionMode: MediaPlayerManager.InsertionMode,
selectedTracks: List<Track> = getSelectedTracks(),
toast: Boolean = false
) {
if (selectedTracks.isNotEmpty()) {
downloadHandler.addTracksToMediaController(
songs = selectedTracks,
insertionMode = insertionMode,
autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR),
playlistName = null,
fragment = this
)
} else {
playAll(false, insertionMode)
}
if (toast) {
val stringInt = when (insertionMode) {
MediaPlayerManager.InsertionMode.CLEAR ->
R.plurals.n_songs_added_play_now
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
R.plurals.n_songs_added_after_current
MediaPlayerManager.InsertionMode.APPEND ->
R.plurals.n_songs_added_to_end
}
val msg = resources.getQuantityString(
stringInt,
selectedTracks.size,
selectedTracks.size
)
Util.toast(requireContext(), msg)
}
}
/** /**
* Get the size of the underlying list * Get the size of the underlying list
*/ */
@ -364,25 +328,65 @@ open class TrackCollectionFragment(
// Need a valid id to recurse sub directories stuff // Need a valid id to recurse sub directories stuff
if (hasSubFolders && navArgs.id != null) { if (hasSubFolders && navArgs.id != null) {
downloadHandler.fetchTracksAndAddToController( mediaPlayerManager.playTracksAndToast(
fragment = this, fragment = this,
id = navArgs.id!!,
insertionMode = insertionMode, insertionMode = insertionMode,
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), id = navArgs.id!!,
shuffle = shuffle, shuffle = shuffle,
isArtist = isArtist isArtist = isArtist
) )
} else { } else {
downloadHandler.addTracksToMediaController( mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName
mediaPlayerManager.addToPlaylist(
songs = getAllSongs(), songs = getAllSongs(),
insertionMode = insertionMode, insertionMode = insertionMode,
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND), autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
shuffle = shuffle, shuffle = shuffle
playlistName = navArgs.playlistName,
fragment = this
) )
if (insertionMode == MediaPlayerManager.InsertionMode.CLEAR) {
navigateToCurrent()
}
} }
} }
private fun unpinSelectedTracks() {
DownloadUtil.justDownload(
action = DownloadAction.UNPIN,
fragment = this,
tracks = getSelectedTracks()
)
}
private fun downloadSelectedOrAllTracks(save: Boolean) {
var tracks = getSelectedTracks()
if (tracks.isEmpty()) tracks = getAllSongs()
DownloadUtil.justDownload(
action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD,
fragment = this,
tracks = tracks
)
}
private fun playSelectedOrAllTracks(
insertionMode: MediaPlayerManager.InsertionMode
) {
var tracks = getSelectedTracks()
if (tracks.isEmpty()) tracks = getAllSongs()
mediaPlayerManager.playTracksAndToast(
fragment = this,
insertionMode = insertionMode,
tracks = tracks
)
}
private fun deleteSelectedTracks() {
DownloadUtil.justDownload(
action = DownloadAction.DELETE,
fragment = this,
tracks = getSelectedTracks()
)
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun getAllSongs(): List<Track> { private fun getAllSongs(): List<Track> {
@ -403,7 +407,7 @@ open class TrackCollectionFragment(
// Display toast: N tracks selected // Display toast: N tracks selected
val toastResId = R.string.select_album_n_selected val toastResId = R.string.select_album_n_selected
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) toast(getString(toastResId, selectedCount.coerceAtLeast(0)))
} }
@Synchronized @Synchronized
@ -431,37 +435,6 @@ open class TrackCollectionFragment(
} }
} }
private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedTracks()) {
var songs = tracks
if (songs.isEmpty()) {
songs = getAllSongs()
}
val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD
downloadHandler.justDownload(
action = action,
fragment = this,
tracks = songs
)
}
internal fun delete(songs: List<Track> = getSelectedTracks()) {
downloadHandler.justDownload(
action = DownloadAction.DELETE,
fragment = this,
tracks = songs
)
}
internal fun unpin(songs: List<Track> = getSelectedTracks()) {
downloadHandler.justDownload(
action = DownloadAction.UNPIN,
fragment = this,
tracks = songs
)
}
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = { override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
Timber.i("Received list") Timber.i("Received list")
@ -606,48 +579,19 @@ open class TrackCollectionFragment(
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
@Suppress("LongMethod")
override fun onContextMenuItemSelected( override fun onContextMenuItemSelected(
menuItem: MenuItem, menuItem: MenuItem,
item: MusicDirectory.Child item: MusicDirectory.Child
): Boolean { ): Boolean {
val songs = getClickedSong(item)
when (menuItem.itemId) { val tracks = getClickedSong(item)
R.id.song_menu_play_now -> {
playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true) return ContextMenuUtil.handleContextMenuTracks(
} menuItem = menuItem,
R.id.song_menu_play_next -> { tracks = tracks,
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true) mediaPlayerManager = mediaPlayerManager,
} fragment = this
R.id.song_menu_play_last -> { )
playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true)
}
R.id.song_menu_pin -> {
downloadBackground(true, songs)
}
R.id.song_menu_unpin -> {
unpin(songs)
}
R.id.song_menu_download -> {
downloadBackground(false, songs)
}
R.id.song_menu_share -> {
if (item is Track) {
shareHandler.createShare(
this,
tracks = listOf(item),
swipe = refreshListView,
cancellationToken = cancellationToken!!,
additionalId = navArgs.id
)
}
}
else -> {
return super.onContextItemSelected(menuItem)
}
}
return true
} }
private fun getClickedSong(item: MusicDirectory.Child): List<Track> { private fun getClickedSong(item: MusicDirectory.Child): List<Track> {

View File

@ -25,11 +25,11 @@ import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.ListView import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale import java.util.Locale
import org.koin.core.component.KoinComponent import org.koin.androidx.scope.ScopeFragment
import org.koin.core.component.KoinScopeComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -39,12 +39,12 @@ import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.OfflineException import org.moire.ultrasonic.service.OfflineException
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.DownloadAction
import org.moire.ultrasonic.util.DownloadUtil
import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LoadingTask import org.moire.ultrasonic.util.LoadingTask
@ -56,14 +56,12 @@ import org.moire.ultrasonic.util.Util.toast
* *
* TODO: This file has been converted from Java, but not modernized yet. * TODO: This file has been converted from Java, but not modernized yet.
*/ */
class PlaylistsFragment : Fragment(), KoinComponent { class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
private var refreshPlaylistsListView: SwipeRefreshLayout? = null private var refreshPlaylistsListView: SwipeRefreshLayout? = null
private var playlistsListView: ListView? = null private var playlistsListView: ListView? = null
private var emptyTextView: View? = null private var emptyTextView: View? = null
private var playlistAdapter: ArrayAdapter<Playlist>? = null private var playlistAdapter: ArrayAdapter<Playlist>? = null
private val downloadHandler by inject<DownloadHandler>()
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -115,7 +113,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
override fun doInBackground(): List<Playlist> { override fun doInBackground(): List<Playlist> {
val musicService = getMusicService() val musicService = getMusicService()
val playlists = musicService.getPlaylists(refresh) val playlists = musicService.getPlaylists(refresh)
if (!isOffline()) CacheCleaner().cleanPlaylists(playlists) val cacheCleaner: CacheCleaner by inject()
if (!isOffline()) cacheCleaner.cleanPlaylists(playlists)
return playlists return playlists
} }
@ -147,8 +146,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.playlist_menu_pin -> { R.id.playlist_menu_pin -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.PIN, action = DownloadAction.PIN,
fragment = this, fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
@ -157,8 +156,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
) )
} }
R.id.playlist_menu_unpin -> { R.id.playlist_menu_unpin -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.UNPIN, action = DownloadAction.UNPIN,
fragment = this, fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
@ -167,8 +166,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
) )
} }
R.id.playlist_menu_download -> { R.id.playlist_menu_download -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.DOWNLOAD, action = DownloadAction.DOWNLOAD,
fragment = this, fragment = this,
id = playlist.id, id = playlist.id,
name = playlist.name, name = playlist.name,
@ -227,7 +226,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
playlistAdapter!!.remove(playlist) playlistAdapter!!.remove(playlist)
playlistAdapter!!.notifyDataSetChanged() playlistAdapter!!.notifyDataSetChanged()
toast( toast(
context,
resources.getString(R.string.menu_deleted_playlist, playlist.name) resources.getString(R.string.menu_deleted_playlist, playlist.name)
) )
} }
@ -246,7 +244,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
), ),
getErrorMessage(error) getErrorMessage(error)
) )
toast(context, msg, false) toast(msg, false)
} }
}.execute() }.execute()
}.setNegativeButton(R.string.common_cancel, null).show() }.setNegativeButton(R.string.common_cancel, null).show()
@ -310,7 +308,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
override fun done(result: Any?) { override fun done(result: Any?) {
load(true) load(true)
toast( toast(
context,
resources.getString(R.string.playlist_updated_info, playlist.name) resources.getString(R.string.playlist_updated_info, playlist.name)
) )
} }
@ -329,7 +326,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
), ),
getErrorMessage(error) getErrorMessage(error)
) )
toast(context, msg, false) toast(msg, false)
} }
}.execute() }.execute()
} }

View File

@ -24,11 +24,11 @@ import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.ListView import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.util.Locale import java.util.Locale
import org.koin.core.component.KoinComponent import org.koin.androidx.scope.ScopeFragment
import org.koin.core.component.KoinScopeComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -38,14 +38,15 @@ import org.moire.ultrasonic.fragment.FragmentTitle
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.OfflineException import org.moire.ultrasonic.service.OfflineException
import org.moire.ultrasonic.subsonic.DownloadAction
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.DownloadAction
import org.moire.ultrasonic.util.DownloadUtil
import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.LoadingTask import org.moire.ultrasonic.util.LoadingTask
import org.moire.ultrasonic.util.TimeSpanPicker import org.moire.ultrasonic.util.TimeSpanPicker
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.view.ShareAdapter import org.moire.ultrasonic.view.ShareAdapter
/** /**
@ -53,12 +54,12 @@ import org.moire.ultrasonic.view.ShareAdapter
* *
* TODO: This file has been converted from Java, but not modernized yet. * TODO: This file has been converted from Java, but not modernized yet.
*/ */
class SharesFragment : Fragment(), KoinComponent { class SharesFragment : ScopeFragment(), KoinScopeComponent {
private var refreshSharesListView: SwipeRefreshLayout? = null private var refreshSharesListView: SwipeRefreshLayout? = null
private var sharesListView: ListView? = null private var sharesListView: ListView? = null
private var emptyTextView: View? = null private var emptyTextView: View? = null
private var shareAdapter: ShareAdapter? = null private var shareAdapter: ShareAdapter? = null
private val downloadHandler: DownloadHandler by inject() private val mediaPlayerManager: MediaPlayerManager by inject()
private var cancellationToken: CancellationToken? = null private var cancellationToken: CancellationToken? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context) Util.applyTheme(this.context)
@ -133,8 +134,8 @@ class SharesFragment : Fragment(), KoinComponent {
val share = sharesListView!!.getItemAtPosition(info.position) as Share val share = sharesListView!!.getItemAtPosition(info.position) as Share
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.share_menu_pin -> { R.id.share_menu_pin -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.PIN, action = DownloadAction.PIN,
fragment = this, fragment = this,
id = share.id, id = share.id,
name = share.name, name = share.name,
@ -143,8 +144,8 @@ class SharesFragment : Fragment(), KoinComponent {
) )
} }
R.id.share_menu_unpin -> { R.id.share_menu_unpin -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.UNPIN, action = DownloadAction.UNPIN,
fragment = this, fragment = this,
id = share.id, id = share.id,
name = share.name, name = share.name,
@ -153,8 +154,8 @@ class SharesFragment : Fragment(), KoinComponent {
) )
} }
R.id.share_menu_download -> { R.id.share_menu_download -> {
downloadHandler.justDownload( DownloadUtil.justDownload(
DownloadAction.DOWNLOAD, action = DownloadAction.DOWNLOAD,
fragment = this, fragment = this,
id = share.id, id = share.id,
name = share.name, name = share.name,
@ -163,22 +164,20 @@ class SharesFragment : Fragment(), KoinComponent {
) )
} }
R.id.share_menu_play_now -> { R.id.share_menu_play_now -> {
downloadHandler.fetchTracksAndAddToController( mediaPlayerManager.playTracksAndToast(
this, this,
share.id,
share.name,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR, insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
autoPlay = true, id = share.id,
name = share.name,
shuffle = false shuffle = false
) )
} }
R.id.share_menu_play_shuffled -> { R.id.share_menu_play_shuffled -> {
downloadHandler.fetchTracksAndAddToController( mediaPlayerManager.playTracksAndToast(
this, this,
share.id,
share.name,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR, insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
autoPlay = true, id = share.id,
name = share.name,
shuffle = true, shuffle = true,
) )
} }
@ -214,8 +213,7 @@ class SharesFragment : Fragment(), KoinComponent {
override fun done(result: Any?) { override fun done(result: Any?) {
shareAdapter!!.remove(share) shareAdapter!!.remove(share)
shareAdapter!!.notifyDataSetChanged() shareAdapter!!.notifyDataSetChanged()
Util.toast( toast(
context,
resources.getString(R.string.menu_deleted_share, share.name) resources.getString(R.string.menu_deleted_share, share.name)
) )
} }
@ -237,7 +235,7 @@ class SharesFragment : Fragment(), KoinComponent {
getErrorMessage(error) getErrorMessage(error)
) )
} }
Util.toast(context, msg, false) toast(msg, false)
} }
}.execute() }.execute()
}.setNegativeButton(R.string.common_cancel, null).show() }.setNegativeButton(R.string.common_cancel, null).show()
@ -315,8 +313,7 @@ class SharesFragment : Fragment(), KoinComponent {
override fun done(result: Any?) { override fun done(result: Any?) {
load(true) load(true)
Util.toast( toast(
context,
resources.getString(R.string.playlist_updated_info, share.name) resources.getString(R.string.playlist_updated_info, share.name)
) )
} }
@ -338,7 +335,7 @@ class SharesFragment : Fragment(), KoinComponent {
getErrorMessage(error) getErrorMessage(error)
) )
} }
Util.toast(context, msg, false) toast(msg, false)
} }
}.execute() }.execute()
} }

View File

@ -190,17 +190,17 @@ class ImageLoader(
if (artist.coverArt == null) return if (artist.coverArt == null) return
val key = FileUtil.getArtistArtKey(artist.name, false) val key = FileUtil.getArtistArtKey(artist.name, false)
val file = FileUtil.getAlbumArtFile(key) val file = FileUtil.getAlbumArtFile(key)
cacheCoverArt(artist.coverArt!!, file) downloadCoverArt(artist.coverArt!!, file)
} }
/** /**
* Download a cover art file of a Track and cache it on disk * Download a cover art file of a Track and cache it on disk
*/ */
fun cacheCoverArt(track: Track) { fun downloadCoverArt(track: Track) {
cacheCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track)) downloadCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
} }
fun cacheCoverArt(id: String, file: String) = launch { fun downloadCoverArt(id: String, file: String) = launch {
if (id.isBlank()) return@launch if (id.isBlank()) return@launch
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -56,8 +56,10 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
val albumArtFile = FileUtil.getAlbumArtFile(parts[1]) val albumArtFile = FileUtil.getAlbumArtFile(parts[1])
Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile) Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile)
// TODO: Check if the dependency on the image loader could be removed.
imageLoaderProvider.executeOn { imageLoaderProvider.executeOn {
it.cacheCoverArt(parts[0], albumArtFile) it.downloadCoverArt(parts[0], albumArtFile)
} }
val file = File(albumArtFile) val file = File(albumArtFile)
if (!file.exists()) return null if (!file.exists()) return null

View File

@ -23,7 +23,7 @@ import android.widget.RemoteViews
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.receiver.UltrasonicIntentReceiver
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import timber.log.Timber import timber.log.Timber
@ -233,7 +233,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
// Emulate media button clicks. // Emulate media button clicks.
intent = Intent(Constants.CMD_PROCESS_KEYCODE) intent = Intent(Constants.CMD_PROCESS_KEYCODE)
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
intent.putExtra( intent.putExtra(
Intent.EXTRA_KEY_EVENT, Intent.EXTRA_KEY_EVENT,
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
@ -241,12 +241,12 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
flags = 0 flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31) // needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE flags = PendingIntent.FLAG_IMMUTABLE
} }
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags) pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
views.setOnClickPendingIntent(R.id.control_play, pendingIntent) views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
intent = Intent(Constants.CMD_PROCESS_KEYCODE) intent = Intent(Constants.CMD_PROCESS_KEYCODE)
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
intent.putExtra( intent.putExtra(
Intent.EXTRA_KEY_EVENT, Intent.EXTRA_KEY_EVENT,
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT) KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
@ -254,7 +254,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags) pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags)
views.setOnClickPendingIntent(R.id.control_next, pendingIntent) views.setOnClickPendingIntent(R.id.control_next, pendingIntent)
intent = Intent(Constants.CMD_PROCESS_KEYCODE) intent = Intent(Constants.CMD_PROCESS_KEYCODE)
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java) intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
intent.putExtra( intent.putExtra(
Intent.EXTRA_KEY_EVENT, Intent.EXTRA_KEY_EVENT,
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS) KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)

View File

@ -1,60 +0,0 @@
/*
* MediaButtonIntentReceiver.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Parcelable
import java.lang.Exception
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
import timber.log.Timber
/**
* This class is used to receive commands from the widget
*/
class MediaButtonIntentReceiver : BroadcastReceiver(), KoinComponent {
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
override fun onReceive(context: Context, intent: Intent) {
val intentAction = intent.action
// If media button are turned off and we received a media button, exit
if (!Settings.mediaButtonsEnabled && Intent.ACTION_MEDIA_BUTTON == intentAction) return
// Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets
if (Intent.ACTION_MEDIA_BUTTON != intentAction &&
Constants.CMD_PROCESS_KEYCODE != intentAction
) return
val extras = intent.extras ?: return
val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
extras.getParcelable(Intent.EXTRA_KEY_EVENT, Parcelable::class.java)
} else {
@Suppress("DEPRECATION")
extras.get(Intent.EXTRA_KEY_EVENT) as Parcelable?
}
Timber.i("Got MEDIA_BUTTON key event: %s", event)
try {
val serviceIntent = Intent(Constants.CMD_PROCESS_KEYCODE)
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event)
lifecycleSupport.receiveIntent(serviceIntent)
if (isOrderedBroadcast) {
abortBroadcast()
}
} catch (ignored: Exception) {
// Ignored.
}
}
}

View File

@ -10,20 +10,19 @@ package org.moire.ultrasonic.receiver
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import org.koin.java.KoinJavaComponent.inject import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import timber.log.Timber import timber.log.Timber
class UltrasonicIntentReceiver : BroadcastReceiver() { class UltrasonicIntentReceiver : BroadcastReceiver(), KoinComponent {
private val lifecycleSupport = inject<MediaPlayerLifecycleSupport>( private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
MediaPlayerLifecycleSupport::class.java
)
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val intentAction = intent.action val intentAction = intent.action
Timber.i("Received Ultrasonic Intent: %s", intentAction) Timber.i("Received Ultrasonic Intent: %s", intentAction)
try { try {
lifecycleSupport.value.receiveIntent(intent) lifecycleSupport.receiveIntent(intent)
if (isOrderedBroadcast) { if (isOrderedBroadcast) {
abortBroadcast() abortBroadcast()
} }

View File

@ -315,6 +315,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
return musicService.getStreamUrl(id, maxBitRate, format) return musicService.getStreamUrl(id, maxBitRate, format)
} }
override fun isJukeboxAvailable(): Boolean {
return musicService.isJukeboxAvailable()
}
@Throws(Exception::class) @Throws(Exception::class)
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus { override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
return musicService.updateJukeboxPlaylist(ids) return musicService.updateJukeboxPlaylist(ids)

View File

@ -29,8 +29,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
@ -61,6 +63,7 @@ private const val CHECK_INTERVAL = 5000L
class DownloadService : Service(), KoinComponent { class DownloadService : Service(), KoinComponent {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private val storageMonitor: ExternalStorageMonitor by inject() private val storageMonitor: ExternalStorageMonitor by inject()
private val cacheCleaner: CacheCleaner by inject()
private val binder: IBinder = SimpleServiceBinder(this) private val binder: IBinder = SimpleServiceBinder(this)
private var isInForeground = false private var isInForeground = false
@ -153,7 +156,7 @@ class DownloadService : Service(), KoinComponent {
// Stop Executor service when done downloading // Stop Executor service when done downloading
if (activeDownloads.isEmpty()) { if (activeDownloads.isEmpty()) {
CacheCleaner().cleanSpace() cacheCleaner.cleanSpace()
stopSelf() stopSelf()
} }
@ -279,7 +282,17 @@ class DownloadService : Service(), KoinComponent {
updateSaveFlag: Boolean = false updateSaveFlag: Boolean = false
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadAsync(tracks, save, isHighPriority, updateSaveFlag)
}
}
suspend fun downloadAsync(
tracks: List<Track>,
save: Boolean = false,
isHighPriority: Boolean = false,
updateSaveFlag: Boolean = false
) {
withContext(Dispatchers.IO) {
// Remove tracks which are already downloaded and update the save flag // Remove tracks which are already downloaded and update the save flag
// if needed // if needed
var filteredTracks = if (updateSaveFlag) { var filteredTracks = if (updateSaveFlag) {
@ -384,8 +397,14 @@ class DownloadService : Service(), KoinComponent {
failedList.clear() failedList.clear()
} }
fun delete(track: Track) { private fun delete(track: Track) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
deleteAsync(track)
}
}
private suspend fun deleteAsync(track: Track) {
withContext(Dispatchers.IO) {
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) } downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
failedList[track.id]?.let { downloadQueue.remove(it) } failedList[track.id]?.let { downloadQueue.remove(it) }
cancelDownload(track) cancelDownload(track)
@ -394,7 +413,8 @@ class DownloadService : Service(), KoinComponent {
Storage.delete(track.getCompleteFile()) Storage.delete(track.getCompleteFile())
Storage.delete(track.getPinnedFile()) Storage.delete(track.getPinnedFile())
postState(track, DownloadState.IDLE) postState(track, DownloadState.IDLE)
CacheCleaner().cleanDatabaseSelective(track) val cacheCleaner: CacheCleaner by inject(CacheCleaner::class.java)
cacheCleaner.cleanDatabaseSelective(track)
Util.scanMedia(track.getPinnedFile()) Util.scanMedia(track.getPinnedFile())
} }
} }
@ -409,22 +429,38 @@ class DownloadService : Service(), KoinComponent {
tracks.forEach(::delete) tracks.forEach(::delete)
} }
fun unpin(track: Track) { suspend fun unpinAsync(tracks: List<Track>) {
// Update Pinned flag of items in progress tracks.forEach { unpinAsync(it) }
downloadQueue.get(track.id)?.pinned = false }
activeDownloads[track.id]?.downloadTrack?.pinned = false
failedList[track.id]?.pinned = false
val pinnedFile = track.getPinnedFile() suspend fun deleteAsync(tracks: List<Track>) {
if (!Storage.isPathExists(pinnedFile)) return tracks.forEach { deleteAsync(it) }
val file = Storage.getFromPath(track.getPinnedFile()) ?: return }
try {
Storage.rename(file, track.getCompleteFile()) private fun unpin(track: Track) {
} catch (ignored: FileAlreadyExistsException) { CoroutineScope(Dispatchers.IO).launch {
// Play console has revealed a crash when for some reason both files exist unpinAsync(track)
Storage.delete(file.path) }
}
private suspend fun unpinAsync(track: Track) {
withContext(Dispatchers.IO) {
// Update Pinned flag of items in progress
downloadQueue.get(track.id)?.pinned = false
activeDownloads[track.id]?.downloadTrack?.pinned = false
failedList[track.id]?.pinned = false
val pinnedFile = track.getPinnedFile()
if (!Storage.isPathExists(pinnedFile)) return@withContext
val file = Storage.getFromPath(track.getPinnedFile()) ?: return@withContext
try {
Storage.rename(file, track.getCompleteFile())
} catch (ignored: FileAlreadyExistsException) {
// Play console has revealed a crash when for some reason both files exist
Storage.delete(file.path)
}
postState(track, DownloadState.DONE)
} }
postState(track, DownloadState.DONE)
} }
@Suppress("ReturnCount") @Suppress("ReturnCount")

View File

@ -256,7 +256,7 @@ class DownloadTask(
// Download the largest size that we can display in the UI // Download the largest size that we can display in the UI
imageLoaderProvider.executeOn { imageLoader -> imageLoaderProvider.executeOn { imageLoader ->
imageLoader.cacheCoverArt(this) imageLoader.downloadCoverArt(this)
// Cache small copies of the Artist picture // Cache small copies of the Artist picture
directArtist?.let { imageLoader.cacheArtistPicture(it) } directArtist?.let { imageLoader.cacheArtistPicture(it) }
compilationArtist?.let { imageLoader.cacheArtistPicture(it) } compilationArtist?.let { imageLoader.cacheArtistPicture(it) }

View File

@ -1,13 +1,12 @@
/* /*
* AutoMediaBrowserCallback.kt * MediaLibrarySessionCallback.kt
* Copyright (C) 2009-2022 Ultrasonic developers * Copyright (C) 2009-2023 Ultrasonic developers
* *
* Distributed under terms of the GNU GPLv3 license. * Distributed under terms of the GNU GPLv3 license.
*/ */
package org.moire.ultrasonic.playback package org.moire.ultrasonic.service
import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
@ -32,11 +31,14 @@ import androidx.media3.session.SessionResult.RESULT_SUCCESS
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.await
import kotlinx.coroutines.guava.future import kotlinx.coroutines.guava.future
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
@ -48,8 +50,6 @@ import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.RatingManager
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.buildMediaItem import org.moire.ultrasonic.util.buildMediaItem
import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toMediaItem
@ -98,10 +98,12 @@ const val PLAY_COMMAND = "play "
* MediaBrowserService implementation for e.g. Android Auto * MediaBrowserService implementation for e.g. Android Auto
*/ */
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") @Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent { class MediaLibrarySessionCallback :
MediaLibraryService.MediaLibrarySession.Callback,
KoinComponent {
private val applicationContext: Context by inject()
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private val playbackStateSerializer: PlaybackStateSerializer by inject()
private val serviceJob = SupervisorJob() private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
@ -243,6 +245,25 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
) )
} }
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
override fun onPlaybackResumption(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
val result = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
serviceScope.launch {
val state = playbackStateSerializer.deserializeNow()
if (state != null) {
result.set(state.toMediaItemsWithStartPosition())
withContext(Dispatchers.Main) {
mediaSession.player.shuffleModeEnabled = state.shufflePlay
mediaSession.player.repeatMode = state.repeatMode
}
}
}
return result
}
private fun configureRepeatMode(player: Player) { private fun configureRepeatMode(player: Player) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Timber.d("Car app library available, observing CarConnection") Timber.d("Car app library available, observing CarConnection")
@ -251,7 +272,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
var lastCarConnectionType = -1 var lastCarConnectionType = -1
CarConnection(applicationContext).type.observeForever { CarConnection(UApp.applicationContext()).type.observeForever {
if (lastCarConnectionType == it) if (lastCarConnectionType == it)
return@observeForever return@observeForever

View File

@ -16,8 +16,6 @@ import android.os.Build
import android.view.KeyEvent import android.view.KeyEvent
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.app.UApp.Companion.applicationContext
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
@ -29,11 +27,13 @@ import timber.log.Timber
/** /**
* This class is responsible for handling received events for the Media Player implementation * This class is responsible for handling received events for the Media Player implementation
*/ */
class MediaPlayerLifecycleSupport : KoinComponent { class MediaPlayerLifecycleSupport(
val mediaPlayerManager: MediaPlayerManager,
private val playbackStateSerializer: PlaybackStateSerializer,
val imageLoaderProvider: ImageLoaderProvider,
private val cacheCleaner: CacheCleaner
) : KoinComponent {
private lateinit var ratingManager: RatingManager private lateinit var ratingManager: RatingManager
private val playbackStateSerializer by inject<PlaybackStateSerializer>()
private val mediaPlayerManager by inject<MediaPlayerManager>()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private var created = false private var created = false
private var headsetEventReceiver: BroadcastReceiver? = null private var headsetEventReceiver: BroadcastReceiver? = null
@ -70,7 +70,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
registerHeadsetReceiver() registerHeadsetReceiver()
CacheCleaner().clean() cacheCleaner.clean()
created = true created = true
ratingManager = RatingManager.instance ratingManager = RatingManager.instance
Timber.i("LifecycleSupport created") Timber.i("LifecycleSupport created")
@ -78,15 +78,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) { private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) {
playbackStateSerializer.deserialize { playbackStateSerializer.deserialize {
if (it == null) return@deserialize null
Timber.i("Restoring %s songs", it.songs.size)
Timber.i("Restoring %s songs", it!!.songs.size) mediaPlayerManager.restore(it, autoPlay)
mediaPlayerManager.restore(
it,
autoPlay,
false
)
afterRestore?.run() afterRestore?.run()
} }
} }
@ -99,7 +94,6 @@ class MediaPlayerLifecycleSupport : KoinComponent {
applicationContext().unregisterReceiver(headsetEventReceiver) applicationContext().unregisterReceiver(headsetEventReceiver)
imageLoaderProvider.clearImageLoader() imageLoaderProvider.clearImageLoader()
UApp.instance!!.shutdownKoin()
created = false created = false
Timber.i("LifecycleSupport destroyed") Timber.i("LifecycleSupport destroyed")

View File

@ -7,10 +7,11 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.IntRange import androidx.annotation.IntRange
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.HeartRating import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -29,17 +30,20 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID
import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.data.RatingUpdate
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.playback.PlaybackService
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.DownloadUtil
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.navigateToCurrent
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.util.launchWithToast
import org.moire.ultrasonic.util.toMediaItem import org.moire.ultrasonic.util.toMediaItem
import org.moire.ultrasonic.util.toTrack import org.moire.ultrasonic.util.toTrack
import timber.log.Timber import timber.log.Timber
@ -56,12 +60,9 @@ private const val VOLUME_DELTA = 0.05f
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class MediaPlayerManager( class MediaPlayerManager(
private val playbackStateSerializer: PlaybackStateSerializer, private val playbackStateSerializer: PlaybackStateSerializer,
private val externalStorageMonitor: ExternalStorageMonitor, private val externalStorageMonitor: ExternalStorageMonitor
val context: Context
) : KoinComponent { ) : KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject()
private var created = false private var created = false
var suggestedPlaylistName: String? = null var suggestedPlaylistName: String? = null
var keepScreenOn = false var keepScreenOn = false
@ -73,8 +74,10 @@ class MediaPlayerManager(
private var mainScope = CoroutineScope(Dispatchers.Main) private var mainScope = CoroutineScope(Dispatchers.Main)
private var sessionToken = private var sessionToken = SessionToken(
SessionToken(context, ComponentName(context, PlaybackService::class.java)) UApp.applicationContext(),
ComponentName(UApp.applicationContext(), PlaybackService::class.java)
)
private var mediaControllerFuture: ListenableFuture<MediaController>? = null private var mediaControllerFuture: ListenableFuture<MediaController>? = null
@ -145,12 +148,11 @@ class MediaPlayerManager(
Timber.w(error.toString()) Timber.w(error.toString())
if (!isJukeboxEnabled) return if (!isJukeboxEnabled) return
val context = UApp.applicationContext()
mainScope.launch { mainScope.launch {
Util.toast( toast(
context,
error.errorCode, error.errorCode,
false false,
UApp.applicationContext()
) )
} }
isJukeboxEnabled = false isJukeboxEnabled = false
@ -199,7 +201,7 @@ class MediaPlayerManager(
} }
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault val jukebox = it.jukeboxByDefault
// Remove all songs when changing servers before turning on Jukebox. // Remove all songs when changing servers before turning on Jukebox.
// Jukebox wouldn't find the songs on the new server. // Jukebox wouldn't find the songs on the new server.
if (jukebox) controller?.clearMediaItems() if (jukebox) controller?.clearMediaItems()
@ -246,10 +248,10 @@ class MediaPlayerManager(
private fun createMediaController(onCreated: () -> Unit) { private fun createMediaController(onCreated: () -> Unit) {
mediaControllerFuture = MediaController.Builder( mediaControllerFuture = MediaController.Builder(
context, UApp.applicationContext(),
sessionToken sessionToken
) )
// Specify mainThread explicitely // Specify mainThread explicitly
.setApplicationLooper(Looper.getMainLooper()) .setApplicationLooper(Looper.getMainLooper())
.buildAsync() .buildAsync()
@ -320,17 +322,15 @@ class MediaPlayerManager(
externalStorageMonitor.onDestroy() externalStorageMonitor.onDestroy()
DownloadService.requestStop() DownloadService.requestStop()
created = false created = false
Timber.i("MediaPlayerController destroyed") Timber.i("MediaPlayerManager destroyed")
} }
@Synchronized @Synchronized
fun restore( fun restore(
state: PlaybackState, state: PlaybackState,
autoPlay: Boolean, autoPlay: Boolean
newPlaylist: Boolean
) { ) {
val insertionMode = if (newPlaylist) InsertionMode.CLEAR val insertionMode = InsertionMode.APPEND
else InsertionMode.APPEND
addToPlaylist( addToPlaylist(
state.songs, state.songs,
@ -474,6 +474,80 @@ class MediaPlayerManager(
} }
} }
private suspend fun addToPlaylistAsync(
songs: List<Track>,
autoPlay: Boolean,
shuffle: Boolean,
insertionMode: InsertionMode
) {
withContext(Dispatchers.Main) {
addToPlaylist(
songs = songs,
autoPlay = autoPlay,
shuffle = shuffle,
insertionMode = insertionMode
)
}
}
@Suppress("LongParameterList")
fun playTracksAndToast(
fragment: Fragment,
insertionMode: InsertionMode,
tracks: List<Track> = listOf(),
id: String? = null,
name: String? = "",
isShare: Boolean = false,
isDirectory: Boolean = true,
shuffle: Boolean = false,
isArtist: Boolean = false
) {
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
scope.launchWithToast {
val list: List<Track> =
tracks.ifEmpty {
requireNotNull(id)
DownloadUtil.getTracksFromServerAsync(isArtist, id, isDirectory, name, isShare)
}
addToPlaylistAsync(
songs = list,
insertionMode = insertionMode,
autoPlay = (insertionMode == InsertionMode.CLEAR),
shuffle = shuffle,
)
if (insertionMode == InsertionMode.CLEAR) {
fragment.navigateToCurrent()
}
when (insertionMode) {
InsertionMode.AFTER_CURRENT ->
quantize(R.plurals.n_songs_added_after_current, list)
InsertionMode.APPEND ->
quantize(R.plurals.n_songs_added_to_end, list)
InsertionMode.CLEAR -> {
if (Settings.shouldTransitionOnPlayback)
null
else
quantize(R.plurals.n_songs_added_play_now, list)
}
}
}
}
private fun quantize(resId: Int, tracks: List<Track>): String {
return UApp.applicationContext().resources.getQuantityString(
resId,
tracks.size,
tracks.size
)
}
@set:Synchronized @set:Synchronized
var isShufflePlayEnabled: Boolean var isShufflePlayEnabled: Boolean
get() = controller?.shuffleModeEnabled == true get() = controller?.shuffleModeEnabled == true
@ -649,21 +723,6 @@ class MediaPlayerManager(
Timber.i("MediaPlayerController released") Timber.i("MediaPlayerController released")
} }
/**
* This function calls the music service directly and
* therefore can't be called from the main thread
*/
val isJukeboxAvailable: Boolean
get() {
try {
val username = activeServerProvider.getActiveServer().userName
return getMusicService().getUser(username).jukeboxRole
} catch (all: Exception) {
Timber.w(all, "Error getting user information")
}
return false
}
fun adjustVolume(up: Boolean) { fun adjustVolume(up: Boolean) {
val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA
var gain = controller?.volume ?: return var gain = controller?.volume ?: return
@ -676,7 +735,7 @@ class MediaPlayerManager(
/* /*
* Sets the rating of the current track * Sets the rating of the current track
*/ */
fun setRating(rating: Rating) { private fun setRating(rating: Rating) {
if (controller is MediaController) { if (controller is MediaController) {
(controller as MediaController).setRating(rating) (controller as MediaController).setRating(rating)
} }
@ -724,7 +783,7 @@ class MediaPlayerManager(
val currentMediaItemIndex: Int val currentMediaItemIndex: Int
get() = controller?.currentMediaItemIndex ?: -1 get() = controller?.currentMediaItemIndex ?: -1
fun getCurrentShuffleIndex(): Int { private fun getCurrentShuffleIndex(): Int {
val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1 val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1
return getShuffledIndexOf(currentMediaItemIndex) return getShuffledIndexOf(currentMediaItemIndex)
} }
@ -768,9 +827,8 @@ class MediaPlayerManager(
* in the shuffled timeline. * in the shuffled timeline.
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found. * @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
*/ */
fun getShuffledIndexOf(searchPosition: Int): Int { private fun getShuffledIndexOf(searchPosition: Int): Int {
return getWindowIndexWhere(false) { return getWindowIndexWhere(false) { _, windowIndex ->
_, windowIndex ->
windowIndex == searchPosition windowIndex == searchPosition
} }
} }
@ -784,8 +842,7 @@ class MediaPlayerManager(
* @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found. * @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found.
*/ */
fun getUnshuffledIndexOf(shufflePosition: Int): Int { fun getUnshuffledIndexOf(shufflePosition: Int): Int {
return getWindowIndexWhere(true) { return getWindowIndexWhere(true) { count, _ ->
count, _ ->
count == shufflePosition count == shufflePosition
} }
} }

View File

@ -138,6 +138,8 @@ interface MusicService {
@Throws(Exception::class) @Throws(Exception::class)
fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String? fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String?
fun isJukeboxAvailable(): Boolean
@Throws(Exception::class) @Throws(Exception::class)
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus

View File

@ -335,6 +335,10 @@ class OfflineMusicService : MusicService, KoinComponent {
} }
} }
override fun isJukeboxAvailable(): Boolean {
return false
}
@Throws(Exception::class) @Throws(Exception::class)
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus { override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode") throw OfflineException("Jukebox not available in offline mode")

View File

@ -1,10 +1,10 @@
/* /*
* PlaybackService.kt * PlaybackService.kt
* Copyright (C) 2009-2022 Ultrasonic developers * Copyright (C) 2009-2023 Ultrasonic developers
* *
* Distributed under terms of the GNU GPLv3 license. * Distributed under terms of the GNU GPLv3 license.
*/ */
package org.moire.ultrasonic.playback package org.moire.ultrasonic.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
@ -42,15 +42,11 @@ import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.CachedDataSource
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.JukeboxMediaPlayer
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -68,7 +64,7 @@ class PlaybackService :
private var equalizer: EqualizerController? = null private var equalizer: EqualizerController? = null
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private lateinit var librarySessionCallback: AutoMediaBrowserCallback private lateinit var librarySessionCallback: MediaLibrarySessionCallback
private var rxBusSubscription = CompositeDisposable() private var rxBusSubscription = CompositeDisposable()
@ -115,6 +111,7 @@ class PlaybackService :
isStarted = false isStarted = false
stopForegroundRemoveNotification() stopForegroundRemoveNotification()
stopSelf() stopSelf()
instance = null
} }
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver { private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
@ -142,7 +139,7 @@ class PlaybackService :
actualBackend = desiredBackend actualBackend = desiredBackend
// Create browser interface // Create browser interface
librarySessionCallback = AutoMediaBrowserCallback() librarySessionCallback = MediaLibrarySessionCallback()
// This will need to use the AutoCalls // This will need to use the AutoCalls
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)

View File

@ -1,7 +1,9 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import androidx.media3.session.MediaSession
import java.io.Serializable import java.io.Serializable
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.util.toMediaItem
/** /**
* Represents the state of the Media Player implementation * Represents the state of the Media Player implementation
@ -17,3 +19,12 @@ data class PlaybackState(
private const val serialVersionUID = -293487987L private const val serialVersionUID = -293487987L
} }
} }
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun PlaybackState.toMediaItemsWithStartPosition(): MediaSession.MediaItemsWithStartPosition {
return MediaSession.MediaItemsWithStartPosition(
songs.map { it.toMediaItem() },
currentPlayingIndex,
currentPlayingPosition.toLong(),
)
}

View File

@ -24,6 +24,8 @@ import timber.log.Timber
* This class is responsible for the serialization / deserialization * This class is responsible for the serialization / deserialization
* of the playlist and the player state (e.g. current playing number and play position) * of the playlist and the player state (e.g. current playing number and play position)
* to the filesystem. * to the filesystem.
*
* TODO: Should use: MediaItemsWithStartPosition
*/ */
class PlaybackStateSerializer : KoinComponent { class PlaybackStateSerializer : KoinComponent {
@ -56,7 +58,7 @@ class PlaybackStateSerializer : KoinComponent {
} }
} }
fun serializeNow( private fun serializeNow(
tracks: Iterable<Track>, tracks: Iterable<Track>,
currentPlayingIndex: Int, currentPlayingIndex: Int,
currentPlayingPosition: Int, currentPlayingPosition: Int,
@ -85,7 +87,10 @@ class PlaybackStateSerializer : KoinComponent {
if (isDeserializing.get()) return if (isDeserializing.get()) return
ioScope.launch { ioScope.launch {
try { try {
deserializeNow(afterDeserialized) val state = deserializeNow()
mainScope.launch {
afterDeserialized(state)
}
isSetup.set(true) isSetup.set(true)
} catch (all: Exception) { } catch (all: Exception) {
Timber.e(all, "Had a problem deserializing:") Timber.e(all, "Had a problem deserializing:")
@ -95,11 +100,11 @@ class PlaybackStateSerializer : KoinComponent {
} }
} }
private fun deserializeNow(afterDeserialized: (PlaybackState?) -> Unit?) { fun deserializeNow(): PlaybackState? {
val state = FileUtil.deserialize<PlaybackState>( val state = FileUtil.deserialize<PlaybackState>(
context, Constants.FILENAME_PLAYLIST_SER context, Constants.FILENAME_PLAYLIST_SER
) ?: return ) ?: return null
Timber.i( Timber.i(
"Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b", "Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b",
@ -108,9 +113,7 @@ class PlaybackStateSerializer : KoinComponent {
state.shufflePlay state.shufflePlay
) )
mainScope.launch { return state
afterDeserialized(state)
}
} }
companion object { companion object {

View File

@ -51,7 +51,7 @@ import timber.log.Timber
*/ */
@Suppress("LargeClass") @Suppress("LargeClass")
open class RESTMusicService( open class RESTMusicService(
val subsonicAPIClient: SubsonicAPIClient, private val subsonicAPIClient: SubsonicAPIClient,
private val activeServerProvider: ActiveServerProvider private val activeServerProvider: ActiveServerProvider
) : MusicService { ) : MusicService {
@ -504,6 +504,11 @@ open class RESTMusicService(
builder.build() builder.build()
} }
override fun isJukeboxAvailable(): Boolean {
val username = activeServerProvider.getActiveServer().userName
return getUser(username).jukeboxRole
}
@Throws(Exception::class) @Throws(Exception::class)
override fun updateJukeboxPlaylist( override fun updateJukeboxPlaylist(
ids: List<String>? ids: List<String>?

View File

@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.moire.ultrasonic.data.RatingUpdate import org.moire.ultrasonic.data.RatingUpdate
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
class RxBus { class RxBus {
@ -32,9 +33,9 @@ class RxBus {
var activeServerChangingObservable: Observable<Int> = var activeServerChangingObservable: Observable<Int> =
activeServerChangingPublisher activeServerChangingPublisher
var activeServerChangedPublisher: PublishSubject<Int> = var activeServerChangedPublisher: PublishSubject<ServerSetting> =
PublishSubject.create() PublishSubject.create()
var activeServerChangedObservable: Observable<Int> = var activeServerChangedObservable: Observable<ServerSetting> =
activeServerChangedPublisher.observeOn(mainThread()) activeServerChangedPublisher.observeOn(mainThread())
val themeChangedEventPublisher: PublishSubject<Unit> = val themeChangedEventPublisher: PublishSubject<Unit> =

View File

@ -1,260 +0,0 @@
/*
* DownloadHandler.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.subsonic
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import java.util.LinkedList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.executeTaskWithToast
/**
* Retrieves a list of songs and adds them to the now playing list
*/
@Suppress("LongParameterList")
class DownloadHandler(
val mediaPlayerManager: MediaPlayerManager,
private val networkAndStorageChecker: NetworkAndStorageChecker
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val maxSongs = 500
fun justDownload(
action: DownloadAction,
fragment: Fragment,
id: String? = null,
name: String? = "",
isShare: Boolean = false,
isDirectory: Boolean = true,
isArtist: Boolean = false,
tracks: List<Track>? = null
) {
var successString: String? = null
// Launch the Job
executeTaskWithToast({
val tracksToDownload: List<Track> = tracks
?: getTracksFromServer(isArtist, id!!, isDirectory, name, isShare)
withContext(Dispatchers.Main) {
// If we are just downloading tracks we don't need to add them to the controller
when (action) {
DownloadAction.DOWNLOAD -> DownloadService.download(
tracksToDownload,
save = false,
updateSaveFlag = true
)
DownloadAction.PIN -> DownloadService.download(
tracksToDownload,
save = true,
updateSaveFlag = true
)
DownloadAction.UNPIN -> DownloadService.unpin(tracksToDownload)
DownloadAction.DELETE -> DownloadService.delete(tracksToDownload)
}
successString = when (action) {
DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString(
R.plurals.n_songs_to_be_downloaded,
tracksToDownload.size,
tracksToDownload.size
)
DownloadAction.UNPIN -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_unpinned,
tracksToDownload.size,
tracksToDownload.size
)
}
DownloadAction.PIN -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_pinned,
tracksToDownload.size,
tracksToDownload.size
)
}
DownloadAction.DELETE -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_deleted,
tracksToDownload.size,
tracksToDownload.size
)
}
}
}
}) { successString }
}
fun fetchTracksAndAddToController(
fragment: Fragment,
id: String,
name: String? = "",
isShare: Boolean = false,
isDirectory: Boolean = true,
insertionMode: MediaPlayerManager.InsertionMode,
autoPlay: Boolean,
shuffle: Boolean = false,
isArtist: Boolean = false
) {
var successString: String? = null
// Launch the Job
executeTaskWithToast({
val songs: MutableList<Track> =
getTracksFromServer(isArtist, id, isDirectory, name, isShare)
withContext(Dispatchers.Main) {
addTracksToMediaController(
songs = songs,
insertionMode = insertionMode,
autoPlay = autoPlay,
shuffle = shuffle,
playlistName = null,
fragment = fragment
)
// Play Now doesn't get a Toast :)
successString = when (insertionMode) {
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
fragment.resources.getQuantityString(
R.plurals.n_songs_added_after_current,
songs.size,
songs.size
)
MediaPlayerManager.InsertionMode.APPEND ->
fragment.resources.getQuantityString(
R.plurals.n_songs_added_to_end,
songs.size,
songs.size
)
else -> null
}
}
}) { successString }
}
fun addTracksToMediaController(
songs: List<Track>,
insertionMode: MediaPlayerManager.InsertionMode,
autoPlay: Boolean,
shuffle: Boolean = false,
playlistName: String? = null,
fragment: Fragment
) {
if (songs.isEmpty()) return
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
if (playlistName != null) {
mediaPlayerManager.suggestedPlaylistName = playlistName
}
mediaPlayerManager.addToPlaylist(
songs,
autoPlay,
shuffle,
insertionMode
)
if (Settings.shouldTransitionOnPlayback &&
insertionMode == MediaPlayerManager.InsertionMode.CLEAR
) {
fragment.findNavController().popBackStack(R.id.playerFragment, true)
fragment.findNavController().navigate(R.id.playerFragment)
}
}
private fun getTracksFromServer(
isArtist: Boolean,
id: String,
isDirectory: Boolean,
name: String?,
isShare: Boolean
): MutableList<Track> {
val musicService = getMusicService()
val songs: MutableList<Track> = LinkedList()
val root: MusicDirectory
if (shouldUseId3Tags() && isArtist) {
return getSongsForArtist(id)
} else {
if (isDirectory) {
root = if (shouldUseId3Tags())
musicService.getAlbumAsDir(id, name, false)
else
musicService.getMusicDirectory(id, name, false)
} else if (isShare) {
root = MusicDirectory()
val shares = musicService.getShares(true)
// Filter the received shares by the given id, and get their entries
val entries = shares.filter { it.id == id }.flatMap { it.getEntries() }
root.addAll(entries)
} else {
root = musicService.getPlaylist(id, name!!)
}
getSongsRecursively(root, songs)
}
return songs
}
@Suppress("DestructuringDeclarationWithTooManyEntries")
@Throws(Exception::class)
private fun getSongsRecursively(
parent: MusicDirectory,
songs: MutableList<Track>
) {
if (songs.size > maxSongs) {
return
}
for (song in parent.getTracks()) {
if (!song.isVideo) {
songs.add(song)
}
}
val musicService = getMusicService()
for ((id1, _, _, title) in parent.getAlbums()) {
val root: MusicDirectory = if (shouldUseId3Tags())
musicService.getAlbumAsDir(id1, title, false)
else
musicService.getMusicDirectory(id1, title, false)
getSongsRecursively(root, songs)
}
}
@Throws(Exception::class)
private fun getSongsForArtist(
id: String
): MutableList<Track> {
val songs: MutableList<Track> = LinkedList()
val musicService = getMusicService()
val artist = musicService.getAlbumsOfArtist(id, "", false)
for ((id1) in artist) {
val albumDirectory = musicService.getAlbumAsDir(
id1,
"",
false
)
for (song in albumDirectory.getTracks()) {
if (!song.isVideo) {
songs.add(song)
}
}
}
return songs
}
}
enum class DownloadAction {
DOWNLOAD, PIN, UNPIN, DELETE
}

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.subsonic package org.moire.ultrasonic.subsonic
import android.content.Context
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -20,8 +19,7 @@ import timber.log.Timber
/** /**
* Handles the lifetime of the Image Loader * Handles the lifetime of the Image Loader
*/ */
class class ImageLoaderProvider :
ImageLoaderProvider(val context: Context) :
KoinComponent, KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.IO) { CoroutineScope by CoroutineScope(Dispatchers.IO) {
private var imageLoader: ImageLoader? = null private var imageLoader: ImageLoader? = null

View File

@ -1,19 +1,19 @@
package org.moire.ultrasonic.subsonic package org.moire.ultrasonic.subsonic
import android.content.Context
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
/** /**
* Utility class for checking the availability of the network and storage * Utility class for checking the availability of the network and storage
*/ */
class NetworkAndStorageChecker(val context: Context) { class NetworkAndStorageChecker {
fun warnIfNetworkOrStorageUnavailable() { fun warnIfNetworkOrStorageUnavailable() {
if (!Util.isExternalStoragePresent()) { if (!Util.isExternalStoragePresent()) {
Util.toast(context, R.string.select_album_no_sdcard) Util.toast(R.string.select_album_no_sdcard, true, UApp.applicationContext())
} else if (!isOffline() && !Util.hasUsableNetwork()) { } else if (!isOffline() && !Util.hasUsableNetwork()) {
Util.toast(context, R.string.select_album_no_network) Util.toast(R.string.select_album_no_network, true, UApp.applicationContext())
} }
} }
} }

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.subsonic package org.moire.ultrasonic.subsonic
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -17,27 +16,27 @@ import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.lifecycle.lifecycleScope
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.collections.ArrayList import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShareDetails import org.moire.ultrasonic.util.ShareDetails
import org.moire.ultrasonic.util.TimeSpanPicker import org.moire.ultrasonic.util.TimeSpanPicker
import org.moire.ultrasonic.util.Util.getString
import org.moire.ultrasonic.util.Util.ifNotNull import org.moire.ultrasonic.util.Util.ifNotNull
/** /**
* This class handles sharing items in the media library * This class handles sharing items in the media library
*/ */
class ShareHandler(val context: Context) { class ShareHandler {
private var shareDescription: EditText? = null private var shareDescription: EditText? = null
private var timeSpanPicker: TimeSpanPicker? = null private var timeSpanPicker: TimeSpanPicker? = null
private var shareOnServerCheckBox: CheckBox? = null private var shareOnServerCheckBox: CheckBox? = null
@ -51,21 +50,26 @@ class ShareHandler(val context: Context) {
fun share( fun share(
fragment: Fragment, fragment: Fragment,
shareDetails: ShareDetails, shareDetails: ShareDetails,
swipe: SwipeRefreshLayout?,
cancellationToken: CancellationToken,
additionalId: String? additionalId: String?
) { ) {
val task: BackgroundTask<Share?> = object : FragmentBackgroundTask<Share?>( val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
fragment.requireActivity(), scope.launch {
true, val share = createShareOnServer(shareDetails, additionalId)
swipe, startActivityForShare(share, shareDetails, fragment)
cancellationToken }
) { }
@Throws(Throwable::class)
override fun doInBackground(): Share? { private suspend fun createShareOnServer(
shareDetails: ShareDetails,
additionalId: String?
): Share? {
return withContext(Dispatchers.IO) {
return@withContext try {
val ids: MutableList<String> = ArrayList() val ids: MutableList<String> = ArrayList()
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1)
return@withContext null
if (shareDetails.Entries.isEmpty()) { if (shareDetails.Entries.isEmpty()) {
additionalId.ifNotNull { additionalId.ifNotNull {
ids.add(it) ids.add(it)
@ -86,78 +90,80 @@ class ShareHandler(val context: Context) {
val shares = val shares =
musicService.createShare(ids, shareDetails.Description, timeInMillis) musicService.createShare(ids, shareDetails.Description, timeInMillis)
return shares[0] // Return the share
} shares[0]
} catch (ignored: Exception) {
override fun done(result: Share?) { null
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
if (result != null) {
// Created a share, send the URL
intent.putExtra(
Intent.EXTRA_TEXT,
String.format(
Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url
)
)
} else {
// Sending only text details
val textBuilder = StringBuilder()
textBuilder.appendLine(Settings.shareGreeting)
if (!shareDetails.Entries[0].title.isNullOrEmpty())
textBuilder.append(context.resources.getString(R.string.common_title))
.append(": ").appendLine(shareDetails.Entries[0].title)
if (!shareDetails.Entries[0].artist.isNullOrEmpty())
textBuilder.append(context.resources.getString(R.string.common_artist))
.append(": ").appendLine(shareDetails.Entries[0].artist)
if (!shareDetails.Entries[0].album.isNullOrEmpty())
textBuilder.append(context.resources.getString(R.string.common_album))
.append(": ").append(shareDetails.Entries[0].album)
intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString())
}
fragment.activity?.startActivity(
Intent.createChooser(
intent,
context.resources.getString(R.string.share_via)
)
)
} }
} }
task.execute() }
private suspend fun startActivityForShare(
result: Share?,
shareDetails: ShareDetails,
fragment: Fragment
) {
return withContext(Dispatchers.Main) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
if (result != null) {
// Created a share, send the URL
intent.putExtra(
Intent.EXTRA_TEXT,
String.format(
Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url
)
)
} else {
// Sending only text details
val textBuilder = StringBuilder()
textBuilder.appendLine(Settings.shareGreeting)
if (!shareDetails.Entries[0].title.isNullOrEmpty())
textBuilder.append(getString(R.string.common_title))
.append(": ").appendLine(shareDetails.Entries[0].title)
if (!shareDetails.Entries[0].artist.isNullOrEmpty())
textBuilder.append(getString(R.string.common_artist))
.append(": ").appendLine(shareDetails.Entries[0].artist)
if (!shareDetails.Entries[0].album.isNullOrEmpty())
textBuilder.append(getString(R.string.common_album))
.append(": ").append(shareDetails.Entries[0].album)
intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString())
}
fragment.activity?.startActivity(
Intent.createChooser(
intent,
getString(R.string.share_via)
)
)
}
} }
fun createShare( fun createShare(
fragment: Fragment, fragment: Fragment,
tracks: List<Track?>?, tracks: List<Track?>?,
swipe: SwipeRefreshLayout?,
cancellationToken: CancellationToken,
additionalId: String? = null additionalId: String? = null
) { ) {
val askForDetails = Settings.shouldAskForShareDetails val askForDetails = Settings.shouldAskForShareDetails
val shareDetails = ShareDetails() val shareDetails = ShareDetails()
shareDetails.Entries = tracks shareDetails.Entries = tracks
if (askForDetails) { if (askForDetails) {
showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId) showDialog(fragment, shareDetails, additionalId)
} else { } else {
shareDetails.Description = Settings.defaultShareDescription shareDetails.Description = Settings.defaultShareDescription
shareDetails.Expiration = System.currentTimeMillis() + shareDetails.Expiration = System.currentTimeMillis() +
Settings.defaultShareExpirationInMillis Settings.defaultShareExpirationInMillis
share(fragment, shareDetails, swipe, cancellationToken, additionalId) share(fragment, shareDetails, additionalId)
} }
} }
@Suppress("LongMethod")
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun showDialog( private fun showDialog(
fragment: Fragment, fragment: Fragment,
shareDetails: ShareDetails, shareDetails: ShareDetails,
swipe: SwipeRefreshLayout?,
cancellationToken: CancellationToken,
additionalId: String? additionalId: String?
) { ) {
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null) val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
@ -175,18 +181,57 @@ class ShareHandler(val context: Context) {
textViewExpiration = layout.findViewById<View>(R.id.textViewExpiration) as TextView textViewExpiration = layout.findViewById<View>(R.id.textViewExpiration) as TextView
} }
// Handle the visibility based on shareDetails.Entries size
if (shareDetails.Entries.size == 1) { if (shareDetails.Entries.size == 1) {
// For single songs the sharing may be done by text only
shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ -> shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ ->
updateVisibility() updateVisibility()
} }
shareOnServerCheckBox?.isChecked = Settings.shareOnServer shareOnServerCheckBox?.isChecked = Settings.shareOnServer
} else { } else {
shareOnServerCheckBox?.isVisible = false shareOnServerCheckBox?.isVisible = false
} }
updateVisibility() updateVisibility()
// Set up the dialog builder
val builder = makeDialogBuilder(fragment, shareDetails, additionalId, layout)
// Initialize UI components with default values
setupDefaultValues()
builder.create()
builder.show()
}
private fun setupDefaultValues() {
val defaultDescription = Settings.defaultShareDescription
val timeSpan = Settings.defaultShareExpiration
val split = pattern.split(timeSpan)
if (split.size == 2) {
val timeSpanAmount = split[0].toInt()
val timeSpanType = split[1]
if (timeSpanAmount > 0) {
noExpirationCheckBox!!.isChecked = false
timeSpanPicker!!.isEnabled = true
timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString())
timeSpanPicker!!.timeSpanType = timeSpanType
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
shareDescription!!.setText(defaultDescription)
}
private fun makeDialogBuilder(
fragment: Fragment,
shareDetails: ShareDetails,
additionalId: String?,
layout: View?
): ConfirmationDialog.Builder {
val builder = ConfirmationDialog.Builder(fragment.requireContext()) val builder = ConfirmationDialog.Builder(fragment.requireContext())
builder.setTitle(R.string.share_set_share_options) builder.setTitle(R.string.share_set_share_options)
@ -214,7 +259,7 @@ class ShareHandler(val context: Context) {
Settings.shareOnServer = shareDetails.ShareOnServer Settings.shareOnServer = shareDetails.ShareOnServer
} }
share(fragment, shareDetails, swipe, cancellationToken, additionalId) share(fragment, shareDetails, additionalId)
} }
builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
@ -224,35 +269,12 @@ class ShareHandler(val context: Context) {
builder.setView(layout) builder.setView(layout)
builder.setCancelable(true) builder.setCancelable(true)
timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration)) // Set up the timeSpanPicker
timeSpanPicker!!.setTimeSpanDisableText(getString(R.string.no_expiration))
noExpirationCheckBox!!.setOnCheckedChangeListener { _, b -> noExpirationCheckBox!!.setOnCheckedChangeListener { _, b ->
timeSpanPicker!!.isEnabled = !b timeSpanPicker!!.isEnabled = !b
} }
return builder
val defaultDescription = Settings.defaultShareDescription
val timeSpan = Settings.defaultShareExpiration
val split = pattern.split(timeSpan)
if (split.size == 2) {
val timeSpanAmount = split[0].toInt()
val timeSpanType = split[1]
if (timeSpanAmount > 0) {
noExpirationCheckBox!!.isChecked = false
timeSpanPicker!!.isEnabled = true
timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString())
timeSpanPicker!!.timeSpanType = timeSpanType
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
} else {
noExpirationCheckBox!!.isChecked = true
timeSpanPicker!!.isEnabled = false
}
shareDescription!!.setText(defaultDescription)
builder.create()
builder.show()
} }
private fun updateVisibility() { private fun updateVisibility() {

View File

@ -16,7 +16,7 @@ class VideoPlayer {
companion object { companion object {
fun playVideo(context: Context, track: Track?) { fun playVideo(context: Context, track: Track?) {
if (!Util.hasUsableNetwork() || track == null) { if (!Util.hasUsableNetwork() || track == null) {
Util.toast(context, R.string.select_album_no_network) Util.toast(R.string.select_album_no_network, true, context)
return return
} }
try { try {
@ -32,7 +32,7 @@ class VideoPlayer {
) )
context.startActivity(intent) context.startActivity(intent)
} catch (all: Exception) { } catch (all: Exception) {
Util.toast(context, all.toString(), false) Util.toast(all.toString(), false, context)
} }
} }
} }

View File

@ -11,7 +11,6 @@ import android.system.Os
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.guava.future
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -19,7 +18,7 @@ import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getCompleteFile
import org.moire.ultrasonic.util.FileUtil.getPartialFile import org.moire.ultrasonic.util.FileUtil.getPartialFile
@ -38,7 +37,6 @@ import timber.log.Timber
*/ */
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent { class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent {
private var mainScope = CoroutineScope(Dispatchers.Main)
private val activeServerProvider by inject<ActiveServerProvider>() private val activeServerProvider by inject<ActiveServerProvider>()
private fun exceptionHandler(tag: String): CoroutineExceptionHandler { private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
@ -235,16 +233,14 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
private fun findFilesToNotDelete(): Set<String> { private fun findFilesToNotDelete(): Set<String> {
val filesToNotDelete: MutableSet<String> = HashSet(5) val filesToNotDelete: MutableSet<String> = HashSet(5)
val mediaPlayerManager: MediaPlayerManager by inject()
val playlist = mainScope.future { mediaPlayerManager.playlist }.get() // We just take the last published playlist from RX
for (item in playlist) { val playlist = RxBus.playlistObservable.blockingLast()
val track = item.toTrack() for (track in playlist) {
filesToNotDelete.add(track.getPartialFile()) filesToNotDelete.add(track.getPartialFile())
filesToNotDelete.add(track.getCompleteFile()) filesToNotDelete.add(track.getCompleteFile())
filesToNotDelete.add(track.getPinnedFile()) filesToNotDelete.add(track.getPinnedFile())
} }
filesToNotDelete.add(musicDirectory.path) filesToNotDelete.add(musicDirectory.path)
return filesToNotDelete return filesToNotDelete
} }

View File

@ -37,4 +37,5 @@ object Constants {
const val FILENAME_PLAYLIST_SER = "downloadstate.ser" const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
const val ALBUM_ART_FILE = "folder.jpeg" const val ALBUM_ART_FILE = "folder.jpeg"
const val RESULT_CLOSE_ALL = 1337 const val RESULT_CLOSE_ALL = 1337
const val MAX_SONGS_RECURSIVE = 500
} }

View File

@ -0,0 +1,141 @@
/*
* ContextMenuUtil.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.view.MenuItem
import androidx.fragment.app.Fragment
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.subsonic.ShareHandler
object ContextMenuUtil : KoinComponent {
/*
* Callback for menu items of collections (albums, artists etc)
*/
fun handleContextMenu(
menuItem: MenuItem,
item: Identifiable,
isArtist: Boolean,
mediaPlayerManager: MediaPlayerManager,
fragment: Fragment
): Boolean {
when (menuItem.itemId) {
R.id.menu_play_now ->
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
id = item.id,
isArtist = isArtist
)
R.id.menu_play_next ->
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
id = item.id,
isArtist = isArtist
)
R.id.menu_play_last ->
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
id = item.id,
isArtist = isArtist
)
R.id.menu_pin ->
DownloadUtil.justDownload(
action = DownloadAction.PIN,
fragment = fragment,
id = item.id,
isArtist = isArtist
)
R.id.menu_unpin ->
DownloadUtil.justDownload(
action = DownloadAction.UNPIN,
fragment = fragment,
id = item.id,
isArtist = isArtist
)
R.id.menu_download ->
DownloadUtil.justDownload(
action = DownloadAction.DOWNLOAD,
fragment = fragment,
id = item.id,
isArtist = isArtist
)
else -> return false
}
return true
}
fun handleContextMenuTracks(
menuItem: MenuItem,
tracks: List<Track>,
mediaPlayerManager: MediaPlayerManager,
fragment: Fragment
): Boolean {
when (menuItem.itemId) {
R.id.song_menu_play_now -> {
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
tracks = tracks
)
}
R.id.song_menu_play_next -> {
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
tracks = tracks
)
}
R.id.song_menu_play_last -> {
mediaPlayerManager.playTracksAndToast(
fragment = fragment,
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
tracks = tracks
)
}
R.id.song_menu_pin -> {
DownloadUtil.justDownload(
action = DownloadAction.PIN,
fragment = fragment,
tracks = tracks
)
}
R.id.song_menu_unpin -> {
DownloadUtil.justDownload(
action = DownloadAction.UNPIN,
fragment = fragment,
tracks = tracks
)
}
R.id.song_menu_download -> {
DownloadUtil.justDownload(
action = DownloadAction.DOWNLOAD,
fragment = fragment,
tracks = tracks
)
}
R.id.song_menu_share -> {
val shareHandler: ShareHandler by inject()
shareHandler.createShare(
fragment = fragment,
tracks = tracks,
additionalId = null
)
}
else -> return false
}
return true
}
}

View File

@ -9,14 +9,12 @@ package org.moire.ultrasonic.util
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import timber.log.Timber import timber.log.Timber
@ -30,53 +28,50 @@ object CoroutinePatterns {
} }
} }
fun CoroutineScope.executeTaskWithToast( fun CoroutineScope.launchWithToast(
task: suspend CoroutineScope.() -> Unit, block: suspend CoroutineScope.() -> String?
successString: () -> String? ) {
): Job {
// Launch the Job // Launch the Job
val job = launch(CoroutinePatterns.loggingExceptionHandler, block = task) val deferred = async(CoroutinePatterns.loggingExceptionHandler, block = block)
// Setup a handler when the job is done // Setup a handler when the job is done
job.invokeOnCompletion { deferred.invokeOnCompletion {
val toastString = if (it != null && it !is CancellationException) { val toastString = if (it != null && it !is CancellationException) {
CommunicationError.getErrorMessage(it) CommunicationError.getErrorMessage(it)
} else { } else {
successString() null
} }
// Return early if nothing to post
if (toastString == null) return@invokeOnCompletion
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
Util.toast(UApp.applicationContext(), toastString) val successString = toastString ?: deferred.await()
} if (successString != null) {
} Util.toast(successString, UApp.applicationContext())
}
return job
}
fun CoroutineScope.executeTaskWithModalDialog(
fragment: Fragment,
task: suspend CoroutineScope.() -> Unit,
successString: () -> String
) {
// Create the job
val job = executeTaskWithToast(task, successString)
// Create the dialog
val builder = InfoDialog.Builder(fragment.requireContext())
builder.setTitle(R.string.background_task_wait)
builder.setMessage(R.string.background_task_loading)
builder.setOnCancelListener { job.cancel() }
builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() }
val dialog = builder.create()
dialog.show()
// Add additional handler to close the dialog
job.invokeOnCompletion {
launch(Dispatchers.Main) {
dialog.dismiss()
} }
} }
} }
// Unused, kept commented for eventual later use
// fun CoroutineScope.executeTaskWithModalDialog(
// fragment: Fragment,
// task: suspend CoroutineScope.() -> String?
// ) {
// // Create the job
// val job = launchWithToast(task)
//
// // Create the dialog
// val builder = InfoDialog.Builder(fragment.requireContext())
// builder.setTitle(R.string.background_task_wait)
// builder.setMessage(R.string.background_task_loading)
// builder.setOnCancelListener { job.cancel() }
// builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() }
// val dialog = builder.create()
// dialog.show()
//
// // Add additional handler to close the dialog
// job.invokeOnCompletion {
// launch(Dispatchers.Main) {
// dialog.dismiss()
// }
// }
// }

View File

@ -0,0 +1,198 @@
/*
* DownloadUtil.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.MusicServiceFactory
/**
* Retrieves a list of songs and adds them to the now playing list
*/
@Suppress("LongParameterList")
object DownloadUtil {
fun justDownload(
action: DownloadAction,
fragment: Fragment,
id: String? = null,
name: String? = "",
isShare: Boolean = false,
isDirectory: Boolean = true,
isArtist: Boolean = false,
tracks: List<Track>? = null
) {
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
// Launch the Job
scope.launchWithToast {
val tracksToDownload: List<Track> = tracks
?: getTracksFromServerAsync(isArtist, id!!, isDirectory, name, isShare)
// If we are just downloading tracks we don't need to add them to the controller
when (action) {
DownloadAction.DOWNLOAD -> DownloadService.downloadAsync(
tracksToDownload,
save = false,
updateSaveFlag = true
)
DownloadAction.PIN -> DownloadService.downloadAsync(
tracksToDownload,
save = true,
updateSaveFlag = true
)
DownloadAction.UNPIN -> DownloadService.unpinAsync(tracksToDownload)
DownloadAction.DELETE -> DownloadService.deleteAsync(tracksToDownload)
}
// Return the string which should be displayed
getToastString(action, fragment, tracksToDownload)
}
}
suspend fun getTracksFromServerAsync(
isArtist: Boolean,
id: String,
isDirectory: Boolean,
name: String?,
isShare: Boolean
): MutableList<Track> {
return withContext(Dispatchers.IO) {
getTracksFromServer(isArtist, id, isDirectory, name, isShare)
}
}
fun getTracksFromServer(
isArtist: Boolean,
id: String,
isDirectory: Boolean,
name: String?,
isShare: Boolean
): MutableList<Track> {
val musicService = MusicServiceFactory.getMusicService()
val songs: MutableList<Track> = LinkedList()
val root: MusicDirectory
if (ActiveServerProvider.shouldUseId3Tags() && isArtist) {
return getSongsForArtist(id)
} else {
if (isDirectory) {
root = if (ActiveServerProvider.shouldUseId3Tags())
musicService.getAlbumAsDir(id, name, false)
else
musicService.getMusicDirectory(id, name, false)
} else if (isShare) {
root = MusicDirectory()
val shares = musicService.getShares(true)
// Filter the received shares by the given id, and get their entries
val entries = shares.filter { it.id == id }.flatMap { it.getEntries() }
root.addAll(entries)
} else {
root = musicService.getPlaylist(id, name!!)
}
getSongsRecursively(root, songs)
}
return songs
}
@Suppress("DestructuringDeclarationWithTooManyEntries")
@Throws(Exception::class)
private fun getSongsRecursively(
parent: MusicDirectory,
songs: MutableList<Track>
) {
if (songs.size > Constants.MAX_SONGS_RECURSIVE) {
return
}
for (song in parent.getTracks()) {
if (!song.isVideo) {
songs.add(song)
}
}
val musicService = MusicServiceFactory.getMusicService()
for ((id1, _, _, title) in parent.getAlbums()) {
val root: MusicDirectory = if (ActiveServerProvider.shouldUseId3Tags())
musicService.getAlbumAsDir(id1, title, false)
else
musicService.getMusicDirectory(id1, title, false)
getSongsRecursively(root, songs)
}
}
@Throws(Exception::class)
private fun getSongsForArtist(
id: String
): MutableList<Track> {
val songs: MutableList<Track> = LinkedList()
val musicService = MusicServiceFactory.getMusicService()
val artist = musicService.getAlbumsOfArtist(id, "", false)
for ((id1) in artist) {
val albumDirectory = musicService.getAlbumAsDir(
id1,
"",
false
)
for (song in albumDirectory.getTracks()) {
if (!song.isVideo) {
songs.add(song)
}
}
}
return songs
}
private fun getToastString(
action: DownloadAction,
fragment: Fragment,
tracksToDownload: List<Track>
): String {
return when (action) {
DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString(
R.plurals.n_songs_to_be_downloaded,
tracksToDownload.size,
tracksToDownload.size
)
DownloadAction.UNPIN -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_unpinned,
tracksToDownload.size,
tracksToDownload.size
)
}
DownloadAction.PIN -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_pinned,
tracksToDownload.size,
tracksToDownload.size
)
}
DownloadAction.DELETE -> {
fragment.resources.getQuantityString(
R.plurals.n_songs_deleted,
tracksToDownload.size,
tracksToDownload.size
)
}
}
}
}
enum class DownloadAction {
DOWNLOAD, PIN, UNPIN, DELETE
}

View File

@ -136,10 +136,6 @@ object Settings {
val seekIntervalMillis: Long val seekIntervalMillis: Long
get() = (seekInterval / 1000).toLong() get() = (seekInterval / 1000).toLong()
@JvmStatic
var mediaButtonsEnabled
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)
var resumePlayOnHeadphonePlug var resumePlayOnHeadphonePlug
by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true) by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true)

View File

@ -41,7 +41,7 @@ object Storage {
if (rootNotFoundError) { if (rootNotFoundError) {
Settings.customCacheLocation = false Settings.customCacheLocation = false
Settings.cacheLocationUri = "" Settings.cacheLocationUri = ""
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error) Util.toast(R.string.settings_cache_location_error, true, UApp.applicationContext())
} }
} }

View File

@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
AdapterView.OnItemSelectedListener { AdapterView.OnItemSelectedListener {
private val timeSpanEditText: EditText private val timeSpanEditText: EditText
private val timeSpanSpinner: Spinner private val timeSpanSpinner: Spinner
private val timeSpanDisableCheckbox: CheckBox val timeSpanDisableCheckbox: CheckBox
private var mTimeSpan: Long = -1L private var mTimeSpan: Long = -1L
private val adapter: ArrayAdapter<CharSequence> private val adapter: ArrayAdapter<CharSequence>
private val dialog: View private val dialog: View

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.Manifest.permission.POST_NOTIFICATIONS import android.Manifest.permission.POST_NOTIFICATIONS
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@ -37,12 +36,15 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import androidx.annotation.StringRes
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline import androidx.media3.common.Timeline
import androidx.navigation.fragment.findNavController
import java.io.Closeable import java.io.Closeable
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
@ -107,33 +109,41 @@ object Util {
context.getString(R.string.setting_key_theme_dark) -> { context.getString(R.string.setting_key_theme_dark) -> {
R.style.UltrasonicTheme_Dark R.style.UltrasonicTheme_Dark
} }
context.getString(R.string.setting_key_theme_black) -> { context.getString(R.string.setting_key_theme_black) -> {
R.style.UltrasonicTheme_Black R.style.UltrasonicTheme_Black
} }
context.getString(R.string.setting_key_theme_light) -> { context.getString(R.string.setting_key_theme_light) -> {
R.style.UltrasonicTheme_Light R.style.UltrasonicTheme_Light
} }
else -> { else -> {
R.style.UltrasonicTheme_DayNight R.style.UltrasonicTheme_DayNight
} }
} }
} }
fun getString(@StringRes resId: Int): String {
return applicationContext().resources.getString(resId)
}
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) { fun toast(messageId: Int, shortDuration: Boolean = true, context: Context?) {
toast(context, context!!.getString(messageId), shortDuration) toast(applicationContext().getString(messageId), shortDuration, context)
} }
@JvmStatic @JvmStatic
fun toast(context: Context?, message: CharSequence?) { fun toast(message: CharSequence, context: Context?) {
toast(context, message, true) toast(message, true, context)
} }
@JvmStatic @JvmStatic
@SuppressLint("ShowToast") // Invalid warning // Toast needs a real context or it will throw a IllegalAccessException
fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) { // We wrap it in a try-catch block, because if called after doing
// If called after doing some background processing, our context might have expired! // some background processing, our context might have expired!
fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) {
try { try {
if (toast == null) { if (toast == null) {
toast = Toast.makeText( toast = Toast.makeText(
@ -153,6 +163,22 @@ object Util {
} }
} }
fun Fragment.toast(message: CharSequence, shortDuration: Boolean = true) {
toast(
message,
shortDuration = shortDuration,
context = this.context
)
}
fun Fragment.toast(messageId: Int = 0, shortDuration: Boolean = true) {
toast(
messageId = messageId,
shortDuration = shortDuration,
context = this.context
)
}
/** /**
* Converts a byte-count to a formatted string suitable for display to the user. * Converts a byte-count to a formatted string suitable for display to the user.
* For instance: * For instance:
@ -479,7 +505,7 @@ object Util {
@JvmStatic @JvmStatic
fun isNullOrWhiteSpace(string: String?): Boolean { fun isNullOrWhiteSpace(string: String?): Boolean {
return string == null || string.isEmpty() || string.trim { it <= ' ' }.isEmpty() return string.isNullOrEmpty() || string.trim { it <= ' ' }.isEmpty()
} }
@JvmOverloads @JvmOverloads
@ -504,18 +530,22 @@ object Util {
seconds seconds
) )
} }
hours > 0 -> { hours > 0 -> {
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds) String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
} }
minutes >= DEGRADE_PRECISION_AFTER -> { minutes >= DEGRADE_PRECISION_AFTER -> {
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
} }
minutes > 0 -> String.format( minutes > 0 -> String.format(
Locale.getDefault(), Locale.getDefault(),
"%d:%02d", "%d:%02d",
minutes, minutes,
seconds seconds
) )
else -> String.format(Locale.getDefault(), "0:%02d", seconds) else -> String.format(Locale.getDefault(), "0:%02d", seconds)
} }
} }
@ -557,7 +587,7 @@ object Util {
val requestPermissionLauncher = val requestPermissionLauncher =
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) { fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (!it) { if (!it) {
toast(applicationContext(), R.string.notification_permission_required) toast(R.string.notification_permission_required, context = fragment)
} }
} }
@ -569,6 +599,7 @@ object Util {
} }
} }
} }
fun postNotificationIfPermitted( fun postNotificationIfPermitted(
notificationManagerCompat: NotificationManagerCompat, notificationManagerCompat: NotificationManagerCompat,
id: Int, id: Int,
@ -583,8 +614,8 @@ object Util {
notificationManagerCompat.notify(id, notification) notificationManagerCompat.notify(id, notification)
} }
} }
@JvmStatic @JvmStatic
@Suppress("DEPRECATION")
fun getVersionName(context: Context): String? { fun getVersionName(context: Context): String? {
var versionName: String? = null var versionName: String? = null
val pm = context.packageManager val pm = context.packageManager
@ -838,4 +869,11 @@ object Util {
Timber.d("${it.key}: ${it.value}") Timber.d("${it.key}: ${it.value}")
} }
} }
fun Fragment.navigateToCurrent() {
if (Settings.shouldTransitionOnPlayback) {
findNavController().popBackStack(R.id.playerFragment, true)
findNavController().navigate(R.id.playerFragment)
}
}
} }

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Načítám&#8230;</string>
<string name="background_task.network_error">Chyba sítě. Ověřte adresu serveru nebo zkuste později.</string> <string name="background_task.network_error">Chyba sítě. Ověřte adresu serveru nebo zkuste později.</string>
<string name="background_task.unsupported_api">API serveru v%1$s nepodporuje tuto funkci.</string> <string name="background_task.unsupported_api">API serveru v%1$s nepodporuje tuto funkci.</string>
<string name="background_task.no_network">Tento program vyžaduje síťové připojení. Zapněte Wi-Fi nebo mobilní připojení.</string> <string name="background_task.no_network">Tento program vyžaduje síťové připojení. Zapněte Wi-Fi nebo mobilní připojení.</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Nesrozumitelná odpověď. Ověřte adresu serveru.</string> <string name="background_task.parse_error">Nesrozumitelná odpověď. Ověřte adresu serveru.</string>
<string name="background_task.ssl_cert_error">Chyba HTTPS certifikátu: %1$s.</string> <string name="background_task.ssl_cert_error">Chyba HTTPS certifikátu: %1$s.</string>
<string name="background_task.ssl_error">Vyjímka SSL připojení. Ověřte certifikát serveru.</string> <string name="background_task.ssl_error">Vyjímka SSL připojení. Ověřte certifikát serveru.</string>
<string name="background_task.wait">Chvilku strpení&#8230;</string>
<string name="button_bar.bookmarks">Záložky</string> <string name="button_bar.bookmarks">Záložky</string>
<string name="button_bar.browse">Knihovna médií</string> <string name="button_bar.browse">Knihovna médií</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -188,8 +186,6 @@
<string name="settings.max_bitrate_unlimited">Neomezené</string> <string name="settings.max_bitrate_unlimited">Neomezené</string>
<string name="settings.max_bitrate_wifi">Max Bitrate - wi-fi</string> <string name="settings.max_bitrate_wifi">Max Bitrate - wi-fi</string>
<string name="settings.max_songs">Maximum skladeb</string> <string name="settings.max_songs">Maximum skladeb</string>
<string name="settings.media_button_summary">Odpovídat na tlačítka ovládání médií telefonu, sluchátek a bluetooth</string>
<string name="settings.media_button_title">Tlačítka médií</string>
<string name="settings.network_timeout">Čas vypršení připojení</string> <string name="settings.network_timeout">Čas vypršení připojení</string>
<string name="settings.network_timeout_105000">105 sekund</string> <string name="settings.network_timeout_105000">105 sekund</string>
<string name="settings.network_timeout_120000">120 sekund</string> <string name="settings.network_timeout_120000">120 sekund</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Lade…</string>
<string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen.</string> <string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen.</string>
<string name="background_task.unsupported_api">Server API v%1$s unterstützt diese Funktion nicht.</string> <string name="background_task.unsupported_api">Server API v%1$s unterstützt diese Funktion nicht.</string>
<string name="background_task.no_network">Dieses Programm benötigt eine Netzwerkverbindung. Bitte das WLAN oder Mobilfunk einschalten.</string> <string name="background_task.no_network">Dieses Programm benötigt eine Netzwerkverbindung. Bitte das WLAN oder Mobilfunk einschalten.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">Antwort nicht verstanden. Bitte die Serveradresse überprüfen.</string> <string name="background_task.parse_error">Antwort nicht verstanden. Bitte die Serveradresse überprüfen.</string>
<string name="background_task.ssl_cert_error">HTTPS Zertifikatsfehler: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS Zertifikatsfehler: %1$s.</string>
<string name="background_task.ssl_error">SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen.</string> <string name="background_task.ssl_error">SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen.</string>
<string name="background_task.wait">Bitte warten…</string>
<string name="button_bar.bookmarks">Lesezeichen</string> <string name="button_bar.bookmarks">Lesezeichen</string>
<string name="button_bar.browse">Medienbibliothek</string> <string name="button_bar.browse">Medienbibliothek</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -230,8 +228,6 @@
<string name="settings.max_bitrate_unlimited">Unbegrenzt</string> <string name="settings.max_bitrate_unlimited">Unbegrenzt</string>
<string name="settings.max_bitrate_wifi">Max. Bitrate - WLAN</string> <string name="settings.max_bitrate_wifi">Max. Bitrate - WLAN</string>
<string name="settings.max_songs">Max. Anzahl der Titel</string> <string name="settings.max_songs">Max. Anzahl der Titel</string>
<string name="settings.media_button_summary">Auf Telefon, Headset und Bluetooth-Media-Tasten reagieren</string>
<string name="settings.media_button_title">Medien Tasten</string>
<string name="settings.network_timeout">Netzwerk Zeitüberschreitung</string> <string name="settings.network_timeout">Netzwerk Zeitüberschreitung</string>
<string name="settings.network_timeout_105000">105 Sekunden</string> <string name="settings.network_timeout_105000">105 Sekunden</string>
<string name="settings.network_timeout_120000">120 Sekunden</string> <string name="settings.network_timeout_120000">120 Sekunden</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Cargando…</string>
<string name="background_task.network_error">Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde.</string> <string name="background_task.network_error">Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde.</string>
<string name="background_task.unsupported_api">La API del servidor v%1$s no admite esta función.</string> <string name="background_task.unsupported_api">La API del servidor v%1$s no admite esta función.</string>
<string name="background_task.no_network">Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil.</string> <string name="background_task.no_network">Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">No se entiende la respuesta. Por favor comprueba la dirección del servidor.</string> <string name="background_task.parse_error">No se entiende la respuesta. Por favor comprueba la dirección del servidor.</string>
<string name="background_task.ssl_cert_error">Error del certificado HTTPS: %1$s.</string> <string name="background_task.ssl_cert_error">Error del certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Excepción de conexión SSL. Compruebe el certificado del servidor.</string> <string name="background_task.ssl_error">Excepción de conexión SSL. Compruebe el certificado del servidor.</string>
<string name="background_task.wait">Por favor espera…</string>
<string name="button_bar.bookmarks">Marcadores</string> <string name="button_bar.bookmarks">Marcadores</string>
<string name="button_bar.browse">Biblioteca</string> <string name="button_bar.browse">Biblioteca</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -232,8 +230,6 @@
<string name="settings.max_bitrate_unlimited">Ilimitado</string> <string name="settings.max_bitrate_unlimited">Ilimitado</string>
<string name="settings.max_bitrate_wifi">Bitrate máximo - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Bitrate máximo - Wi-Fi</string>
<string name="settings.max_songs">Máximo de Canciones</string> <string name="settings.max_songs">Máximo de Canciones</string>
<string name="settings.media_button_summary">Responder a los botones multimedia del dispositivo, auriculares y Bluetooth</string>
<string name="settings.media_button_title">Botones multimedia</string>
<string name="settings.network_timeout">Tiempo de espera de la red</string> <string name="settings.network_timeout">Tiempo de espera de la red</string>
<string name="settings.network_timeout_105000">105 segundos</string> <string name="settings.network_timeout_105000">105 segundos</string>
<string name="settings.network_timeout_120000">120 segundos</string> <string name="settings.network_timeout_120000">120 segundos</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Chargement…</string>
<string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string> <string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string>
<string name="background_task.unsupported_api">LAPI v%1$s du serveur ne supporte pas cette fonction.</string> <string name="background_task.unsupported_api">LAPI v%1$s du serveur ne supporte pas cette fonction.</string>
<string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string> <string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">Réponse incorrecte. Veuillez vérifier l\'adresse du serveur.</string> <string name="background_task.parse_error">Réponse incorrecte. Veuillez vérifier l\'adresse du serveur.</string>
<string name="background_task.ssl_cert_error">Erreur de certificat HTTPS : %1$s.</string> <string name="background_task.ssl_cert_error">Erreur de certificat HTTPS : %1$s.</string>
<string name="background_task.ssl_error">Erreur de connexion SSL. Veuillez vérifier le certificat du serveur.</string> <string name="background_task.ssl_error">Erreur de connexion SSL. Veuillez vérifier le certificat du serveur.</string>
<string name="background_task.wait">Veuillez patienter…</string>
<string name="button_bar.bookmarks">Signets</string> <string name="button_bar.bookmarks">Signets</string>
<string name="button_bar.browse">Bibliothèque musicale</string> <string name="button_bar.browse">Bibliothèque musicale</string>
<string name="button_bar.chat">Salon de discussion</string> <string name="button_bar.chat">Salon de discussion</string>
@ -225,8 +223,6 @@
<string name="settings.max_bitrate_unlimited">Illimité</string> <string name="settings.max_bitrate_unlimited">Illimité</string>
<string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string>
<string name="settings.max_songs">Titres maximum</string> <string name="settings.max_songs">Titres maximum</string>
<string name="settings.media_button_summary">Répondre au boutons média de l\'appareil, du casque et du Bluetooth</string>
<string name="settings.media_button_title">Boutons média</string>
<string name="settings.network_timeout">Délai d\'attente de connexion</string> <string name="settings.network_timeout">Délai d\'attente de connexion</string>
<string name="settings.network_timeout_105000">105 secondes</string> <string name="settings.network_timeout_105000">105 secondes</string>
<string name="settings.network_timeout_120000">120 secondes</string> <string name="settings.network_timeout_120000">120 secondes</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="background_task.loading">Cargando…</string>
<string name="background_task.network_error">Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde.</string> <string name="background_task.network_error">Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde.</string>
<string name="background_task.unsupported_api">A API do servidor v%1$s non admite esta función.</string> <string name="background_task.unsupported_api">A API do servidor v%1$s non admite esta función.</string>
<string name="background_task.no_network">Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil.</string> <string name="background_task.no_network">Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">Non se entende a resposta. Por favor comproba a dirección do servidor.</string> <string name="background_task.parse_error">Non se entende a resposta. Por favor comproba a dirección do servidor.</string>
<string name="background_task.ssl_cert_error">Erro do certificado HTTPS: %1$s.</string> <string name="background_task.ssl_cert_error">Erro do certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Excepción de conexión SSL. Comprobe o certificado do servidor.</string> <string name="background_task.ssl_error">Excepción de conexión SSL. Comprobe o certificado do servidor.</string>
<string name="background_task.wait">Por favor agarde…</string>
<string name="button_bar.browse">Biblioteca</string> <string name="button_bar.browse">Biblioteca</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
<string name="button_bar.now_playing">Reproducindo agora</string> <string name="button_bar.now_playing">Reproducindo agora</string>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Betöltés&#8230;</string>
<string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét vagy próbálja később!</string> <string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét vagy próbálja később!</string>
<string name="background_task.unsupported_api">A v%1$s verziójú Szerver api nem támogatja ezt a funkciót.</string> <string name="background_task.unsupported_api">A v%1$s verziójú Szerver api nem támogatja ezt a funkciót.</string>
<string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string> <string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét!</string> <string name="background_task.parse_error">Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét!</string>
<string name="background_task.ssl_cert_error">HTTPS tanúsítványhiba: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS tanúsítványhiba: %1$s.</string>
<string name="background_task.ssl_error">SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát.</string> <string name="background_task.ssl_error">SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát.</string>
<string name="background_task.wait">Kérem várjon!&#8230;</string>
<string name="button_bar.bookmarks">Könyvjelzők</string> <string name="button_bar.bookmarks">Könyvjelzők</string>
<string name="button_bar.browse">Médiakönyvtár</string> <string name="button_bar.browse">Médiakönyvtár</string>
<string name="button_bar.chat">Csevegés (Chat)</string> <string name="button_bar.chat">Csevegés (Chat)</string>
@ -194,8 +192,6 @@
<string name="settings.max_bitrate_unlimited">Korlátlan</string> <string name="settings.max_bitrate_unlimited">Korlátlan</string>
<string name="settings.max_bitrate_wifi">Max. bitráta - Wi-Fi kapcsolat</string> <string name="settings.max_bitrate_wifi">Max. bitráta - Wi-Fi kapcsolat</string>
<string name="settings.max_songs">Dalok max. találati száma</string> <string name="settings.max_songs">Dalok max. találati száma</string>
<string name="settings.media_button_summary">Telefon irányítása a Bluetooth eszköz, vagy a fülhallgató vezérlőgombjaival.</string>
<string name="settings.media_button_title">Média vezérlőgombok</string>
<string name="settings.network_timeout">Hálózati időtúllépés</string> <string name="settings.network_timeout">Hálózati időtúllépés</string>
<string name="settings.network_timeout_105000">105 másodperc</string> <string name="settings.network_timeout_105000">105 másodperc</string>
<string name="settings.network_timeout_120000">120 másodperc</string> <string name="settings.network_timeout_120000">120 másodperc</string>

View File

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Caricamento…</string>
<string name="background_task.network_error">Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi.</string> <string name="background_task.network_error">Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi.</string>
<string name="background_task.no_network">Questo programma richiede l\'accesso alla rete. Per favore, abilita la connessione Wi-FI o la rete mobile.</string> <string name="background_task.no_network">Questo programma richiede l\'accesso alla rete. Per favore, abilita la connessione Wi-FI o la rete mobile.</string>
<string name="background_task.not_found">Risorsa non trovata. Si prega di controllare l\'indirizzo del server.</string> <string name="background_task.not_found">Risorsa non trovata. Si prega di controllare l\'indirizzo del server.</string>
<string name="background_task.parse_error">Risposta non comprensibile. Si prega di controllare l\'indirizzo del server.</string> <string name="background_task.parse_error">Risposta non comprensibile. Si prega di controllare l\'indirizzo del server.</string>
<string name="background_task.ssl_cert_error">Errore certificato HTTPS. %1$s.</string> <string name="background_task.ssl_cert_error">Errore certificato HTTPS. %1$s.</string>
<string name="background_task.ssl_error">Anomalia connessione SSL. Si prega di controllare il certificato del server.</string> <string name="background_task.ssl_error">Anomalia connessione SSL. Si prega di controllare il certificato del server.</string>
<string name="background_task.wait">Attendere per favore#8230;</string>
<string name="button_bar.bookmarks">Segnalibri</string> <string name="button_bar.bookmarks">Segnalibri</string>
<string name="button_bar.browse">Libreria multimediale</string> <string name="button_bar.browse">Libreria multimediale</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -184,8 +182,6 @@
<string name="settings.max_bitrate_unlimited">Illimitato</string> <string name="settings.max_bitrate_unlimited">Illimitato</string>
<string name="settings.max_bitrate_wifi">Bitrate Max - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Bitrate Max - Wi-Fi</string>
<string name="settings.max_songs">N° Max Canzoni</string> <string name="settings.max_songs">N° Max Canzoni</string>
<string name="settings.media_button_summary">Controlli per risposta alle chiamate, per cuffie e Bluetooth</string>
<string name="settings.media_button_title">Tasti Media</string>
<string name="settings.network_timeout">Timeout Rete</string> <string name="settings.network_timeout">Timeout Rete</string>
<string name="settings.network_timeout_105000">105 seconds</string> <string name="settings.network_timeout_105000">105 seconds</string>
<string name="settings.network_timeout_120000">120 seconds</string> <string name="settings.network_timeout_120000">120 seconds</string>

View File

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">読み込み中…</string>
<string name="background_task.network_error">ネットワークエラーが発生しました。サーバーのアドレスを確認してやり直してください。</string> <string name="background_task.network_error">ネットワークエラーが発生しました。サーバーのアドレスを確認してやり直してください。</string>
<string name="background_task.parse_error">応答が確認できません。サーバーのアドレスを確認してください。</string> <string name="background_task.parse_error">応答が確認できません。サーバーのアドレスを確認してください。</string>
<string name="background_task.ssl_cert_error">HTTPS証明書エラー: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS証明書エラー: %1$s.</string>
<string name="background_task.ssl_error">SSL接続が異常です。サーバーの証明書を確認してください。</string> <string name="background_task.ssl_error">SSL接続が異常です。サーバーの証明書を確認してください。</string>
<string name="background_task.wait">お待ち下さい…</string>
<string name="button_bar.bookmarks">ブックマーク</string> <string name="button_bar.bookmarks">ブックマーク</string>
<string name="button_bar.browse">メディアライブラリ</string> <string name="button_bar.browse">メディアライブラリ</string>
<string name="button_bar.chat">チャット</string> <string name="button_bar.chat">チャット</string>
@ -181,8 +179,6 @@
<string name="settings.max_bitrate_64">64 Kbps</string> <string name="settings.max_bitrate_64">64 Kbps</string>
<string name="settings.max_bitrate_80">80 Kbps</string> <string name="settings.max_bitrate_80">80 Kbps</string>
<string name="settings.max_bitrate_96">96 Kbps</string> <string name="settings.max_bitrate_96">96 Kbps</string>
<string name="settings.media_button_summary">端末本体、ヘッドセットやBluetoothの再生コントロールボタンに対応します</string>
<string name="settings.media_button_title">メディアボタン</string>
<string name="settings.network_timeout_15000">15秒</string> <string name="settings.network_timeout_15000">15秒</string>
<string name="settings.network_timeout_75000">75秒</string> <string name="settings.network_timeout_75000">75秒</string>
<string name="settings.network_timeout_90000">90秒</string> <string name="settings.network_timeout_90000">90秒</string>

View File

@ -37,7 +37,6 @@
<string name="buttons.stop">Stopp</string> <string name="buttons.stop">Stopp</string>
<string name="button_bar.search">Søk</string> <string name="button_bar.search">Søk</string>
<string name="chat.send_a_message">Send en melding</string> <string name="chat.send_a_message">Send en melding</string>
<string name="background_task.loading">Laster inn …</string>
<string name="common.appname">Ultrasonic</string> <string name="common.appname">Ultrasonic</string>
<string name="button_bar.browse">Mediebibliotek</string> <string name="button_bar.browse">Mediebibliotek</string>
<string name="buttons.play">Spill</string> <string name="buttons.play">Spill</string>
@ -137,7 +136,6 @@
<string name="background_task.not_found">Fant ikke ressursen. Sjekk tjeneradressen.</string> <string name="background_task.not_found">Fant ikke ressursen. Sjekk tjeneradressen.</string>
<string name="background_task.parse_error">Forsto ikke svaret. Sjekk tjeneradressen.</string> <string name="background_task.parse_error">Forsto ikke svaret. Sjekk tjeneradressen.</string>
<string name="background_task.ssl_cert_error">HTTPS-sertifikatsfeil: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS-sertifikatsfeil: %1$s.</string>
<string name="background_task.wait">Vent …</string>
<string name="button_bar.bookmarks">Bokmerker</string> <string name="button_bar.bookmarks">Bokmerker</string>
<string name="button_bar.now_playing">Spilles nå</string> <string name="button_bar.now_playing">Spilles nå</string>
<string name="podcasts_channels.empty">Ingen nettradioopptakskanaler registrert.</string> <string name="podcasts_channels.empty">Ingen nettradioopptakskanaler registrert.</string>
@ -219,7 +217,6 @@
<string name="settings.invalid_url">Angi en gyldig nettadresse.</string> <string name="settings.invalid_url">Angi en gyldig nettadresse.</string>
<string name="settings.max_artists">Maks. artister</string> <string name="settings.max_artists">Maks. artister</string>
<string name="settings.max_bitrate_unlimited">Ubegrenset</string> <string name="settings.max_bitrate_unlimited">Ubegrenset</string>
<string name="settings.media_button_title">Medieknapper</string>
<string name="settings.network_timeout">Nettverkstidsavbrudd</string> <string name="settings.network_timeout">Nettverkstidsavbrudd</string>
<string name="settings.network_timeout_90000">90 sekunder</string> <string name="settings.network_timeout_90000">90 sekunder</string>
<string name="settings.network_timeout_75000">75 sekunder</string> <string name="settings.network_timeout_75000">75 sekunder</string>
@ -317,7 +314,6 @@
<string name="menu.deleted_playlist_error">Klarte ikke å slette %s-spillelisten</string> <string name="menu.deleted_playlist_error">Klarte ikke å slette %s-spillelisten</string>
<string name="settings.download_transition_summary">Bytt til «Spilles nå» etter at avspilling startes i medievisning</string> <string name="settings.download_transition_summary">Bytt til «Spilles nå» etter at avspilling startes i medievisning</string>
<string name="settings.hide_media_toast">Trer i effekt neste gang Android skanner enheten din for musikk.</string> <string name="settings.hide_media_toast">Trer i effekt neste gang Android skanner enheten din for musikk.</string>
<string name="settings.media_button_summary">Svarer på enhets-, hodesett, og Blåtannsmedieknapper</string>
<string name="settings.wifi_required_summary">Kun last ned på ubegrensede tilkoblinger</string> <string name="settings.wifi_required_summary">Kun last ned på ubegrensede tilkoblinger</string>
<string name="notification.downloading_title">Last ned medier i bakgrunnen …\?</string> <string name="notification.downloading_title">Last ned medier i bakgrunnen …\?</string>
<string name="notification.permission_required">Merknader kreves for medieavspilling. Du kan innvilge tilgangen når som helst i Android-innstillingene.</string> <string name="notification.permission_required">Merknader kreves for medieavspilling. Du kan innvilge tilgangen når som helst i Android-innstillingene.</string>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Bezig met laden&#8230;</string>
<string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw.</string> <string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw.</string>
<string name="background_task.unsupported_api">De server-api, v %1$s, heeft geen ondersteuning voor deze functie.</string> <string name="background_task.unsupported_api">De server-api, v %1$s, heeft geen ondersteuning voor deze functie.</string>
<string name="background_task.no_network">Deze app vereist netwerktoegang. Schakel wifi of mobiel internet in.</string> <string name="background_task.no_network">Deze app vereist netwerktoegang. Schakel wifi of mobiel internet in.</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Het antwoord werd niet begrepen. Controleer het serveradres.</string> <string name="background_task.parse_error">Het antwoord werd niet begrepen. Controleer het serveradres.</string>
<string name="background_task.ssl_cert_error">HTTPS-certificaatfout: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS-certificaatfout: %1$s.</string>
<string name="background_task.ssl_error">SSL-verbindingsuitzondering. Controleer het servercertificaat.</string> <string name="background_task.ssl_error">SSL-verbindingsuitzondering. Controleer het servercertificaat.</string>
<string name="background_task.wait">Even geduld&#8230;</string>
<string name="button_bar.bookmarks">Bladwijzers</string> <string name="button_bar.bookmarks">Bladwijzers</string>
<string name="button_bar.browse">Mediabibliotheek</string> <string name="button_bar.browse">Mediabibliotheek</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -233,8 +231,6 @@
<string name="settings.max_bitrate_unlimited">Ongelimiteerd</string> <string name="settings.max_bitrate_unlimited">Ongelimiteerd</string>
<string name="settings.max_bitrate_wifi">Max. bitsnelheid via wifi</string> <string name="settings.max_bitrate_wifi">Max. bitsnelheid via wifi</string>
<string name="settings.max_songs">Max. aantal nummers</string> <string name="settings.max_songs">Max. aantal nummers</string>
<string name="settings.media_button_summary">Reageren op telefoon-, headset- en bluetooth-mediatoetsen.</string>
<string name="settings.media_button_title">Mediatoetsen</string>
<string name="settings.network_timeout">Netwerktime-out</string> <string name="settings.network_timeout">Netwerktime-out</string>
<string name="settings.network_timeout_105000">105 seconden</string> <string name="settings.network_timeout_105000">105 seconden</string>
<string name="settings.network_timeout_120000">120 seconden</string> <string name="settings.network_timeout_120000">120 seconden</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <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 lub spróbować później.</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.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.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">Brak prawidłowej odpowiedzi. 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_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> <string name="background_task.ssl_error">Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.</string>
<string name="background_task.wait">Proszę czekać…</string>
<string name="button_bar.bookmarks">Zakładki</string> <string name="button_bar.bookmarks">Zakładki</string>
<string name="button_bar.browse">Biblioteka</string> <string name="button_bar.browse">Biblioteka</string>
<string name="button_bar.chat">Czat</string> <string name="button_bar.chat">Czat</string>
@ -187,8 +185,6 @@
<string name="settings.max_bitrate_unlimited">Bez limitu</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.max_songs">Maksymalna ilość wyników - utwory</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">Przekroczenie limitu czasu sieci</string>
<string name="settings.network_timeout_105000">105 sekund</string> <string name="settings.network_timeout_105000">105 sekund</string>
<string name="settings.network_timeout_120000">120 sekund</string> <string name="settings.network_timeout_120000">120 sekund</string>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Carregando&#8230;</string>
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string> <string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
<string name="background_task.unsupported_api">O servidor api v%1$s não tem suporte para esta função.</string> <string name="background_task.unsupported_api">O servidor api v%1$s não tem suporte para esta função.</string>
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string> <string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string> <string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string> <string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string> <string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
<string name="background_task.wait">Por favor aguarde&#8230;</string>
<string name="button_bar.bookmarks">Favoritos</string> <string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string> <string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -231,8 +229,6 @@
<string name="settings.max_bitrate_unlimited">Ilimitado</string> <string name="settings.max_bitrate_unlimited">Ilimitado</string>
<string name="settings.max_bitrate_wifi">Taxa Máxima de Bits - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Taxa Máxima de Bits - Wi-Fi</string>
<string name="settings.max_songs">Máximo de Músicas</string> <string name="settings.max_songs">Máximo de Músicas</string>
<string name="settings.media_button_summary">Obedecer aos botões do celular, fones e botões de mídia do Bluetooth</string>
<string name="settings.media_button_title">Botões de Mídia</string>
<string name="settings.network_timeout">Timeout da Rede</string> <string name="settings.network_timeout">Timeout da Rede</string>
<string name="settings.network_timeout_105000">105 segundos</string> <string name="settings.network_timeout_105000">105 segundos</string>
<string name="settings.network_timeout_120000">120 segundos</string> <string name="settings.network_timeout_120000">120 segundos</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Carregando…</string>
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string> <string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
<string name="background_task.unsupported_api">Server api v%1$s does not support this function.</string> <string name="background_task.unsupported_api">Server api v%1$s does not support this function.</string>
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string> <string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string> <string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string> <string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string> <string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
<string name="background_task.wait">Por favor aguarde…</string>
<string name="button_bar.bookmarks">Favoritos</string> <string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string> <string name="button_bar.browse">Biblioteca de Mídia</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -187,8 +185,6 @@
<string name="settings.max_bitrate_unlimited">Ilimitado</string> <string name="settings.max_bitrate_unlimited">Ilimitado</string>
<string name="settings.max_bitrate_wifi">Máx. de Taxa de Bits - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Máx. de Taxa de Bits - Wi-Fi</string>
<string name="settings.max_songs">Máximo de Músicas</string> <string name="settings.max_songs">Máximo de Músicas</string>
<string name="settings.media_button_summary">Obedecer aos botões do telemóvel, auricular e botões de mídia do Bluetooth</string>
<string name="settings.media_button_title">Botões de Mídia</string>
<string name="settings.network_timeout">Timeout da Rede</string> <string name="settings.network_timeout">Timeout da Rede</string>
<string name="settings.network_timeout_105000">105 segundos</string> <string name="settings.network_timeout_105000">105 segundos</string>
<string name="settings.network_timeout_120000">120 segundos</string> <string name="settings.network_timeout_120000">120 segundos</string>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Загрузка&#8230;</string>
<string name="background_task.network_error">Произошла ошибка сети. Пожалуйста, проверьте адрес сервера или повторите попытку позже.</string> <string name="background_task.network_error">Произошла ошибка сети. Пожалуйста, проверьте адрес сервера или повторите попытку позже.</string>
<string name="background_task.unsupported_api">Серверный api версии %1$s не поддерживает эту функцию.</string> <string name="background_task.unsupported_api">Серверный api версии %1$s не поддерживает эту функцию.</string>
<string name="background_task.no_network">Эта программа требует доступа к сети. Пожалуйста, включите Wi-Fi или мобильную сеть.</string> <string name="background_task.no_network">Эта программа требует доступа к сети. Пожалуйста, включите Wi-Fi или мобильную сеть.</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Не понятный ответ. Пожалуйста, проверьте адрес сервера.</string> <string name="background_task.parse_error">Не понятный ответ. Пожалуйста, проверьте адрес сервера.</string>
<string name="background_task.ssl_cert_error">Ошибка сертификата HTTPS: %1$s.</string> <string name="background_task.ssl_cert_error">Ошибка сертификата HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Исключение SSL-соединения. Пожалуйста, проверьте сертификат сервера.</string> <string name="background_task.ssl_error">Исключение SSL-соединения. Пожалуйста, проверьте сертификат сервера.</string>
<string name="background_task.wait">Пожалуйста, подождите&#8230;</string>
<string name="button_bar.bookmarks">Закладки</string> <string name="button_bar.bookmarks">Закладки</string>
<string name="button_bar.browse">Медиа библиотека</string> <string name="button_bar.browse">Медиа библиотека</string>
<string name="button_bar.chat">Чат</string> <string name="button_bar.chat">Чат</string>
@ -212,8 +210,6 @@
<string name="settings.max_bitrate_unlimited">Неограниченный</string> <string name="settings.max_bitrate_unlimited">Неограниченный</string>
<string name="settings.max_bitrate_wifi">Максимальный битрейт - Wi-Fi подключение</string> <string name="settings.max_bitrate_wifi">Максимальный битрейт - Wi-Fi подключение</string>
<string name="settings.max_songs">Максимум треков</string> <string name="settings.max_songs">Максимум треков</string>
<string name="settings.media_button_summary">Отвечать на телефон, гарнитуру и мультимедийные кнопки Bluetooth</string>
<string name="settings.media_button_title">Медиа кнопки</string>
<string name="settings.network_timeout">Таймаут сети</string> <string name="settings.network_timeout">Таймаут сети</string>
<string name="settings.network_timeout_105000">105 секунд</string> <string name="settings.network_timeout_105000">105 секунд</string>
<string name="settings.network_timeout_120000">120 секунд</string> <string name="settings.network_timeout_120000">120 секунд</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">加载中…</string>
<string name="background_task.network_error">发生网络错误。请检查服务器地址或稍后重试。</string> <string name="background_task.network_error">发生网络错误。请检查服务器地址或稍后重试。</string>
<string name="background_task.unsupported_api">服务端 API v%1$s 不支持此功能。</string> <string name="background_task.unsupported_api">服务端 API v%1$s 不支持此功能。</string>
<string name="background_task.no_network">此软件需要连接网络,请打开 Wi-Fi 或移动网络。</string> <string name="background_task.no_network">此软件需要连接网络,请打开 Wi-Fi 或移动网络。</string>
@ -8,7 +7,6 @@
<string name="background_task.parse_error">未知回复内容,请检查服务器地址。</string> <string name="background_task.parse_error">未知回复内容,请检查服务器地址。</string>
<string name="background_task.ssl_cert_error">HTTPS 证书错误:%1$s.</string> <string name="background_task.ssl_cert_error">HTTPS 证书错误:%1$s.</string>
<string name="background_task.ssl_error">SSL 连接异常。请检查服务器证书。</string> <string name="background_task.ssl_error">SSL 连接异常。请检查服务器证书。</string>
<string name="background_task.wait">请稍等…</string>
<string name="button_bar.bookmarks">书签</string> <string name="button_bar.bookmarks">书签</string>
<string name="button_bar.browse">媒体库</string> <string name="button_bar.browse">媒体库</string>
<string name="button_bar.chat">聊天</string> <string name="button_bar.chat">聊天</string>
@ -218,8 +216,6 @@
<string name="settings.max_bitrate_unlimited">不限制</string> <string name="settings.max_bitrate_unlimited">不限制</string>
<string name="settings.max_bitrate_wifi">最大比特率 - WIFI</string> <string name="settings.max_bitrate_wifi">最大比特率 - WIFI</string>
<string name="settings.max_songs">最大歌曲</string> <string name="settings.max_songs">最大歌曲</string>
<string name="settings.media_button_summary">响应手机、耳机和蓝牙设备的媒体按钮</string>
<string name="settings.media_button_title">媒体按钮</string>
<string name="settings.network_timeout">网络超时</string> <string name="settings.network_timeout">网络超时</string>
<string name="settings.network_timeout_105000">105 秒</string> <string name="settings.network_timeout_105000">105 秒</string>
<string name="settings.network_timeout_120000">120 秒</string> <string name="settings.network_timeout_120000">120 秒</string>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">載入中…</string>
<string name="button_bar.bookmarks">書籤</string> <string name="button_bar.bookmarks">書籤</string>
<string name="button_bar.browse">媒體庫</string> <string name="button_bar.browse">媒體庫</string>
<string name="button_bar.now_playing">正在播放</string> <string name="button_bar.now_playing">正在播放</string>
@ -146,7 +145,6 @@
<string name="language.nl">荷蘭語</string> <string name="language.nl">荷蘭語</string>
<string name="download.jukebox_off">已關閉遠端控制,音樂將在手機上播放。</string> <string name="download.jukebox_off">已關閉遠端控制,音樂將在手機上播放。</string>
<string name="language.de">德語</string> <string name="language.de">德語</string>
<string name="background_task.wait">請稍候…</string>
<string name="common.unpin">取消固定</string> <string name="common.unpin">取消固定</string>
<string name="download.playlist_name">輸入播放清單名稱:</string> <string name="download.playlist_name">輸入播放清單名稱:</string>
<string name="main.albums_by_year">依照時間排列</string> <string name="main.albums_by_year">依照時間排列</string>

View File

@ -19,7 +19,6 @@
<string name="setting_key.preload_count" translatable="false">preloadCount</string> <string name="setting_key.preload_count" translatable="false">preloadCount</string>
<string name="setting_key.parallel_downloads" translatable="false">parallelDownloads</string> <string name="setting_key.parallel_downloads" translatable="false">parallelDownloads</string>
<string name="setting_key.hide_media" translatable="false">hideMedia</string> <string name="setting_key.hide_media" translatable="false">hideMedia</string>
<string name="setting_key.media_buttons" translatable="false">mediaButtons</string>
<string name="setting_key.scrobble" translatable="false">scrobble</string> <string name="setting_key.scrobble" translatable="false">scrobble</string>
<string name="setting_key.server_scaling" translatable="false">serverScaling</string> <string name="setting_key.server_scaling" translatable="false">serverScaling</string>
<string name="setting_key.wifi_required_for_download" translatable="false">wifiRequiredForDownload</string> <string name="setting_key.wifi_required_for_download" translatable="false">wifiRequiredForDownload</string>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Loading…</string>
<string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string> <string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
<string name="background_task.unsupported_api">Server API v%1$s does not support this function.</string> <string name="background_task.unsupported_api">Server API v%1$s does not support this function.</string>
<string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string> <string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
@ -9,7 +8,6 @@
<string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string> <string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
<string name="background_task.ssl_cert_error">HTTPS certificate error: %1$s.</string> <string name="background_task.ssl_cert_error">HTTPS certificate error: %1$s.</string>
<string name="background_task.ssl_error">SSL connection exception. Please check server certificate.</string> <string name="background_task.ssl_error">SSL connection exception. Please check server certificate.</string>
<string name="background_task.wait">Please wait…</string>
<string name="button_bar.bookmarks">Bookmarks</string> <string name="button_bar.bookmarks">Bookmarks</string>
<string name="button_bar.browse">Media Library</string> <string name="button_bar.browse">Media Library</string>
<string name="button_bar.chat">Chat</string> <string name="button_bar.chat">Chat</string>
@ -234,8 +232,6 @@
<string name="settings.max_bitrate_wifi">Max Bitrate - Wi-Fi</string> <string name="settings.max_bitrate_wifi">Max Bitrate - Wi-Fi</string>
<string name="settings.max_bitrate_pinning">Max Bitrate - When pinning a song permanently</string> <string name="settings.max_bitrate_pinning">Max Bitrate - When pinning a song permanently</string>
<string name="settings.max_songs">Max Songs</string> <string name="settings.max_songs">Max Songs</string>
<string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
<string name="settings.media_button_title">Media Buttons</string>
<string name="settings.network_timeout">Network Timeout</string> <string name="settings.network_timeout">Network Timeout</string>
<string name="settings.network_timeout_105000">105 seconds</string> <string name="settings.network_timeout_105000">105 seconds</string>
<string name="settings.network_timeout_120000">120 seconds</string> <string name="settings.network_timeout_120000">120 seconds</string>

View File

@ -72,12 +72,6 @@
a:summary="@string/settings.show_artist_picture_summary" a:summary="@string/settings.show_artist_picture_summary"
a:title="@string/settings.show_artist_picture" a:title="@string/settings.show_artist_picture"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="@string/setting_key.media_buttons"
a:summary="@string/settings.media_button_summary"
a:title="@string/settings.media_button_title"
app:iconSpaceReserved="false"/>
<CheckBoxPreference <CheckBoxPreference
a:defaultValue="true" a:defaultValue="true"
a:key="@string/setting_key.download_transition" a:key="@string/setting_key.download_transition"