mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-18 18:17:43 +03:00
Merge branch 'refactorScopes' into 'develop'
Refactor Koin, Scopes & Lifecycles See merge request ultrasonic/ultrasonic!1130
This commit is contained in:
commit
de523a6451
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
@ -1,11 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
|
||||||
<value />
|
|
||||||
</option>
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
|
||||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
|
@ -70,50 +70,6 @@
|
|||||||
column="1"/>
|
column="1"/>
|
||||||
</issue>
|
</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
|
<issue
|
||||||
id="LabelFor"
|
id="LabelFor"
|
||||||
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||||
<service android:name=".playback.PlaybackService"
|
<service android:name=".service.PlaybackService"
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
android:foregroundServiceType="mediaPlayback"
|
android:foregroundServiceType="mediaPlayback"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@ -107,6 +107,12 @@
|
|||||||
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".provider.UltrasonicAppWidgetProvider"
|
android:name=".provider.UltrasonicAppWidgetProvider"
|
||||||
android:label="Ultrasonic"
|
android:label="Ultrasonic"
|
||||||
@ -119,12 +125,6 @@
|
|||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/appwidget_info"/>
|
android:resource="@xml/appwidget_info"/>
|
||||||
</receiver>
|
</receiver>
|
||||||
<receiver android:name=".receiver.MediaButtonIntentReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter android:priority="2147483647">
|
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
||||||
|
@ -23,7 +23,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@ -39,6 +38,7 @@ import androidx.media3.common.Player.STATE_READY
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.navigateUp
|
import androidx.navigation.ui.navigateUp
|
||||||
import androidx.navigation.ui.onNavDestinationSelected
|
import androidx.navigation.ui.onNavDestinationSelected
|
||||||
@ -50,6 +50,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.androidx.scope.ScopeActivity
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -63,7 +64,6 @@ import org.moire.ultrasonic.service.MediaPlayerManager
|
|||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.InfoDialog
|
import org.moire.ultrasonic.util.InfoDialog
|
||||||
import org.moire.ultrasonic.util.LocaleHelper
|
import org.moire.ultrasonic.util.LocaleHelper
|
||||||
@ -81,7 +81,7 @@ import timber.log.Timber
|
|||||||
* onCreate/onResume/onDestroy methods...
|
* onCreate/onResume/onDestroy methods...
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class NavigationActivity : AppCompatActivity() {
|
class NavigationActivity : ScopeActivity() {
|
||||||
private var videoMenuItem: MenuItem? = null
|
private var videoMenuItem: MenuItem? = null
|
||||||
private var chatMenuItem: MenuItem? = null
|
private var chatMenuItem: MenuItem? = null
|
||||||
private var bookmarksMenuItem: MenuItem? = null
|
private var bookmarksMenuItem: MenuItem? = null
|
||||||
@ -485,15 +485,19 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
||||||
val downloadHandler: DownloadHandler by inject()
|
|
||||||
downloadHandler.addTracksToMediaController(
|
mediaPlayerManager.addToPlaylist(
|
||||||
songs = musicDirectory.getTracks(),
|
songs = musicDirectory.getTracks(),
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
fragment = currentFragment,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||||
playlistName = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (Settings.shouldTransitionOnPlayback) {
|
||||||
|
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
|
||||||
|
currentFragment.findNavController().navigate(R.id.playerFragment)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,10 @@ import android.widget.TextView
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -63,16 +67,21 @@ class ArtistRowBinder(
|
|||||||
|
|
||||||
if (showArtistPicture()) {
|
if (showArtistPicture()) {
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
imageLoaderProvider.executeOn {
|
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||||
it.loadImage(
|
|
||||||
view = holder.coverArt,
|
withContext(Dispatchers.Main) {
|
||||||
id = holder.coverArtId,
|
imageLoaderProvider.executeOn {
|
||||||
key = key,
|
it.loadImage(
|
||||||
large = false,
|
view = holder.coverArt,
|
||||||
size = 0,
|
id = holder.coverArtId,
|
||||||
defaultResourceId = R.drawable.ic_contact_picture
|
key = key,
|
||||||
)
|
large = false,
|
||||||
|
size = 0,
|
||||||
|
defaultResourceId = R.drawable.ic_contact_picture
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.coverArt.visibility = View.GONE
|
holder.coverArt.visibility = View.GONE
|
||||||
|
@ -31,7 +31,6 @@ import org.moire.ultrasonic.service.RxBus
|
|||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
||||||
const val INDICATOR_THICKNESS_DEFINITE = 10
|
const val INDICATOR_THICKNESS_DEFINITE = 10
|
||||||
@ -79,10 +78,6 @@ class TrackViewHolder(val view: View) :
|
|||||||
|
|
||||||
private var rxBusSubscription: CompositeDisposable? = null
|
private var rxBusSubscription: CompositeDisposable? = null
|
||||||
|
|
||||||
init {
|
|
||||||
Timber.v("New ViewHolder created")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
fun setSong(
|
fun setSong(
|
||||||
song: Track,
|
song: Track,
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -106,6 +104,30 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Active Server by its unique id
|
||||||
|
* @param serverId: The id of the desired server
|
||||||
|
*/
|
||||||
|
fun setActiveServerById(serverId: Int) {
|
||||||
|
val oldServerId = Settings.activeServer
|
||||||
|
if (oldServerId == serverId) return
|
||||||
|
|
||||||
|
// Notify components about the change before actually resetting the MusicService
|
||||||
|
// so they can react by e.g. stopping playback on the old server
|
||||||
|
RxBus.activeServerChangingPublisher.onNext(oldServerId)
|
||||||
|
|
||||||
|
// Use a coroutine to post the server change to the end of the message queue
|
||||||
|
launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
resetMusicService()
|
||||||
|
Settings.activeServer = serverId
|
||||||
|
|
||||||
|
RxBus.activeServerChangedPublisher.onNext(getActiveServer(serverId))
|
||||||
|
Timber.i("setActiveServerById done, new id: %s", serverId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getActiveMetaDatabase(): MetaDatabase {
|
fun getActiveMetaDatabase(): MetaDatabase {
|
||||||
val activeServer = getActiveServerId()
|
val activeServer = getActiveServerId()
|
||||||
@ -234,29 +256,6 @@ class ActiveServerProvider(
|
|||||||
return Settings.activeServer
|
return Settings.activeServer
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the Active Server by its unique id
|
|
||||||
* @param serverId: The id of the desired server
|
|
||||||
*/
|
|
||||||
fun setActiveServerById(serverId: Int) {
|
|
||||||
val oldServerId = Settings.activeServer
|
|
||||||
if (oldServerId == serverId) return
|
|
||||||
|
|
||||||
// Notify components about the change before actually resetting the MusicService
|
|
||||||
// so they can react by e.g. stopping playback on the old server
|
|
||||||
RxBus.activeServerChangingPublisher.onNext(oldServerId)
|
|
||||||
|
|
||||||
// Post the server change to the end of the message queue,
|
|
||||||
// so the cleanup have time to finish
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
resetMusicService()
|
|
||||||
Settings.activeServer = serverId
|
|
||||||
|
|
||||||
RxBus.activeServerChangedPublisher.onNext(serverId)
|
|
||||||
Timber.i("setActiveServerById done, new id: %s", serverId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if Scrobbling is enabled
|
* Queries if Scrobbling is enabled
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* CachedDataSource.kt
|
* CachedDataSource.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
@ -1,14 +1,15 @@
|
|||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Koin module contains the registration of general classes needed for Ultrasonic
|
* This Koin module contains the registration of general classes needed for Ultrasonic
|
||||||
*/
|
*/
|
||||||
val applicationModule = module {
|
val applicationModule = module {
|
||||||
single { ActiveServerProvider(get()) }
|
single { ActiveServerProvider(get()) }
|
||||||
single { ImageLoaderProvider(androidContext()) }
|
single { ImageLoaderProvider() }
|
||||||
|
single { CacheCleaner() }
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
import org.moire.ultrasonic.service.PlaybackStateSerializer
|
||||||
|
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
||||||
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Koin module contains the registration of classes related to the media player
|
* This Koin module contains the registration of classes related to the media player
|
||||||
*/
|
*/
|
||||||
val mediaPlayerModule = module {
|
val mediaPlayerModule = module {
|
||||||
single { MediaPlayerLifecycleSupport() }
|
|
||||||
|
// These are dependency-free
|
||||||
single { PlaybackStateSerializer() }
|
single { PlaybackStateSerializer() }
|
||||||
single { ExternalStorageMonitor() }
|
single { ExternalStorageMonitor() }
|
||||||
|
single { NetworkAndStorageChecker() }
|
||||||
|
single { ShareHandler() }
|
||||||
|
|
||||||
// TODO Ideally this can be cleaned up when all circular references are removed.
|
scope<NavigationActivity> {
|
||||||
single { MediaPlayerManager(get(), get(), get()) }
|
scoped { MediaPlayerManager(get(), get()) }
|
||||||
|
scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.di
|
|||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.android.ext.koin.androidContext
|
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.BuildConfig
|
import org.moire.ultrasonic.BuildConfig
|
||||||
@ -16,9 +15,6 @@ import org.moire.ultrasonic.service.CachedMusicService
|
|||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.service.OfflineMusicService
|
import org.moire.ultrasonic.service.OfflineMusicService
|
||||||
import org.moire.ultrasonic.service.RESTMusicService
|
import org.moire.ultrasonic.service.RESTMusicService
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -68,8 +64,4 @@ val musicServiceModule = module {
|
|||||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||||
OfflineMusicService()
|
OfflineMusicService()
|
||||||
}
|
}
|
||||||
|
|
||||||
single { DownloadHandler(get(), get()) }
|
|
||||||
single { NetworkAndStorageChecker(androidContext()) }
|
|
||||||
single { ShareHandler(androidContext()) }
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import org.moire.ultrasonic.adapters.BaseAdapter
|
|||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
import org.moire.ultrasonic.service.PlaybackState
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the Bookmarks available on the server
|
* Lists the Bookmarks available on the server
|
||||||
@ -61,22 +61,21 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom playback function which uses the restore functionality. A bit of a hack..
|
* Play the selected tracks at the bookmarked position
|
||||||
*/
|
*/
|
||||||
private fun playNow(songs: List<Track>) {
|
private fun playNow(songs: List<Track>) {
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
|
|
||||||
val state = PlaybackState(
|
mediaPlayerManager.addToPlaylist(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
currentPlayingIndex = 0,
|
autoPlay = false,
|
||||||
currentPlayingPosition = songs[0].bookmarkPosition
|
shuffle = false,
|
||||||
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaPlayerManager.restore(
|
mediaPlayerManager.seekTo(0, songs[0].bookmarkPosition)
|
||||||
state = state,
|
mediaPlayerManager.prepare()
|
||||||
autoPlay = true,
|
mediaPlayerManager.play()
|
||||||
newPlaylist = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ import android.os.Bundle
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import org.moire.ultrasonic.R
|
import org.koin.core.component.KoinScopeComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.adapters.FolderSelectorBinder
|
import org.moire.ultrasonic.adapters.FolderSelectorBinder
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
@ -23,17 +23,17 @@ import org.moire.ultrasonic.domain.Identifiable
|
|||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension of the MultiListFragment, with a few helper functions geared
|
* An extension of the MultiListFragment, with a few helper functions geared
|
||||||
* towards the display of MusicDirectory.Entries.
|
* towards the display of MusicDirectory.Entries.
|
||||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||||
*/
|
*/
|
||||||
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), KoinScopeComponent {
|
||||||
|
|
||||||
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||||
|
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show the folder selector
|
* Whether to show the folder selector
|
||||||
@ -46,7 +46,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
|||||||
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
|
override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
|
||||||
val isArtist = (item is Artist)
|
val isArtist = (item is Artist)
|
||||||
|
|
||||||
return handleContextMenu(menuItem, item, isArtist, downloadHandler, this)
|
return handleContextMenu(menuItem, item, isArtist, mediaPlayerManager, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: T) {
|
override fun onItemClick(item: T) {
|
||||||
@ -119,65 +119,4 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
|||||||
|
|
||||||
header
|
header
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
internal fun handleContextMenu(
|
|
||||||
menuItem: MenuItem,
|
|
||||||
item: Identifiable,
|
|
||||||
isArtist: Boolean,
|
|
||||||
downloadHandler: DownloadHandler,
|
|
||||||
fragment: Fragment
|
|
||||||
): Boolean {
|
|
||||||
when (menuItem.itemId) {
|
|
||||||
R.id.menu_play_now ->
|
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
|
||||||
autoPlay = true,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
R.id.menu_play_next ->
|
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
|
||||||
autoPlay = true,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
R.id.menu_play_last ->
|
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
|
||||||
autoPlay = false,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
R.id.menu_pin ->
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = DownloadAction.PIN,
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
R.id.menu_unpin ->
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = DownloadAction.UNPIN,
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
R.id.menu_download ->
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = DownloadAction.DOWNLOAD,
|
|
||||||
fragment,
|
|
||||||
item.id,
|
|
||||||
isArtist = isArtist
|
|
||||||
)
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,11 @@ import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
|||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import java.lang.ref.SoftReference
|
import java.lang.ref.SoftReference
|
||||||
import org.koin.core.component.KoinComponent
|
import kotlin.collections.HashMap
|
||||||
|
import kotlin.collections.hashMapOf
|
||||||
|
import kotlin.collections.set
|
||||||
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
|
import org.koin.core.component.KoinScopeComponent
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
@ -34,7 +38,7 @@ import org.moire.ultrasonic.view.SortOrder
|
|||||||
import org.moire.ultrasonic.view.ViewCapabilities
|
import org.moire.ultrasonic.view.ViewCapabilities
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class MainFragment : Fragment(), KoinComponent {
|
class MainFragment : ScopeFragment(), KoinScopeComponent {
|
||||||
|
|
||||||
private var filterButtonBar: FilterButtonBar? = null
|
private var filterButtonBar: FilterButtonBar? = null
|
||||||
private var layoutType: LayoutType = LayoutType.COVER
|
private var layoutType: LayoutType = LayoutType.COVER
|
||||||
|
@ -17,7 +17,6 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -26,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||||
@ -33,7 +33,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider
|
|||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.model.GenericListModel
|
import org.moire.ultrasonic.model.GenericListModel
|
||||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.CommunicationError
|
import org.moire.ultrasonic.util.CommunicationError
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
@ -42,11 +41,10 @@ import org.moire.ultrasonic.util.Util
|
|||||||
* An abstract Model, which can be extended to display a list of items of type T from the API
|
* An abstract Model, which can be extended to display a list of items of type T from the API
|
||||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||||
*/
|
*/
|
||||||
abstract class MultiListFragment<T : Identifiable> : Fragment() {
|
abstract class MultiListFragment<T : Identifiable> : ScopeFragment() {
|
||||||
internal val activeServerProvider: ActiveServerProvider by inject()
|
internal val activeServerProvider: ActiveServerProvider by inject()
|
||||||
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||||
internal val imageLoaderProvider: ImageLoaderProvider by inject()
|
internal val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||||
protected val downloadHandler: DownloadHandler by inject()
|
|
||||||
protected var refreshListView: SwipeRefreshLayout? = null
|
protected var refreshListView: SwipeRefreshLayout? = null
|
||||||
internal var listView: RecyclerView? = null
|
internal var listView: RecyclerView? = null
|
||||||
internal lateinit var viewManager: LinearLayoutManager
|
internal lateinit var viewManager: LinearLayoutManager
|
||||||
|
@ -15,7 +15,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
@ -23,6 +22,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
|||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
@ -37,7 +37,7 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Contains the mini-now playing information box displayed at the bottom of the screen
|
* Contains the mini-now playing information box displayed at the bottom of the screen
|
||||||
*/
|
*/
|
||||||
class NowPlayingFragment : Fragment() {
|
class NowPlayingFragment : ScopeFragment() {
|
||||||
|
|
||||||
private var downX = 0f
|
private var downX = 0f
|
||||||
private var downY = 0f
|
private var downY = 0f
|
||||||
|
@ -45,7 +45,6 @@ import androidx.core.content.res.ResourcesCompat
|
|||||||
import androidx.core.view.MenuHost
|
import androidx.core.view.MenuHost
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.media3.common.HeartRating
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
@ -79,12 +78,12 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
|
import org.koin.core.component.KoinScopeComponent
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.BaseAdapter
|
import org.moire.ultrasonic.adapters.BaseAdapter
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
import org.moire.ultrasonic.app.UApp
|
|
||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.shouldUseId3Tags
|
||||||
@ -106,6 +105,7 @@ import org.moire.ultrasonic.util.CommunicationError
|
|||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.util.toTrack
|
import org.moire.ultrasonic.util.toTrack
|
||||||
import org.moire.ultrasonic.view.AutoRepeatButton
|
import org.moire.ultrasonic.view.AutoRepeatButton
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -116,9 +116,9 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
|
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
|
||||||
class PlayerFragment :
|
class PlayerFragment :
|
||||||
Fragment(),
|
ScopeFragment(),
|
||||||
GestureDetector.OnGestureListener,
|
GestureDetector.OnGestureListener,
|
||||||
KoinComponent,
|
KoinScopeComponent,
|
||||||
CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
CoroutineScope by CoroutineScope(Dispatchers.Main) {
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
@ -356,14 +356,14 @@ class PlayerFragment :
|
|||||||
onPlaylistChanged()
|
onPlaylistChanged()
|
||||||
|
|
||||||
when (newRepeat) {
|
when (newRepeat) {
|
||||||
0 -> Util.toast(
|
0 -> toast(
|
||||||
context, R.string.download_repeat_off
|
R.string.download_repeat_off
|
||||||
)
|
)
|
||||||
1 -> Util.toast(
|
1 -> toast(
|
||||||
context, R.string.download_repeat_single
|
R.string.download_repeat_single
|
||||||
)
|
)
|
||||||
2 -> Util.toast(
|
2 -> toast(
|
||||||
context, R.string.download_repeat_all
|
R.string.download_repeat_all
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
@ -410,7 +410,7 @@ class PlayerFragment :
|
|||||||
// Query the Jukebox state in an IO Context
|
// Query the Jukebox state in an IO Context
|
||||||
ioScope.launch(CommunicationError.getHandler(context)) {
|
ioScope.launch(CommunicationError.getHandler(context)) {
|
||||||
try {
|
try {
|
||||||
jukeboxAvailable = mediaPlayerManager.isJukeboxAvailable
|
jukeboxAvailable = getMusicService().isJukeboxAvailable()
|
||||||
} catch (all: Exception) {
|
} catch (all: Exception) {
|
||||||
Timber.e(all)
|
Timber.e(all)
|
||||||
}
|
}
|
||||||
@ -457,9 +457,9 @@ class PlayerFragment :
|
|||||||
val isEnabled = mediaPlayerManager.toggleShuffle()
|
val isEnabled = mediaPlayerManager.toggleShuffle()
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
Util.toast(activity, R.string.download_menu_shuffle_on)
|
toast(R.string.download_menu_shuffle_on)
|
||||||
} else {
|
} else {
|
||||||
Util.toast(activity, R.string.download_menu_shuffle_off)
|
toast(R.string.download_menu_shuffle_off)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateShuffleButtonState(isEnabled)
|
updateShuffleButtonState(isEnabled)
|
||||||
@ -579,8 +579,7 @@ class PlayerFragment :
|
|||||||
equalizerMenuItem.isVisible = isEqualizerAvailable
|
equalizerMenuItem.isVisible = isEqualizerAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaPlayerController = mediaPlayerManager
|
val track = mediaPlayerManager.currentMediaItem?.toTrack()
|
||||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
|
||||||
|
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
currentSong = track
|
currentSong = track
|
||||||
@ -600,7 +599,7 @@ class PlayerFragment :
|
|||||||
goToArtist.isVisible = false
|
goToArtist.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaPlayerController.keepScreenOn) {
|
if (mediaPlayerManager.keepScreenOn) {
|
||||||
screenOption?.setTitle(R.string.download_menu_screen_off)
|
screenOption?.setTitle(R.string.download_menu_screen_off)
|
||||||
} else {
|
} else {
|
||||||
screenOption?.setTitle(R.string.download_menu_screen_on)
|
screenOption?.setTitle(R.string.download_menu_screen_on)
|
||||||
@ -609,7 +608,7 @@ class PlayerFragment :
|
|||||||
if (jukeboxOption != null) {
|
if (jukeboxOption != null) {
|
||||||
jukeboxOption.isEnabled = jukeboxAvailable
|
jukeboxOption.isEnabled = jukeboxAvailable
|
||||||
jukeboxOption.isVisible = jukeboxAvailable
|
jukeboxOption.isVisible = jukeboxAvailable
|
||||||
if (mediaPlayerController.isJukeboxEnabled) {
|
if (mediaPlayerManager.isJukeboxEnabled) {
|
||||||
jukeboxOption.setTitle(R.string.download_menu_jukebox_off)
|
jukeboxOption.setTitle(R.string.download_menu_jukebox_off)
|
||||||
} else {
|
} else {
|
||||||
jukeboxOption.setTitle(R.string.download_menu_jukebox_on)
|
jukeboxOption.setTitle(R.string.download_menu_jukebox_on)
|
||||||
@ -707,8 +706,7 @@ class PlayerFragment :
|
|||||||
R.id.menu_item_jukebox -> {
|
R.id.menu_item_jukebox -> {
|
||||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
||||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
||||||
Util.toast(
|
toast(
|
||||||
context,
|
|
||||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
if (jukeboxEnabled) R.string.download_jukebox_on
|
||||||
else R.string.download_jukebox_off,
|
else R.string.download_jukebox_off,
|
||||||
false
|
false
|
||||||
@ -760,7 +758,7 @@ class PlayerFragment :
|
|||||||
R.string.download_bookmark_set_at_position,
|
R.string.download_bookmark_set_at_position,
|
||||||
bookmarkTime
|
bookmarkTime
|
||||||
)
|
)
|
||||||
Util.toast(context, msg)
|
toast(msg)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_bookmark_delete -> {
|
R.id.menu_item_bookmark_delete -> {
|
||||||
@ -776,13 +774,12 @@ class PlayerFragment :
|
|||||||
Timber.e(all)
|
Timber.e(all)
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
Util.toast(context, R.string.download_bookmark_removed)
|
toast(R.string.download_bookmark_removed)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_share -> {
|
R.id.menu_item_share -> {
|
||||||
val mediaPlayerController = mediaPlayerManager
|
|
||||||
val tracks: MutableList<Track?> = ArrayList()
|
val tracks: MutableList<Track?> = ArrayList()
|
||||||
val playlist = mediaPlayerController.playlist
|
val playlist = mediaPlayerManager.playlist
|
||||||
for (item in playlist) {
|
for (item in playlist) {
|
||||||
val playlistEntry = item.toTrack()
|
val playlistEntry = item.toTrack()
|
||||||
tracks.add(playlistEntry)
|
tracks.add(playlistEntry)
|
||||||
@ -790,8 +787,6 @@ class PlayerFragment :
|
|||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
tracks = tracks,
|
tracks = tracks,
|
||||||
swipe = null,
|
|
||||||
cancellationToken = cancellationToken,
|
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -804,8 +799,6 @@ class PlayerFragment :
|
|||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
tracks,
|
tracks,
|
||||||
swipe = null,
|
|
||||||
cancellationToken = cancellationToken
|
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -822,7 +815,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun savePlaylistInBackground(playlistName: String) {
|
private fun savePlaylistInBackground(playlistName: String) {
|
||||||
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName))
|
toast(resources.getString(R.string.download_playlist_saving, playlistName))
|
||||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
mediaPlayerManager.suggestedPlaylistName = playlistName
|
||||||
|
|
||||||
// The playlist can be acquired only from the main thread
|
// The playlist can be acquired only from the main thread
|
||||||
@ -835,7 +828,7 @@ class PlayerFragment :
|
|||||||
musicService.createPlaylist(null, playlistName, entries)
|
musicService.createPlaylist(null, playlistName, entries)
|
||||||
}.invokeOnCompletion {
|
}.invokeOnCompletion {
|
||||||
if (it == null || it is CancellationException) {
|
if (it == null || it is CancellationException) {
|
||||||
Util.toast(UApp.applicationContext(), R.string.download_playlist_done)
|
toast(R.string.download_playlist_done)
|
||||||
} else {
|
} else {
|
||||||
Timber.e(it, "Exception has occurred in savePlaylistInBackground")
|
Timber.e(it, "Exception has occurred in savePlaylistInBackground")
|
||||||
val msg = String.format(
|
val msg = String.format(
|
||||||
@ -844,7 +837,7 @@ class PlayerFragment :
|
|||||||
resources.getString(R.string.download_playlist_error),
|
resources.getString(R.string.download_playlist_error),
|
||||||
CommunicationError.getErrorMessage(it)
|
CommunicationError.getErrorMessage(it)
|
||||||
)
|
)
|
||||||
Util.toast(UApp.applicationContext(), msg)
|
toast(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -958,7 +951,7 @@ class PlayerFragment :
|
|||||||
item?.mediaMetadata?.title
|
item?.mediaMetadata?.title
|
||||||
)
|
)
|
||||||
|
|
||||||
Util.toast(context, songRemoved)
|
toast(songRemoved)
|
||||||
|
|
||||||
// Remove the item from the playlist
|
// Remove the item from the playlist
|
||||||
mediaPlayerManager.removeFromPlaylist(pos)
|
mediaPlayerManager.removeFromPlaylist(pos)
|
||||||
@ -1059,15 +1052,14 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPlaylistChanged() {
|
private fun onPlaylistChanged() {
|
||||||
val mediaPlayerController = mediaPlayerManager
|
|
||||||
// Try to display playlist in play order
|
// Try to display playlist in play order
|
||||||
val list = mediaPlayerController.playlistInPlayOrder
|
val list = mediaPlayerManager.playlistInPlayOrder
|
||||||
emptyTextView.setText(R.string.playlist_empty)
|
emptyTextView.setText(R.string.playlist_empty)
|
||||||
viewAdapter.submitList(list.map(MediaItem::toTrack))
|
viewAdapter.submitList(list.map(MediaItem::toTrack))
|
||||||
progressIndicator.isVisible = false
|
progressIndicator.isVisible = false
|
||||||
emptyView.isVisible = list.isEmpty()
|
emptyView.isVisible = list.isEmpty()
|
||||||
|
|
||||||
updateRepeatButtonState(mediaPlayerController.repeatMode)
|
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTrackChanged() {
|
private fun onTrackChanged() {
|
||||||
|
@ -17,7 +17,7 @@ import androidx.navigation.fragment.findNavController
|
|||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinScopeComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.AlbumRowDelegate
|
import org.moire.ultrasonic.adapters.AlbumRowDelegate
|
||||||
@ -36,13 +36,12 @@ import org.moire.ultrasonic.domain.SearchResult
|
|||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
import org.moire.ultrasonic.model.SearchListModel
|
import org.moire.ultrasonic.model.SearchListModel
|
||||||
import org.moire.ultrasonic.service.DownloadService
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
|
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
|
||||||
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
import org.moire.ultrasonic.util.CommunicationError
|
import org.moire.ultrasonic.util.CommunicationError
|
||||||
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||||
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
@ -51,15 +50,12 @@ import org.moire.ultrasonic.util.Util.toast
|
|||||||
* Initiates a search on the media library and displays the results
|
* Initiates a search on the media library and displays the results
|
||||||
|
|
||||||
*/
|
*/
|
||||||
class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent {
|
||||||
private var searchResult: SearchResult? = null
|
private var searchResult: SearchResult? = null
|
||||||
private var searchRefresh: SwipeRefreshLayout? = null
|
private var searchRefresh: SwipeRefreshLayout? = null
|
||||||
|
|
||||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||||
|
|
||||||
private val shareHandler: ShareHandler by inject()
|
|
||||||
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
|
|
||||||
|
|
||||||
private var cancellationToken: CancellationToken? = null
|
private var cancellationToken: CancellationToken? = null
|
||||||
|
|
||||||
private val navArgs by navArgs<SearchFragmentArgs>()
|
private val navArgs by navArgs<SearchFragmentArgs>()
|
||||||
@ -137,18 +133,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadBackground(save: Boolean, songs: List<Track?>) {
|
|
||||||
val onValid = Runnable {
|
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
|
||||||
DownloadService.download(
|
|
||||||
songs.filterNotNull(),
|
|
||||||
save = save,
|
|
||||||
updateSaveFlag = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onValid.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun search(query: String, autoplay: Boolean) {
|
private fun search(query: String, autoplay: Boolean) {
|
||||||
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
listModel.viewModelScope.launch(CommunicationError.getHandler(context)) {
|
||||||
refreshListView?.isRefreshing = true
|
refreshListView?.isRefreshing = true
|
||||||
@ -253,7 +237,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
||||||
)
|
)
|
||||||
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
||||||
toast(context, resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1))
|
toast(resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onVideoSelected(track: Track) {
|
private fun onVideoSelected(track: Track) {
|
||||||
@ -288,103 +272,23 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
|
override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean {
|
||||||
val isArtist = (item is Artist)
|
// Here the Item could be a track or an album or an artist
|
||||||
|
if (item is Track) {
|
||||||
val found = EntryListFragment.handleContextMenu(
|
return handleContextMenuTracks(
|
||||||
menuItem,
|
menuItem = menuItem,
|
||||||
item,
|
tracks = listOf(item),
|
||||||
isArtist,
|
mediaPlayerManager = mediaPlayerManager,
|
||||||
downloadHandler,
|
fragment = this
|
||||||
this
|
)
|
||||||
)
|
} else {
|
||||||
|
return handleContextMenu(
|
||||||
if (found || item !is Track) return true
|
menuItem = menuItem,
|
||||||
|
item = item,
|
||||||
val songs = mutableListOf<Track>()
|
isArtist = item is Artist,
|
||||||
|
mediaPlayerManager = mediaPlayerManager,
|
||||||
when (menuItem.itemId) {
|
fragment = this
|
||||||
R.id.song_menu_play_now -> {
|
)
|
||||||
songs.add(item)
|
|
||||||
downloadHandler.addTracksToMediaController(
|
|
||||||
songs = songs,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
|
||||||
autoPlay = true,
|
|
||||||
shuffle = false,
|
|
||||||
fragment = this,
|
|
||||||
playlistName = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.song_menu_play_next -> {
|
|
||||||
songs.add(item)
|
|
||||||
downloadHandler.addTracksToMediaController(
|
|
||||||
songs = songs,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
|
||||||
autoPlay = false,
|
|
||||||
shuffle = false,
|
|
||||||
fragment = this,
|
|
||||||
playlistName = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.song_menu_play_last -> {
|
|
||||||
songs.add(item)
|
|
||||||
downloadHandler.addTracksToMediaController(
|
|
||||||
songs = songs,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
|
||||||
autoPlay = false,
|
|
||||||
shuffle = false,
|
|
||||||
fragment = this,
|
|
||||||
playlistName = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
R.id.song_menu_pin -> {
|
|
||||||
songs.add(item)
|
|
||||||
toast(
|
|
||||||
context,
|
|
||||||
resources.getQuantityString(
|
|
||||||
R.plurals.n_songs_pinned,
|
|
||||||
songs.size,
|
|
||||||
songs.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
downloadBackground(true, songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_download -> {
|
|
||||||
songs.add(item)
|
|
||||||
toast(
|
|
||||||
context,
|
|
||||||
resources.getQuantityString(
|
|
||||||
R.plurals.n_songs_to_be_downloaded,
|
|
||||||
songs.size,
|
|
||||||
songs.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
downloadBackground(false, songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_unpin -> {
|
|
||||||
songs.add(item)
|
|
||||||
toast(
|
|
||||||
context,
|
|
||||||
resources.getQuantityString(
|
|
||||||
R.plurals.n_songs_unpinned,
|
|
||||||
songs.size,
|
|
||||||
songs.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
DownloadService.unpin(songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_share -> {
|
|
||||||
songs.add(item)
|
|
||||||
shareHandler.createShare(
|
|
||||||
fragment = this,
|
|
||||||
tracks = songs,
|
|
||||||
swipe = searchRefresh,
|
|
||||||
cancellationToken = cancellationToken!!,
|
|
||||||
additionalId = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -64,7 +64,7 @@ class ServerSelectorFragment : Fragment() {
|
|||||||
|
|
||||||
listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
|
listView?.onItemClickListener = AdapterView.OnItemClickListener { parent, _, position, _ ->
|
||||||
val server = parent.getItemAtPosition(position) as ServerSetting
|
val server = parent.getItemAtPosition(position) as ServerSetting
|
||||||
ActiveServerProvider.setActiveServerById(server.id)
|
activeServerProvider.setActiveServerById(server.id)
|
||||||
findNavController().popBackStack(R.id.mainFragment, false)
|
findNavController().popBackStack(R.id.mainFragment, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ class ServerSelectorFragment : Fragment() {
|
|||||||
val activeServerId = ActiveServerProvider.getActiveServerId()
|
val activeServerId = ActiveServerProvider.getActiveServerId()
|
||||||
|
|
||||||
// If the currently active server is deleted, go offline
|
// If the currently active server is deleted, go offline
|
||||||
if (id == activeServerId) ActiveServerProvider.setActiveServerById(OFFLINE_DB_ID)
|
if (id == activeServerId) activeServerProvider.setActiveServerById(OFFLINE_DB_ID)
|
||||||
|
|
||||||
serverSettingsModel.deleteItemById(id)
|
serverSettingsModel.deleteItemById(id)
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ class SettingsFragment :
|
|||||||
SearchSuggestionProvider.MODE
|
SearchSuggestionProvider.MODE
|
||||||
)
|
)
|
||||||
suggestions.clearHistory()
|
suggestions.clearHistory()
|
||||||
toast(activity, R.string.settings_search_history_cleared)
|
toast(R.string.settings_search_history_cleared)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -332,7 +332,7 @@ class SettingsFragment :
|
|||||||
Timber.w("Failed to delete %s", nomediaDir)
|
Timber.w("Failed to delete %s", nomediaDir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toast(activity, R.string.settings_hide_media_toast, false)
|
toast(R.string.settings_hide_media_toast, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCacheLocation(path: String) {
|
private fun setCacheLocation(path: String) {
|
||||||
|
@ -46,14 +46,16 @@ import org.moire.ultrasonic.model.TrackCollectionModel
|
|||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
|
||||||
import org.moire.ultrasonic.subsonic.ShareHandler
|
import org.moire.ultrasonic.subsonic.ShareHandler
|
||||||
import org.moire.ultrasonic.subsonic.VideoPlayer
|
import org.moire.ultrasonic.subsonic.VideoPlayer
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
|
||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
|
import org.moire.ultrasonic.util.ContextMenuUtil
|
||||||
|
import org.moire.ultrasonic.util.DownloadAction
|
||||||
|
import org.moire.ultrasonic.util.DownloadUtil
|
||||||
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util.navigateToCurrent
|
||||||
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.view.SortOrder
|
import org.moire.ultrasonic.view.SortOrder
|
||||||
import org.moire.ultrasonic.view.ViewCapabilities
|
import org.moire.ultrasonic.view.ViewCapabilities
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -86,7 +88,6 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
internal val mediaPlayerManager: MediaPlayerManager by inject()
|
internal val mediaPlayerManager: MediaPlayerManager by inject()
|
||||||
private val shareHandler: ShareHandler by inject()
|
private val shareHandler: ShareHandler by inject()
|
||||||
internal var cancellationToken: CancellationToken? = null
|
|
||||||
|
|
||||||
override val listModel: TrackCollectionModel by viewModels()
|
override val listModel: TrackCollectionModel by viewModels()
|
||||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||||
@ -102,7 +103,6 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
cancellationToken = CancellationToken()
|
|
||||||
|
|
||||||
albumButtons = view.findViewById(R.id.menu_album)
|
albumButtons = view.findViewById(R.id.menu_album)
|
||||||
|
|
||||||
@ -211,19 +211,23 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
playNowButton?.setOnClickListener {
|
playNowButton?.setOnClickListener {
|
||||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true)
|
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.CLEAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
playNextButton?.setOnClickListener {
|
playNextButton?.setOnClickListener {
|
||||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true)
|
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.AFTER_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
playLastButton!!.setOnClickListener {
|
playLastButton!!.setOnClickListener {
|
||||||
playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true)
|
playSelectedOrAllTracks(MediaPlayerManager.InsertionMode.APPEND)
|
||||||
}
|
}
|
||||||
|
|
||||||
pinButton?.setOnClickListener {
|
pinButton?.setOnClickListener {
|
||||||
downloadBackground(true)
|
downloadSelectedOrAllTracks(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadButton?.setOnClickListener {
|
||||||
|
downloadSelectedOrAllTracks(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
unpinButton?.setOnClickListener {
|
unpinButton?.setOnClickListener {
|
||||||
@ -231,26 +235,22 @@ open class TrackCollectionFragment(
|
|||||||
ConfirmationDialog.Builder(requireContext())
|
ConfirmationDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.common_unpin_selection_confirmation)
|
.setMessage(R.string.common_unpin_selection_confirmation)
|
||||||
.setPositiveButton(R.string.common_unpin) { _, _ ->
|
.setPositiveButton(R.string.common_unpin) { _, _ ->
|
||||||
unpin()
|
unpinSelectedTracks()
|
||||||
}.show()
|
}.show()
|
||||||
} else {
|
} else {
|
||||||
unpin()
|
unpinSelectedTracks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadButton?.setOnClickListener {
|
|
||||||
downloadBackground(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteButton?.setOnClickListener {
|
deleteButton?.setOnClickListener {
|
||||||
if (Settings.showConfirmationDialog) {
|
if (Settings.showConfirmationDialog) {
|
||||||
ConfirmationDialog.Builder(requireContext())
|
ConfirmationDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.common_delete_selection_confirmation)
|
.setMessage(R.string.common_delete_selection_confirmation)
|
||||||
.setPositiveButton(R.string.common_delete) { _, _ ->
|
.setPositiveButton(R.string.common_delete) { _, _ ->
|
||||||
delete()
|
deleteSelectedTracks()
|
||||||
}.show()
|
}.show()
|
||||||
} else {
|
} else {
|
||||||
delete()
|
deleteSelectedTracks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,9 +283,9 @@ open class TrackCollectionFragment(
|
|||||||
return true
|
return true
|
||||||
} else if (item.itemId == R.id.menu_item_share) {
|
} else if (item.itemId == R.id.menu_item_share) {
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this@TrackCollectionFragment, getSelectedTracks(),
|
fragment = this@TrackCollectionFragment,
|
||||||
refreshListView, cancellationToken!!,
|
tracks = getSelectedTracks(),
|
||||||
navArgs.id
|
additionalId = navArgs.id
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -294,46 +294,10 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
cancellationToken!!.cancel()
|
|
||||||
rxBusSubscription.dispose()
|
rxBusSubscription.dispose()
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playNow(
|
|
||||||
insertionMode: MediaPlayerManager.InsertionMode,
|
|
||||||
selectedTracks: List<Track> = getSelectedTracks(),
|
|
||||||
toast: Boolean = false
|
|
||||||
) {
|
|
||||||
if (selectedTracks.isNotEmpty()) {
|
|
||||||
downloadHandler.addTracksToMediaController(
|
|
||||||
songs = selectedTracks,
|
|
||||||
insertionMode = insertionMode,
|
|
||||||
autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR),
|
|
||||||
playlistName = null,
|
|
||||||
fragment = this
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
playAll(false, insertionMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toast) {
|
|
||||||
val stringInt = when (insertionMode) {
|
|
||||||
MediaPlayerManager.InsertionMode.CLEAR ->
|
|
||||||
R.plurals.n_songs_added_play_now
|
|
||||||
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
|
|
||||||
R.plurals.n_songs_added_after_current
|
|
||||||
MediaPlayerManager.InsertionMode.APPEND ->
|
|
||||||
R.plurals.n_songs_added_to_end
|
|
||||||
}
|
|
||||||
val msg = resources.getQuantityString(
|
|
||||||
stringInt,
|
|
||||||
selectedTracks.size,
|
|
||||||
selectedTracks.size
|
|
||||||
)
|
|
||||||
Util.toast(requireContext(), msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of the underlying list
|
* Get the size of the underlying list
|
||||||
*/
|
*/
|
||||||
@ -364,25 +328,65 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
// Need a valid id to recurse sub directories stuff
|
// Need a valid id to recurse sub directories stuff
|
||||||
if (hasSubFolders && navArgs.id != null) {
|
if (hasSubFolders && navArgs.id != null) {
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
mediaPlayerManager.playTracksAndToast(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = navArgs.id!!,
|
|
||||||
insertionMode = insertionMode,
|
insertionMode = insertionMode,
|
||||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
id = navArgs.id!!,
|
||||||
shuffle = shuffle,
|
shuffle = shuffle,
|
||||||
isArtist = isArtist
|
isArtist = isArtist
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
downloadHandler.addTracksToMediaController(
|
mediaPlayerManager.suggestedPlaylistName = navArgs.playlistName
|
||||||
|
mediaPlayerManager.addToPlaylist(
|
||||||
songs = getAllSongs(),
|
songs = getAllSongs(),
|
||||||
insertionMode = insertionMode,
|
insertionMode = insertionMode,
|
||||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||||
shuffle = shuffle,
|
shuffle = shuffle
|
||||||
playlistName = navArgs.playlistName,
|
|
||||||
fragment = this
|
|
||||||
)
|
)
|
||||||
|
if (insertionMode == MediaPlayerManager.InsertionMode.CLEAR) {
|
||||||
|
navigateToCurrent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun unpinSelectedTracks() {
|
||||||
|
DownloadUtil.justDownload(
|
||||||
|
action = DownloadAction.UNPIN,
|
||||||
|
fragment = this,
|
||||||
|
tracks = getSelectedTracks()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadSelectedOrAllTracks(save: Boolean) {
|
||||||
|
var tracks = getSelectedTracks()
|
||||||
|
if (tracks.isEmpty()) tracks = getAllSongs()
|
||||||
|
|
||||||
|
DownloadUtil.justDownload(
|
||||||
|
action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD,
|
||||||
|
fragment = this,
|
||||||
|
tracks = tracks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playSelectedOrAllTracks(
|
||||||
|
insertionMode: MediaPlayerManager.InsertionMode
|
||||||
|
) {
|
||||||
|
var tracks = getSelectedTracks()
|
||||||
|
if (tracks.isEmpty()) tracks = getAllSongs()
|
||||||
|
|
||||||
|
mediaPlayerManager.playTracksAndToast(
|
||||||
|
fragment = this,
|
||||||
|
insertionMode = insertionMode,
|
||||||
|
tracks = tracks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteSelectedTracks() {
|
||||||
|
DownloadUtil.justDownload(
|
||||||
|
action = DownloadAction.DELETE,
|
||||||
|
fragment = this,
|
||||||
|
tracks = getSelectedTracks()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun getAllSongs(): List<Track> {
|
private fun getAllSongs(): List<Track> {
|
||||||
@ -403,7 +407,7 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
// Display toast: N tracks selected
|
// Display toast: N tracks selected
|
||||||
val toastResId = R.string.select_album_n_selected
|
val toastResId = R.string.select_album_n_selected
|
||||||
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
|
toast(getString(toastResId, selectedCount.coerceAtLeast(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -431,37 +435,6 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedTracks()) {
|
|
||||||
var songs = tracks
|
|
||||||
|
|
||||||
if (songs.isEmpty()) {
|
|
||||||
songs = getAllSongs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = action,
|
|
||||||
fragment = this,
|
|
||||||
tracks = songs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun delete(songs: List<Track> = getSelectedTracks()) {
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = DownloadAction.DELETE,
|
|
||||||
fragment = this,
|
|
||||||
tracks = songs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun unpin(songs: List<Track> = getSelectedTracks()) {
|
|
||||||
downloadHandler.justDownload(
|
|
||||||
action = DownloadAction.UNPIN,
|
|
||||||
fragment = this,
|
|
||||||
tracks = songs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
|
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
|
||||||
|
|
||||||
Timber.i("Received list")
|
Timber.i("Received list")
|
||||||
@ -606,48 +579,19 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
|
private fun displayRandom() = (sortOrder == SortOrder.RANDOM) || navArgs.getRandom
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
override fun onContextMenuItemSelected(
|
override fun onContextMenuItemSelected(
|
||||||
menuItem: MenuItem,
|
menuItem: MenuItem,
|
||||||
item: MusicDirectory.Child
|
item: MusicDirectory.Child
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val songs = getClickedSong(item)
|
|
||||||
|
|
||||||
when (menuItem.itemId) {
|
val tracks = getClickedSong(item)
|
||||||
R.id.song_menu_play_now -> {
|
|
||||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true)
|
return ContextMenuUtil.handleContextMenuTracks(
|
||||||
}
|
menuItem = menuItem,
|
||||||
R.id.song_menu_play_next -> {
|
tracks = tracks,
|
||||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true)
|
mediaPlayerManager = mediaPlayerManager,
|
||||||
}
|
fragment = this
|
||||||
R.id.song_menu_play_last -> {
|
)
|
||||||
playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true)
|
|
||||||
}
|
|
||||||
R.id.song_menu_pin -> {
|
|
||||||
downloadBackground(true, songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_unpin -> {
|
|
||||||
unpin(songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_download -> {
|
|
||||||
downloadBackground(false, songs)
|
|
||||||
}
|
|
||||||
R.id.song_menu_share -> {
|
|
||||||
if (item is Track) {
|
|
||||||
shareHandler.createShare(
|
|
||||||
this,
|
|
||||||
tracks = listOf(item),
|
|
||||||
swipe = refreshListView,
|
|
||||||
cancellationToken = cancellationToken!!,
|
|
||||||
additionalId = navArgs.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
return super.onContextItemSelected(menuItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
||||||
|
@ -25,11 +25,11 @@ import android.widget.CheckBox
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
|
import org.koin.core.component.KoinScopeComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -39,12 +39,12 @@ import org.moire.ultrasonic.domain.Playlist
|
|||||||
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.service.OfflineException
|
import org.moire.ultrasonic.service.OfflineException
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
import org.moire.ultrasonic.util.BackgroundTask
|
import org.moire.ultrasonic.util.BackgroundTask
|
||||||
import org.moire.ultrasonic.util.CacheCleaner
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
|
import org.moire.ultrasonic.util.DownloadAction
|
||||||
|
import org.moire.ultrasonic.util.DownloadUtil
|
||||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||||
import org.moire.ultrasonic.util.InfoDialog
|
import org.moire.ultrasonic.util.InfoDialog
|
||||||
import org.moire.ultrasonic.util.LoadingTask
|
import org.moire.ultrasonic.util.LoadingTask
|
||||||
@ -56,14 +56,12 @@ import org.moire.ultrasonic.util.Util.toast
|
|||||||
*
|
*
|
||||||
* TODO: This file has been converted from Java, but not modernized yet.
|
* TODO: This file has been converted from Java, but not modernized yet.
|
||||||
*/
|
*/
|
||||||
class PlaylistsFragment : Fragment(), KoinComponent {
|
class PlaylistsFragment : ScopeFragment(), KoinScopeComponent {
|
||||||
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
|
private var refreshPlaylistsListView: SwipeRefreshLayout? = null
|
||||||
private var playlistsListView: ListView? = null
|
private var playlistsListView: ListView? = null
|
||||||
private var emptyTextView: View? = null
|
private var emptyTextView: View? = null
|
||||||
private var playlistAdapter: ArrayAdapter<Playlist>? = null
|
private var playlistAdapter: ArrayAdapter<Playlist>? = null
|
||||||
|
|
||||||
private val downloadHandler by inject<DownloadHandler>()
|
|
||||||
|
|
||||||
private var cancellationToken: CancellationToken? = null
|
private var cancellationToken: CancellationToken? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -115,7 +113,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
override fun doInBackground(): List<Playlist> {
|
override fun doInBackground(): List<Playlist> {
|
||||||
val musicService = getMusicService()
|
val musicService = getMusicService()
|
||||||
val playlists = musicService.getPlaylists(refresh)
|
val playlists = musicService.getPlaylists(refresh)
|
||||||
if (!isOffline()) CacheCleaner().cleanPlaylists(playlists)
|
val cacheCleaner: CacheCleaner by inject()
|
||||||
|
if (!isOffline()) cacheCleaner.cleanPlaylists(playlists)
|
||||||
return playlists
|
return playlists
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,8 +146,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
|
val playlist = playlistsListView!!.getItemAtPosition(info.position) as Playlist
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
R.id.playlist_menu_pin -> {
|
R.id.playlist_menu_pin -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.PIN,
|
action = DownloadAction.PIN,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = playlist.id,
|
id = playlist.id,
|
||||||
name = playlist.name,
|
name = playlist.name,
|
||||||
@ -157,8 +156,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.playlist_menu_unpin -> {
|
R.id.playlist_menu_unpin -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.UNPIN,
|
action = DownloadAction.UNPIN,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = playlist.id,
|
id = playlist.id,
|
||||||
name = playlist.name,
|
name = playlist.name,
|
||||||
@ -167,8 +166,8 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.playlist_menu_download -> {
|
R.id.playlist_menu_download -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.DOWNLOAD,
|
action = DownloadAction.DOWNLOAD,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = playlist.id,
|
id = playlist.id,
|
||||||
name = playlist.name,
|
name = playlist.name,
|
||||||
@ -227,7 +226,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
playlistAdapter!!.remove(playlist)
|
playlistAdapter!!.remove(playlist)
|
||||||
playlistAdapter!!.notifyDataSetChanged()
|
playlistAdapter!!.notifyDataSetChanged()
|
||||||
toast(
|
toast(
|
||||||
context,
|
|
||||||
resources.getString(R.string.menu_deleted_playlist, playlist.name)
|
resources.getString(R.string.menu_deleted_playlist, playlist.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -246,7 +244,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
),
|
),
|
||||||
getErrorMessage(error)
|
getErrorMessage(error)
|
||||||
)
|
)
|
||||||
toast(context, msg, false)
|
toast(msg, false)
|
||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||||
@ -310,7 +308,6 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
override fun done(result: Any?) {
|
override fun done(result: Any?) {
|
||||||
load(true)
|
load(true)
|
||||||
toast(
|
toast(
|
||||||
context,
|
|
||||||
resources.getString(R.string.playlist_updated_info, playlist.name)
|
resources.getString(R.string.playlist_updated_info, playlist.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -329,7 +326,7 @@ class PlaylistsFragment : Fragment(), KoinComponent {
|
|||||||
),
|
),
|
||||||
getErrorMessage(error)
|
getErrorMessage(error)
|
||||||
)
|
)
|
||||||
toast(context, msg, false)
|
toast(msg, false)
|
||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
}
|
}
|
||||||
|
@ -24,11 +24,11 @@ import android.widget.CheckBox
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.androidx.scope.ScopeFragment
|
||||||
|
import org.koin.core.component.KoinScopeComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -38,14 +38,15 @@ import org.moire.ultrasonic.fragment.FragmentTitle
|
|||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.OfflineException
|
import org.moire.ultrasonic.service.OfflineException
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
|
||||||
import org.moire.ultrasonic.subsonic.DownloadHandler
|
|
||||||
import org.moire.ultrasonic.util.BackgroundTask
|
import org.moire.ultrasonic.util.BackgroundTask
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
import org.moire.ultrasonic.util.CancellationToken
|
||||||
|
import org.moire.ultrasonic.util.DownloadAction
|
||||||
|
import org.moire.ultrasonic.util.DownloadUtil
|
||||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
||||||
import org.moire.ultrasonic.util.LoadingTask
|
import org.moire.ultrasonic.util.LoadingTask
|
||||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.view.ShareAdapter
|
import org.moire.ultrasonic.view.ShareAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,12 +54,12 @@ import org.moire.ultrasonic.view.ShareAdapter
|
|||||||
*
|
*
|
||||||
* TODO: This file has been converted from Java, but not modernized yet.
|
* TODO: This file has been converted from Java, but not modernized yet.
|
||||||
*/
|
*/
|
||||||
class SharesFragment : Fragment(), KoinComponent {
|
class SharesFragment : ScopeFragment(), KoinScopeComponent {
|
||||||
private var refreshSharesListView: SwipeRefreshLayout? = null
|
private var refreshSharesListView: SwipeRefreshLayout? = null
|
||||||
private var sharesListView: ListView? = null
|
private var sharesListView: ListView? = null
|
||||||
private var emptyTextView: View? = null
|
private var emptyTextView: View? = null
|
||||||
private var shareAdapter: ShareAdapter? = null
|
private var shareAdapter: ShareAdapter? = null
|
||||||
private val downloadHandler: DownloadHandler by inject()
|
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||||
private var cancellationToken: CancellationToken? = null
|
private var cancellationToken: CancellationToken? = null
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Util.applyTheme(this.context)
|
Util.applyTheme(this.context)
|
||||||
@ -133,8 +134,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
val share = sharesListView!!.getItemAtPosition(info.position) as Share
|
val share = sharesListView!!.getItemAtPosition(info.position) as Share
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
R.id.share_menu_pin -> {
|
R.id.share_menu_pin -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.PIN,
|
action = DownloadAction.PIN,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = share.id,
|
id = share.id,
|
||||||
name = share.name,
|
name = share.name,
|
||||||
@ -143,8 +144,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_unpin -> {
|
R.id.share_menu_unpin -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.UNPIN,
|
action = DownloadAction.UNPIN,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = share.id,
|
id = share.id,
|
||||||
name = share.name,
|
name = share.name,
|
||||||
@ -153,8 +154,8 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_download -> {
|
R.id.share_menu_download -> {
|
||||||
downloadHandler.justDownload(
|
DownloadUtil.justDownload(
|
||||||
DownloadAction.DOWNLOAD,
|
action = DownloadAction.DOWNLOAD,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = share.id,
|
id = share.id,
|
||||||
name = share.name,
|
name = share.name,
|
||||||
@ -163,22 +164,20 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_play_now -> {
|
R.id.share_menu_play_now -> {
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
mediaPlayerManager.playTracksAndToast(
|
||||||
this,
|
this,
|
||||||
share.id,
|
|
||||||
share.name,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
autoPlay = true,
|
id = share.id,
|
||||||
|
name = share.name,
|
||||||
shuffle = false
|
shuffle = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_play_shuffled -> {
|
R.id.share_menu_play_shuffled -> {
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
mediaPlayerManager.playTracksAndToast(
|
||||||
this,
|
this,
|
||||||
share.id,
|
|
||||||
share.name,
|
|
||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
autoPlay = true,
|
id = share.id,
|
||||||
|
name = share.name,
|
||||||
shuffle = true,
|
shuffle = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -214,8 +213,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
override fun done(result: Any?) {
|
override fun done(result: Any?) {
|
||||||
shareAdapter!!.remove(share)
|
shareAdapter!!.remove(share)
|
||||||
shareAdapter!!.notifyDataSetChanged()
|
shareAdapter!!.notifyDataSetChanged()
|
||||||
Util.toast(
|
toast(
|
||||||
context,
|
|
||||||
resources.getString(R.string.menu_deleted_share, share.name)
|
resources.getString(R.string.menu_deleted_share, share.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -237,7 +235,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
getErrorMessage(error)
|
getErrorMessage(error)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Util.toast(context, msg, false)
|
toast(msg, false)
|
||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
}.setNegativeButton(R.string.common_cancel, null).show()
|
}.setNegativeButton(R.string.common_cancel, null).show()
|
||||||
@ -315,8 +313,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
|
|
||||||
override fun done(result: Any?) {
|
override fun done(result: Any?) {
|
||||||
load(true)
|
load(true)
|
||||||
Util.toast(
|
toast(
|
||||||
context,
|
|
||||||
resources.getString(R.string.playlist_updated_info, share.name)
|
resources.getString(R.string.playlist_updated_info, share.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -338,7 +335,7 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
getErrorMessage(error)
|
getErrorMessage(error)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Util.toast(context, msg, false)
|
toast(msg, false)
|
||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
}
|
}
|
||||||
|
@ -190,17 +190,17 @@ class ImageLoader(
|
|||||||
if (artist.coverArt == null) return
|
if (artist.coverArt == null) return
|
||||||
val key = FileUtil.getArtistArtKey(artist.name, false)
|
val key = FileUtil.getArtistArtKey(artist.name, false)
|
||||||
val file = FileUtil.getAlbumArtFile(key)
|
val file = FileUtil.getAlbumArtFile(key)
|
||||||
cacheCoverArt(artist.coverArt!!, file)
|
downloadCoverArt(artist.coverArt!!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a cover art file of a Track and cache it on disk
|
* Download a cover art file of a Track and cache it on disk
|
||||||
*/
|
*/
|
||||||
fun cacheCoverArt(track: Track) {
|
fun downloadCoverArt(track: Track) {
|
||||||
cacheCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
|
downloadCoverArt(track.coverArt!!, FileUtil.getAlbumArtFile(track))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cacheCoverArt(id: String, file: String) = launch {
|
fun downloadCoverArt(id: String, file: String) = launch {
|
||||||
if (id.isBlank()) return@launch
|
if (id.isBlank()) return@launch
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
@ -56,8 +56,10 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
|
|||||||
|
|
||||||
val albumArtFile = FileUtil.getAlbumArtFile(parts[1])
|
val albumArtFile = FileUtil.getAlbumArtFile(parts[1])
|
||||||
Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile)
|
Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile)
|
||||||
|
|
||||||
|
// TODO: Check if the dependency on the image loader could be removed.
|
||||||
imageLoaderProvider.executeOn {
|
imageLoaderProvider.executeOn {
|
||||||
it.cacheCoverArt(parts[0], albumArtFile)
|
it.downloadCoverArt(parts[0], albumArtFile)
|
||||||
}
|
}
|
||||||
val file = File(albumArtFile)
|
val file = File(albumArtFile)
|
||||||
if (!file.exists()) return null
|
if (!file.exists()) return null
|
||||||
|
@ -23,7 +23,7 @@ import android.widget.RemoteViews
|
|||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
|
import org.moire.ultrasonic.receiver.UltrasonicIntentReceiver
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
|
|
||||||
// Emulate media button clicks.
|
// Emulate media button clicks.
|
||||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
Intent.EXTRA_KEY_EVENT,
|
Intent.EXTRA_KEY_EVENT,
|
||||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
||||||
@ -241,12 +241,12 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
flags = 0
|
flags = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// needed starting Android 12 (S = 31)
|
// needed starting Android 12 (S = 31)
|
||||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
flags = PendingIntent.FLAG_IMMUTABLE
|
||||||
}
|
}
|
||||||
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
|
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
|
||||||
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
|
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
|
||||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
Intent.EXTRA_KEY_EVENT,
|
Intent.EXTRA_KEY_EVENT,
|
||||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
|
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)
|
||||||
@ -254,7 +254,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags)
|
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, flags)
|
||||||
views.setOnClickPendingIntent(R.id.control_next, pendingIntent)
|
views.setOnClickPendingIntent(R.id.control_next, pendingIntent)
|
||||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
intent.component = ComponentName(context, MediaButtonIntentReceiver::class.java)
|
intent.component = ComponentName(context, UltrasonicIntentReceiver::class.java)
|
||||||
intent.putExtra(
|
intent.putExtra(
|
||||||
Intent.EXTRA_KEY_EVENT,
|
Intent.EXTRA_KEY_EVENT,
|
||||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS)
|
||||||
|
@ -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.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class UltrasonicIntentReceiver : BroadcastReceiver() {
|
class UltrasonicIntentReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
private val lifecycleSupport = inject<MediaPlayerLifecycleSupport>(
|
private val lifecycleSupport by inject<MediaPlayerLifecycleSupport>()
|
||||||
MediaPlayerLifecycleSupport::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val intentAction = intent.action
|
val intentAction = intent.action
|
||||||
Timber.i("Received Ultrasonic Intent: %s", intentAction)
|
Timber.i("Received Ultrasonic Intent: %s", intentAction)
|
||||||
try {
|
try {
|
||||||
lifecycleSupport.value.receiveIntent(intent)
|
lifecycleSupport.receiveIntent(intent)
|
||||||
if (isOrderedBroadcast) {
|
if (isOrderedBroadcast) {
|
||||||
abortBroadcast()
|
abortBroadcast()
|
||||||
}
|
}
|
||||||
|
@ -315,6 +315,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
return musicService.getStreamUrl(id, maxBitRate, format)
|
return musicService.getStreamUrl(id, maxBitRate, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isJukeboxAvailable(): Boolean {
|
||||||
|
return musicService.isJukeboxAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||||
return musicService.updateJukeboxPlaylist(ids)
|
return musicService.updateJukeboxPlaylist(ids)
|
||||||
|
@ -29,8 +29,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
@ -61,6 +63,7 @@ private const val CHECK_INTERVAL = 5000L
|
|||||||
class DownloadService : Service(), KoinComponent {
|
class DownloadService : Service(), KoinComponent {
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private val storageMonitor: ExternalStorageMonitor by inject()
|
private val storageMonitor: ExternalStorageMonitor by inject()
|
||||||
|
private val cacheCleaner: CacheCleaner by inject()
|
||||||
private val binder: IBinder = SimpleServiceBinder(this)
|
private val binder: IBinder = SimpleServiceBinder(this)
|
||||||
|
|
||||||
private var isInForeground = false
|
private var isInForeground = false
|
||||||
@ -153,7 +156,7 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
|
|
||||||
// Stop Executor service when done downloading
|
// Stop Executor service when done downloading
|
||||||
if (activeDownloads.isEmpty()) {
|
if (activeDownloads.isEmpty()) {
|
||||||
CacheCleaner().cleanSpace()
|
cacheCleaner.cleanSpace()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +282,17 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
updateSaveFlag: Boolean = false
|
updateSaveFlag: Boolean = false
|
||||||
) {
|
) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
downloadAsync(tracks, save, isHighPriority, updateSaveFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadAsync(
|
||||||
|
tracks: List<Track>,
|
||||||
|
save: Boolean = false,
|
||||||
|
isHighPriority: Boolean = false,
|
||||||
|
updateSaveFlag: Boolean = false
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
// Remove tracks which are already downloaded and update the save flag
|
// Remove tracks which are already downloaded and update the save flag
|
||||||
// if needed
|
// if needed
|
||||||
var filteredTracks = if (updateSaveFlag) {
|
var filteredTracks = if (updateSaveFlag) {
|
||||||
@ -384,8 +397,14 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
failedList.clear()
|
failedList.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(track: Track) {
|
private fun delete(track: Track) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
deleteAsync(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun deleteAsync(track: Track) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
|
downloadQueue.get(track.id)?.let { downloadQueue.remove(it) }
|
||||||
failedList[track.id]?.let { downloadQueue.remove(it) }
|
failedList[track.id]?.let { downloadQueue.remove(it) }
|
||||||
cancelDownload(track)
|
cancelDownload(track)
|
||||||
@ -394,7 +413,8 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
Storage.delete(track.getCompleteFile())
|
Storage.delete(track.getCompleteFile())
|
||||||
Storage.delete(track.getPinnedFile())
|
Storage.delete(track.getPinnedFile())
|
||||||
postState(track, DownloadState.IDLE)
|
postState(track, DownloadState.IDLE)
|
||||||
CacheCleaner().cleanDatabaseSelective(track)
|
val cacheCleaner: CacheCleaner by inject(CacheCleaner::class.java)
|
||||||
|
cacheCleaner.cleanDatabaseSelective(track)
|
||||||
Util.scanMedia(track.getPinnedFile())
|
Util.scanMedia(track.getPinnedFile())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -409,22 +429,38 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
tracks.forEach(::delete)
|
tracks.forEach(::delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unpin(track: Track) {
|
suspend fun unpinAsync(tracks: List<Track>) {
|
||||||
// Update Pinned flag of items in progress
|
tracks.forEach { unpinAsync(it) }
|
||||||
downloadQueue.get(track.id)?.pinned = false
|
}
|
||||||
activeDownloads[track.id]?.downloadTrack?.pinned = false
|
|
||||||
failedList[track.id]?.pinned = false
|
|
||||||
|
|
||||||
val pinnedFile = track.getPinnedFile()
|
suspend fun deleteAsync(tracks: List<Track>) {
|
||||||
if (!Storage.isPathExists(pinnedFile)) return
|
tracks.forEach { deleteAsync(it) }
|
||||||
val file = Storage.getFromPath(track.getPinnedFile()) ?: return
|
}
|
||||||
try {
|
|
||||||
Storage.rename(file, track.getCompleteFile())
|
private fun unpin(track: Track) {
|
||||||
} catch (ignored: FileAlreadyExistsException) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
// Play console has revealed a crash when for some reason both files exist
|
unpinAsync(track)
|
||||||
Storage.delete(file.path)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun unpinAsync(track: Track) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// Update Pinned flag of items in progress
|
||||||
|
downloadQueue.get(track.id)?.pinned = false
|
||||||
|
activeDownloads[track.id]?.downloadTrack?.pinned = false
|
||||||
|
failedList[track.id]?.pinned = false
|
||||||
|
|
||||||
|
val pinnedFile = track.getPinnedFile()
|
||||||
|
if (!Storage.isPathExists(pinnedFile)) return@withContext
|
||||||
|
val file = Storage.getFromPath(track.getPinnedFile()) ?: return@withContext
|
||||||
|
try {
|
||||||
|
Storage.rename(file, track.getCompleteFile())
|
||||||
|
} catch (ignored: FileAlreadyExistsException) {
|
||||||
|
// Play console has revealed a crash when for some reason both files exist
|
||||||
|
Storage.delete(file.path)
|
||||||
|
}
|
||||||
|
postState(track, DownloadState.DONE)
|
||||||
}
|
}
|
||||||
postState(track, DownloadState.DONE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReturnCount")
|
@Suppress("ReturnCount")
|
||||||
|
@ -256,7 +256,7 @@ class DownloadTask(
|
|||||||
|
|
||||||
// Download the largest size that we can display in the UI
|
// Download the largest size that we can display in the UI
|
||||||
imageLoaderProvider.executeOn { imageLoader ->
|
imageLoaderProvider.executeOn { imageLoader ->
|
||||||
imageLoader.cacheCoverArt(this)
|
imageLoader.downloadCoverArt(this)
|
||||||
// Cache small copies of the Artist picture
|
// Cache small copies of the Artist picture
|
||||||
directArtist?.let { imageLoader.cacheArtistPicture(it) }
|
directArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||||
compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
|
compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* AutoMediaBrowserCallback.kt
|
* MediaLibrarySessionCallback.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.car.app.connection.CarConnection
|
import androidx.car.app.connection.CarConnection
|
||||||
@ -32,11 +31,14 @@ import androidx.media3.session.SessionResult.RESULT_SUCCESS
|
|||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.guava.await
|
import kotlinx.coroutines.guava.await
|
||||||
import kotlinx.coroutines.guava.future
|
import kotlinx.coroutines.guava.future
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -48,8 +50,6 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
|||||||
import org.moire.ultrasonic.domain.SearchCriteria
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
import org.moire.ultrasonic.domain.SearchResult
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
|
||||||
import org.moire.ultrasonic.service.RatingManager
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.util.buildMediaItem
|
import org.moire.ultrasonic.util.buildMediaItem
|
||||||
import org.moire.ultrasonic.util.toMediaItem
|
import org.moire.ultrasonic.util.toMediaItem
|
||||||
@ -98,10 +98,12 @@ const val PLAY_COMMAND = "play "
|
|||||||
* MediaBrowserService implementation for e.g. Android Auto
|
* MediaBrowserService implementation for e.g. Android Auto
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||||
class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
class MediaLibrarySessionCallback :
|
||||||
|
MediaLibraryService.MediaLibrarySession.Callback,
|
||||||
|
KoinComponent {
|
||||||
|
|
||||||
private val applicationContext: Context by inject()
|
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
private val playbackStateSerializer: PlaybackStateSerializer by inject()
|
||||||
|
|
||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
@ -243,6 +245,25 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
|
override fun onPlaybackResumption(
|
||||||
|
mediaSession: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo
|
||||||
|
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
||||||
|
val result = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
|
||||||
|
serviceScope.launch {
|
||||||
|
val state = playbackStateSerializer.deserializeNow()
|
||||||
|
if (state != null) {
|
||||||
|
result.set(state.toMediaItemsWithStartPosition())
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
mediaSession.player.shuffleModeEnabled = state.shufflePlay
|
||||||
|
mediaSession.player.repeatMode = state.repeatMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private fun configureRepeatMode(player: Player) {
|
private fun configureRepeatMode(player: Player) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Timber.d("Car app library available, observing CarConnection")
|
Timber.d("Car app library available, observing CarConnection")
|
||||||
@ -251,7 +272,7 @@ class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callbac
|
|||||||
|
|
||||||
var lastCarConnectionType = -1
|
var lastCarConnectionType = -1
|
||||||
|
|
||||||
CarConnection(applicationContext).type.observeForever {
|
CarConnection(UApp.applicationContext()).type.observeForever {
|
||||||
if (lastCarConnectionType == it)
|
if (lastCarConnectionType == it)
|
||||||
return@observeForever
|
return@observeForever
|
||||||
|
|
@ -16,8 +16,6 @@ import android.os.Build
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.moire.ultrasonic.app.UApp
|
|
||||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.CacheCleaner
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
@ -29,11 +27,13 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* This class is responsible for handling received events for the Media Player implementation
|
* This class is responsible for handling received events for the Media Player implementation
|
||||||
*/
|
*/
|
||||||
class MediaPlayerLifecycleSupport : KoinComponent {
|
class MediaPlayerLifecycleSupport(
|
||||||
|
val mediaPlayerManager: MediaPlayerManager,
|
||||||
|
private val playbackStateSerializer: PlaybackStateSerializer,
|
||||||
|
val imageLoaderProvider: ImageLoaderProvider,
|
||||||
|
private val cacheCleaner: CacheCleaner
|
||||||
|
) : KoinComponent {
|
||||||
private lateinit var ratingManager: RatingManager
|
private lateinit var ratingManager: RatingManager
|
||||||
private val playbackStateSerializer by inject<PlaybackStateSerializer>()
|
|
||||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
|
||||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
|
||||||
|
|
||||||
private var created = false
|
private var created = false
|
||||||
private var headsetEventReceiver: BroadcastReceiver? = null
|
private var headsetEventReceiver: BroadcastReceiver? = null
|
||||||
@ -70,7 +70,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||||||
|
|
||||||
registerHeadsetReceiver()
|
registerHeadsetReceiver()
|
||||||
|
|
||||||
CacheCleaner().clean()
|
cacheCleaner.clean()
|
||||||
created = true
|
created = true
|
||||||
ratingManager = RatingManager.instance
|
ratingManager = RatingManager.instance
|
||||||
Timber.i("LifecycleSupport created")
|
Timber.i("LifecycleSupport created")
|
||||||
@ -78,15 +78,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||||||
|
|
||||||
private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) {
|
private fun restoreLastSession(autoPlay: Boolean, afterRestore: Runnable?) {
|
||||||
playbackStateSerializer.deserialize {
|
playbackStateSerializer.deserialize {
|
||||||
|
if (it == null) return@deserialize null
|
||||||
|
Timber.i("Restoring %s songs", it.songs.size)
|
||||||
|
|
||||||
Timber.i("Restoring %s songs", it!!.songs.size)
|
mediaPlayerManager.restore(it, autoPlay)
|
||||||
|
|
||||||
mediaPlayerManager.restore(
|
|
||||||
it,
|
|
||||||
autoPlay,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
afterRestore?.run()
|
afterRestore?.run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +94,6 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||||||
applicationContext().unregisterReceiver(headsetEventReceiver)
|
applicationContext().unregisterReceiver(headsetEventReceiver)
|
||||||
|
|
||||||
imageLoaderProvider.clearImageLoader()
|
imageLoaderProvider.clearImageLoader()
|
||||||
UApp.instance!!.shutdownKoin()
|
|
||||||
|
|
||||||
created = false
|
created = false
|
||||||
Timber.i("LifecycleSupport destroyed")
|
Timber.i("LifecycleSupport destroyed")
|
||||||
|
@ -7,10 +7,11 @@
|
|||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.HeartRating
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
@ -29,17 +30,20 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID
|
||||||
import org.moire.ultrasonic.data.RatingUpdate
|
import org.moire.ultrasonic.data.RatingUpdate
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.playback.PlaybackService
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
|
import org.moire.ultrasonic.util.DownloadUtil
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import org.moire.ultrasonic.util.Util.navigateToCurrent
|
||||||
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
|
import org.moire.ultrasonic.util.launchWithToast
|
||||||
import org.moire.ultrasonic.util.toMediaItem
|
import org.moire.ultrasonic.util.toMediaItem
|
||||||
import org.moire.ultrasonic.util.toTrack
|
import org.moire.ultrasonic.util.toTrack
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -56,12 +60,9 @@ private const val VOLUME_DELTA = 0.05f
|
|||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class MediaPlayerManager(
|
class MediaPlayerManager(
|
||||||
private val playbackStateSerializer: PlaybackStateSerializer,
|
private val playbackStateSerializer: PlaybackStateSerializer,
|
||||||
private val externalStorageMonitor: ExternalStorageMonitor,
|
private val externalStorageMonitor: ExternalStorageMonitor
|
||||||
val context: Context
|
|
||||||
) : KoinComponent {
|
) : KoinComponent {
|
||||||
|
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
|
||||||
|
|
||||||
private var created = false
|
private var created = false
|
||||||
var suggestedPlaylistName: String? = null
|
var suggestedPlaylistName: String? = null
|
||||||
var keepScreenOn = false
|
var keepScreenOn = false
|
||||||
@ -73,8 +74,10 @@ class MediaPlayerManager(
|
|||||||
|
|
||||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
private var mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
private var sessionToken =
|
private var sessionToken = SessionToken(
|
||||||
SessionToken(context, ComponentName(context, PlaybackService::class.java))
|
UApp.applicationContext(),
|
||||||
|
ComponentName(UApp.applicationContext(), PlaybackService::class.java)
|
||||||
|
)
|
||||||
|
|
||||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||||
|
|
||||||
@ -145,12 +148,11 @@ class MediaPlayerManager(
|
|||||||
Timber.w(error.toString())
|
Timber.w(error.toString())
|
||||||
if (!isJukeboxEnabled) return
|
if (!isJukeboxEnabled) return
|
||||||
|
|
||||||
val context = UApp.applicationContext()
|
|
||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
Util.toast(
|
toast(
|
||||||
context,
|
|
||||||
error.errorCode,
|
error.errorCode,
|
||||||
false
|
false,
|
||||||
|
UApp.applicationContext()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isJukeboxEnabled = false
|
isJukeboxEnabled = false
|
||||||
@ -199,7 +201,7 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||||
val jukebox = activeServerProvider.getActiveServer().jukeboxByDefault
|
val jukebox = it.jukeboxByDefault
|
||||||
// Remove all songs when changing servers before turning on Jukebox.
|
// Remove all songs when changing servers before turning on Jukebox.
|
||||||
// Jukebox wouldn't find the songs on the new server.
|
// Jukebox wouldn't find the songs on the new server.
|
||||||
if (jukebox) controller?.clearMediaItems()
|
if (jukebox) controller?.clearMediaItems()
|
||||||
@ -246,10 +248,10 @@ class MediaPlayerManager(
|
|||||||
|
|
||||||
private fun createMediaController(onCreated: () -> Unit) {
|
private fun createMediaController(onCreated: () -> Unit) {
|
||||||
mediaControllerFuture = MediaController.Builder(
|
mediaControllerFuture = MediaController.Builder(
|
||||||
context,
|
UApp.applicationContext(),
|
||||||
sessionToken
|
sessionToken
|
||||||
)
|
)
|
||||||
// Specify mainThread explicitely
|
// Specify mainThread explicitly
|
||||||
.setApplicationLooper(Looper.getMainLooper())
|
.setApplicationLooper(Looper.getMainLooper())
|
||||||
.buildAsync()
|
.buildAsync()
|
||||||
|
|
||||||
@ -320,17 +322,15 @@ class MediaPlayerManager(
|
|||||||
externalStorageMonitor.onDestroy()
|
externalStorageMonitor.onDestroy()
|
||||||
DownloadService.requestStop()
|
DownloadService.requestStop()
|
||||||
created = false
|
created = false
|
||||||
Timber.i("MediaPlayerController destroyed")
|
Timber.i("MediaPlayerManager destroyed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun restore(
|
fun restore(
|
||||||
state: PlaybackState,
|
state: PlaybackState,
|
||||||
autoPlay: Boolean,
|
autoPlay: Boolean
|
||||||
newPlaylist: Boolean
|
|
||||||
) {
|
) {
|
||||||
val insertionMode = if (newPlaylist) InsertionMode.CLEAR
|
val insertionMode = InsertionMode.APPEND
|
||||||
else InsertionMode.APPEND
|
|
||||||
|
|
||||||
addToPlaylist(
|
addToPlaylist(
|
||||||
state.songs,
|
state.songs,
|
||||||
@ -474,6 +474,80 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun addToPlaylistAsync(
|
||||||
|
songs: List<Track>,
|
||||||
|
autoPlay: Boolean,
|
||||||
|
shuffle: Boolean,
|
||||||
|
insertionMode: InsertionMode
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
addToPlaylist(
|
||||||
|
songs = songs,
|
||||||
|
autoPlay = autoPlay,
|
||||||
|
shuffle = shuffle,
|
||||||
|
insertionMode = insertionMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun playTracksAndToast(
|
||||||
|
fragment: Fragment,
|
||||||
|
insertionMode: InsertionMode,
|
||||||
|
tracks: List<Track> = listOf(),
|
||||||
|
id: String? = null,
|
||||||
|
name: String? = "",
|
||||||
|
isShare: Boolean = false,
|
||||||
|
isDirectory: Boolean = true,
|
||||||
|
shuffle: Boolean = false,
|
||||||
|
isArtist: Boolean = false
|
||||||
|
) {
|
||||||
|
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
|
||||||
|
|
||||||
|
scope.launchWithToast {
|
||||||
|
|
||||||
|
val list: List<Track> =
|
||||||
|
tracks.ifEmpty {
|
||||||
|
requireNotNull(id)
|
||||||
|
DownloadUtil.getTracksFromServerAsync(isArtist, id, isDirectory, name, isShare)
|
||||||
|
}
|
||||||
|
|
||||||
|
addToPlaylistAsync(
|
||||||
|
songs = list,
|
||||||
|
insertionMode = insertionMode,
|
||||||
|
autoPlay = (insertionMode == InsertionMode.CLEAR),
|
||||||
|
shuffle = shuffle,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (insertionMode == InsertionMode.CLEAR) {
|
||||||
|
fragment.navigateToCurrent()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (insertionMode) {
|
||||||
|
InsertionMode.AFTER_CURRENT ->
|
||||||
|
quantize(R.plurals.n_songs_added_after_current, list)
|
||||||
|
|
||||||
|
InsertionMode.APPEND ->
|
||||||
|
quantize(R.plurals.n_songs_added_to_end, list)
|
||||||
|
|
||||||
|
InsertionMode.CLEAR -> {
|
||||||
|
if (Settings.shouldTransitionOnPlayback)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
quantize(R.plurals.n_songs_added_play_now, list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun quantize(resId: Int, tracks: List<Track>): String {
|
||||||
|
return UApp.applicationContext().resources.getQuantityString(
|
||||||
|
resId,
|
||||||
|
tracks.size,
|
||||||
|
tracks.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@set:Synchronized
|
@set:Synchronized
|
||||||
var isShufflePlayEnabled: Boolean
|
var isShufflePlayEnabled: Boolean
|
||||||
get() = controller?.shuffleModeEnabled == true
|
get() = controller?.shuffleModeEnabled == true
|
||||||
@ -649,21 +723,6 @@ class MediaPlayerManager(
|
|||||||
Timber.i("MediaPlayerController released")
|
Timber.i("MediaPlayerController released")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function calls the music service directly and
|
|
||||||
* therefore can't be called from the main thread
|
|
||||||
*/
|
|
||||||
val isJukeboxAvailable: Boolean
|
|
||||||
get() {
|
|
||||||
try {
|
|
||||||
val username = activeServerProvider.getActiveServer().userName
|
|
||||||
return getMusicService().getUser(username).jukeboxRole
|
|
||||||
} catch (all: Exception) {
|
|
||||||
Timber.w(all, "Error getting user information")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun adjustVolume(up: Boolean) {
|
fun adjustVolume(up: Boolean) {
|
||||||
val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA
|
val delta = if (up) VOLUME_DELTA else -VOLUME_DELTA
|
||||||
var gain = controller?.volume ?: return
|
var gain = controller?.volume ?: return
|
||||||
@ -676,7 +735,7 @@ class MediaPlayerManager(
|
|||||||
/*
|
/*
|
||||||
* Sets the rating of the current track
|
* Sets the rating of the current track
|
||||||
*/
|
*/
|
||||||
fun setRating(rating: Rating) {
|
private fun setRating(rating: Rating) {
|
||||||
if (controller is MediaController) {
|
if (controller is MediaController) {
|
||||||
(controller as MediaController).setRating(rating)
|
(controller as MediaController).setRating(rating)
|
||||||
}
|
}
|
||||||
@ -724,7 +783,7 @@ class MediaPlayerManager(
|
|||||||
val currentMediaItemIndex: Int
|
val currentMediaItemIndex: Int
|
||||||
get() = controller?.currentMediaItemIndex ?: -1
|
get() = controller?.currentMediaItemIndex ?: -1
|
||||||
|
|
||||||
fun getCurrentShuffleIndex(): Int {
|
private fun getCurrentShuffleIndex(): Int {
|
||||||
val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1
|
val currentMediaItemIndex = controller?.currentMediaItemIndex ?: return -1
|
||||||
return getShuffledIndexOf(currentMediaItemIndex)
|
return getShuffledIndexOf(currentMediaItemIndex)
|
||||||
}
|
}
|
||||||
@ -768,9 +827,8 @@ class MediaPlayerManager(
|
|||||||
* in the shuffled timeline.
|
* in the shuffled timeline.
|
||||||
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
|
* @return The index of the item in the shuffled timeline, or [C.INDEX_UNSET] if not found.
|
||||||
*/
|
*/
|
||||||
fun getShuffledIndexOf(searchPosition: Int): Int {
|
private fun getShuffledIndexOf(searchPosition: Int): Int {
|
||||||
return getWindowIndexWhere(false) {
|
return getWindowIndexWhere(false) { _, windowIndex ->
|
||||||
_, windowIndex ->
|
|
||||||
windowIndex == searchPosition
|
windowIndex == searchPosition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -784,8 +842,7 @@ class MediaPlayerManager(
|
|||||||
* @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found.
|
* @return the index of the item in the unshuffled timeline, or [C.INDEX_UNSET] if not found.
|
||||||
*/
|
*/
|
||||||
fun getUnshuffledIndexOf(shufflePosition: Int): Int {
|
fun getUnshuffledIndexOf(shufflePosition: Int): Int {
|
||||||
return getWindowIndexWhere(true) {
|
return getWindowIndexWhere(true) { count, _ ->
|
||||||
count, _ ->
|
|
||||||
count == shufflePosition
|
count == shufflePosition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,6 +138,8 @@ interface MusicService {
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String?
|
fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String?
|
||||||
|
|
||||||
|
fun isJukeboxAvailable(): Boolean
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus
|
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus
|
||||||
|
|
||||||
|
@ -335,6 +335,10 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isJukeboxAvailable(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||||
throw OfflineException("Jukebox not available in offline mode")
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* PlaybackService.kt
|
* PlaybackService.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@ -42,15 +42,11 @@ import org.moire.ultrasonic.activity.NavigationActivity
|
|||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.data.CachedDataSource
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider
|
||||||
import org.moire.ultrasonic.service.DownloadService
|
|
||||||
import org.moire.ultrasonic.service.JukeboxMediaPlayer
|
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.service.RxBus
|
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
@ -68,7 +64,7 @@ class PlaybackService :
|
|||||||
private var equalizer: EqualizerController? = null
|
private var equalizer: EqualizerController? = null
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
private lateinit var librarySessionCallback: AutoMediaBrowserCallback
|
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
|
||||||
|
|
||||||
private var rxBusSubscription = CompositeDisposable()
|
private var rxBusSubscription = CompositeDisposable()
|
||||||
|
|
||||||
@ -115,6 +111,7 @@ class PlaybackService :
|
|||||||
isStarted = false
|
isStarted = false
|
||||||
stopForegroundRemoveNotification()
|
stopForegroundRemoveNotification()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
instance = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
|
private val resolver: ResolvingDataSource.Resolver = ResolvingDataSource.Resolver {
|
||||||
@ -142,7 +139,7 @@ class PlaybackService :
|
|||||||
actualBackend = desiredBackend
|
actualBackend = desiredBackend
|
||||||
|
|
||||||
// Create browser interface
|
// Create browser interface
|
||||||
librarySessionCallback = AutoMediaBrowserCallback()
|
librarySessionCallback = MediaLibrarySessionCallback()
|
||||||
|
|
||||||
// This will need to use the AutoCalls
|
// This will need to use the AutoCalls
|
||||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
@ -1,7 +1,9 @@
|
|||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import androidx.media3.session.MediaSession
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
import org.moire.ultrasonic.util.toMediaItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the state of the Media Player implementation
|
* Represents the state of the Media Player implementation
|
||||||
@ -17,3 +19,12 @@ data class PlaybackState(
|
|||||||
private const val serialVersionUID = -293487987L
|
private const val serialVersionUID = -293487987L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
|
fun PlaybackState.toMediaItemsWithStartPosition(): MediaSession.MediaItemsWithStartPosition {
|
||||||
|
return MediaSession.MediaItemsWithStartPosition(
|
||||||
|
songs.map { it.toMediaItem() },
|
||||||
|
currentPlayingIndex,
|
||||||
|
currentPlayingPosition.toLong(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -24,6 +24,8 @@ import timber.log.Timber
|
|||||||
* This class is responsible for the serialization / deserialization
|
* This class is responsible for the serialization / deserialization
|
||||||
* of the playlist and the player state (e.g. current playing number and play position)
|
* of the playlist and the player state (e.g. current playing number and play position)
|
||||||
* to the filesystem.
|
* to the filesystem.
|
||||||
|
*
|
||||||
|
* TODO: Should use: MediaItemsWithStartPosition
|
||||||
*/
|
*/
|
||||||
class PlaybackStateSerializer : KoinComponent {
|
class PlaybackStateSerializer : KoinComponent {
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ class PlaybackStateSerializer : KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun serializeNow(
|
private fun serializeNow(
|
||||||
tracks: Iterable<Track>,
|
tracks: Iterable<Track>,
|
||||||
currentPlayingIndex: Int,
|
currentPlayingIndex: Int,
|
||||||
currentPlayingPosition: Int,
|
currentPlayingPosition: Int,
|
||||||
@ -85,7 +87,10 @@ class PlaybackStateSerializer : KoinComponent {
|
|||||||
if (isDeserializing.get()) return
|
if (isDeserializing.get()) return
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
try {
|
try {
|
||||||
deserializeNow(afterDeserialized)
|
val state = deserializeNow()
|
||||||
|
mainScope.launch {
|
||||||
|
afterDeserialized(state)
|
||||||
|
}
|
||||||
isSetup.set(true)
|
isSetup.set(true)
|
||||||
} catch (all: Exception) {
|
} catch (all: Exception) {
|
||||||
Timber.e(all, "Had a problem deserializing:")
|
Timber.e(all, "Had a problem deserializing:")
|
||||||
@ -95,11 +100,11 @@ class PlaybackStateSerializer : KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deserializeNow(afterDeserialized: (PlaybackState?) -> Unit?) {
|
fun deserializeNow(): PlaybackState? {
|
||||||
|
|
||||||
val state = FileUtil.deserialize<PlaybackState>(
|
val state = FileUtil.deserialize<PlaybackState>(
|
||||||
context, Constants.FILENAME_PLAYLIST_SER
|
context, Constants.FILENAME_PLAYLIST_SER
|
||||||
) ?: return
|
) ?: return null
|
||||||
|
|
||||||
Timber.i(
|
Timber.i(
|
||||||
"Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b",
|
"Deserialized currentPlayingIndex: %d, currentPlayingPosition: %d, shuffle: %b",
|
||||||
@ -108,9 +113,7 @@ class PlaybackStateSerializer : KoinComponent {
|
|||||||
state.shufflePlay
|
state.shufflePlay
|
||||||
)
|
)
|
||||||
|
|
||||||
mainScope.launch {
|
return state
|
||||||
afterDeserialized(state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -51,7 +51,7 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
open class RESTMusicService(
|
open class RESTMusicService(
|
||||||
val subsonicAPIClient: SubsonicAPIClient,
|
private val subsonicAPIClient: SubsonicAPIClient,
|
||||||
private val activeServerProvider: ActiveServerProvider
|
private val activeServerProvider: ActiveServerProvider
|
||||||
) : MusicService {
|
) : MusicService {
|
||||||
|
|
||||||
@ -504,6 +504,11 @@ open class RESTMusicService(
|
|||||||
builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isJukeboxAvailable(): Boolean {
|
||||||
|
val username = activeServerProvider.getActiveServer().userName
|
||||||
|
return getUser(username).jukeboxRole
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateJukeboxPlaylist(
|
override fun updateJukeboxPlaylist(
|
||||||
ids: List<String>?
|
ids: List<String>?
|
||||||
|
@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.Disposable
|
|||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import org.moire.ultrasonic.data.RatingUpdate
|
import org.moire.ultrasonic.data.RatingUpdate
|
||||||
|
import org.moire.ultrasonic.data.ServerSetting
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
|
||||||
class RxBus {
|
class RxBus {
|
||||||
@ -32,9 +33,9 @@ class RxBus {
|
|||||||
var activeServerChangingObservable: Observable<Int> =
|
var activeServerChangingObservable: Observable<Int> =
|
||||||
activeServerChangingPublisher
|
activeServerChangingPublisher
|
||||||
|
|
||||||
var activeServerChangedPublisher: PublishSubject<Int> =
|
var activeServerChangedPublisher: PublishSubject<ServerSetting> =
|
||||||
PublishSubject.create()
|
PublishSubject.create()
|
||||||
var activeServerChangedObservable: Observable<Int> =
|
var activeServerChangedObservable: Observable<ServerSetting> =
|
||||||
activeServerChangedPublisher.observeOn(mainThread())
|
activeServerChangedPublisher.observeOn(mainThread())
|
||||||
|
|
||||||
val themeChangedEventPublisher: PublishSubject<Unit> =
|
val themeChangedEventPublisher: PublishSubject<Unit> =
|
||||||
|
@ -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
|
package org.moire.ultrasonic.subsonic
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -20,8 +19,7 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Handles the lifetime of the Image Loader
|
* Handles the lifetime of the Image Loader
|
||||||
*/
|
*/
|
||||||
class
|
class ImageLoaderProvider :
|
||||||
ImageLoaderProvider(val context: Context) :
|
|
||||||
KoinComponent,
|
KoinComponent,
|
||||||
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
private var imageLoader: ImageLoader? = null
|
private var imageLoader: ImageLoader? = null
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
package org.moire.ultrasonic.subsonic
|
package org.moire.ultrasonic.subsonic
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for checking the availability of the network and storage
|
* Utility class for checking the availability of the network and storage
|
||||||
*/
|
*/
|
||||||
class NetworkAndStorageChecker(val context: Context) {
|
class NetworkAndStorageChecker {
|
||||||
fun warnIfNetworkOrStorageUnavailable() {
|
fun warnIfNetworkOrStorageUnavailable() {
|
||||||
if (!Util.isExternalStoragePresent()) {
|
if (!Util.isExternalStoragePresent()) {
|
||||||
Util.toast(context, R.string.select_album_no_sdcard)
|
Util.toast(R.string.select_album_no_sdcard, true, UApp.applicationContext())
|
||||||
} else if (!isOffline() && !Util.hasUsableNetwork()) {
|
} else if (!isOffline() && !Util.hasUsableNetwork()) {
|
||||||
Util.toast(context, R.string.select_album_no_network)
|
Util.toast(R.string.select_album_no_network, true, UApp.applicationContext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
package org.moire.ultrasonic.subsonic
|
package org.moire.ultrasonic.subsonic
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -17,27 +16,27 @@ import android.widget.EditText
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.lifecycle.lifecycleScope
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.collections.ArrayList
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.Share
|
import org.moire.ultrasonic.domain.Share
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.util.BackgroundTask
|
|
||||||
import org.moire.ultrasonic.util.CancellationToken
|
|
||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
import org.moire.ultrasonic.util.FragmentBackgroundTask
|
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.ShareDetails
|
import org.moire.ultrasonic.util.ShareDetails
|
||||||
import org.moire.ultrasonic.util.TimeSpanPicker
|
import org.moire.ultrasonic.util.TimeSpanPicker
|
||||||
|
import org.moire.ultrasonic.util.Util.getString
|
||||||
import org.moire.ultrasonic.util.Util.ifNotNull
|
import org.moire.ultrasonic.util.Util.ifNotNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class handles sharing items in the media library
|
* This class handles sharing items in the media library
|
||||||
*/
|
*/
|
||||||
class ShareHandler(val context: Context) {
|
class ShareHandler {
|
||||||
private var shareDescription: EditText? = null
|
private var shareDescription: EditText? = null
|
||||||
private var timeSpanPicker: TimeSpanPicker? = null
|
private var timeSpanPicker: TimeSpanPicker? = null
|
||||||
private var shareOnServerCheckBox: CheckBox? = null
|
private var shareOnServerCheckBox: CheckBox? = null
|
||||||
@ -51,21 +50,26 @@ class ShareHandler(val context: Context) {
|
|||||||
fun share(
|
fun share(
|
||||||
fragment: Fragment,
|
fragment: Fragment,
|
||||||
shareDetails: ShareDetails,
|
shareDetails: ShareDetails,
|
||||||
swipe: SwipeRefreshLayout?,
|
|
||||||
cancellationToken: CancellationToken,
|
|
||||||
additionalId: String?
|
additionalId: String?
|
||||||
) {
|
) {
|
||||||
val task: BackgroundTask<Share?> = object : FragmentBackgroundTask<Share?>(
|
val scope = fragment.activity?.lifecycleScope ?: fragment.lifecycleScope
|
||||||
fragment.requireActivity(),
|
scope.launch {
|
||||||
true,
|
val share = createShareOnServer(shareDetails, additionalId)
|
||||||
swipe,
|
startActivityForShare(share, shareDetails, fragment)
|
||||||
cancellationToken
|
}
|
||||||
) {
|
}
|
||||||
@Throws(Throwable::class)
|
|
||||||
override fun doInBackground(): Share? {
|
private suspend fun createShareOnServer(
|
||||||
|
shareDetails: ShareDetails,
|
||||||
|
additionalId: String?
|
||||||
|
): Share? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
return@withContext try {
|
||||||
|
|
||||||
val ids: MutableList<String> = ArrayList()
|
val ids: MutableList<String> = ArrayList()
|
||||||
|
|
||||||
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1) return null
|
if (!shareDetails.ShareOnServer && shareDetails.Entries.size == 1)
|
||||||
|
return@withContext null
|
||||||
if (shareDetails.Entries.isEmpty()) {
|
if (shareDetails.Entries.isEmpty()) {
|
||||||
additionalId.ifNotNull {
|
additionalId.ifNotNull {
|
||||||
ids.add(it)
|
ids.add(it)
|
||||||
@ -86,78 +90,80 @@ class ShareHandler(val context: Context) {
|
|||||||
val shares =
|
val shares =
|
||||||
musicService.createShare(ids, shareDetails.Description, timeInMillis)
|
musicService.createShare(ids, shareDetails.Description, timeInMillis)
|
||||||
|
|
||||||
return shares[0]
|
// Return the share
|
||||||
}
|
shares[0]
|
||||||
|
} catch (ignored: Exception) {
|
||||||
override fun done(result: Share?) {
|
null
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.type = "text/plain"
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
// Created a share, send the URL
|
|
||||||
intent.putExtra(
|
|
||||||
Intent.EXTRA_TEXT,
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Sending only text details
|
|
||||||
val textBuilder = StringBuilder()
|
|
||||||
textBuilder.appendLine(Settings.shareGreeting)
|
|
||||||
|
|
||||||
if (!shareDetails.Entries[0].title.isNullOrEmpty())
|
|
||||||
textBuilder.append(context.resources.getString(R.string.common_title))
|
|
||||||
.append(": ").appendLine(shareDetails.Entries[0].title)
|
|
||||||
if (!shareDetails.Entries[0].artist.isNullOrEmpty())
|
|
||||||
textBuilder.append(context.resources.getString(R.string.common_artist))
|
|
||||||
.append(": ").appendLine(shareDetails.Entries[0].artist)
|
|
||||||
if (!shareDetails.Entries[0].album.isNullOrEmpty())
|
|
||||||
textBuilder.append(context.resources.getString(R.string.common_album))
|
|
||||||
.append(": ").append(shareDetails.Entries[0].album)
|
|
||||||
|
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment.activity?.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
intent,
|
|
||||||
context.resources.getString(R.string.share_via)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
task.execute()
|
}
|
||||||
|
|
||||||
|
private suspend fun startActivityForShare(
|
||||||
|
result: Share?,
|
||||||
|
shareDetails: ShareDetails,
|
||||||
|
fragment: Fragment
|
||||||
|
) {
|
||||||
|
return withContext(Dispatchers.Main) {
|
||||||
|
val intent = Intent(Intent.ACTION_SEND)
|
||||||
|
intent.type = "text/plain"
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
// Created a share, send the URL
|
||||||
|
intent.putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
String.format(
|
||||||
|
Locale.ROOT, "%s\n\n%s", Settings.shareGreeting, result.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Sending only text details
|
||||||
|
val textBuilder = StringBuilder()
|
||||||
|
textBuilder.appendLine(Settings.shareGreeting)
|
||||||
|
|
||||||
|
if (!shareDetails.Entries[0].title.isNullOrEmpty())
|
||||||
|
textBuilder.append(getString(R.string.common_title))
|
||||||
|
.append(": ").appendLine(shareDetails.Entries[0].title)
|
||||||
|
if (!shareDetails.Entries[0].artist.isNullOrEmpty())
|
||||||
|
textBuilder.append(getString(R.string.common_artist))
|
||||||
|
.append(": ").appendLine(shareDetails.Entries[0].artist)
|
||||||
|
if (!shareDetails.Entries[0].album.isNullOrEmpty())
|
||||||
|
textBuilder.append(getString(R.string.common_album))
|
||||||
|
.append(": ").append(shareDetails.Entries[0].album)
|
||||||
|
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, textBuilder.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.activity?.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
intent,
|
||||||
|
getString(R.string.share_via)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createShare(
|
fun createShare(
|
||||||
fragment: Fragment,
|
fragment: Fragment,
|
||||||
tracks: List<Track?>?,
|
tracks: List<Track?>?,
|
||||||
swipe: SwipeRefreshLayout?,
|
|
||||||
cancellationToken: CancellationToken,
|
|
||||||
additionalId: String? = null
|
additionalId: String? = null
|
||||||
) {
|
) {
|
||||||
val askForDetails = Settings.shouldAskForShareDetails
|
val askForDetails = Settings.shouldAskForShareDetails
|
||||||
val shareDetails = ShareDetails()
|
val shareDetails = ShareDetails()
|
||||||
shareDetails.Entries = tracks
|
shareDetails.Entries = tracks
|
||||||
if (askForDetails) {
|
if (askForDetails) {
|
||||||
showDialog(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
showDialog(fragment, shareDetails, additionalId)
|
||||||
} else {
|
} else {
|
||||||
shareDetails.Description = Settings.defaultShareDescription
|
shareDetails.Description = Settings.defaultShareDescription
|
||||||
shareDetails.Expiration = System.currentTimeMillis() +
|
shareDetails.Expiration = System.currentTimeMillis() +
|
||||||
Settings.defaultShareExpirationInMillis
|
Settings.defaultShareExpirationInMillis
|
||||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
share(fragment, shareDetails, additionalId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun showDialog(
|
private fun showDialog(
|
||||||
fragment: Fragment,
|
fragment: Fragment,
|
||||||
shareDetails: ShareDetails,
|
shareDetails: ShareDetails,
|
||||||
swipe: SwipeRefreshLayout?,
|
|
||||||
cancellationToken: CancellationToken,
|
|
||||||
additionalId: String?
|
additionalId: String?
|
||||||
) {
|
) {
|
||||||
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
|
val layout = LayoutInflater.from(fragment.context).inflate(R.layout.share_details, null)
|
||||||
@ -175,18 +181,57 @@ class ShareHandler(val context: Context) {
|
|||||||
textViewExpiration = layout.findViewById<View>(R.id.textViewExpiration) as TextView
|
textViewExpiration = layout.findViewById<View>(R.id.textViewExpiration) as TextView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle the visibility based on shareDetails.Entries size
|
||||||
if (shareDetails.Entries.size == 1) {
|
if (shareDetails.Entries.size == 1) {
|
||||||
// For single songs the sharing may be done by text only
|
|
||||||
shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ ->
|
shareOnServerCheckBox?.setOnCheckedChangeListener { _, _ ->
|
||||||
updateVisibility()
|
updateVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
shareOnServerCheckBox?.isChecked = Settings.shareOnServer
|
shareOnServerCheckBox?.isChecked = Settings.shareOnServer
|
||||||
} else {
|
} else {
|
||||||
shareOnServerCheckBox?.isVisible = false
|
shareOnServerCheckBox?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisibility()
|
updateVisibility()
|
||||||
|
|
||||||
|
// Set up the dialog builder
|
||||||
|
val builder = makeDialogBuilder(fragment, shareDetails, additionalId, layout)
|
||||||
|
|
||||||
|
// Initialize UI components with default values
|
||||||
|
setupDefaultValues()
|
||||||
|
|
||||||
|
builder.create()
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDefaultValues() {
|
||||||
|
val defaultDescription = Settings.defaultShareDescription
|
||||||
|
val timeSpan = Settings.defaultShareExpiration
|
||||||
|
val split = pattern.split(timeSpan)
|
||||||
|
if (split.size == 2) {
|
||||||
|
val timeSpanAmount = split[0].toInt()
|
||||||
|
val timeSpanType = split[1]
|
||||||
|
if (timeSpanAmount > 0) {
|
||||||
|
noExpirationCheckBox!!.isChecked = false
|
||||||
|
timeSpanPicker!!.isEnabled = true
|
||||||
|
timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString())
|
||||||
|
timeSpanPicker!!.timeSpanType = timeSpanType
|
||||||
|
} else {
|
||||||
|
noExpirationCheckBox!!.isChecked = true
|
||||||
|
timeSpanPicker!!.isEnabled = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
noExpirationCheckBox!!.isChecked = true
|
||||||
|
timeSpanPicker!!.isEnabled = false
|
||||||
|
}
|
||||||
|
shareDescription!!.setText(defaultDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeDialogBuilder(
|
||||||
|
fragment: Fragment,
|
||||||
|
shareDetails: ShareDetails,
|
||||||
|
additionalId: String?,
|
||||||
|
layout: View?
|
||||||
|
): ConfirmationDialog.Builder {
|
||||||
val builder = ConfirmationDialog.Builder(fragment.requireContext())
|
val builder = ConfirmationDialog.Builder(fragment.requireContext())
|
||||||
builder.setTitle(R.string.share_set_share_options)
|
builder.setTitle(R.string.share_set_share_options)
|
||||||
|
|
||||||
@ -214,7 +259,7 @@ class ShareHandler(val context: Context) {
|
|||||||
Settings.shareOnServer = shareDetails.ShareOnServer
|
Settings.shareOnServer = shareDetails.ShareOnServer
|
||||||
}
|
}
|
||||||
|
|
||||||
share(fragment, shareDetails, swipe, cancellationToken, additionalId)
|
share(fragment, shareDetails, additionalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
builder.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||||
@ -224,35 +269,12 @@ class ShareHandler(val context: Context) {
|
|||||||
builder.setView(layout)
|
builder.setView(layout)
|
||||||
builder.setCancelable(true)
|
builder.setCancelable(true)
|
||||||
|
|
||||||
timeSpanPicker!!.setTimeSpanDisableText(context.resources.getString(R.string.no_expiration))
|
// Set up the timeSpanPicker
|
||||||
|
timeSpanPicker!!.setTimeSpanDisableText(getString(R.string.no_expiration))
|
||||||
noExpirationCheckBox!!.setOnCheckedChangeListener { _, b ->
|
noExpirationCheckBox!!.setOnCheckedChangeListener { _, b ->
|
||||||
timeSpanPicker!!.isEnabled = !b
|
timeSpanPicker!!.isEnabled = !b
|
||||||
}
|
}
|
||||||
|
return builder
|
||||||
val defaultDescription = Settings.defaultShareDescription
|
|
||||||
val timeSpan = Settings.defaultShareExpiration
|
|
||||||
|
|
||||||
val split = pattern.split(timeSpan)
|
|
||||||
if (split.size == 2) {
|
|
||||||
val timeSpanAmount = split[0].toInt()
|
|
||||||
val timeSpanType = split[1]
|
|
||||||
if (timeSpanAmount > 0) {
|
|
||||||
noExpirationCheckBox!!.isChecked = false
|
|
||||||
timeSpanPicker!!.isEnabled = true
|
|
||||||
timeSpanPicker!!.setTimeSpanAmount(timeSpanAmount.toString())
|
|
||||||
timeSpanPicker!!.timeSpanType = timeSpanType
|
|
||||||
} else {
|
|
||||||
noExpirationCheckBox!!.isChecked = true
|
|
||||||
timeSpanPicker!!.isEnabled = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
noExpirationCheckBox!!.isChecked = true
|
|
||||||
timeSpanPicker!!.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
shareDescription!!.setText(defaultDescription)
|
|
||||||
builder.create()
|
|
||||||
builder.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateVisibility() {
|
private fun updateVisibility() {
|
||||||
|
@ -16,7 +16,7 @@ class VideoPlayer {
|
|||||||
companion object {
|
companion object {
|
||||||
fun playVideo(context: Context, track: Track?) {
|
fun playVideo(context: Context, track: Track?) {
|
||||||
if (!Util.hasUsableNetwork() || track == null) {
|
if (!Util.hasUsableNetwork() || track == null) {
|
||||||
Util.toast(context, R.string.select_album_no_network)
|
Util.toast(R.string.select_album_no_network, true, context)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -32,7 +32,7 @@ class VideoPlayer {
|
|||||||
)
|
)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (all: Exception) {
|
} catch (all: Exception) {
|
||||||
Util.toast(context, all.toString(), false)
|
Util.toast(all.toString(), false, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import android.system.Os
|
|||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.future
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -19,7 +18,7 @@ import org.koin.java.KoinJavaComponent.inject
|
|||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Playlist
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
|
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
|
||||||
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
||||||
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
||||||
@ -38,7 +37,6 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent {
|
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent {
|
||||||
|
|
||||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
|
||||||
private val activeServerProvider by inject<ActiveServerProvider>()
|
private val activeServerProvider by inject<ActiveServerProvider>()
|
||||||
|
|
||||||
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
|
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
|
||||||
@ -235,16 +233,14 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
|
|||||||
|
|
||||||
private fun findFilesToNotDelete(): Set<String> {
|
private fun findFilesToNotDelete(): Set<String> {
|
||||||
val filesToNotDelete: MutableSet<String> = HashSet(5)
|
val filesToNotDelete: MutableSet<String> = HashSet(5)
|
||||||
val mediaPlayerManager: MediaPlayerManager by inject()
|
|
||||||
|
|
||||||
val playlist = mainScope.future { mediaPlayerManager.playlist }.get()
|
// We just take the last published playlist from RX
|
||||||
for (item in playlist) {
|
val playlist = RxBus.playlistObservable.blockingLast()
|
||||||
val track = item.toTrack()
|
for (track in playlist) {
|
||||||
filesToNotDelete.add(track.getPartialFile())
|
filesToNotDelete.add(track.getPartialFile())
|
||||||
filesToNotDelete.add(track.getCompleteFile())
|
filesToNotDelete.add(track.getCompleteFile())
|
||||||
filesToNotDelete.add(track.getPinnedFile())
|
filesToNotDelete.add(track.getPinnedFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
filesToNotDelete.add(musicDirectory.path)
|
filesToNotDelete.add(musicDirectory.path)
|
||||||
return filesToNotDelete
|
return filesToNotDelete
|
||||||
}
|
}
|
||||||
|
@ -37,4 +37,5 @@ object Constants {
|
|||||||
const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
|
const val FILENAME_PLAYLIST_SER = "downloadstate.ser"
|
||||||
const val ALBUM_ART_FILE = "folder.jpeg"
|
const val ALBUM_ART_FILE = "folder.jpeg"
|
||||||
const val RESULT_CLOSE_ALL = 1337
|
const val RESULT_CLOSE_ALL = 1337
|
||||||
|
const val MAX_SONGS_RECURSIVE = 500
|
||||||
}
|
}
|
||||||
|
@ -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.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.moire.ultrasonic.R
|
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -30,53 +28,50 @@ object CoroutinePatterns {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CoroutineScope.executeTaskWithToast(
|
fun CoroutineScope.launchWithToast(
|
||||||
task: suspend CoroutineScope.() -> Unit,
|
block: suspend CoroutineScope.() -> String?
|
||||||
successString: () -> String?
|
) {
|
||||||
): Job {
|
|
||||||
// Launch the Job
|
// Launch the Job
|
||||||
val job = launch(CoroutinePatterns.loggingExceptionHandler, block = task)
|
val deferred = async(CoroutinePatterns.loggingExceptionHandler, block = block)
|
||||||
|
|
||||||
// Setup a handler when the job is done
|
// Setup a handler when the job is done
|
||||||
job.invokeOnCompletion {
|
deferred.invokeOnCompletion {
|
||||||
val toastString = if (it != null && it !is CancellationException) {
|
val toastString = if (it != null && it !is CancellationException) {
|
||||||
CommunicationError.getErrorMessage(it)
|
CommunicationError.getErrorMessage(it)
|
||||||
} else {
|
} else {
|
||||||
successString()
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return early if nothing to post
|
|
||||||
if (toastString == null) return@invokeOnCompletion
|
|
||||||
|
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
Util.toast(UApp.applicationContext(), toastString)
|
val successString = toastString ?: deferred.await()
|
||||||
}
|
if (successString != null) {
|
||||||
}
|
Util.toast(successString, UApp.applicationContext())
|
||||||
|
}
|
||||||
return job
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CoroutineScope.executeTaskWithModalDialog(
|
|
||||||
fragment: Fragment,
|
|
||||||
task: suspend CoroutineScope.() -> Unit,
|
|
||||||
successString: () -> String
|
|
||||||
) {
|
|
||||||
// Create the job
|
|
||||||
val job = executeTaskWithToast(task, successString)
|
|
||||||
|
|
||||||
// Create the dialog
|
|
||||||
val builder = InfoDialog.Builder(fragment.requireContext())
|
|
||||||
builder.setTitle(R.string.background_task_wait)
|
|
||||||
builder.setMessage(R.string.background_task_loading)
|
|
||||||
builder.setOnCancelListener { job.cancel() }
|
|
||||||
builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() }
|
|
||||||
val dialog = builder.create()
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
// Add additional handler to close the dialog
|
|
||||||
job.invokeOnCompletion {
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unused, kept commented for eventual later use
|
||||||
|
// fun CoroutineScope.executeTaskWithModalDialog(
|
||||||
|
// fragment: Fragment,
|
||||||
|
// task: suspend CoroutineScope.() -> String?
|
||||||
|
// ) {
|
||||||
|
// // Create the job
|
||||||
|
// val job = launchWithToast(task)
|
||||||
|
//
|
||||||
|
// // Create the dialog
|
||||||
|
// val builder = InfoDialog.Builder(fragment.requireContext())
|
||||||
|
// builder.setTitle(R.string.background_task_wait)
|
||||||
|
// builder.setMessage(R.string.background_task_loading)
|
||||||
|
// builder.setOnCancelListener { job.cancel() }
|
||||||
|
// builder.setPositiveButton(R.string.common_cancel) { _, _ -> job.cancel() }
|
||||||
|
// val dialog = builder.create()
|
||||||
|
// dialog.show()
|
||||||
|
//
|
||||||
|
// // Add additional handler to close the dialog
|
||||||
|
// job.invokeOnCompletion {
|
||||||
|
// launch(Dispatchers.Main) {
|
||||||
|
// dialog.dismiss()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
@ -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
|
val seekIntervalMillis: Long
|
||||||
get() = (seekInterval / 1000).toLong()
|
get() = (seekInterval / 1000).toLong()
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
var mediaButtonsEnabled
|
|
||||||
by BooleanSetting(getKey(R.string.setting_key_media_buttons), true)
|
|
||||||
|
|
||||||
var resumePlayOnHeadphonePlug
|
var resumePlayOnHeadphonePlug
|
||||||
by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true)
|
by BooleanSetting(R.string.setting_key_resume_play_on_headphones_plug, true)
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ object Storage {
|
|||||||
if (rootNotFoundError) {
|
if (rootNotFoundError) {
|
||||||
Settings.customCacheLocation = false
|
Settings.customCacheLocation = false
|
||||||
Settings.cacheLocationUri = ""
|
Settings.cacheLocationUri = ""
|
||||||
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
|
Util.toast(R.string.settings_cache_location_error, true, UApp.applicationContext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class TimeSpanPicker(private var mContext: Context, attrs: AttributeSet?, defSty
|
|||||||
AdapterView.OnItemSelectedListener {
|
AdapterView.OnItemSelectedListener {
|
||||||
private val timeSpanEditText: EditText
|
private val timeSpanEditText: EditText
|
||||||
private val timeSpanSpinner: Spinner
|
private val timeSpanSpinner: Spinner
|
||||||
private val timeSpanDisableCheckbox: CheckBox
|
val timeSpanDisableCheckbox: CheckBox
|
||||||
private var mTimeSpan: Long = -1L
|
private var mTimeSpan: Long = -1L
|
||||||
private val adapter: ArrayAdapter<CharSequence>
|
private val adapter: ArrayAdapter<CharSequence>
|
||||||
private val dialog: View
|
private val dialog: View
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
package org.moire.ultrasonic.util
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
@ -37,12 +36,15 @@ import android.widget.Toast
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@ -107,33 +109,41 @@ object Util {
|
|||||||
context.getString(R.string.setting_key_theme_dark) -> {
|
context.getString(R.string.setting_key_theme_dark) -> {
|
||||||
R.style.UltrasonicTheme_Dark
|
R.style.UltrasonicTheme_Dark
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getString(R.string.setting_key_theme_black) -> {
|
context.getString(R.string.setting_key_theme_black) -> {
|
||||||
R.style.UltrasonicTheme_Black
|
R.style.UltrasonicTheme_Black
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getString(R.string.setting_key_theme_light) -> {
|
context.getString(R.string.setting_key_theme_light) -> {
|
||||||
R.style.UltrasonicTheme_Light
|
R.style.UltrasonicTheme_Light
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
R.style.UltrasonicTheme_DayNight
|
R.style.UltrasonicTheme_DayNight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getString(@StringRes resId: Int): String {
|
||||||
|
return applicationContext().resources.getString(resId)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) {
|
fun toast(messageId: Int, shortDuration: Boolean = true, context: Context?) {
|
||||||
toast(context, context!!.getString(messageId), shortDuration)
|
toast(applicationContext().getString(messageId), shortDuration, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun toast(context: Context?, message: CharSequence?) {
|
fun toast(message: CharSequence, context: Context?) {
|
||||||
toast(context, message, true)
|
toast(message, true, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@SuppressLint("ShowToast") // Invalid warning
|
// Toast needs a real context or it will throw a IllegalAccessException
|
||||||
fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) {
|
// We wrap it in a try-catch block, because if called after doing
|
||||||
// If called after doing some background processing, our context might have expired!
|
// some background processing, our context might have expired!
|
||||||
|
fun toast(message: CharSequence, shortDuration: Boolean, context: Context?) {
|
||||||
try {
|
try {
|
||||||
if (toast == null) {
|
if (toast == null) {
|
||||||
toast = Toast.makeText(
|
toast = Toast.makeText(
|
||||||
@ -153,6 +163,22 @@ object Util {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Fragment.toast(message: CharSequence, shortDuration: Boolean = true) {
|
||||||
|
toast(
|
||||||
|
message,
|
||||||
|
shortDuration = shortDuration,
|
||||||
|
context = this.context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.toast(messageId: Int = 0, shortDuration: Boolean = true) {
|
||||||
|
toast(
|
||||||
|
messageId = messageId,
|
||||||
|
shortDuration = shortDuration,
|
||||||
|
context = this.context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a byte-count to a formatted string suitable for display to the user.
|
* Converts a byte-count to a formatted string suitable for display to the user.
|
||||||
* For instance:
|
* For instance:
|
||||||
@ -479,7 +505,7 @@ object Util {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isNullOrWhiteSpace(string: String?): Boolean {
|
fun isNullOrWhiteSpace(string: String?): Boolean {
|
||||||
return string == null || string.isEmpty() || string.trim { it <= ' ' }.isEmpty()
|
return string.isNullOrEmpty() || string.trim { it <= ' ' }.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
@ -504,18 +530,22 @@ object Util {
|
|||||||
seconds
|
seconds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
hours > 0 -> {
|
hours > 0 -> {
|
||||||
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
|
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
minutes >= DEGRADE_PRECISION_AFTER -> {
|
minutes >= DEGRADE_PRECISION_AFTER -> {
|
||||||
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
|
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
minutes > 0 -> String.format(
|
minutes > 0 -> String.format(
|
||||||
Locale.getDefault(),
|
Locale.getDefault(),
|
||||||
"%d:%02d",
|
"%d:%02d",
|
||||||
minutes,
|
minutes,
|
||||||
seconds
|
seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> String.format(Locale.getDefault(), "0:%02d", seconds)
|
else -> String.format(Locale.getDefault(), "0:%02d", seconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -557,7 +587,7 @@ object Util {
|
|||||||
val requestPermissionLauncher =
|
val requestPermissionLauncher =
|
||||||
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
toast(applicationContext(), R.string.notification_permission_required)
|
toast(R.string.notification_permission_required, context = fragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,6 +599,7 @@ object Util {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postNotificationIfPermitted(
|
fun postNotificationIfPermitted(
|
||||||
notificationManagerCompat: NotificationManagerCompat,
|
notificationManagerCompat: NotificationManagerCompat,
|
||||||
id: Int,
|
id: Int,
|
||||||
@ -583,8 +614,8 @@ object Util {
|
|||||||
notificationManagerCompat.notify(id, notification)
|
notificationManagerCompat.notify(id, notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun getVersionName(context: Context): String? {
|
fun getVersionName(context: Context): String? {
|
||||||
var versionName: String? = null
|
var versionName: String? = null
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
@ -838,4 +869,11 @@ object Util {
|
|||||||
Timber.d("${it.key}: ${it.value}")
|
Timber.d("${it.key}: ${it.value}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Fragment.navigateToCurrent() {
|
||||||
|
if (Settings.shouldTransitionOnPlayback) {
|
||||||
|
findNavController().popBackStack(R.id.playerFragment, true)
|
||||||
|
findNavController().navigate(R.id.playerFragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Načítám…</string>
|
|
||||||
<string name="background_task.network_error">Chyba sítě. Ověřte adresu serveru nebo zkuste později.</string>
|
<string name="background_task.network_error">Chyba sítě. Ověřte adresu serveru nebo zkuste později.</string>
|
||||||
<string name="background_task.unsupported_api">API serveru v%1$s nepodporuje tuto funkci.</string>
|
<string name="background_task.unsupported_api">API serveru v%1$s nepodporuje tuto funkci.</string>
|
||||||
<string name="background_task.no_network">Tento program vyžaduje síťové připojení. Zapněte Wi-Fi nebo mobilní připojení.</string>
|
<string name="background_task.no_network">Tento program vyžaduje síťové připojení. Zapněte Wi-Fi nebo mobilní připojení.</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Nesrozumitelná odpověď. Ověřte adresu serveru.</string>
|
<string name="background_task.parse_error">Nesrozumitelná odpověď. Ověřte adresu serveru.</string>
|
||||||
<string name="background_task.ssl_cert_error">Chyba HTTPS certifikátu: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Chyba HTTPS certifikátu: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Vyjímka SSL připojení. Ověřte certifikát serveru.</string>
|
<string name="background_task.ssl_error">Vyjímka SSL připojení. Ověřte certifikát serveru.</string>
|
||||||
<string name="background_task.wait">Chvilku strpení…</string>
|
|
||||||
<string name="button_bar.bookmarks">Záložky</string>
|
<string name="button_bar.bookmarks">Záložky</string>
|
||||||
<string name="button_bar.browse">Knihovna médií</string>
|
<string name="button_bar.browse">Knihovna médií</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -188,8 +186,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Neomezené</string>
|
<string name="settings.max_bitrate_unlimited">Neomezené</string>
|
||||||
<string name="settings.max_bitrate_wifi">Max Bitrate - wi-fi</string>
|
<string name="settings.max_bitrate_wifi">Max Bitrate - wi-fi</string>
|
||||||
<string name="settings.max_songs">Maximum skladeb</string>
|
<string name="settings.max_songs">Maximum skladeb</string>
|
||||||
<string name="settings.media_button_summary">Odpovídat na tlačítka ovládání médií telefonu, sluchátek a bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Tlačítka médií</string>
|
|
||||||
<string name="settings.network_timeout">Čas vypršení připojení</string>
|
<string name="settings.network_timeout">Čas vypršení připojení</string>
|
||||||
<string name="settings.network_timeout_105000">105 sekund</string>
|
<string name="settings.network_timeout_105000">105 sekund</string>
|
||||||
<string name="settings.network_timeout_120000">120 sekund</string>
|
<string name="settings.network_timeout_120000">120 sekund</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Lade…</string>
|
|
||||||
<string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen.</string>
|
<string name="background_task.network_error">Ein Netzwerkfehler ist aufgetreten. Bitte die Serveradresse prüfen oder später noch einmal versuchen.</string>
|
||||||
<string name="background_task.unsupported_api">Server API v%1$s unterstützt diese Funktion nicht.</string>
|
<string name="background_task.unsupported_api">Server API v%1$s unterstützt diese Funktion nicht.</string>
|
||||||
<string name="background_task.no_network">Dieses Programm benötigt eine Netzwerkverbindung. Bitte das WLAN oder Mobilfunk einschalten.</string>
|
<string name="background_task.no_network">Dieses Programm benötigt eine Netzwerkverbindung. Bitte das WLAN oder Mobilfunk einschalten.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">Antwort nicht verstanden. Bitte die Serveradresse überprüfen.</string>
|
<string name="background_task.parse_error">Antwort nicht verstanden. Bitte die Serveradresse überprüfen.</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS Zertifikatsfehler: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS Zertifikatsfehler: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen.</string>
|
<string name="background_task.ssl_error">SSL Verbindungsfehler. Bitte das Serverzertifikat überprüfen.</string>
|
||||||
<string name="background_task.wait">Bitte warten…</string>
|
|
||||||
<string name="button_bar.bookmarks">Lesezeichen</string>
|
<string name="button_bar.bookmarks">Lesezeichen</string>
|
||||||
<string name="button_bar.browse">Medienbibliothek</string>
|
<string name="button_bar.browse">Medienbibliothek</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -230,8 +228,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Unbegrenzt</string>
|
<string name="settings.max_bitrate_unlimited">Unbegrenzt</string>
|
||||||
<string name="settings.max_bitrate_wifi">Max. Bitrate - WLAN</string>
|
<string name="settings.max_bitrate_wifi">Max. Bitrate - WLAN</string>
|
||||||
<string name="settings.max_songs">Max. Anzahl der Titel</string>
|
<string name="settings.max_songs">Max. Anzahl der Titel</string>
|
||||||
<string name="settings.media_button_summary">Auf Telefon, Headset und Bluetooth-Media-Tasten reagieren</string>
|
|
||||||
<string name="settings.media_button_title">Medien Tasten</string>
|
|
||||||
<string name="settings.network_timeout">Netzwerk Zeitüberschreitung</string>
|
<string name="settings.network_timeout">Netzwerk Zeitüberschreitung</string>
|
||||||
<string name="settings.network_timeout_105000">105 Sekunden</string>
|
<string name="settings.network_timeout_105000">105 Sekunden</string>
|
||||||
<string name="settings.network_timeout_120000">120 Sekunden</string>
|
<string name="settings.network_timeout_120000">120 Sekunden</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Cargando…</string>
|
|
||||||
<string name="background_task.network_error">Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde.</string>
|
<string name="background_task.network_error">Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde.</string>
|
||||||
<string name="background_task.unsupported_api">La API del servidor v%1$s no admite esta función.</string>
|
<string name="background_task.unsupported_api">La API del servidor v%1$s no admite esta función.</string>
|
||||||
<string name="background_task.no_network">Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil.</string>
|
<string name="background_task.no_network">Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">No se entiende la respuesta. Por favor comprueba la dirección del servidor.</string>
|
<string name="background_task.parse_error">No se entiende la respuesta. Por favor comprueba la dirección del servidor.</string>
|
||||||
<string name="background_task.ssl_cert_error">Error del certificado HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Error del certificado HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Excepción de conexión SSL. Compruebe el certificado del servidor.</string>
|
<string name="background_task.ssl_error">Excepción de conexión SSL. Compruebe el certificado del servidor.</string>
|
||||||
<string name="background_task.wait">Por favor espera…</string>
|
|
||||||
<string name="button_bar.bookmarks">Marcadores</string>
|
<string name="button_bar.bookmarks">Marcadores</string>
|
||||||
<string name="button_bar.browse">Biblioteca</string>
|
<string name="button_bar.browse">Biblioteca</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -232,8 +230,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
||||||
<string name="settings.max_bitrate_wifi">Bitrate máximo - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Bitrate máximo - Wi-Fi</string>
|
||||||
<string name="settings.max_songs">Máximo de Canciones</string>
|
<string name="settings.max_songs">Máximo de Canciones</string>
|
||||||
<string name="settings.media_button_summary">Responder a los botones multimedia del dispositivo, auriculares y Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Botones multimedia</string>
|
|
||||||
<string name="settings.network_timeout">Tiempo de espera de la red</string>
|
<string name="settings.network_timeout">Tiempo de espera de la red</string>
|
||||||
<string name="settings.network_timeout_105000">105 segundos</string>
|
<string name="settings.network_timeout_105000">105 segundos</string>
|
||||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Chargement…</string>
|
|
||||||
<string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string>
|
<string name="background_task.network_error">Une erreur réseau est survenue. Veuillez vérifier l\'adresse du serveur ou réessayer plus tard.</string>
|
||||||
<string name="background_task.unsupported_api">L’API v%1$s du serveur ne supporte pas cette fonction.</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>
|
<string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">Réponse incorrecte. Veuillez vérifier l\'adresse du serveur.</string>
|
<string name="background_task.parse_error">Réponse incorrecte. Veuillez vérifier l\'adresse du serveur.</string>
|
||||||
<string name="background_task.ssl_cert_error">Erreur de certificat HTTPS : %1$s.</string>
|
<string name="background_task.ssl_cert_error">Erreur de certificat HTTPS : %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Erreur de connexion SSL. Veuillez vérifier le certificat du serveur.</string>
|
<string name="background_task.ssl_error">Erreur de connexion SSL. Veuillez vérifier le certificat du serveur.</string>
|
||||||
<string name="background_task.wait">Veuillez patienter…</string>
|
|
||||||
<string name="button_bar.bookmarks">Signets</string>
|
<string name="button_bar.bookmarks">Signets</string>
|
||||||
<string name="button_bar.browse">Bibliothèque musicale</string>
|
<string name="button_bar.browse">Bibliothèque musicale</string>
|
||||||
<string name="button_bar.chat">Salon de discussion</string>
|
<string name="button_bar.chat">Salon de discussion</string>
|
||||||
@ -225,8 +223,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Illimité</string>
|
<string name="settings.max_bitrate_unlimited">Illimité</string>
|
||||||
<string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Débit maximal - Wi-Fi</string>
|
||||||
<string name="settings.max_songs">Titres maximum</string>
|
<string name="settings.max_songs">Titres maximum</string>
|
||||||
<string name="settings.media_button_summary">Répondre au boutons média de l\'appareil, du casque et du Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Boutons média</string>
|
|
||||||
<string name="settings.network_timeout">Délai d\'attente de connexion</string>
|
<string name="settings.network_timeout">Délai d\'attente de connexion</string>
|
||||||
<string name="settings.network_timeout_105000">105 secondes</string>
|
<string name="settings.network_timeout_105000">105 secondes</string>
|
||||||
<string name="settings.network_timeout_120000">120 secondes</string>
|
<string name="settings.network_timeout_120000">120 secondes</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="background_task.loading">Cargando…</string>
|
|
||||||
<string name="background_task.network_error">Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde.</string>
|
<string name="background_task.network_error">Ocorreu un erro de rede. Por favor comproba a dirección do servidor ou téntao de novo mais tarde.</string>
|
||||||
<string name="background_task.unsupported_api">A API do servidor v%1$s non admite esta función.</string>
|
<string name="background_task.unsupported_api">A API do servidor v%1$s non admite esta función.</string>
|
||||||
<string name="background_task.no_network">Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil.</string>
|
<string name="background_task.no_network">Este programa require acceso á rede. Por favor acende a Wi-Fi ou a rede móbil.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">Non se entende a resposta. Por favor comproba a dirección do servidor.</string>
|
<string name="background_task.parse_error">Non se entende a resposta. Por favor comproba a dirección do servidor.</string>
|
||||||
<string name="background_task.ssl_cert_error">Erro do certificado HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Erro do certificado HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Excepción de conexión SSL. Comprobe o certificado do servidor.</string>
|
<string name="background_task.ssl_error">Excepción de conexión SSL. Comprobe o certificado do servidor.</string>
|
||||||
<string name="background_task.wait">Por favor agarde…</string>
|
|
||||||
<string name="button_bar.browse">Biblioteca</string>
|
<string name="button_bar.browse">Biblioteca</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
<string name="button_bar.now_playing">Reproducindo agora</string>
|
<string name="button_bar.now_playing">Reproducindo agora</string>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Betöltés…</string>
|
|
||||||
<string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét vagy próbálja később!</string>
|
<string name="background_task.network_error">Hálózati hiba történt! Kérjük, ellenőrizze a kiszolgáló címét vagy próbálja később!</string>
|
||||||
<string name="background_task.unsupported_api">A v%1$s verziójú Szerver api nem támogatja ezt a funkciót.</string>
|
<string name="background_task.unsupported_api">A v%1$s verziójú Szerver api nem támogatja ezt a funkciót.</string>
|
||||||
<string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
|
<string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét!</string>
|
<string name="background_task.parse_error">Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét!</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS tanúsítványhiba: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS tanúsítványhiba: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát.</string>
|
<string name="background_task.ssl_error">SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát.</string>
|
||||||
<string name="background_task.wait">Kérem várjon!…</string>
|
|
||||||
<string name="button_bar.bookmarks">Könyvjelzők</string>
|
<string name="button_bar.bookmarks">Könyvjelzők</string>
|
||||||
<string name="button_bar.browse">Médiakönyvtár</string>
|
<string name="button_bar.browse">Médiakönyvtár</string>
|
||||||
<string name="button_bar.chat">Csevegés (Chat)</string>
|
<string name="button_bar.chat">Csevegés (Chat)</string>
|
||||||
@ -194,8 +192,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Korlátlan</string>
|
<string name="settings.max_bitrate_unlimited">Korlátlan</string>
|
||||||
<string name="settings.max_bitrate_wifi">Max. bitráta - Wi-Fi kapcsolat</string>
|
<string name="settings.max_bitrate_wifi">Max. bitráta - Wi-Fi kapcsolat</string>
|
||||||
<string name="settings.max_songs">Dalok max. találati száma</string>
|
<string name="settings.max_songs">Dalok max. találati száma</string>
|
||||||
<string name="settings.media_button_summary">Telefon irányítása a Bluetooth eszköz, vagy a fülhallgató vezérlőgombjaival.</string>
|
|
||||||
<string name="settings.media_button_title">Média vezérlőgombok</string>
|
|
||||||
<string name="settings.network_timeout">Hálózati időtúllépés</string>
|
<string name="settings.network_timeout">Hálózati időtúllépés</string>
|
||||||
<string name="settings.network_timeout_105000">105 másodperc</string>
|
<string name="settings.network_timeout_105000">105 másodperc</string>
|
||||||
<string name="settings.network_timeout_120000">120 másodperc</string>
|
<string name="settings.network_timeout_120000">120 másodperc</string>
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Caricamento…</string>
|
|
||||||
<string name="background_task.network_error">Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi.</string>
|
<string name="background_task.network_error">Si è verificato un errore di rete. Si prega di controllare l\'indirizzo del server o riprovare più tardi.</string>
|
||||||
<string name="background_task.no_network">Questo programma richiede l\'accesso alla rete. Per favore, abilita la connessione Wi-FI o la rete mobile.</string>
|
<string name="background_task.no_network">Questo programma richiede l\'accesso alla rete. Per favore, abilita la connessione Wi-FI o la rete mobile.</string>
|
||||||
<string name="background_task.not_found">Risorsa non trovata. Si prega di controllare l\'indirizzo del server.</string>
|
<string name="background_task.not_found">Risorsa non trovata. Si prega di controllare l\'indirizzo del server.</string>
|
||||||
<string name="background_task.parse_error">Risposta non comprensibile. Si prega di controllare l\'indirizzo del server.</string>
|
<string name="background_task.parse_error">Risposta non comprensibile. Si prega di controllare l\'indirizzo del server.</string>
|
||||||
<string name="background_task.ssl_cert_error">Errore certificato HTTPS. %1$s.</string>
|
<string name="background_task.ssl_cert_error">Errore certificato HTTPS. %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Anomalia connessione SSL. Si prega di controllare il certificato del server.</string>
|
<string name="background_task.ssl_error">Anomalia connessione SSL. Si prega di controllare il certificato del server.</string>
|
||||||
<string name="background_task.wait">Attendere per favore#8230;</string>
|
|
||||||
<string name="button_bar.bookmarks">Segnalibri</string>
|
<string name="button_bar.bookmarks">Segnalibri</string>
|
||||||
<string name="button_bar.browse">Libreria multimediale</string>
|
<string name="button_bar.browse">Libreria multimediale</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -184,8 +182,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Illimitato</string>
|
<string name="settings.max_bitrate_unlimited">Illimitato</string>
|
||||||
<string name="settings.max_bitrate_wifi">Bitrate Max - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Bitrate Max - Wi-Fi</string>
|
||||||
<string name="settings.max_songs">N° Max Canzoni</string>
|
<string name="settings.max_songs">N° Max Canzoni</string>
|
||||||
<string name="settings.media_button_summary">Controlli per risposta alle chiamate, per cuffie e Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Tasti Media</string>
|
|
||||||
<string name="settings.network_timeout">Timeout Rete</string>
|
<string name="settings.network_timeout">Timeout Rete</string>
|
||||||
<string name="settings.network_timeout_105000">105 seconds</string>
|
<string name="settings.network_timeout_105000">105 seconds</string>
|
||||||
<string name="settings.network_timeout_120000">120 seconds</string>
|
<string name="settings.network_timeout_120000">120 seconds</string>
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">読み込み中…</string>
|
|
||||||
<string name="background_task.network_error">ネットワークエラーが発生しました。サーバーのアドレスを確認してやり直してください。</string>
|
<string name="background_task.network_error">ネットワークエラーが発生しました。サーバーのアドレスを確認してやり直してください。</string>
|
||||||
<string name="background_task.parse_error">応答が確認できません。サーバーのアドレスを確認してください。</string>
|
<string name="background_task.parse_error">応答が確認できません。サーバーのアドレスを確認してください。</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS証明書エラー: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS証明書エラー: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL接続が異常です。サーバーの証明書を確認してください。</string>
|
<string name="background_task.ssl_error">SSL接続が異常です。サーバーの証明書を確認してください。</string>
|
||||||
<string name="background_task.wait">お待ち下さい…</string>
|
|
||||||
<string name="button_bar.bookmarks">ブックマーク</string>
|
<string name="button_bar.bookmarks">ブックマーク</string>
|
||||||
<string name="button_bar.browse">メディアライブラリ</string>
|
<string name="button_bar.browse">メディアライブラリ</string>
|
||||||
<string name="button_bar.chat">チャット</string>
|
<string name="button_bar.chat">チャット</string>
|
||||||
@ -181,8 +179,6 @@
|
|||||||
<string name="settings.max_bitrate_64">64 Kbps</string>
|
<string name="settings.max_bitrate_64">64 Kbps</string>
|
||||||
<string name="settings.max_bitrate_80">80 Kbps</string>
|
<string name="settings.max_bitrate_80">80 Kbps</string>
|
||||||
<string name="settings.max_bitrate_96">96 Kbps</string>
|
<string name="settings.max_bitrate_96">96 Kbps</string>
|
||||||
<string name="settings.media_button_summary">端末本体、ヘッドセットやBluetoothの再生コントロールボタンに対応します</string>
|
|
||||||
<string name="settings.media_button_title">メディアボタン</string>
|
|
||||||
<string name="settings.network_timeout_15000">15秒</string>
|
<string name="settings.network_timeout_15000">15秒</string>
|
||||||
<string name="settings.network_timeout_75000">75秒</string>
|
<string name="settings.network_timeout_75000">75秒</string>
|
||||||
<string name="settings.network_timeout_90000">90秒</string>
|
<string name="settings.network_timeout_90000">90秒</string>
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
<string name="buttons.stop">Stopp</string>
|
<string name="buttons.stop">Stopp</string>
|
||||||
<string name="button_bar.search">Søk</string>
|
<string name="button_bar.search">Søk</string>
|
||||||
<string name="chat.send_a_message">Send en melding</string>
|
<string name="chat.send_a_message">Send en melding</string>
|
||||||
<string name="background_task.loading">Laster inn …</string>
|
|
||||||
<string name="common.appname">Ultrasonic</string>
|
<string name="common.appname">Ultrasonic</string>
|
||||||
<string name="button_bar.browse">Mediebibliotek</string>
|
<string name="button_bar.browse">Mediebibliotek</string>
|
||||||
<string name="buttons.play">Spill</string>
|
<string name="buttons.play">Spill</string>
|
||||||
@ -137,7 +136,6 @@
|
|||||||
<string name="background_task.not_found">Fant ikke ressursen. Sjekk tjeneradressen.</string>
|
<string name="background_task.not_found">Fant ikke ressursen. Sjekk tjeneradressen.</string>
|
||||||
<string name="background_task.parse_error">Forsto ikke svaret. Sjekk tjeneradressen.</string>
|
<string name="background_task.parse_error">Forsto ikke svaret. Sjekk tjeneradressen.</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS-sertifikatsfeil: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS-sertifikatsfeil: %1$s.</string>
|
||||||
<string name="background_task.wait">Vent …</string>
|
|
||||||
<string name="button_bar.bookmarks">Bokmerker</string>
|
<string name="button_bar.bookmarks">Bokmerker</string>
|
||||||
<string name="button_bar.now_playing">Spilles nå</string>
|
<string name="button_bar.now_playing">Spilles nå</string>
|
||||||
<string name="podcasts_channels.empty">Ingen nettradioopptakskanaler registrert.</string>
|
<string name="podcasts_channels.empty">Ingen nettradioopptakskanaler registrert.</string>
|
||||||
@ -219,7 +217,6 @@
|
|||||||
<string name="settings.invalid_url">Angi en gyldig nettadresse.</string>
|
<string name="settings.invalid_url">Angi en gyldig nettadresse.</string>
|
||||||
<string name="settings.max_artists">Maks. artister</string>
|
<string name="settings.max_artists">Maks. artister</string>
|
||||||
<string name="settings.max_bitrate_unlimited">Ubegrenset</string>
|
<string name="settings.max_bitrate_unlimited">Ubegrenset</string>
|
||||||
<string name="settings.media_button_title">Medieknapper</string>
|
|
||||||
<string name="settings.network_timeout">Nettverkstidsavbrudd</string>
|
<string name="settings.network_timeout">Nettverkstidsavbrudd</string>
|
||||||
<string name="settings.network_timeout_90000">90 sekunder</string>
|
<string name="settings.network_timeout_90000">90 sekunder</string>
|
||||||
<string name="settings.network_timeout_75000">75 sekunder</string>
|
<string name="settings.network_timeout_75000">75 sekunder</string>
|
||||||
@ -317,7 +314,6 @@
|
|||||||
<string name="menu.deleted_playlist_error">Klarte ikke å slette %s-spillelisten</string>
|
<string name="menu.deleted_playlist_error">Klarte ikke å slette %s-spillelisten</string>
|
||||||
<string name="settings.download_transition_summary">Bytt til «Spilles nå» etter at avspilling startes i medievisning</string>
|
<string name="settings.download_transition_summary">Bytt til «Spilles nå» etter at avspilling startes i medievisning</string>
|
||||||
<string name="settings.hide_media_toast">Trer i effekt neste gang Android skanner enheten din for musikk.</string>
|
<string name="settings.hide_media_toast">Trer i effekt neste gang Android skanner enheten din for musikk.</string>
|
||||||
<string name="settings.media_button_summary">Svarer på enhets-, hodesett, og Blåtannsmedieknapper</string>
|
|
||||||
<string name="settings.wifi_required_summary">Kun last ned på ubegrensede tilkoblinger</string>
|
<string name="settings.wifi_required_summary">Kun last ned på ubegrensede tilkoblinger</string>
|
||||||
<string name="notification.downloading_title">Last ned medier i bakgrunnen …\?</string>
|
<string name="notification.downloading_title">Last ned medier i bakgrunnen …\?</string>
|
||||||
<string name="notification.permission_required">Merknader kreves for medieavspilling. Du kan innvilge tilgangen når som helst i Android-innstillingene.</string>
|
<string name="notification.permission_required">Merknader kreves for medieavspilling. Du kan innvilge tilgangen når som helst i Android-innstillingene.</string>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Bezig met laden…</string>
|
|
||||||
<string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw.</string>
|
<string name="background_task.network_error">Er is een netwerkfout opgetreden. Controleer het serveradres of probeer het later opnieuw.</string>
|
||||||
<string name="background_task.unsupported_api">De server-api, v %1$s, heeft geen ondersteuning voor deze functie.</string>
|
<string name="background_task.unsupported_api">De server-api, v %1$s, heeft geen ondersteuning voor deze functie.</string>
|
||||||
<string name="background_task.no_network">Deze app vereist netwerktoegang. Schakel wifi of mobiel internet in.</string>
|
<string name="background_task.no_network">Deze app vereist netwerktoegang. Schakel wifi of mobiel internet in.</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Het antwoord werd niet begrepen. Controleer het serveradres.</string>
|
<string name="background_task.parse_error">Het antwoord werd niet begrepen. Controleer het serveradres.</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS-certificaatfout: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS-certificaatfout: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL-verbindingsuitzondering. Controleer het servercertificaat.</string>
|
<string name="background_task.ssl_error">SSL-verbindingsuitzondering. Controleer het servercertificaat.</string>
|
||||||
<string name="background_task.wait">Even geduld…</string>
|
|
||||||
<string name="button_bar.bookmarks">Bladwijzers</string>
|
<string name="button_bar.bookmarks">Bladwijzers</string>
|
||||||
<string name="button_bar.browse">Mediabibliotheek</string>
|
<string name="button_bar.browse">Mediabibliotheek</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -233,8 +231,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Ongelimiteerd</string>
|
<string name="settings.max_bitrate_unlimited">Ongelimiteerd</string>
|
||||||
<string name="settings.max_bitrate_wifi">Max. bitsnelheid via wifi</string>
|
<string name="settings.max_bitrate_wifi">Max. bitsnelheid via wifi</string>
|
||||||
<string name="settings.max_songs">Max. aantal nummers</string>
|
<string name="settings.max_songs">Max. aantal nummers</string>
|
||||||
<string name="settings.media_button_summary">Reageren op telefoon-, headset- en bluetooth-mediatoetsen.</string>
|
|
||||||
<string name="settings.media_button_title">Mediatoetsen</string>
|
|
||||||
<string name="settings.network_timeout">Netwerktime-out</string>
|
<string name="settings.network_timeout">Netwerktime-out</string>
|
||||||
<string name="settings.network_timeout_105000">105 seconden</string>
|
<string name="settings.network_timeout_105000">105 seconden</string>
|
||||||
<string name="settings.network_timeout_120000">120 seconden</string>
|
<string name="settings.network_timeout_120000">120 seconden</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Ładowanie…</string>
|
|
||||||
<string name="background_task.network_error">Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później.</string>
|
<string name="background_task.network_error">Wystąpił błąd sieci. Proszę sprawdzić adres serwera lub spróbować później.</string>
|
||||||
<string name="background_task.unsupported_api">API serwera w wersji v%1$s nie wspiera tej funkcjonalności.</string>
|
<string name="background_task.unsupported_api">API serwera w wersji v%1$s nie wspiera tej funkcjonalności.</string>
|
||||||
<string name="background_task.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.</string>
|
<string name="background_task.no_network">Ta aplikacja wymaga dostępu do sieci. Proszę włączyć Wi-Fi lub dane komórkowe.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera.</string>
|
<string name="background_task.parse_error">Brak prawidłowej odpowiedzi. Proszę sprawdzić adres serwera.</string>
|
||||||
<string name="background_task.ssl_cert_error">Błąd certyfikatu HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Błąd certyfikatu HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.</string>
|
<string name="background_task.ssl_error">Błąd połączenia SSL. Proszę sprawdzić certyfikat serwera.</string>
|
||||||
<string name="background_task.wait">Proszę czekać…</string>
|
|
||||||
<string name="button_bar.bookmarks">Zakładki</string>
|
<string name="button_bar.bookmarks">Zakładki</string>
|
||||||
<string name="button_bar.browse">Biblioteka</string>
|
<string name="button_bar.browse">Biblioteka</string>
|
||||||
<string name="button_bar.chat">Czat</string>
|
<string name="button_bar.chat">Czat</string>
|
||||||
@ -187,8 +185,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Bez limitu</string>
|
<string name="settings.max_bitrate_unlimited">Bez limitu</string>
|
||||||
<string name="settings.max_bitrate_wifi">Maksymalny bitrate dla połączenia Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Maksymalny bitrate dla połączenia Wi-Fi</string>
|
||||||
<string name="settings.max_songs">Maksymalna ilość wyników - utwory</string>
|
<string name="settings.max_songs">Maksymalna ilość wyników - utwory</string>
|
||||||
<string name="settings.media_button_summary">Reaguj na przyciski multimedialne telefonu, słuchawek i urządzeń Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Przyciski</string>
|
|
||||||
<string name="settings.network_timeout">Przekroczenie limitu czasu sieci</string>
|
<string name="settings.network_timeout">Przekroczenie limitu czasu sieci</string>
|
||||||
<string name="settings.network_timeout_105000">105 sekund</string>
|
<string name="settings.network_timeout_105000">105 sekund</string>
|
||||||
<string name="settings.network_timeout_120000">120 sekund</string>
|
<string name="settings.network_timeout_120000">120 sekund</string>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Carregando…</string>
|
|
||||||
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
|
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
|
||||||
<string name="background_task.unsupported_api">O servidor api v%1$s não tem suporte para esta função.</string>
|
<string name="background_task.unsupported_api">O servidor api v%1$s não tem suporte para esta função.</string>
|
||||||
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
|
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
|
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
|
||||||
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
|
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
|
||||||
<string name="background_task.wait">Por favor aguarde…</string>
|
|
||||||
<string name="button_bar.bookmarks">Favoritos</string>
|
<string name="button_bar.bookmarks">Favoritos</string>
|
||||||
<string name="button_bar.browse">Biblioteca de Mídia</string>
|
<string name="button_bar.browse">Biblioteca de Mídia</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -231,8 +229,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
||||||
<string name="settings.max_bitrate_wifi">Taxa Máxima de Bits - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Taxa Máxima de Bits - Wi-Fi</string>
|
||||||
<string name="settings.max_songs">Máximo de Músicas</string>
|
<string name="settings.max_songs">Máximo de Músicas</string>
|
||||||
<string name="settings.media_button_summary">Obedecer aos botões do celular, fones e botões de mídia do Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Botões de Mídia</string>
|
|
||||||
<string name="settings.network_timeout">Timeout da Rede</string>
|
<string name="settings.network_timeout">Timeout da Rede</string>
|
||||||
<string name="settings.network_timeout_105000">105 segundos</string>
|
<string name="settings.network_timeout_105000">105 segundos</string>
|
||||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">Carregando…</string>
|
|
||||||
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
|
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
|
||||||
<string name="background_task.unsupported_api">Server api v%1$s does not support this function.</string>
|
<string name="background_task.unsupported_api">Server api v%1$s does not support this function.</string>
|
||||||
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
|
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
|
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
|
||||||
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
|
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
|
||||||
<string name="background_task.wait">Por favor aguarde…</string>
|
|
||||||
<string name="button_bar.bookmarks">Favoritos</string>
|
<string name="button_bar.bookmarks">Favoritos</string>
|
||||||
<string name="button_bar.browse">Biblioteca de Mídia</string>
|
<string name="button_bar.browse">Biblioteca de Mídia</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -187,8 +185,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
<string name="settings.max_bitrate_unlimited">Ilimitado</string>
|
||||||
<string name="settings.max_bitrate_wifi">Máx. de Taxa de Bits - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Máx. de Taxa de Bits - Wi-Fi</string>
|
||||||
<string name="settings.max_songs">Máximo de Músicas</string>
|
<string name="settings.max_songs">Máximo de Músicas</string>
|
||||||
<string name="settings.media_button_summary">Obedecer aos botões do telemóvel, auricular e botões de mídia do Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Botões de Mídia</string>
|
|
||||||
<string name="settings.network_timeout">Timeout da Rede</string>
|
<string name="settings.network_timeout">Timeout da Rede</string>
|
||||||
<string name="settings.network_timeout_105000">105 segundos</string>
|
<string name="settings.network_timeout_105000">105 segundos</string>
|
||||||
<string name="settings.network_timeout_120000">120 segundos</string>
|
<string name="settings.network_timeout_120000">120 segundos</string>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Загрузка…</string>
|
|
||||||
<string name="background_task.network_error">Произошла ошибка сети. Пожалуйста, проверьте адрес сервера или повторите попытку позже.</string>
|
<string name="background_task.network_error">Произошла ошибка сети. Пожалуйста, проверьте адрес сервера или повторите попытку позже.</string>
|
||||||
<string name="background_task.unsupported_api">Серверный api версии %1$s не поддерживает эту функцию.</string>
|
<string name="background_task.unsupported_api">Серверный api версии %1$s не поддерживает эту функцию.</string>
|
||||||
<string name="background_task.no_network">Эта программа требует доступа к сети. Пожалуйста, включите Wi-Fi или мобильную сеть.</string>
|
<string name="background_task.no_network">Эта программа требует доступа к сети. Пожалуйста, включите Wi-Fi или мобильную сеть.</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Не понятный ответ. Пожалуйста, проверьте адрес сервера.</string>
|
<string name="background_task.parse_error">Не понятный ответ. Пожалуйста, проверьте адрес сервера.</string>
|
||||||
<string name="background_task.ssl_cert_error">Ошибка сертификата HTTPS: %1$s.</string>
|
<string name="background_task.ssl_cert_error">Ошибка сертификата HTTPS: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">Исключение SSL-соединения. Пожалуйста, проверьте сертификат сервера.</string>
|
<string name="background_task.ssl_error">Исключение SSL-соединения. Пожалуйста, проверьте сертификат сервера.</string>
|
||||||
<string name="background_task.wait">Пожалуйста, подождите…</string>
|
|
||||||
<string name="button_bar.bookmarks">Закладки</string>
|
<string name="button_bar.bookmarks">Закладки</string>
|
||||||
<string name="button_bar.browse">Медиа библиотека</string>
|
<string name="button_bar.browse">Медиа библиотека</string>
|
||||||
<string name="button_bar.chat">Чат</string>
|
<string name="button_bar.chat">Чат</string>
|
||||||
@ -212,8 +210,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">Неограниченный</string>
|
<string name="settings.max_bitrate_unlimited">Неограниченный</string>
|
||||||
<string name="settings.max_bitrate_wifi">Максимальный битрейт - Wi-Fi подключение</string>
|
<string name="settings.max_bitrate_wifi">Максимальный битрейт - Wi-Fi подключение</string>
|
||||||
<string name="settings.max_songs">Максимум треков</string>
|
<string name="settings.max_songs">Максимум треков</string>
|
||||||
<string name="settings.media_button_summary">Отвечать на телефон, гарнитуру и мультимедийные кнопки Bluetooth</string>
|
|
||||||
<string name="settings.media_button_title">Медиа кнопки</string>
|
|
||||||
<string name="settings.network_timeout">Таймаут сети</string>
|
<string name="settings.network_timeout">Таймаут сети</string>
|
||||||
<string name="settings.network_timeout_105000">105 секунд</string>
|
<string name="settings.network_timeout_105000">105 секунд</string>
|
||||||
<string name="settings.network_timeout_120000">120 секунд</string>
|
<string name="settings.network_timeout_120000">120 секунд</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">加载中…</string>
|
|
||||||
<string name="background_task.network_error">发生网络错误。请检查服务器地址或稍后重试。</string>
|
<string name="background_task.network_error">发生网络错误。请检查服务器地址或稍后重试。</string>
|
||||||
<string name="background_task.unsupported_api">服务端 API v%1$s 不支持此功能。</string>
|
<string name="background_task.unsupported_api">服务端 API v%1$s 不支持此功能。</string>
|
||||||
<string name="background_task.no_network">此软件需要连接网络,请打开 Wi-Fi 或移动网络。</string>
|
<string name="background_task.no_network">此软件需要连接网络,请打开 Wi-Fi 或移动网络。</string>
|
||||||
@ -8,7 +7,6 @@
|
|||||||
<string name="background_task.parse_error">未知回复内容,请检查服务器地址。</string>
|
<string name="background_task.parse_error">未知回复内容,请检查服务器地址。</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS 证书错误:%1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS 证书错误:%1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL 连接异常。请检查服务器证书。</string>
|
<string name="background_task.ssl_error">SSL 连接异常。请检查服务器证书。</string>
|
||||||
<string name="background_task.wait">请稍等…</string>
|
|
||||||
<string name="button_bar.bookmarks">书签</string>
|
<string name="button_bar.bookmarks">书签</string>
|
||||||
<string name="button_bar.browse">媒体库</string>
|
<string name="button_bar.browse">媒体库</string>
|
||||||
<string name="button_bar.chat">聊天</string>
|
<string name="button_bar.chat">聊天</string>
|
||||||
@ -218,8 +216,6 @@
|
|||||||
<string name="settings.max_bitrate_unlimited">不限制</string>
|
<string name="settings.max_bitrate_unlimited">不限制</string>
|
||||||
<string name="settings.max_bitrate_wifi">最大比特率 - WIFI</string>
|
<string name="settings.max_bitrate_wifi">最大比特率 - WIFI</string>
|
||||||
<string name="settings.max_songs">最大歌曲</string>
|
<string name="settings.max_songs">最大歌曲</string>
|
||||||
<string name="settings.media_button_summary">响应手机、耳机和蓝牙设备的媒体按钮</string>
|
|
||||||
<string name="settings.media_button_title">媒体按钮</string>
|
|
||||||
<string name="settings.network_timeout">网络超时</string>
|
<string name="settings.network_timeout">网络超时</string>
|
||||||
<string name="settings.network_timeout_105000">105 秒</string>
|
<string name="settings.network_timeout_105000">105 秒</string>
|
||||||
<string name="settings.network_timeout_120000">120 秒</string>
|
<string name="settings.network_timeout_120000">120 秒</string>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="background_task.loading">載入中…</string>
|
|
||||||
<string name="button_bar.bookmarks">書籤</string>
|
<string name="button_bar.bookmarks">書籤</string>
|
||||||
<string name="button_bar.browse">媒體庫</string>
|
<string name="button_bar.browse">媒體庫</string>
|
||||||
<string name="button_bar.now_playing">正在播放</string>
|
<string name="button_bar.now_playing">正在播放</string>
|
||||||
@ -146,7 +145,6 @@
|
|||||||
<string name="language.nl">荷蘭語</string>
|
<string name="language.nl">荷蘭語</string>
|
||||||
<string name="download.jukebox_off">已關閉遠端控制,音樂將在手機上播放。</string>
|
<string name="download.jukebox_off">已關閉遠端控制,音樂將在手機上播放。</string>
|
||||||
<string name="language.de">德語</string>
|
<string name="language.de">德語</string>
|
||||||
<string name="background_task.wait">請稍候…</string>
|
|
||||||
<string name="common.unpin">取消固定</string>
|
<string name="common.unpin">取消固定</string>
|
||||||
<string name="download.playlist_name">輸入播放清單名稱:</string>
|
<string name="download.playlist_name">輸入播放清單名稱:</string>
|
||||||
<string name="main.albums_by_year">依照時間排列</string>
|
<string name="main.albums_by_year">依照時間排列</string>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
<string name="setting_key.preload_count" translatable="false">preloadCount</string>
|
<string name="setting_key.preload_count" translatable="false">preloadCount</string>
|
||||||
<string name="setting_key.parallel_downloads" translatable="false">parallelDownloads</string>
|
<string name="setting_key.parallel_downloads" translatable="false">parallelDownloads</string>
|
||||||
<string name="setting_key.hide_media" translatable="false">hideMedia</string>
|
<string name="setting_key.hide_media" translatable="false">hideMedia</string>
|
||||||
<string name="setting_key.media_buttons" translatable="false">mediaButtons</string>
|
|
||||||
<string name="setting_key.scrobble" translatable="false">scrobble</string>
|
<string name="setting_key.scrobble" translatable="false">scrobble</string>
|
||||||
<string name="setting_key.server_scaling" translatable="false">serverScaling</string>
|
<string name="setting_key.server_scaling" translatable="false">serverScaling</string>
|
||||||
<string name="setting_key.wifi_required_for_download" translatable="false">wifiRequiredForDownload</string>
|
<string name="setting_key.wifi_required_for_download" translatable="false">wifiRequiredForDownload</string>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<string name="background_task.loading">Loading…</string>
|
|
||||||
<string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
|
<string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
|
||||||
<string name="background_task.unsupported_api">Server API v%1$s does not support this function.</string>
|
<string name="background_task.unsupported_api">Server API v%1$s does not support this function.</string>
|
||||||
<string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
|
<string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
|
||||||
@ -9,7 +8,6 @@
|
|||||||
<string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
|
<string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
|
||||||
<string name="background_task.ssl_cert_error">HTTPS certificate error: %1$s.</string>
|
<string name="background_task.ssl_cert_error">HTTPS certificate error: %1$s.</string>
|
||||||
<string name="background_task.ssl_error">SSL connection exception. Please check server certificate.</string>
|
<string name="background_task.ssl_error">SSL connection exception. Please check server certificate.</string>
|
||||||
<string name="background_task.wait">Please wait…</string>
|
|
||||||
<string name="button_bar.bookmarks">Bookmarks</string>
|
<string name="button_bar.bookmarks">Bookmarks</string>
|
||||||
<string name="button_bar.browse">Media Library</string>
|
<string name="button_bar.browse">Media Library</string>
|
||||||
<string name="button_bar.chat">Chat</string>
|
<string name="button_bar.chat">Chat</string>
|
||||||
@ -234,8 +232,6 @@
|
|||||||
<string name="settings.max_bitrate_wifi">Max Bitrate - Wi-Fi</string>
|
<string name="settings.max_bitrate_wifi">Max Bitrate - Wi-Fi</string>
|
||||||
<string name="settings.max_bitrate_pinning">Max Bitrate - When pinning a song permanently</string>
|
<string name="settings.max_bitrate_pinning">Max Bitrate - When pinning a song permanently</string>
|
||||||
<string name="settings.max_songs">Max Songs</string>
|
<string name="settings.max_songs">Max Songs</string>
|
||||||
<string name="settings.media_button_summary">Respond to phone, headset and Bluetooth media buttons</string>
|
|
||||||
<string name="settings.media_button_title">Media Buttons</string>
|
|
||||||
<string name="settings.network_timeout">Network Timeout</string>
|
<string name="settings.network_timeout">Network Timeout</string>
|
||||||
<string name="settings.network_timeout_105000">105 seconds</string>
|
<string name="settings.network_timeout_105000">105 seconds</string>
|
||||||
<string name="settings.network_timeout_120000">120 seconds</string>
|
<string name="settings.network_timeout_120000">120 seconds</string>
|
||||||
|
@ -72,12 +72,6 @@
|
|||||||
a:summary="@string/settings.show_artist_picture_summary"
|
a:summary="@string/settings.show_artist_picture_summary"
|
||||||
a:title="@string/settings.show_artist_picture"
|
a:title="@string/settings.show_artist_picture"
|
||||||
app:iconSpaceReserved="false"/>
|
app:iconSpaceReserved="false"/>
|
||||||
<CheckBoxPreference
|
|
||||||
a:defaultValue="true"
|
|
||||||
a:key="@string/setting_key.media_buttons"
|
|
||||||
a:summary="@string/settings.media_button_summary"
|
|
||||||
a:title="@string/settings.media_button_title"
|
|
||||||
app:iconSpaceReserved="false"/>
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
a:defaultValue="true"
|
a:defaultValue="true"
|
||||||
a:key="@string/setting_key.download_transition"
|
a:key="@string/setting_key.download_transition"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user