mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-12 15:37:17 +03:00
Refactor Koin, Scopes & Lifecycles
This commit is contained in:
parent
823465d6fd
commit
17260878ac
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
@ -1,11 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<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" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
@ -70,50 +70,6 @@
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/chat.xml"
|
||||
line="33"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/save_playlist.xml"
|
||||
line="9"
|
||||
column="6"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/share_details.xml"
|
||||
line="29"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/time_span_dialog.xml"
|
||||
line="28"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="LabelFor"
|
||||
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
||||
|
@ -72,7 +72,7 @@
|
||||
</service>
|
||||
|
||||
<!-- 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:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true"
|
||||
@ -107,6 +107,12 @@
|
||||
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name=".provider.UltrasonicAppWidgetProvider"
|
||||
android:label="Ultrasonic"
|
||||
@ -119,12 +125,6 @@
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/appwidget_info"/>
|
||||
</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
|
||||
android:name=".provider.SearchSuggestionProvider"
|
||||
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
||||
|
@ -23,7 +23,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -39,6 +38,7 @@ import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.onNavDestinationSelected
|
||||
@ -50,6 +50,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.scope.ScopeActivity
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
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.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.InfoDialog
|
||||
import org.moire.ultrasonic.util.LocaleHelper
|
||||
@ -81,7 +81,7 @@ import timber.log.Timber
|
||||
* onCreate/onResume/onDestroy methods...
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class NavigationActivity : AppCompatActivity() {
|
||||
class NavigationActivity : ScopeActivity() {
|
||||
private var videoMenuItem: MenuItem? = null
|
||||
private var chatMenuItem: MenuItem? = null
|
||||
private var bookmarksMenuItem: MenuItem? = null
|
||||
@ -485,15 +485,19 @@ class NavigationActivity : AppCompatActivity() {
|
||||
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
||||
val downloadHandler: DownloadHandler by inject()
|
||||
downloadHandler.addTracksToMediaController(
|
||||
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs = musicDirectory.getTracks(),
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
fragment = currentFragment,
|
||||
playlistName = null
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||
)
|
||||
|
||||
if (Settings.shouldTransitionOnPlayback) {
|
||||
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
|
||||
currentFragment.findNavController().navigate(R.id.playerFragment)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,10 @@ import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.inject
|
||||
import org.moire.ultrasonic.R
|
||||
@ -63,16 +67,21 @@ class ArtistRowBinder(
|
||||
|
||||
if (showArtistPicture()) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
imageLoaderProvider.executeOn {
|
||||
it.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
imageLoaderProvider.executeOn {
|
||||
it.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.coverArt.visibility = View.GONE
|
||||
|
@ -31,7 +31,6 @@ import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
||||
const val INDICATOR_THICKNESS_DEFINITE = 10
|
||||
@ -79,10 +78,6 @@ class TrackViewHolder(val view: View) :
|
||||
|
||||
private var rxBusSubscription: CompositeDisposable? = null
|
||||
|
||||
init {
|
||||
Timber.v("New ViewHolder created")
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
fun setSong(
|
||||
song: Track,
|
||||
|
@ -7,8 +7,6 @@
|
||||
|
||||
package org.moire.ultrasonic.data
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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
|
||||
fun getActiveMetaDatabase(): MetaDatabase {
|
||||
val activeServer = getActiveServerId()
|
||||
@ -234,29 +256,6 @@ class ActiveServerProvider(
|
||||
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
|
||||
*/
|
||||
|
@ -1,11 +1,11 @@
|
||||
/*
|
||||
* CachedDataSource.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.playback
|
||||
package org.moire.ultrasonic.data
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
@ -1,14 +1,15 @@
|
||||
package org.moire.ultrasonic.di
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.CacheCleaner
|
||||
|
||||
/**
|
||||
* This Koin module contains the registration of general classes needed for Ultrasonic
|
||||
*/
|
||||
val applicationModule = module {
|
||||
single { ActiveServerProvider(get()) }
|
||||
single { ImageLoaderProvider(androidContext()) }
|
||||
single { ImageLoaderProvider() }
|
||||
single { CacheCleaner() }
|
||||
}
|
||||
|
@ -1,19 +1,27 @@
|
||||
package org.moire.ultrasonic.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
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
|
||||
*/
|
||||
val mediaPlayerModule = module {
|
||||
single { MediaPlayerLifecycleSupport() }
|
||||
|
||||
// These are dependency-free
|
||||
single { PlaybackStateSerializer() }
|
||||
single { ExternalStorageMonitor() }
|
||||
single { NetworkAndStorageChecker() }
|
||||
single { ShareHandler() }
|
||||
|
||||
// TODO Ideally this can be cleaned up when all circular references are removed.
|
||||
single { MediaPlayerManager(get(), get(), get()) }
|
||||
scope<NavigationActivity> {
|
||||
scoped { MediaPlayerManager(get(), get()) }
|
||||
scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.di
|
||||
|
||||
import kotlin.math.abs
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
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.OfflineMusicService
|
||||
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
|
||||
|
||||
/**
|
||||
@ -68,8 +64,4 @@ val musicServiceModule = module {
|
||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||
OfflineMusicService()
|
||||
}
|
||||
|
||||
single { DownloadHandler(get(), get()) }
|
||||
single { NetworkAndStorageChecker(androidContext()) }
|
||||
single { ShareHandler(androidContext()) }
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import org.moire.ultrasonic.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
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
|
||||
@ -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>) {
|
||||
if (songs.isNotEmpty()) {
|
||||
|
||||
val state = PlaybackState(
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs = songs,
|
||||
currentPlayingIndex = 0,
|
||||
currentPlayingPosition = songs[0].bookmarkPosition
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||
)
|
||||
|
||||
mediaPlayerManager.restore(
|
||||
state = state,
|
||||
autoPlay = true,
|
||||
newPlaylist = true
|
||||
)
|
||||
mediaPlayerManager.seekTo(0, songs[0].bookmarkPosition)
|
||||
mediaPlayerManager.prepare()
|
||||
mediaPlayerManager.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,10 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
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.data.ActiveServerProvider
|
||||
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.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||
|
||||
/**
|
||||
* An extension of the MultiListFragment, with a few helper functions geared
|
||||
* towards the display of MusicDirectory.Entries.
|
||||
* @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 val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
val isArtist = (item is Artist)
|
||||
|
||||
return handleContextMenu(menuItem, item, isArtist, downloadHandler, this)
|
||||
return handleContextMenu(menuItem, item, isArtist, mediaPlayerManager, this)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: T) {
|
||||
@ -119,65 +119,4 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,11 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import java.lang.ref.SoftReference
|
||||
import org.koin.core.component.KoinComponent
|
||||
import 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.R
|
||||
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 timber.log.Timber
|
||||
|
||||
class MainFragment : Fragment(), KoinComponent {
|
||||
class MainFragment : ScopeFragment(), KoinScopeComponent {
|
||||
|
||||
private var filterButtonBar: FilterButtonBar? = null
|
||||
private var layoutType: LayoutType = LayoutType.COVER
|
||||
|
@ -17,7 +17,6 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@ -26,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
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.model.GenericListModel
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.CommunicationError
|
||||
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
|
||||
* @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 serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
internal val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
protected val downloadHandler: DownloadHandler by inject()
|
||||
protected var refreshListView: SwipeRefreshLayout? = null
|
||||
internal var listView: RecyclerView? = null
|
||||
internal lateinit var viewManager: LinearLayoutManager
|
||||
|
@ -15,7 +15,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.button.MaterialButton
|
||||
@ -23,6 +22,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.lang.Exception
|
||||
import kotlin.math.abs
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.scope.ScopeFragment
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
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
|
||||
*/
|
||||
class NowPlayingFragment : Fragment() {
|
||||
class NowPlayingFragment : ScopeFragment() {
|
||||
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
|
@ -45,7 +45,6 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
@ -79,12 +78,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
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.adapters.BaseAdapter
|
||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
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.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.toTrack
|
||||
import org.moire.ultrasonic.view.AutoRepeatButton
|
||||
import timber.log.Timber
|
||||
@ -116,9 +116,9 @@ import timber.log.Timber
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
|
||||
class PlayerFragment :
|
||||
Fragment(),
|
||||
ScopeFragment(),
|
||||
GestureDetector.OnGestureListener,
|
||||
KoinComponent,
|
||||
KoinScopeComponent,
|
||||
CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
||||
|
||||
// Settings
|
||||
@ -356,14 +356,14 @@ class PlayerFragment :
|
||||
onPlaylistChanged()
|
||||
|
||||
when (newRepeat) {
|
||||
0 -> Util.toast(
|
||||
context, R.string.download_repeat_off
|
||||
0 -> toast(
|
||||
R.string.download_repeat_off
|
||||
)
|
||||
1 -> Util.toast(
|
||||
context, R.string.download_repeat_single
|
||||
1 -> toast(
|
||||
R.string.download_repeat_single
|
||||
)
|
||||
2 -> Util.toast(
|
||||
context, R.string.download_repeat_all
|
||||
2 -> toast(
|
||||
R.string.download_repeat_all
|
||||
)
|
||||
else -> {
|
||||
}
|
||||
@ -410,7 +410,7 @@ class PlayerFragment :
|
||||
// Query the Jukebox state in an IO Context
|
||||
ioScope.launch(CommunicationError.getHandler(context)) {
|
||||
try {
|
||||
jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable
|
||||
jukeboxAvailable = getMusicService().isJukeboxAvailable()
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
@ -457,9 +457,9 @@ class PlayerFragment :
|
||||
val isEnabled = mediaPlayerManager.toggleShuffle()
|
||||
|
||||
if (isEnabled) {
|
||||
Util.toast(activity, R.string.download_menu_shuffle_on)
|
||||
toast(R.string.download_menu_shuffle_on)
|
||||
} else {
|
||||
Util.toast(activity, R.string.download_menu_shuffle_off)
|
||||
toast(R.string.download_menu_shuffle_off)
|
||||
}
|
||||
|
||||
updateShuffleButtonState(isEnabled)
|
||||
@ -579,8 +579,7 @@ class PlayerFragment :
|
||||
equalizerMenuItem.isVisible = isEqualizerAvailable
|
||||
}
|
||||
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
val track = mediaPlayerManager.currentMediaItem?.toTrack()
|
||||
|
||||
if (track != null) {
|
||||
currentSong = track
|
||||
@ -600,7 +599,7 @@ class PlayerFragment :
|
||||
goToArtist.isVisible = false
|
||||
}
|
||||
|
||||
if (mediaPlayerController.keepScreenOn) {
|
||||
if (mediaPlayerManager.keepScreenOn) {
|
||||
screenOption?.setTitle(R.string.download_menu_screen_off)
|
||||
} else {
|
||||
screenOption?.setTitle(R.string.download_menu_screen_on)
|
||||
@ -609,7 +608,7 @@ class PlayerFragment :
|
||||
if (jukeboxOption != null) {
|
||||
jukeboxOption.isEnabled = jukeboxAvailable
|
||||
jukeboxOption.isVisible = jukeboxAvailable
|
||||
if (mediaPlayerController.isJukeboxEnabled) {
|
||||
if (mediaPlayerManager.isJukeboxEnabled) {
|
||||
jukeboxOption.setTitle(R.string.download_menu_jukebox_off)
|
||||
} else {
|
||||
jukeboxOption.setTitle(R.string.download_menu_jukebox_on)
|
||||
@ -707,8 +706,7 @@ class PlayerFragment :
|
||||
R.id.menu_item_jukebox -> {
|
||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
||||
Util.toast(
|
||||
context,
|
||||
toast(
|
||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
||||
else R.string.download_jukebox_off,
|
||||
false
|
||||
@ -760,7 +758,7 @@ class PlayerFragment :
|
||||
R.string.download_bookmark_set_at_position,
|
||||
bookmarkTime
|
||||
)
|
||||
Util.toast(context, msg)
|
||||
toast(msg)
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_bookmark_delete -> {
|
||||
@ -776,13 +774,12 @@ class PlayerFragment :
|
||||
Timber.e(all)
|
||||
}
|
||||
}.start()
|
||||
Util.toast(context, R.string.download_bookmark_removed)
|
||||
toast(R.string.download_bookmark_removed)
|
||||
return true
|
||||
}
|
||||
R.id.menu_item_share -> {
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
val tracks: MutableList<Track?> = ArrayList()
|
||||
val playlist = mediaPlayerController.playlist
|
||||
val playlist = mediaPlayerManager.playlist
|
||||
for (item in playlist) {
|
||||
val playlistEntry = item.toTrack()
|
||||
tracks.add(playlistEntry)
|
||||
@ -790,8 +787,6 @@ class PlayerFragment :
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
tracks = tracks,
|
||||
swipe = null,
|
||||
cancellationToken = cancellationToken,
|
||||
)
|
||||
return true
|
||||
}
|
||||
@ -804,8 +799,6 @@ class PlayerFragment :
|
||||
shareHandler.createShare(
|
||||
this,
|
||||
tracks,
|
||||
swipe = null,
|
||||
cancellationToken = cancellationToken
|
||||
)
|
||||
return true
|
||||
}
|
||||
@ -822,7 +815,7 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// The playlist can be acquired only from the main thread
|
||||
@ -835,7 +828,7 @@ class PlayerFragment :
|
||||
musicService.createPlaylist(null, playlistName, entries)
|
||||
}.invokeOnCompletion {
|
||||
if (it == null || it is CancellationException) {
|
||||
Util.toast(UApp.applicationContext(), R.string.download_playlist_done)
|
||||
toast(R.string.download_playlist_done)
|
||||
} else {
|
||||
Timber.e(it, "Exception has occurred in savePlaylistInBackground")
|
||||
val msg = String.format(
|
||||
@ -844,7 +837,7 @@ class PlayerFragment :
|
||||
resources.getString(R.string.download_playlist_error),
|
||||
CommunicationError.getErrorMessage(it)
|
||||
)
|
||||
Util.toast(UApp.applicationContext(), msg)
|
||||
toast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -958,7 +951,7 @@ class PlayerFragment :
|
||||
item?.mediaMetadata?.title
|
||||
)
|
||||
|
||||
Util.toast(context, songRemoved)
|
||||
toast(songRemoved)
|
||||
|
||||
// Remove the item from the playlist
|
||||
mediaPlayerManager.removeFromPlaylist(pos)
|
||||
@ -1059,15 +1052,14 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun onPlaylistChanged() {
|
||||
val mediaPlayerController = mediaPlayerManager
|
||||
// Try to display playlist in play order
|
||||
val list = mediaPlayerController.playlistInPlayOrder
|
||||
val list = mediaPlayerManager.playlistInPlayOrder
|
||||
emptyTextView.setText(R.string.playlist_empty)
|
||||
viewAdapter.submitList(list.map(MediaItem::toTrack))
|
||||
progressIndicator.isVisible = false
|
||||
emptyView.isVisible = list.isEmpty()
|
||||
|
||||
updateRepeatButtonState(mediaPlayerController.repeatMode)
|
||||
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
||||
}
|
||||
|
||||
private fun onTrackChanged() {
|
||||
|
@ -17,7 +17,7 @@ import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.KoinScopeComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
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.fragment.FragmentTitle.Companion.setTitle
|
||||
import org.moire.ultrasonic.model.SearchListModel
|
||||
import org.moire.ultrasonic.service.DownloadService
|
||||
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.util.CancellationToken
|
||||
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.Util
|
||||
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
|
||||
|
||||
*/
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
|
||||
private var searchResult: SearchResult? = null
|
||||
private var searchRefresh: SwipeRefreshLayout? = null
|
||||
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
||||
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
private val navArgs by navArgs<SearchFragmentArgs>()
|
||||
@ -137,18 +133,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
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) {
|
||||
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||
refreshListView?.isRefreshing = true
|
||||
@ -253,7 +237,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
||||
)
|
||||
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) {
|
||||
@ -288,103 +272,23 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
|
||||
val isArtist = (item is Artist)
|
||||
|
||||
val found = EntryListFragment.handleContextMenu(
|
||||
menuItem,
|
||||
item,
|
||||
isArtist,
|
||||
downloadHandler,
|
||||
this
|
||||
)
|
||||
|
||||
if (found || item !is Track) return true
|
||||
|
||||
val songs = mutableListOf<Track>()
|
||||
|
||||
when (menuItem.itemId) {
|
||||
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
|
||||
)
|
||||
}
|
||||
// Here the Item could be a track or an album or an artist
|
||||
if (item is Track) {
|
||||
return handleContextMenuTracks(
|
||||
menuItem = menuItem,
|
||||
tracks = listOf(item),
|
||||
mediaPlayerManager = mediaPlayerManager,
|
||||
fragment = this
|
||||
)
|
||||
} else {
|
||||
return handleContextMenu(
|
||||
menuItem = menuItem,
|
||||
item = item,
|
||||
isArtist = item is Artist,
|
||||
mediaPlayerManager = mediaPlayerManager,
|
||||
fragment = this
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -64,7 +64,7 @@ class ServerSelectorFragment : Fragment() {
|
||||
|
||||
listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
|
||||
val server = parent.getItemAtPosition(position) as ServerSetting
|
||||
ActiveServerProvider.setActiveServerById(server.id)
|
||||
activeServerProvider.setActiveServerById(server.id)
|
||||
findNavController().popBackStack(R.id.mainFragment, false)
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ class ServerSelectorFragment : Fragment() {
|
||||
val activeServerId = ActiveServerProvider.getActiveServerId()
|
||||
|
||||
// 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)
|
||||
|
||||
|
@ -300,7 +300,7 @@ class SettingsFragment :
|
||||
SearchSuggestionProvider.MODE
|
||||
)
|
||||
suggestions.clearHistory()
|
||||
toast(activity, R.string.settings_search_history_cleared)
|
||||
toast(R.string.settings_search_history_cleared)
|
||||
false
|
||||
}
|
||||
}
|
||||
@ -332,7 +332,7 @@ class SettingsFragment :
|
||||
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) {
|
||||
|
@ -46,14 +46,16 @@ import org.moire.ultrasonic.model.TrackCollectionModel
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
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.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.ViewCapabilities
|
||||
import timber.log.Timber
|
||||
@ -86,7 +88,6 @@ open class TrackCollectionFragment(
|
||||
|
||||
internal val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val shareHandler: ShareHandler by inject()
|
||||
internal var cancellationToken: CancellationToken? = null
|
||||
|
||||
override val listModel: TrackCollectionModel by viewModels()
|
||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
@ -102,7 +103,6 @@ open class TrackCollectionFragment(
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
cancellationToken = CancellationToken()
|
||||
|
||||
albumButtons = view.findViewById(R.id.menu_album)
|
||||
|
||||
@ -211,19 +211,23 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
playNowButton?.setOnClickListener {
|
||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true)
|
||||
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.CLEAR)
|
||||
}
|
||||
|
||||
playNextButton?.setOnClickListener {
|
||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true)
|
||||
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.AFTER_CURRENT)
|
||||
}
|
||||
|
||||
playLastButton!!.setOnClickListener {
|
||||
playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true)
|
||||
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.APPEND)
|
||||
}
|
||||
|
||||
pinButton?.setOnClickListener {
|
||||
downloadBackground(true)
|
||||
downloadSelectedOrAllTracks(true)
|
||||
}
|
||||
|
||||
downloadButton?.setOnClickListener {
|
||||
downloadSelectedOrAllTracks(false)
|
||||
}
|
||||
|
||||
unpinButton?.setOnClickListener {
|
||||
@ -231,26 +235,22 @@ open class TrackCollectionFragment(
|
||||
ConfirmationDialog.Builder(requireContext())
|
||||
.setMessage(R.string.common_unpin_selection_confirmation)
|
||||
.setPositiveButton(R.string.common_unpin) { _, _ ->
|
||||
unpin()
|
||||
unpinSelectedTracks()
|
||||
}.show()
|
||||
} else {
|
||||
unpin()
|
||||
unpinSelectedTracks()
|
||||
}
|
||||
}
|
||||
|
||||
downloadButton?.setOnClickListener {
|
||||
downloadBackground(false)
|
||||
}
|
||||
|
||||
deleteButton?.setOnClickListener {
|
||||
if (Settings.showConfirmationDialog) {
|
||||
ConfirmationDialog.Builder(requireContext())
|
||||
.setMessage(R.string.common_delete_selection_confirmation)
|
||||
.setPositiveButton(R.string.common_delete) { _, _ ->
|
||||
delete()
|
||||
deleteSelectedTracks()
|
||||
}.show()
|
||||
} else {
|
||||
delete()
|
||||
deleteSelectedTracks()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,9 +283,9 @@ open class TrackCollectionFragment(
|
||||
return true
|
||||
} else if (item.itemId == R.id.menu_item_share) {
|
||||
shareHandler.createShare(
|
||||
this@TrackCollectionFragment, getSelectedTracks(),
|
||||
refreshListView, cancellationToken!!,
|
||||
navArgs.id
|
||||
fragment = this@TrackCollectionFragment,
|
||||
tracks = getSelectedTracks(),
|
||||
additionalId = navArgs.id
|
||||
)
|
||||
return true
|
||||
}
|
||||
@ -294,46 +294,10 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cancellationToken!!.cancel()
|
||||
rxBusSubscription.dispose()
|
||||
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
|
||||
*/
|
||||
@ -364,25 +328,65 @@ open class TrackCollectionFragment(
|
||||
|
||||
// Need a valid id to recurse sub directories stuff
|
||||
if (hasSubFolders && navArgs.id != null) {
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
mediaPlayerManager.playTracksAndToast(
|
||||
fragment = this,
|
||||
id = navArgs.id!!,
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||
id = navArgs.id!!,
|
||||
shuffle = shuffle,
|
||||
isArtist = isArtist
|
||||
)
|
||||
} else {
|
||||
downloadHandler.addTracksToMediaController(
|
||||
mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs = getAllSongs(),
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||
shuffle = shuffle,
|
||||
playlistName = navArgs.playlistName,
|
||||
fragment = this
|
||||
shuffle = shuffle
|
||||
)
|
||||
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")
|
||||
private fun getAllSongs(): List<Track> {
|
||||
@ -403,7 +407,7 @@ open class TrackCollectionFragment(
|
||||
|
||||
// Display toast: N tracks selected
|
||||
val toastResId = R.string.select_album_n_selected
|
||||
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
|
||||
toast(getString(toastResId, selectedCount.coerceAtLeast(0)))
|
||||
}
|
||||
|
||||
@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 = {
|
||||
|
||||
Timber.i("Received list")
|
||||
@ -606,48 +579,19 @@ open class TrackCollectionFragment(
|
||||
|
||||
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onContextMenuItemSelected(
|
||||
menuItem: MenuItem,
|
||||
item: MusicDirectory.Child
|
||||
): Boolean {
|
||||
val songs = getClickedSong(item)
|
||||
|
||||
when (menuItem.itemId) {
|
||||
R.id.song_menu_play_now -> {
|
||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true)
|
||||
}
|
||||
R.id.song_menu_play_next -> {
|
||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true)
|
||||
}
|
||||
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
|
||||
val tracks = getClickedSong(item)
|
||||
|
||||
return ContextMenuUtil.handleContextMenuTracks(
|
||||
menuItem = menuItem,
|
||||
tracks = tracks,
|
||||
mediaPlayerManager = mediaPlayerManager,
|
||||
fragment = this
|
||||
)
|
||||
}
|
||||
|
||||
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
||||
|
@ -25,11 +25,11 @@ import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
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.moire.ultrasonic.NavigationGraphDirections
|
||||
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.service.MusicServiceFactory.getMusicService
|
||||
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.CacheCleaner
|
||||
import org.moire.ultrasonic.util.CancellationToken
|
||||
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.InfoDialog
|
||||
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.
|
||||
*/
|
||||
class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
|
||||
private var playlistsListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var playlistAdapter: ArrayAdapter<Playlist>? = null
|
||||
|
||||
private val downloadHandler by inject<DownloadHandler>()
|
||||
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -115,7 +113,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
override fun doInBackground(): List<Playlist> {
|
||||
val musicService = getMusicService()
|
||||
val playlists = musicService.getPlaylists(refresh)
|
||||
if (!isOffline()) CacheCleaner().cleanPlaylists(playlists)
|
||||
val cacheCleaner: CacheCleaner by inject()
|
||||
if (!isOffline()) cacheCleaner.cleanPlaylists(playlists)
|
||||
return playlists
|
||||
}
|
||||
|
||||
@ -147,8 +146,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
|
||||
when (menuItem.itemId) {
|
||||
R.id.playlist_menu_pin -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.PIN,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.PIN,
|
||||
fragment = this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
@ -157,8 +156,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.playlist_menu_unpin -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.UNPIN,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.UNPIN,
|
||||
fragment = this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
@ -167,8 +166,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.playlist_menu_download -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.DOWNLOAD,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.DOWNLOAD,
|
||||
fragment = this,
|
||||
id = playlist.id,
|
||||
name = playlist.name,
|
||||
@ -227,7 +226,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
playlistAdapter!!.remove(playlist)
|
||||
playlistAdapter!!.notifyDataSetChanged()
|
||||
toast(
|
||||
context,
|
||||
resources.getString(R.string.menu_deleted_playlist, playlist.name)
|
||||
)
|
||||
}
|
||||
@ -246,7 +244,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(context, msg, false)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
@ -310,7 +308,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
override fun done(result: Any?) {
|
||||
load(true)
|
||||
toast(
|
||||
context,
|
||||
resources.getString(R.string.playlist_updated_info, playlist.name)
|
||||
)
|
||||
}
|
||||
@ -329,7 +326,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
||||
),
|
||||
getErrorMessage(error)
|
||||
)
|
||||
toast(context, msg, false)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
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.moire.ultrasonic.NavigationGraphDirections
|
||||
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.MusicServiceFactory
|
||||
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.CancellationToken
|
||||
import org.moire.ultrasonic.util.DownloadAction
|
||||
import org.moire.ultrasonic.util.DownloadUtil
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.LoadingTask
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
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.
|
||||
*/
|
||||
class SharesFragment : Fragment(), KoinComponent {
|
||||
class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||
private var refreshSharesListView: SwipeRefreshLayout? = null
|
||||
private var sharesListView: ListView? = null
|
||||
private var emptyTextView: View? = null
|
||||
private var shareAdapter: ShareAdapter? = null
|
||||
private val downloadHandler: DownloadHandler by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
@ -133,8 +134,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
val share = sharesListView!!.getItemAtPosition(info.position) as Share
|
||||
when (menuItem.itemId) {
|
||||
R.id.share_menu_pin -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.PIN,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.PIN,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
name = share.name,
|
||||
@ -143,8 +144,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_unpin -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.UNPIN,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.UNPIN,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
name = share.name,
|
||||
@ -153,8 +154,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_download -> {
|
||||
downloadHandler.justDownload(
|
||||
DownloadAction.DOWNLOAD,
|
||||
DownloadUtil.justDownload(
|
||||
action = DownloadAction.DOWNLOAD,
|
||||
fragment = this,
|
||||
id = share.id,
|
||||
name = share.name,
|
||||
@ -163,22 +164,20 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_now -> {
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
mediaPlayerManager.playTracksAndToast(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
id = share.id,
|
||||
name = share.name,
|
||||
shuffle = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_shuffled -> {
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
mediaPlayerManager.playTracksAndToast(
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
id = share.id,
|
||||
name = share.name,
|
||||
shuffle = true,
|
||||
)
|
||||
}
|
||||
@ -214,8 +213,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
override fun done(result: Any?) {
|
||||
shareAdapter!!.remove(share)
|
||||
shareAdapter!!.notifyDataSetChanged()
|
||||
Util.toast(
|
||||
context,
|
||||
toast(
|
||||
resources.getString(R.string.menu_deleted_share, share.name)
|
||||
)
|
||||
}
|
||||
@ -237,7 +235,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
Util.toast(context, msg, false)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||
@ -315,8 +313,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
|
||||
override fun done(result: Any?) {
|
||||
load(true)
|
||||
Util.toast(
|
||||
context,
|
||||
toast(
|
||||
resources.getString(R.string.playlist_updated_info, share.name)
|
||||
)
|
||||
}
|
||||
@ -338,7 +335,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
getErrorMessage(error)
|
||||
)
|
||||
}
|
||||
Util.toast(context, msg, false)
|
||||
toast(msg, false)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
@ -190,17 +190,17 @@ class ImageLoader(
|
||||
if (artist.coverArt == null) return
|
||||
val key = FileUtil.getArtistArtKey(artist.name, false)
|
||||
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
|
||||
*/
|
||||
fun cacheCoverArt(track: Track) {
|
||||
cacheCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
|
||||
fun downloadCoverArt(track: 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
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -56,8 +56,10 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
|
||||
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(parts[1])
|
||||
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 {
|
||||
it.cacheCoverArt(parts[0], albumArtFile)
|
||||
it.downloadCoverArt(parts[0], albumArtFile)
|
||||
}
|
||||
val file = File(albumArtFile)
|
||||
if (!file.exists()) return null
|
||||
|
@ -23,7 +23,7 @@ import android.widget.RemoteViews
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
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 timber.log.Timber
|
||||
|
||||
@ -233,7 +233,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
||||
|
||||
// Emulate media button clicks.
|
||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
||||
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_KEY_EVENT,
|
||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
||||
@ -241,12 +241,12 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
||||
flags = 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// needed starting Android 12 (S = 31)
|
||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||
flags = PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
|
||||
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
|
||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
||||
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_KEY_EVENT,
|
||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
|
||||
@ -254,7 +254,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
||||
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags)
|
||||
views.setOnClickPendingIntent(R.id.control_next, pendingIntent)
|
||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
||||
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_KEY_EVENT,
|
||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
||||
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
@ -10,20 +10,19 @@ package org.moire.ultrasonic.receiver
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
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 timber.log.Timber
|
||||
|
||||
class UltrasonicIntentReceiver : BroadcastReceiver() {
|
||||
private val lifecycleSupport = inject<MediaPlayerLifecycleSupport>(
|
||||
MediaPlayerLifecycleSupport::class.java
|
||||
)
|
||||
class UltrasonicIntentReceiver : BroadcastReceiver(), KoinComponent {
|
||||
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intentAction = intent.action
|
||||
Timber.i("Received Ultrasonic Intent: %s", intentAction)
|
||||
try {
|
||||
lifecycleSupport.value.receiveIntent(intent)
|
||||
lifecycleSupport.receiveIntent(intent)
|
||||
if (isOrderedBroadcast) {
|
||||
abortBroadcast()
|
||||
}
|
||||
|
@ -315,6 +315,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
return musicService.getStreamUrl(id, maxBitRate, format)
|
||||
}
|
||||
|
||||
override fun isJukeboxAvailable(): Boolean {
|
||||
return musicService.isJukeboxAvailable()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||
return musicService.updateJukeboxPlaylist(ids)
|
||||
|
@ -29,8 +29,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
@ -61,6 +63,7 @@ private const val CHECK_INTERVAL = 5000L
|
||||
class DownloadService : Service(), KoinComponent {
|
||||
private var scope: CoroutineScope? = null
|
||||
private val storageMonitor: ExternalStorageMonitor by inject()
|
||||
private val cacheCleaner: CacheCleaner by inject()
|
||||
private val binder: IBinder = SimpleServiceBinder(this)
|
||||
|
||||
private var isInForeground = false
|
||||
@ -153,7 +156,7 @@ class DownloadService : Service(), KoinComponent {
|
||||
|
||||
// Stop Executor service when done downloading
|
||||
if (activeDownloads.isEmpty()) {
|
||||
CacheCleaner().cleanSpace()
|
||||
cacheCleaner.cleanSpace()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
@ -279,7 +282,17 @@ class DownloadService : Service(), KoinComponent {
|
||||
updateSaveFlag: Boolean = false
|
||||
) {
|
||||
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
|
||||
// if needed
|
||||
var filteredTracks = if (updateSaveFlag) {
|
||||
@ -384,8 +397,14 @@ class DownloadService : Service(), KoinComponent {
|
||||
failedList.clear()
|
||||
}
|
||||
|
||||
fun delete(track: Track) {
|
||||
private fun delete(track: Track) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
deleteAsync(track)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteAsync(track: Track) {
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
|
||||
failedList[track.id]?.let { downloadQueue.remove(it) }
|
||||
cancelDownload(track)
|
||||
@ -394,7 +413,8 @@ class DownloadService : Service(), KoinComponent {
|
||||
Storage.delete(track.getCompleteFile())
|
||||
Storage.delete(track.getPinnedFile())
|
||||
postState(track, DownloadState.IDLE)
|
||||
CacheCleaner().cleanDatabaseSelective(track)
|
||||
val cacheCleaner: CacheCleaner by inject(CacheCleaner::class.java)
|
||||
cacheCleaner.cleanDatabaseSelective(track)
|
||||
Util.scanMedia(track.getPinnedFile())
|
||||
}
|
||||
}
|
||||
@ -409,22 +429,38 @@ class DownloadService : Service(), KoinComponent {
|
||||
tracks.forEach(::delete)
|
||||
}
|
||||
|
||||
fun unpin(track: Track) {
|
||||
// Update Pinned flag of items in progress
|
||||
downloadQueue.get(track.id)?.pinned = false
|
||||
activeDownloads[track.id]?.downloadTrack?.pinned = false
|
||||
failedList[track.id]?.pinned = false
|
||||
suspend fun unpinAsync(tracks: List<Track>) {
|
||||
tracks.forEach { unpinAsync(it) }
|
||||
}
|
||||
|
||||
val pinnedFile = track.getPinnedFile()
|
||||
if (!Storage.isPathExists(pinnedFile)) return
|
||||
val file = Storage.getFromPath(track.getPinnedFile()) ?: return
|
||||
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)
|
||||
suspend fun deleteAsync(tracks: List<Track>) {
|
||||
tracks.forEach { deleteAsync(it) }
|
||||
}
|
||||
|
||||
private fun unpin(track: Track) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
unpinAsync(track)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
@ -256,7 +256,7 @@ class DownloadTask(
|
||||
|
||||
// Download the largest size that we can display in the UI
|
||||
imageLoaderProvider.executeOn { imageLoader ->
|
||||
imageLoader.cacheCoverArt(this)
|
||||
imageLoader.downloadCoverArt(this)
|
||||
// Cache small copies of the Artist picture
|
||||
directArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||
compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||
|
@ -1,13 +1,12 @@
|
||||
/*
|
||||
* AutoMediaBrowserCallback.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* MediaLibrarySessionCallback.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* 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.Bundle
|
||||
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.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.guava.await
|
||||
import kotlinx.coroutines.guava.future
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
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.SearchResult
|
||||
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.buildMediaItem
|
||||
import org.moire.ultrasonic.util.toMediaItem
|
||||
@ -98,10 +98,12 @@ const val PLAY_COMMAND = "play "
|
||||
* MediaBrowserService implementation for e.g. Android Auto
|
||||
*/
|
||||
@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 playbackStateSerializer: PlaybackStateSerializer by inject()
|
||||
|
||||
private val serviceJob = SupervisorJob()
|
||||
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) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Timber.d("Car app library available, observing CarConnection")
|
||||
@ -251,7 +272,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
|
||||
|
||||
var lastCarConnectionType = -1
|
||||
|
||||
CarConnection(applicationContext).type.observeForever {
|
||||
CarConnection(UApp.applicationContext()).type.observeForever {
|
||||
if (lastCarConnectionType == it)
|
||||
return@observeForever
|
||||
|
@ -16,8 +16,6 @@ import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
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.subsonic.ImageLoaderProvider
|
||||
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
|
||||
*/
|
||||
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 val playbackStateSerializer by inject<PlaybackStateSerializer>()
|
||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
|
||||
private var created = false
|
||||
private var headsetEventReceiver: BroadcastReceiver? = null
|
||||
@ -70,7 +70,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
|
||||
registerHeadsetReceiver()
|
||||
|
||||
CacheCleaner().clean()
|
||||
cacheCleaner.clean()
|
||||
created = true
|
||||
ratingManager = RatingManager.instance
|
||||
Timber.i("LifecycleSupport created")
|
||||
@ -78,15 +78,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
|
||||
private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) {
|
||||
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,
|
||||
false
|
||||
)
|
||||
|
||||
mediaPlayerManager.restore(it, autoPlay)
|
||||
afterRestore?.run()
|
||||
}
|
||||
}
|
||||
@ -99,7 +94,6 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
applicationContext().unregisterReceiver(headsetEventReceiver)
|
||||
|
||||
imageLoaderProvider.clearImageLoader()
|
||||
UApp.instance!!.shutdownKoin()
|
||||
|
||||
created = false
|
||||
Timber.i("LifecycleSupport destroyed")
|
||||
|
@ -7,10 +7,11 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
@ -29,17 +30,20 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
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.inject
|
||||
import org.moire.ultrasonic.R
|
||||
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.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.playback.PlaybackService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.DownloadUtil
|
||||
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.util.launchWithToast
|
||||
import org.moire.ultrasonic.util.toMediaItem
|
||||
import org.moire.ultrasonic.util.toTrack
|
||||
import timber.log.Timber
|
||||
@ -56,12 +60,9 @@ private const val VOLUME_DELTA = 0.05f
|
||||
@Suppress("TooManyFunctions")
|
||||
class MediaPlayerManager(
|
||||
private val playbackStateSerializer: PlaybackStateSerializer,
|
||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
||||
val context: Context
|
||||
private val externalStorageMonitor: ExternalStorageMonitor
|
||||
) : KoinComponent {
|
||||
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private var created = false
|
||||
var suggestedPlaylistName: String? = null
|
||||
var keepScreenOn = false
|
||||
@ -73,8 +74,10 @@ class MediaPlayerManager(
|
||||
|
||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private var sessionToken =
|
||||
SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
||||
private var sessionToken = SessionToken(
|
||||
UApp.applicationContext(),
|
||||
ComponentName(UApp.applicationContext(), PlaybackService::class.java)
|
||||
)
|
||||
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
|
||||
@ -145,12 +148,11 @@ class MediaPlayerManager(
|
||||
Timber.w(error.toString())
|
||||
if (!isJukeboxEnabled) return
|
||||
|
||||
val context = UApp.applicationContext()
|
||||
mainScope.launch {
|
||||
Util.toast(
|
||||
context,
|
||||
toast(
|
||||
error.errorCode,
|
||||
false
|
||||
false,
|
||||
UApp.applicationContext()
|
||||
)
|
||||
}
|
||||
isJukeboxEnabled = false
|
||||
@ -199,7 +201,7 @@ class MediaPlayerManager(
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||
val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault
|
||||
val jukebox = it.jukeboxByDefault
|
||||
// Remove all songs when changing servers before turning on Jukebox.
|
||||
// Jukebox wouldn't find the songs on the new server.
|
||||
if (jukebox) controller?.clearMediaItems()
|
||||
@ -246,10 +248,10 @@ class MediaPlayerManager(
|
||||
|
||||
private fun createMediaController(onCreated: () -> Unit) {
|
||||
mediaControllerFuture = MediaController.Builder(
|
||||
context,
|
||||
UApp.applicationContext(),
|
||||
sessionToken
|
||||
)
|
||||
// Specify mainThread explicitely
|
||||
// Specify mainThread explicitly
|
||||
.setApplicationLooper(Looper.getMainLooper())
|
||||
.buildAsync()
|
||||
|
||||
@ -320,17 +322,15 @@ class MediaPlayerManager(
|
||||
externalStorageMonitor.onDestroy()
|
||||
DownloadService.requestStop()
|
||||
created = false
|
||||
Timber.i("MediaPlayerController destroyed")
|
||||
Timber.i("MediaPlayerManager destroyed")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun restore(
|
||||
state: PlaybackState,
|
||||
autoPlay: Boolean,
|
||||
newPlaylist: Boolean
|
||||
autoPlay: Boolean
|
||||
) {
|
||||
val insertionMode = if (newPlaylist) InsertionMode.CLEAR
|
||||
else InsertionMode.APPEND
|
||||
val insertionMode = InsertionMode.APPEND
|
||||
|
||||
addToPlaylist(
|
||||
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
|
||||
var isShufflePlayEnabled: Boolean
|
||||
get() = controller?.shuffleModeEnabled == true
|
||||
@ -649,21 +723,6 @@ class MediaPlayerManager(
|
||||
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) {
|
||||
val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA
|
||||
var gain = controller?.volume ?: return
|
||||
@ -676,7 +735,7 @@ class MediaPlayerManager(
|
||||
/*
|
||||
* Sets the rating of the current track
|
||||
*/
|
||||
fun setRating(rating: Rating) {
|
||||
private fun setRating(rating: Rating) {
|
||||
if (controller is MediaController) {
|
||||
(controller as MediaController).setRating(rating)
|
||||
}
|
||||
@ -724,7 +783,7 @@ class MediaPlayerManager(
|
||||
val currentMediaItemIndex: Int
|
||||
get() = controller?.currentMediaItemIndex ?: -1
|
||||
|
||||
fun getCurrentShuffleIndex(): Int {
|
||||
private fun getCurrentShuffleIndex(): Int {
|
||||
val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1
|
||||
return getShuffledIndexOf(currentMediaItemIndex)
|
||||
}
|
||||
@ -768,9 +827,8 @@ class MediaPlayerManager(
|
||||
* in the shuffled timeline.
|
||||
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
|
||||
*/
|
||||
fun getShuffledIndexOf(searchPosition: Int): Int {
|
||||
return getWindowIndexWhere(false) {
|
||||
_, windowIndex ->
|
||||
private fun getShuffledIndexOf(searchPosition: Int): Int {
|
||||
return getWindowIndexWhere(false) { _, windowIndex ->
|
||||
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.
|
||||
*/
|
||||
fun getUnshuffledIndexOf(shufflePosition: Int): Int {
|
||||
return getWindowIndexWhere(true) {
|
||||
count, _ ->
|
||||
return getWindowIndexWhere(true) { count, _ ->
|
||||
count == shufflePosition
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +138,8 @@ interface MusicService {
|
||||
@Throws(Exception::class)
|
||||
fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String?
|
||||
|
||||
fun isJukeboxAvailable(): Boolean
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus
|
||||
|
||||
|
@ -335,6 +335,10 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
override fun isJukeboxAvailable(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||
throw OfflineException("Jukebox not available in offline mode")
|
||||
|
@ -1,10 +1,10 @@
|
||||
/*
|
||||
* PlaybackService.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
package org.moire.ultrasonic.playback
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
@ -42,15 +42,11 @@ import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.CachedDataSource
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
||||
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.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -68,7 +64,7 @@ class PlaybackService :
|
||||
private var equalizer: EqualizerController? = null
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private lateinit var librarySessionCallback: AutoMediaBrowserCallback
|
||||
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
|
||||
|
||||
private var rxBusSubscription = CompositeDisposable()
|
||||
|
||||
@ -115,6 +111,7 @@ class PlaybackService :
|
||||
isStarted = false
|
||||
stopForegroundRemoveNotification()
|
||||
stopSelf()
|
||||
instance = null
|
||||
}
|
||||
|
||||
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
|
||||
@ -142,7 +139,7 @@ class PlaybackService :
|
||||
actualBackend = desiredBackend
|
||||
|
||||
// Create browser interface
|
||||
librarySessionCallback = AutoMediaBrowserCallback()
|
||||
librarySessionCallback = MediaLibrarySessionCallback()
|
||||
|
||||
// This will need to use the AutoCalls
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
@ -1,7 +1,9 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import androidx.media3.session.MediaSession
|
||||
import java.io.Serializable
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.util.toMediaItem
|
||||
|
||||
/**
|
||||
* Represents the state of the Media Player implementation
|
||||
@ -17,3 +19,12 @@ data class PlaybackState(
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ import timber.log.Timber
|
||||
* This class is responsible for the serialization / deserialization
|
||||
* of the playlist and the player state (e.g. current playing number and play position)
|
||||
* to the filesystem.
|
||||
*
|
||||
* TODO: Should use: MediaItemsWithStartPosition
|
||||
*/
|
||||
class PlaybackStateSerializer : KoinComponent {
|
||||
|
||||
@ -56,7 +58,7 @@ class PlaybackStateSerializer : KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
fun serializeNow(
|
||||
private fun serializeNow(
|
||||
tracks: Iterable<Track>,
|
||||
currentPlayingIndex: Int,
|
||||
currentPlayingPosition: Int,
|
||||
@ -85,7 +87,10 @@ class PlaybackStateSerializer : KoinComponent {
|
||||
if (isDeserializing.get()) return
|
||||
ioScope.launch {
|
||||
try {
|
||||
deserializeNow(afterDeserialized)
|
||||
val state = deserializeNow()
|
||||
mainScope.launch {
|
||||
afterDeserialized(state)
|
||||
}
|
||||
isSetup.set(true)
|
||||
} catch (all: Exception) {
|
||||
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>(
|
||||
context, Constants.FILENAME_PLAYLIST_SER
|
||||
) ?: return
|
||||
) ?: return null
|
||||
|
||||
Timber.i(
|
||||
"Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b",
|
||||
@ -108,9 +113,7 @@ class PlaybackStateSerializer : KoinComponent {
|
||||
state.shufflePlay
|
||||
)
|
||||
|
||||
mainScope.launch {
|
||||
afterDeserialized(state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -51,7 +51,7 @@ import timber.log.Timber
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
open class RESTMusicService(
|
||||
val subsonicAPIClient: SubsonicAPIClient,
|
||||
private val subsonicAPIClient: SubsonicAPIClient,
|
||||
private val activeServerProvider: ActiveServerProvider
|
||||
) : MusicService {
|
||||
|
||||
@ -504,6 +504,11 @@ open class RESTMusicService(
|
||||
builder.build()
|
||||
}
|
||||
|
||||
override fun isJukeboxAvailable(): Boolean {
|
||||
val username = activeServerProvider.getActiveServer().userName
|
||||
return getUser(username).jukeboxRole
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun updateJukeboxPlaylist(
|
||||
ids: List<String>?
|
||||
|
@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.data.ServerSetting
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
|
||||
class RxBus {
|
||||
@ -32,9 +33,9 @@ class RxBus {
|
||||
var activeServerChangingObservable: Observable<Int> =
|
||||
activeServerChangingPublisher
|
||||
|
||||
var activeServerChangedPublisher: PublishSubject<Int> =
|
||||
var activeServerChangedPublisher: PublishSubject<ServerSetting> =
|
||||
PublishSubject.create()
|
||||
var activeServerChangedObservable: Observable<Int> =
|
||||
var activeServerChangedObservable: Observable<ServerSetting> =
|
||||
activeServerChangedPublisher.observeOn(mainThread())
|
||||
|
||||
val themeChangedEventPublisher: PublishSubject<Unit> =
|
||||
|
@ -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
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -20,8 +19,7 @@ import timber.log.Timber
|
||||
/**
|
||||
* Handles the lifetime of the Image Loader
|
||||
*/
|
||||
class
|
||||
ImageLoaderProvider(val context: Context) :
|
||||
class ImageLoaderProvider :
|
||||
KoinComponent,
|
||||
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private var imageLoader: ImageLoader? = null
|
||||
|
@ -1,19 +1,19 @@
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.content.Context
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
/**
|
||||
* Utility class for checking the availability of the network and storage
|
||||
*/
|
||||
class NetworkAndStorageChecker(val context: Context) {
|
||||
class NetworkAndStorageChecker {
|
||||
fun warnIfNetworkOrStorageUnavailable() {
|
||||
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()) {
|
||||
Util.toast(context, R.string.select_album_no_network)
|
||||
Util.toast(R.string.select_album_no_network, true, UApp.applicationContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -17,27 +16,27 @@ import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import java.util.Locale
|
||||
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.domain.Share
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
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.FragmentBackgroundTask
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.ShareDetails
|
||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||
import org.moire.ultrasonic.util.Util.getString
|
||||
import org.moire.ultrasonic.util.Util.ifNotNull
|
||||
|
||||
/**
|
||||
* This class handles sharing items in the media library
|
||||
*/
|
||||
class ShareHandler(val context: Context) {
|
||||
class ShareHandler {
|
||||
private var shareDescription: EditText? = null
|
||||
private var timeSpanPicker: TimeSpanPicker? = null
|
||||
private var shareOnServerCheckBox: CheckBox? = null
|
||||
@ -51,21 +50,26 @@ class ShareHandler(val context: Context) {
|
||||
fun share(
|
||||
fragment: Fragment,
|
||||
shareDetails: ShareDetails,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String?
|
||||
) {
|
||||
val task: BackgroundTask<Share?> = object : FragmentBackgroundTask<Share?>(
|
||||
fragment.requireActivity(),
|
||||
true,
|
||||
swipe,
|
||||
cancellationToken
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Share? {
|
||||
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
|
||||
scope.launch {
|
||||
val share = createShareOnServer(shareDetails, additionalId)
|
||||
startActivityForShare(share, shareDetails, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createShareOnServer(
|
||||
shareDetails: ShareDetails,
|
||||
additionalId: String?
|
||||
): Share? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
return@withContext try {
|
||||
|
||||
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()) {
|
||||
additionalId.ifNotNull {
|
||||
ids.add(it)
|
||||
@ -86,78 +90,80 @@ class ShareHandler(val context: Context) {
|
||||
val shares =
|
||||
musicService.createShare(ids, shareDetails.Description, timeInMillis)
|
||||
|
||||
return shares[0]
|
||||
}
|
||||
|
||||
override fun done(result: Share?) {
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
// Return the share
|
||||
shares[0]
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
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(
|
||||
fragment: Fragment,
|
||||
tracks: List<Track?>?,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String? = null
|
||||
) {
|
||||
val askForDetails = Settings.shouldAskForShareDetails
|
||||
val shareDetails = ShareDetails()
|
||||
shareDetails.Entries = tracks
|
||||
if (askForDetails) {
|
||||
showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
showDialog(fragment, shareDetails, additionalId)
|
||||
} else {
|
||||
shareDetails.Description = Settings.defaultShareDescription
|
||||
shareDetails.Expiration = System.currentTimeMillis() +
|
||||
Settings.defaultShareExpirationInMillis
|
||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
share(fragment, shareDetails, additionalId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@SuppressLint("InflateParams")
|
||||
private fun showDialog(
|
||||
fragment: Fragment,
|
||||
shareDetails: ShareDetails,
|
||||
swipe: SwipeRefreshLayout?,
|
||||
cancellationToken: CancellationToken,
|
||||
additionalId: String?
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
// Handle the visibility based on shareDetails.Entries size
|
||||
if (shareDetails.Entries.size == 1) {
|
||||
// For single songs the sharing may be done by text only
|
||||
shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ ->
|
||||
updateVisibility()
|
||||
}
|
||||
|
||||
shareOnServerCheckBox?.isChecked = Settings.shareOnServer
|
||||
} else {
|
||||
shareOnServerCheckBox?.isVisible = false
|
||||
}
|
||||
|
||||
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())
|
||||
builder.setTitle(R.string.share_set_share_options)
|
||||
|
||||
@ -214,7 +259,7 @@ class ShareHandler(val context: Context) {
|
||||
Settings.shareOnServer = shareDetails.ShareOnServer
|
||||
}
|
||||
|
||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
||||
share(fragment, shareDetails, additionalId)
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||
@ -224,35 +269,12 @@ class ShareHandler(val context: Context) {
|
||||
builder.setView(layout)
|
||||
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 ->
|
||||
timeSpanPicker!!.isEnabled = !b
|
||||
}
|
||||
|
||||
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()
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun updateVisibility() {
|
||||
|
@ -16,7 +16,7 @@ class VideoPlayer {
|
||||
companion object {
|
||||
fun playVideo(context: Context, track: Track?) {
|
||||
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
|
||||
}
|
||||
try {
|
||||
@ -32,7 +32,7 @@ class VideoPlayer {
|
||||
)
|
||||
context.startActivity(intent)
|
||||
} catch (all: Exception) {
|
||||
Util.toast(context, all.toString(), false)
|
||||
Util.toast(all.toString(), false, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import android.system.Os
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.guava.future
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
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.domain.Playlist
|
||||
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.getCompleteFile
|
||||
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
||||
@ -38,7 +37,6 @@ import timber.log.Timber
|
||||
*/
|
||||
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent {
|
||||
|
||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
||||
private val activeServerProvider by inject<ActiveServerProvider>()
|
||||
|
||||
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
|
||||
@ -235,16 +233,14 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
|
||||
|
||||
private fun findFilesToNotDelete(): Set<String> {
|
||||
val filesToNotDelete: MutableSet<String> = HashSet(5)
|
||||
val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
|
||||
val playlist = mainScope.future { mediaPlayerManager.playlist }.get()
|
||||
for (item in playlist) {
|
||||
val track = item.toTrack()
|
||||
// We just take the last published playlist from RX
|
||||
val playlist = RxBus.playlistObservable.blockingLast()
|
||||
for (track in playlist) {
|
||||
filesToNotDelete.add(track.getPartialFile())
|
||||
filesToNotDelete.add(track.getCompleteFile())
|
||||
filesToNotDelete.add(track.getPinnedFile())
|
||||
}
|
||||
|
||||
filesToNotDelete.add(musicDirectory.path)
|
||||
return filesToNotDelete
|
||||
}
|
||||
|
@ -37,4 +37,5 @@ object Constants {
|
||||
const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
|
||||
const val ALBUM_ART_FILE = "folder.jpeg"
|
||||
const val RESULT_CLOSE_ALL = 1337
|
||||
const val MAX_SONGS_RECURSIVE = 500
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -9,14 +9,12 @@ package org.moire.ultrasonic.util
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import timber.log.Timber
|
||||
|
||||
@ -30,53 +28,50 @@ object CoroutinePatterns {
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.executeTaskWithToast(
|
||||
task: suspend CoroutineScope.() -> Unit,
|
||||
successString: () -> String?
|
||||
): Job {
|
||||
fun CoroutineScope.launchWithToast(
|
||||
block: suspend CoroutineScope.() -> String?
|
||||
) {
|
||||
// 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
|
||||
job.invokeOnCompletion {
|
||||
deferred.invokeOnCompletion {
|
||||
val toastString = if (it != null && it !is CancellationException) {
|
||||
CommunicationError.getErrorMessage(it)
|
||||
} else {
|
||||
successString()
|
||||
null
|
||||
}
|
||||
|
||||
// Return early if nothing to post
|
||||
if (toastString == null) return@invokeOnCompletion
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
Util.toast(UApp.applicationContext(), toastString)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
val successString = toastString ?: deferred.await()
|
||||
if (successString != null) {
|
||||
Util.toast(successString, UApp.applicationContext())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
@ -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
|
||||
}
|
@ -136,10 +136,6 @@ object Settings {
|
||||
val seekIntervalMillis: Long
|
||||
get() = (seekInterval / 1000).toLong()
|
||||
|
||||
@JvmStatic
|
||||
var mediaButtonsEnabled
|
||||
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)
|
||||
|
||||
var resumePlayOnHeadphonePlug
|
||||
by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true)
|
||||
|
||||
|
@ -41,7 +41,7 @@ object Storage {
|
||||
if (rootNotFoundError) {
|
||||
Settings.customCacheLocation = false
|
||||
Settings.cacheLocationUri = ""
|
||||
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
|
||||
Util.toast(R.string.settings_cache_location_error, true, UApp.applicationContext())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
|
||||
AdapterView.OnItemSelectedListener {
|
||||
private val timeSpanEditText: EditText
|
||||
private val timeSpanSpinner: Spinner
|
||||
private val timeSpanDisableCheckbox: CheckBox
|
||||
val timeSpanDisableCheckbox: CheckBox
|
||||
private var mTimeSpan: Long = -1L
|
||||
private val adapter: ArrayAdapter<CharSequence>
|
||||
private val dialog: View
|
||||
|
@ -8,7 +8,6 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@ -37,12 +36,15 @@ import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.AnyRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import java.io.Closeable
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
@ -107,33 +109,41 @@ object Util {
|
||||
context.getString(R.string.setting_key_theme_dark) -> {
|
||||
R.style.UltrasonicTheme_Dark
|
||||
}
|
||||
|
||||
context.getString(R.string.setting_key_theme_black) -> {
|
||||
R.style.UltrasonicTheme_Black
|
||||
}
|
||||
|
||||
context.getString(R.string.setting_key_theme_light) -> {
|
||||
R.style.UltrasonicTheme_Light
|
||||
}
|
||||
|
||||
else -> {
|
||||
R.style.UltrasonicTheme_DayNight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getString(@StringRes resId: Int): String {
|
||||
return applicationContext().resources.getString(resId)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) {
|
||||
toast(context, context!!.getString(messageId), shortDuration)
|
||||
fun toast(messageId: Int, shortDuration: Boolean = true, context: Context?) {
|
||||
toast(applicationContext().getString(messageId), shortDuration, context)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toast(context: Context?, message: CharSequence?) {
|
||||
toast(context, message, true)
|
||||
fun toast(message: CharSequence, context: Context?) {
|
||||
toast(message, true, context)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@SuppressLint("ShowToast") // Invalid warning
|
||||
fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) {
|
||||
// If called after doing some background processing, our context might have expired!
|
||||
// Toast needs a real context or it will throw a IllegalAccessException
|
||||
// We wrap it in a try-catch block, because if called after doing
|
||||
// some background processing, our context might have expired!
|
||||
fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) {
|
||||
try {
|
||||
if (toast == null) {
|
||||
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.
|
||||
* For instance:
|
||||
@ -479,7 +505,7 @@ object Util {
|
||||
|
||||
@JvmStatic
|
||||
fun isNullOrWhiteSpace(string: String?): Boolean {
|
||||
return string == null || string.isEmpty() || string.trim { it <= ' ' }.isEmpty()
|
||||
return string.isNullOrEmpty() || string.trim { it <= ' ' }.isEmpty()
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@ -504,18 +530,22 @@ object Util {
|
||||
seconds
|
||||
)
|
||||
}
|
||||
|
||||
hours > 0 -> {
|
||||
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
minutes >= DEGRADE_PRECISION_AFTER -> {
|
||||
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
minutes > 0 -> String.format(
|
||||
Locale.getDefault(),
|
||||
"%d:%02d",
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
|
||||
else -> String.format(Locale.getDefault(), "0:%02d", seconds)
|
||||
}
|
||||
}
|
||||
@ -557,7 +587,7 @@ object Util {
|
||||
val requestPermissionLauncher =
|
||||
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
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(
|
||||
notificationManagerCompat: NotificationManagerCompat,
|
||||
id: Int,
|
||||
@ -583,8 +614,8 @@ object Util {
|
||||
notificationManagerCompat.notify(id, notification)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("DEPRECATION")
|
||||
fun getVersionName(context: Context): String? {
|
||||
var versionName: String? = null
|
||||
val pm = context.packageManager
|
||||
@ -838,4 +869,11 @@ object Util {
|
||||
Timber.d("${it.key}: ${it.value}")
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.navigateToCurrent() {
|
||||
if (Settings.shouldTransitionOnPlayback) {
|
||||
findNavController().popBackStack(R.id.playerFragment, true)
|
||||
findNavController().navigate(R.id.playerFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">Načítám…</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.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.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.wait">Chvilku strpení…</string>
|
||||
<string name="button_bar.bookmarks">Záložky</string>
|
||||
<string name="button_bar.browse">Knihovna médií</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_wifi">Max Bitrate - wi-fi</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_105000">105 sekund</string>
|
||||
<string name="settings.network_timeout_120000">120 sekund</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -8,7 +7,6 @@
|
||||
<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_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.browse">Medienbibliothek</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_wifi">Max. Bitrate - WLAN</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_105000">105 Sekunden</string>
|
||||
<string name="settings.network_timeout_120000">120 Sekunden</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -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.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.wait">Por favor espera…</string>
|
||||
<string name="button_bar.bookmarks">Marcadores</string>
|
||||
<string name="button_bar.browse">Biblioteca</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_wifi">Bitrate máximo - Wi-Fi</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_105000">105 segundos</string>
|
||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.unsupported_api">L’API 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>
|
||||
@ -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.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.wait">Veuillez patienter…</string>
|
||||
<string name="button_bar.bookmarks">Signets</string>
|
||||
<string name="button_bar.browse">Bibliothèque musicale</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_wifi">Débit maximal - Wi-Fi</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_105000">105 secondes</string>
|
||||
<string name="settings.network_timeout_120000">120 secondes</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -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.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.wait">Por favor agarde…</string>
|
||||
<string name="button_bar.browse">Biblioteca</string>
|
||||
<string name="button_bar.chat">Chat</string>
|
||||
<string name="button_bar.now_playing">Reproducindo agora</string>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">Betöltés…</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.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.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.wait">Kérem várjon!…</string>
|
||||
<string name="button_bar.bookmarks">Könyvjelzők</string>
|
||||
<string name="button_bar.browse">Médiakönyvtár</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_wifi">Max. bitráta - Wi-Fi kapcsolat</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_105000">105 másodperc</string>
|
||||
<string name="settings.network_timeout_120000">120 másodperc</string>
|
||||
|
@ -1,13 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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.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_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.browse">Libreria multimediale</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_wifi">Bitrate Max - Wi-Fi</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_105000">105 seconds</string>
|
||||
<string name="settings.network_timeout_120000">120 seconds</string>
|
||||
|
@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.parse_error">応答が確認できません。サーバーのアドレスを確認してください。</string>
|
||||
<string name="background_task.ssl_cert_error">HTTPS証明書エラー: %1$s.</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.browse">メディアライブラリ</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_80">80 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_75000">75秒</string>
|
||||
<string name="settings.network_timeout_90000">90秒</string>
|
||||
|
@ -37,7 +37,6 @@
|
||||
<string name="buttons.stop">Stopp</string>
|
||||
<string name="button_bar.search">Søk</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="button_bar.browse">Mediebibliotek</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.parse_error">Forsto ikke svaret. Sjekk tjeneradressen.</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.now_playing">Spilles nå</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.max_artists">Maks. artister</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_90000">90 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="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.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="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>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">Bezig met laden…</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.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.ssl_cert_error">HTTPS-certificaatfout: %1$s.</string>
|
||||
<string name="background_task.ssl_error">SSL-verbindingsuitzondering. Controleer het servercertificaat.</string>
|
||||
<string name="background_task.wait">Even geduld…</string>
|
||||
<string name="button_bar.bookmarks">Bladwijzers</string>
|
||||
<string name="button_bar.browse">Mediabibliotheek</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_wifi">Max. bitsnelheid via wifi</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_105000">105 seconden</string>
|
||||
<string name="settings.network_timeout_120000">120 seconden</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="background_task.loading">Ładowanie…</string>
|
||||
<string name="background_task.network_error">Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później.</string>
|
||||
<string name="background_task.unsupported_api">API serwera w wersji v%1$s nie wspiera tej funkcjonalności.</string>
|
||||
<string name="background_task.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.</string>
|
||||
@ -8,7 +7,6 @@
|
||||
<string name="background_task.parse_error">Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera.</string>
|
||||
<string name="background_task.ssl_cert_error">Błąd certyfikatu HTTPS: %1$s.</string>
|
||||
<string name="background_task.ssl_error">Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.</string>
|
||||
<string name="background_task.wait">Proszę czekać…</string>
|
||||
<string name="button_bar.bookmarks">Zakładki</string>
|
||||
<string name="button_bar.browse">Biblioteka</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_wifi">Maksymalny bitrate dla połączenia Wi-Fi</string>
|
||||
<string name="settings.max_songs">Maksymalna ilość wyników - utwory</string>
|
||||
<string name="settings.media_button_summary">Reaguj na przyciski multimedialne telefonu, słuchawek i urządzeń Bluetooth</string>
|
||||
<string name="settings.media_button_title">Przyciski</string>
|
||||
<string name="settings.network_timeout">Przekroczenie limitu czasu sieci</string>
|
||||
<string name="settings.network_timeout_105000">105 sekund</string>
|
||||
<string name="settings.network_timeout_120000">120 sekund</string>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -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.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.wait">Por favor aguarde…</string>
|
||||
<string name="button_bar.bookmarks">Favoritos</string>
|
||||
<string name="button_bar.browse">Biblioteca de Mídia</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_wifi">Taxa Máxima de Bits - Wi-Fi</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_105000">105 segundos</string>
|
||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -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.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.wait">Por favor aguarde…</string>
|
||||
<string name="button_bar.bookmarks">Favoritos</string>
|
||||
<string name="button_bar.browse">Biblioteca de Mídia</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_wifi">Máx. de Taxa de Bits - Wi-Fi</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_105000">105 segundos</string>
|
||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.unsupported_api">Серверный api версии %1$s не поддерживает эту функцию.</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.ssl_cert_error">Ошибка сертификата HTTPS: %1$s.</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.browse">Медиа библиотека</string>
|
||||
<string name="button_bar.chat">Чат</string>
|
||||
@ -212,8 +210,6 @@
|
||||
<string name="settings.max_bitrate_unlimited">Неограниченный</string>
|
||||
<string name="settings.max_bitrate_wifi">Максимальный битрейт - Wi-Fi подключение</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_105000">105 секунд</string>
|
||||
<string name="settings.network_timeout_120000">120 секунд</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.unsupported_api">服务端 API v%1$s 不支持此功能。</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.ssl_cert_error">HTTPS 证书错误:%1$s.</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.browse">媒体库</string>
|
||||
<string name="button_bar.chat">聊天</string>
|
||||
@ -218,8 +216,6 @@
|
||||
<string name="settings.max_bitrate_unlimited">不限制</string>
|
||||
<string name="settings.max_bitrate_wifi">最大比特率 - WIFI</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_105000">105 秒</string>
|
||||
<string name="settings.network_timeout_120000">120 秒</string>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="background_task.loading">載入中…</string>
|
||||
<string name="button_bar.bookmarks">書籤</string>
|
||||
<string name="button_bar.browse">媒體庫</string>
|
||||
<string name="button_bar.now_playing">正在播放</string>
|
||||
@ -146,7 +145,6 @@
|
||||
<string name="language.nl">荷蘭語</string>
|
||||
<string name="download.jukebox_off">已關閉遠端控制,音樂將在手機上播放。</string>
|
||||
<string name="language.de">德語</string>
|
||||
<string name="background_task.wait">請稍候…</string>
|
||||
<string name="common.unpin">取消固定</string>
|
||||
<string name="download.playlist_name">輸入播放清單名稱:</string>
|
||||
<string name="main.albums_by_year">依照時間排列</string>
|
||||
|
@ -19,7 +19,6 @@
|
||||
<string name="setting_key.preload_count" translatable="false">preloadCount</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.media_buttons" translatable="false">mediaButtons</string>
|
||||
<string name="setting_key.scrobble" translatable="false">scrobble</string>
|
||||
<string name="setting_key.server_scaling" translatable="false">serverScaling</string>
|
||||
<string name="setting_key.wifi_required_for_download" translatable="false">wifiRequiredForDownload</string>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.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>
|
||||
@ -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.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.wait">Please wait…</string>
|
||||
<string name="button_bar.bookmarks">Bookmarks</string>
|
||||
<string name="button_bar.browse">Media Library</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_pinning">Max Bitrate - When pinning a song permanently</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_105000">105 seconds</string>
|
||||
<string name="settings.network_timeout_120000">120 seconds</string>
|
||||
|
@ -72,12 +72,6 @@
|
||||
a:summary="@string/settings.show_artist_picture_summary"
|
||||
a:title="@string/settings.show_artist_picture"
|
||||
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
|
||||
a:defaultValue="true"
|
||||
a:key="@string/setting_key.download_transition"
|
||||
|
Loading…
x
Reference in New Issue
Block a user