Merge pull request #493 from tzugen/generic-views

Introduce new Generic Fragments, ViewModels, and Adapters for the display of API data.
This commit is contained in:
Nite 2021-05-18 11:44:51 +02:00 committed by GitHub
commit 950cb6254f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1576 additions and 1050 deletions

View File

@ -3,13 +3,13 @@ package org.moire.ultrasonic.domain
import java.io.Serializable
data class Artist(
var id: String? = null,
var name: String? = null,
override var id: String? = null,
override var name: String? = null,
var index: String? = null,
var coverArt: String? = null,
var albumCount: Long? = null,
var closeness: Int = 0
) : Serializable {
) : Serializable, GenericEntry() {
companion object {
private const val serialVersionUID = -5790532593784846982L
}

View File

@ -0,0 +1,7 @@
package org.moire.ultrasonic.domain
abstract class GenericEntry {
// TODO Should be non-null!
abstract val id: String?
open val name: String? = null
}

View File

@ -36,7 +36,7 @@ class MusicDirectory {
}
data class Entry(
var id: String? = null,
override var id: String? = null,
var parent: String? = null,
var isDirectory: Boolean = false,
var title: String? = null,
@ -66,7 +66,7 @@ class MusicDirectory {
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null
) : Serializable {
) : Serializable, GenericEntry() {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}

View File

@ -4,6 +4,6 @@ package org.moire.ultrasonic.domain
* Represents a top level directory in which music or other media is stored.
*/
data class MusicFolder(
val id: String,
val name: String
)
override val id: String,
override val name: String
) : GenericEntry()

View File

@ -3,14 +3,14 @@ package org.moire.ultrasonic.domain
import java.io.Serializable
data class Playlist @JvmOverloads constructor(
val id: String,
var name: String,
override val id: String,
override var name: String,
val owner: String = "",
val comment: String = "",
val songCount: String = "",
val created: String = "",
val public: Boolean? = null
) : Serializable {
) : Serializable, GenericEntry() {
companion object {
private const val serialVersionUID = -4160515427075433798L
}

View File

@ -3,12 +3,12 @@ package org.moire.ultrasonic.domain
import java.io.Serializable
data class PodcastsChannel(
val id: String,
override val id: String,
val title: String?,
val url: String?,
val description: String?,
val status: String?
) : Serializable {
) : Serializable, GenericEntry() {
companion object {
private const val serialVersionUID = -4160515427075433798L
}

View File

@ -4,7 +4,7 @@ import java.io.Serializable
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Share(
var id: String? = null,
override var id: String? = null,
var url: String? = null,
var description: String? = null,
var username: String? = null,
@ -13,8 +13,8 @@ data class Share(
var expires: String? = null,
var visitCount: Long? = null,
private val entries: MutableList<Entry> = mutableListOf()
) : Serializable {
val name: String?
) : Serializable, GenericEntry() {
override val name: String?
get() = url?.let { urlPattern.matcher(url).replaceFirst("$1") }
fun getEntries(): List<Entry> {

View File

@ -24,25 +24,11 @@
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo &amp;&amp; Util.getVideoPlayerType() !== VideoPlayerType.FLASH</ID>
<ID>ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
<ID>ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
<ID>ComplexMethod:SongView.kt$SongView$public override fun update()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ }</ID>
<ID>EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$()</ID>
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
@ -61,94 +47,23 @@
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:DownloadFile.kt$DownloadFile</ID>
<ID>LargeClass:DownloadHandler.kt$DownloadHandler</ID>
<ID>LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler</ID>
<ID>LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter</ID>
<ID>LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
<ID>LargeClass:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity</ID>
<ID>LargeClass:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
<ID>LargeClass:SelectAlbumModel.kt$SelectAlbumModel : AndroidViewModelKoinComponent</ID>
<ID>LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment</ID>
<ID>LargeClass:ServerSettingsModel.kt$ServerSettingsModel : AndroidViewModel</ID>
<ID>LargeClass:SongView.kt$SongView : UpdateViewCheckable</ID>
<ID>LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry</ID>
<ID>LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
<ID>LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout)</ID>
<ID>LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)</ID>
<ID>LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:ArtistListFragment.kt$ArtistListFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ArtistListFragment.kt$ArtistListFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): List&lt;MusicDirectory.Entry&gt;</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$override fun done(songs: List&lt;MusicDirectory.Entry&gt;)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): Boolean</ID>
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?)</ID>
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder()</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List&lt;File&gt;): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context)</ID>
<ID>LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init()</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release()</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$override fun onCompletion(mediaPlayer: MediaPlayer)</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$// TODO Test if this works with external Intents // android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here override fun onNewIntent(intent: Intent?)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying()</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, playlist: MusicDirectory )</ID>
<ID>LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun createHeader( entries: List&lt;MusicDirectory.Entry&gt;, name: CharSequence?, songCount: Int ): View?</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List&lt;MusicDirectory.Entry?&gt;)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbumList(albumListType: String, size: Int, offset: Int)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getArtist(refresh: Boolean, id: String?, name: String?)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getMusicDirectory( refresh: Boolean, id: String?, name: String?, parentId: String? )</ID>
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
<ID>LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean</ID>
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
<ID>LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry)</ID>
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
<ID>LongMethod:SongView.kt$SongView$public override fun update()</ID>
<ID>LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable)</ID>
<ID>LongMethod:UApp.kt$UApp$override fun onCreate()</ID>
<ID>LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List&lt;Artist&gt;, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -&gt; Unit, val onContextMenuClick: (MenuItem, Artist) -&gt; Boolean, private val imageLoader: ImageLoader )</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean )</ID>
@ -188,11 +103,11 @@
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$5</ID>
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10</ID>
<ID>MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
@ -202,9 +117,9 @@
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions)</ID>
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) }</ID>
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) }</ID>
@ -231,7 +146,7 @@
<ID>TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt"</ID>
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty()</ID>

View File

@ -2,14 +2,8 @@
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ComplexMethod:AlbumListType.kt$AlbumListType.Companion$@JvmStatic fun fromName(typeName: String): AlbumListType</ID>
<ID>ComplexMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
<ID>ComplexMethod:SubsonicError.kt$SubsonicError.Companion$fun getError(code: Int, message: String)</ID>
<ID>EmptyFunctionBlock:SubsonicAPIClient.kt$SubsonicAPIClient.&lt;no name provided&gt;${}</ID>
<ID>LargeClass:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition</ID>
<ID>LargeClass:SubsonicAPIDefinition.kt$SubsonicAPIDefinition</ID>
<ID>LongMethod:SubsonicAPIClient.kt$SubsonicAPIClient$private inline fun handleStreamResponse(apiCall: () -&gt; Response&lt;ResponseBody&gt;): StreamResponse</ID>
<ID>LongMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions</ID>
<ID>MagicNumber:PasswordExt.kt$0xFF</ID>
<ID>MagicNumber:PasswordExt.kt$4</ID>
<ID>MagicNumber:PasswordMD5Interceptor.kt$PasswordMD5Interceptor$16</ID>

View File

@ -24,25 +24,11 @@
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo &amp;&amp; Util.getVideoPlayerType() !== VideoPlayerType.FLASH</ID>
<ID>ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
<ID>ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
<ID>ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
<ID>ComplexMethod:SongView.kt$SongView$public override fun update()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ }</ID>
<ID>EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$()</ID>
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
@ -61,94 +47,23 @@
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:DownloadFile.kt$DownloadFile</ID>
<ID>LargeClass:DownloadHandler.kt$DownloadHandler</ID>
<ID>LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler</ID>
<ID>LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter</ID>
<ID>LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
<ID>LargeClass:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity</ID>
<ID>LargeClass:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
<ID>LargeClass:SelectAlbumModel.kt$SelectAlbumModel : AndroidViewModelKoinComponent</ID>
<ID>LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment</ID>
<ID>LargeClass:ServerSettingsModel.kt$ServerSettingsModel : AndroidViewModel</ID>
<ID>LargeClass:SongView.kt$SongView : UpdateViewCheckable</ID>
<ID>LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry</ID>
<ID>LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
<ID>LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout)</ID>
<ID>LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)</ID>
<ID>LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:ArtistListFragment.kt$ArtistListFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ArtistListFragment.kt$ArtistListFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList&lt;MusicDirectory.Entry&gt; )</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): List&lt;MusicDirectory.Entry&gt;</ID>
<ID>LongMethod:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$override fun done(songs: List&lt;MusicDirectory.Entry&gt;)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment.&lt;no name provided&gt;$@Throws(Throwable::class) override fun doInBackground(): Boolean</ID>
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?)</ID>
<ID>LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder()</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List&lt;File&gt;): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList&lt;FileListItem&gt;</ID>
<ID>LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context)</ID>
<ID>LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init()</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release()</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$override fun onCompletion(mediaPlayer: MediaPlayer)</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$// TODO Test if this works with external Intents // android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here override fun onNewIntent(intent: Intent?)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying()</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, playlist: MusicDirectory )</ID>
<ID>LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun createHeader( entries: List&lt;MusicDirectory.Entry&gt;, name: CharSequence?, songCount: Int ): View?</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List&lt;MusicDirectory.Entry?&gt;)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons()</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbumList(albumListType: String, size: Int, offset: Int)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getArtist(refresh: Boolean, id: String?, name: String?)</ID>
<ID>LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getMusicDirectory( refresh: Boolean, id: String?, name: String?, parentId: String? )</ID>
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View?</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int)</ID>
<ID>LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean</ID>
<ID>LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting?</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
<ID>LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry)</ID>
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile)</ID>
<ID>LongMethod:SongView.kt$SongView$public override fun update()</ID>
<ID>LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable)</ID>
<ID>LongMethod:UApp.kt$UApp$override fun onCreate()</ID>
<ID>LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List&lt;Artist&gt;, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -&gt; Unit, val onContextMenuClick: (MenuItem, Artist) -&gt; Boolean, private val imageLoader: ImageLoader )</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List&lt;MusicDirectory.Entry?&gt; )</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean )</ID>
@ -188,11 +103,11 @@
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$5</ID>
<ID>MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10</ID>
<ID>MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
@ -202,9 +117,9 @@
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions)</ID>
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) }</ID>
<ID>SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) }</ID>
@ -231,7 +146,7 @@
<ID>TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt"</ID>
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
<ID>UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty()</ID>

View File

@ -36,15 +36,6 @@ empty-blocks:
complexity:
active: true
LongMethod:
threshold: 20
LongParameterList:
functionThreshold: 5
constructorThreshold: 5
LargeClass:
threshold: 150
ComplexMethod:
threshold: 10
TooManyFunctions:
thresholdInFiles: 20
thresholdInClasses: 20

View File

@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
@ -142,71 +141,66 @@ public class MainFragment extends Fragment {
}
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
list.setOnItemClickListener((parent, view, position, id) -> {
if (view == serverButton)
{
if (view == serverButton)
{
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
showServers();
}
else if (view == albumsNewestButton)
{
showAlbumList("newest", R.string.main_albums_newest);
}
else if (view == albumsRandomButton)
{
showAlbumList("random", R.string.main_albums_random);
}
else if (view == albumsHighestButton)
{
showAlbumList("highest", R.string.main_albums_highest);
}
else if (view == albumsRecentButton)
{
showAlbumList("recent", R.string.main_albums_recent);
}
else if (view == albumsFrequentButton)
{
showAlbumList("frequent", R.string.main_albums_frequent);
}
else if (view == albumsStarredButton)
{
showAlbumList(Constants.STARRED, R.string.main_albums_starred);
}
else if (view == albumsAlphaByNameButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_alphaByName);
}
else if (view == albumsAlphaByArtistButton)
{
showAlbumList("alphabeticalByArtist", R.string.main_albums_alphaByArtist);
}
else if (view == songsStarredButton)
{
showStarredSongs();
}
else if (view == artistsButton)
{
showArtists();
}
else if (view == albumsButton)
{
showAlbumList(Constants.ALPHABETICAL_BY_NAME, R.string.main_albums_title);
}
else if (view == randomSongsButton)
{
showRandomSongs();
}
else if (view == genresButton)
{
showGenres();
}
else if (view == videosButton)
{
showVideos();
}
});
}
@ -219,21 +213,11 @@ public class MainFragment extends Fragment {
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
private void showAlbumList(final String type, final int title)
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
}
private void showStarredSongs()
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_STARRED, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
}
private void showRandomSongs()
@ -241,14 +225,23 @@ public class MainFragment extends Fragment {
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_RANDOM, 1);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs());
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
}
private void showArtists()
{
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, getContext().getResources().getString(R.string.main_artists_title));
Navigation.findNavController(getView()).navigate(R.id.selectArtistFragment, bundle);
Navigation.findNavController(getView()).navigate(R.id.mainToArtistList, bundle);
}
private void showAlbumList(final String type, final int title) {
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, title);
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxAlbums());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(getView()).navigate(R.id.mainToAlbumList, bundle);
}
private void showGenres()
@ -260,7 +253,7 @@ public class MainFragment extends Fragment {
{
Bundle bundle = new Bundle();
bundle.putInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 1);
Navigation.findNavController(getView()).navigate(R.id.mainToSelectAlbum, bundle);
Navigation.findNavController(getView()).navigate(R.id.mainToTrackCollection, bundle);
}
private void showServers()

View File

@ -125,7 +125,7 @@ public class NowPlayingFragment extends Fragment {
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, song.getAlbum());
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(getActivity(), R.id.nav_host_fragment).navigate(R.id.trackCollectionFragment, bundle);
});
}

View File

@ -105,7 +105,7 @@ public class PlaylistsFragment extends Fragment {
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
}
});
registerForContextMenu(playlistsListView);
@ -154,7 +154,7 @@ public class PlaylistsFragment extends Fragment {
if (ActiveServerProvider.Companion.isOffline()) inflater.inflate(R.menu.select_playlist_context_offline, menu);
else inflater.inflate(R.menu.select_playlist_context, menu);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
MenuItem downloadMenuItem = menu.findItem(R.id.playlist_menu_download);
if (downloadMenuItem != null)
{
@ -190,14 +190,14 @@ public class PlaylistsFragment extends Fragment {
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
} else if (itemId == R.id.playlist_menu_play_shuffled) {
bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_ID, playlist.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME, playlist.getName());
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, true);
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_SHUFFLE, true);
Navigation.findNavController(getView()).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(getView()).navigate(R.id.trackCollectionFragment, bundle);
} else if (itemId == R.id.playlist_menu_delete) {
deletePlaylist(playlist);
} else if (itemId == R.id.playlist_info) {

View File

@ -76,7 +76,7 @@ public class PodcastFragment extends Fragment {
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_PODCAST_CHANNEL_ID, pc.getId());
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
}
});

View File

@ -272,11 +272,11 @@ public class SearchFragment extends Fragment {
}
else
{
inflater.inflate(R.menu.select_album_context, menu);
inflater.inflate(R.menu.generic_context_menu, menu);
}
MenuItem shareButton = menu.findItem(R.id.menu_item_share);
MenuItem downloadMenuItem = menu.findItem(R.id.album_menu_download);
MenuItem downloadMenuItem = menu.findItem(R.id.menu_download);
if (downloadMenuItem != null)
{
@ -324,17 +324,17 @@ public class SearchFragment extends Fragment {
List<MusicDirectory.Entry> songs = new ArrayList<>(1);
int itemId = menuItem.getItemId();
if (itemId == R.id.album_menu_play_now) {
if (itemId == R.id.menu_play_now) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, true, false, false, false, false, false);
} else if (itemId == R.id.album_menu_play_next) {
} else if (itemId == R.id.menu_play_next) {
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, true, false, true, false, false);
} else if (itemId == R.id.album_menu_play_last) {
} else if (itemId == R.id.menu_play_last) {
downloadHandler.getValue().downloadRecursively(this, id, false, true, false, false, false, false, false, false);
} else if (itemId == R.id.album_menu_pin) {
} else if (itemId == R.id.menu_pin) {
downloadHandler.getValue().downloadRecursively(this, id, true, true, false, false, false, false, false, false);
} else if (itemId == R.id.album_menu_unpin) {
} else if (itemId == R.id.menu_unpin) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, false, false, true, false);
} else if (itemId == R.id.album_menu_download) {
} else if (itemId == R.id.menu_download) {
downloadHandler.getValue().downloadRecursively(this, id, false, false, false, false, true, false, false, false);
} else if (itemId == R.id.song_menu_play_now) {
if (entry != null) {

View File

@ -77,7 +77,7 @@ public class SelectGenreFragment extends Fragment {
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, genre.getName());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, Util.getMaxSongs());
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0);
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
}
}
});

View File

@ -106,7 +106,7 @@ public class SharesFragment extends Fragment {
Bundle bundle = new Bundle();
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_ID, share.getId());
bundle.putString(Constants.INTENT_EXTRA_NAME_SHARE_NAME, share.getName());
Navigation.findNavController(view).navigate(R.id.selectAlbumFragment, bundle);
Navigation.findNavController(view).navigate(R.id.trackCollectionFragment, bundle);
}
});
registerForContextMenu(sharesListView);

View File

@ -58,6 +58,7 @@ public final class Constants
public static final String INTENT_EXTRA_NAME_IS_ALBUM = "subsonic.isalbum";
public static final String INTENT_EXTRA_NAME_VIDEOS = "subsonic.videos";
public static final String INTENT_EXTRA_NAME_SHOW_PLAYER = "subsonic.showplayer";
public static final String INTENT_EXTRA_NAME_APPEND = "subsonic.append";
// Names for Intent Actions
public static final String CMD_PROCESS_KEYCODE = "org.moire.ultrasonic.CMD_PROCESS_KEYCODE";

View File

@ -71,7 +71,7 @@ public class AlbumView extends UpdateView
public void setLayout()
{
LayoutInflater.from(context).inflate(R.layout.album_list_item, this, true);
LayoutInflater.from(context).inflate(R.layout.album_list_item_legacy, this, true);
viewHolder = new EntryAdapter.AlbumViewHolder();
viewHolder.title = findViewById(R.id.album_title);
viewHolder.artist = findViewById(R.id.album_artist);

View File

@ -103,7 +103,7 @@ class NavigationActivity : AppCompatActivity() {
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.selectArtistFragment,
R.id.mediaLibraryFragment,
R.id.searchFragment,
R.id.playlistsFragment,
R.id.sharesFragment,

View File

@ -4,7 +4,6 @@ package org.moire.ultrasonic.di
import kotlin.math.abs
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig
@ -13,7 +12,6 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.fragment.ArtistListModel
import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.ApiCallResponseChecker
import org.moire.ultrasonic.service.CachedMusicService
@ -81,8 +79,6 @@ val musicServiceModule = module {
single { SubsonicImageLoader(androidContext(), get()) }
viewModel { ArtistListModel(get()) }
single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) }
single { VideoPlayer() }

View File

@ -0,0 +1,100 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Constants
/**
* Displays a list of Albums from the media library
* TODO: Check refresh is working
*/
@KoinApiExtension
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
/**
* The ViewModel to use to get the data
*/
override val listModel: AlbumListModel by viewModels()
/**
* The id of the main layout
*/
override val mainLayout: Int = R.layout.generic_list
/**
* The id of the refresh view
*/
override val refreshListId: Int = R.id.generic_list_refresh
/**
* The id of the RecyclerView
*/
override val recyclerViewId = R.id.generic_list_recycler
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
override val itemClickTarget: Int = R.id.trackCollectionFragment
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<MusicDirectory.Entry>> {
if (args == null) throw IllegalArgumentException("Required arguments are missing")
val refresh = args.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH)
return listModel.getAlbumList(refresh, refreshListView!!, args)
}
/**
* Provide the Adapter for the RecyclerView with a lazy delegate
*/
override val viewAdapter: AlbumRowAdapter by lazy {
AlbumRowAdapter(
liveDataItems.value ?: listOf(),
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader(),
onMusicFolderUpdate
)
}
val newBundleClone: Bundle
get() = arguments?.clone() as Bundle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Attach our onScrollListener
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
val scrollListener = object : EndlessScrollListener(viewManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
// Triggered only when new data needs to be appended to the list
// Add whatever code is needed to append new items to the bottom of the list
val appendArgs = newBundleClone
appendArgs.putBoolean(Constants.INTENT_EXTRA_NAME_APPEND, true)
getLiveData(appendArgs)
}
}
addOnScrollListener(scrollListener)
}
}
override fun onItemClick(item: MusicDirectory.Entry) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, item.isDirectory)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.title)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.parent)
findNavController().navigate(itemClickTarget, bundle)
}
}

View File

@ -0,0 +1,98 @@
package org.moire.ultrasonic.fragment
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
@KoinApiExtension
class AlbumListModel(application: Application) : GenericListModel(application) {
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
private var loadedUntil: Int = 0
fun getAlbumList(
refresh: Boolean,
swipe: SwipeRefreshLayout,
args: Bundle
): LiveData<List<MusicDirectory.Entry>> {
backgroundLoadFromServer(refresh, swipe, args)
return albumList
}
override fun load(
isOffline: Boolean,
useId3Tags: Boolean,
musicService: MusicService,
refresh: Boolean,
args: Bundle
) {
super.load(isOffline, useId3Tags, musicService, refresh, args)
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
val size = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0)
var offset = args.getInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0)
val append = args.getBoolean(Constants.INTENT_EXTRA_NAME_APPEND, false)
val musicDirectory: MusicDirectory
val musicFolderId = if (showSelectFolderHeader(args)) {
activeServerProvider.getActiveServer().musicFolderId
} else {
null
}
// Handle the logic for endless scrolling:
// If appending the existing list, set the offset from where to load
if (append) offset += (size + loadedUntil)
if (useId3Tags) {
musicDirectory = musicService.getAlbumList2(
albumListType, size,
offset, musicFolderId
)
} else {
musicDirectory = musicService.getAlbumList(
albumListType, size,
offset, musicFolderId
)
}
currentListIsSortable = isCollectionSortable(albumListType)
if (append && albumList.value != null) {
val list = ArrayList<MusicDirectory.Entry>()
list.addAll(albumList.value!!)
list.addAll(musicDirectory.getAllChild())
albumList.postValue(list)
} else {
albumList.postValue(musicDirectory.getAllChild())
}
loadedUntil = offset
}
override fun showSelectFolderHeader(args: Bundle?): Boolean {
if (args == null) return false
val albumListType = args.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE)!!
val isAlphabetical = (albumListType == AlbumListType.SORTED_BY_NAME.toString()) ||
(albumListType == AlbumListType.SORTED_BY_ARTIST.toString())
return !isOffline() && !Util.getShouldUseId3Tags() && isAlphabetical
}
private fun isCollectionSortable(albumListType: String): Boolean {
return albumListType != "newest" && albumListType != "random" &&
albumListType != "highest" && albumListType != "recent" &&
albumListType != "frequent"
}
}

View File

@ -0,0 +1,92 @@
/*
* AlbumRowAdapter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.ImageLoader
/**
* Creates a Row in a RecyclerView which contains the details of an Album
*/
class AlbumRowAdapter(
albumList: List<MusicDirectory.Entry>,
onItemClick: (MusicDirectory.Entry) -> Unit,
onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
private val imageLoader: ImageLoader,
onMusicFolderUpdate: (String?) -> Unit
) : GenericRowAdapter<MusicDirectory.Entry>(
onItemClick,
onContextMenuClick,
imageLoader,
onMusicFolderUpdate
) {
override var itemList = albumList
// Set our layout files
override val layout = R.layout.album_list_item
override val contextMenuLayout = R.menu.artist_context_menu
// Sets the data to be displayed in the RecyclerView
override fun setData(data: List<MusicDirectory.Entry>) {
itemList = data
super.notifyDataSetChanged()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder) {
val listPosition = if (selectFolderHeader != null) position - 1 else position
val entry = itemList[listPosition]
holder.album.text = entry.title
holder.artist.text = entry.artist
holder.details.setOnClickListener { onItemClick(entry) }
holder.details.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
holder.coverArtId = entry.coverArt
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.unknown_album
)
}
}
override fun getItemCount(): Int {
if (selectFolderHeader != null)
return itemList.size + 1
else
return itemList.size
}
/**
* Holds the view properties of an Item row
*/
class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
var album: TextView = view.findViewById(R.id.album_title)
var artist: TextView = view.findViewById(R.id.album_artist)
var details: LinearLayout = view.findViewById(R.id.row_album_details)
var coverArt: ImageView = view.findViewById(R.id.album_coverart)
var coverArtId: String? = null
}
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
}
}

View File

@ -0,0 +1,63 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.util.Constants
/**
* Displays the list of Artists from the media library
*/
@KoinApiExtension
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
/**
* The ViewModel to use to get the data
*/
override val listModel: ArtistListModel by viewModels()
/**
* The id of the main layout
*/
override val mainLayout = R.layout.generic_list
/**
* The id of the refresh view
*/
override val refreshListId = R.id.generic_list_refresh
/**
* The id of the RecyclerView
*/
override val recyclerViewId = R.id.generic_list_recycler
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
override val itemClickTarget = R.id.selectArtistToSelectAlbum
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(args: Bundle?): LiveData<List<Artist>> {
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
return listModel.getItems(refresh, refreshListView!!)
}
/**
* Provide the Adapter for the RecyclerView with a lazy delegate
*/
override val viewAdapter: ArtistRowAdapter by lazy {
ArtistRowAdapter(
liveDataItems.value ?: listOf(),
{ entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
imageLoaderProvider.getImageLoader(),
onMusicFolderUpdate
)
}
}

View File

@ -18,90 +18,49 @@
*/
package org.moire.ultrasonic.fragment
import android.os.Handler
import android.os.Looper
import android.app.Application
import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.data.ActiveServerProvider
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.service.MusicService
/**
* Provides ViewModel which contains the list of available Artists
*/
class ArtistListModel(
private val activeServerProvider: ActiveServerProvider
) : ViewModel() {
private val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
@KoinApiExtension
class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
/**
* Retrieves the available Artists in a LiveData
* Retrieves all available Artists in a LiveData
*/
fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
backgroundLoadFromServer(refresh, swipe)
return artists
}
/**
* Retrieves the available Music Folders in a LiveData
*/
fun getMusicFolders(): LiveData<List<MusicFolder>> {
return musicFolders
override fun load(
isOffline: Boolean,
useId3Tags: Boolean,
musicService: MusicService,
refresh: Boolean,
args: Bundle
) {
super.load(isOffline, useId3Tags, musicService, refresh, args)
val musicFolderId = activeServer.musicFolderId
val result = if (!isOffline && useId3Tags)
musicService.getArtists(refresh)
else musicService.getIndexes(musicFolderId, refresh)
val retrievedArtists: MutableList<Artist> =
ArrayList(result.shortcuts.size + result.artists.size)
retrievedArtists.addAll(result.shortcuts)
retrievedArtists.addAll(result.artists)
artists.postValue(retrievedArtists)
}
/**
* Refreshes the cached Artists from the server
*/
fun refresh(swipe: SwipeRefreshLayout) {
backgroundLoadFromServer(true, swipe)
}
private fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
viewModelScope.launch {
swipe.isRefreshing = true
loadFromServer(refresh, swipe)
swipe.isRefreshing = false
}
}
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) =
withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService()
val isOffline = ActiveServerProvider.isOffline()
val useId3Tags = Util.getShouldUseId3Tags()
try {
if (!isOffline && !useId3Tags) {
musicFolders.postValue(
musicService.getMusicFolders(refresh)
)
}
val musicFolderId = activeServerProvider.getActiveServer().musicFolderId
val result = if (!isOffline && useId3Tags)
musicService.getArtists(refresh)
else musicService.getIndexes(musicFolderId, refresh)
val retrievedArtists: MutableList<Artist> =
ArrayList(result.shortcuts.size + result.artists.size)
retrievedArtists.addAll(result.shortcuts)
retrievedArtists.addAll(result.artists)
artists.postValue(retrievedArtists)
} catch (exception: Exception) {
Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, swipe.context)
}
}
}
}

View File

@ -1,102 +1,62 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
* ArtistRowAdapter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
import java.text.Collator
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SelectMusicFolderView
/**
* Creates a Row in a RecyclerView which contains the details of an Artist
*/
class ArtistRowAdapter(
private var artistList: List<Artist>,
private var selectFolderHeader: SelectMusicFolderView?,
val onArtistClick: (Artist) -> Unit,
val onContextMenuClick: (MenuItem, Artist) -> Boolean,
private val imageLoader: ImageLoader
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), SectionedAdapter {
artistList: List<Artist>,
onItemClick: (Artist) -> Unit,
onContextMenuClick: (MenuItem, Artist) -> Boolean,
private val imageLoader: ImageLoader,
onMusicFolderUpdate: (String?) -> Unit
) : GenericRowAdapter<Artist>(
onItemClick,
onContextMenuClick,
imageLoader,
onMusicFolderUpdate
),
SectionedAdapter {
override var itemList = artistList
// Set our layout files
override val layout = R.layout.artist_list_item
override val contextMenuLayout = R.menu.artist_context_menu
/**
* Sets the data to be displayed in the RecyclerView
*/
fun setData(data: List<Artist>) {
artistList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
notifyDataSetChanged()
}
/**
* Holds the view properties of an Artist row
*/
class ArtistViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
var section: TextView = itemView.findViewById(R.id.row_section)
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart)
var coverArtId: String? = null
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
if (viewType == TYPE_ITEM) {
val row = LayoutInflater.from(parent.context)
.inflate(R.layout.artist_list_item, parent, false)
return ArtistViewHolder(row)
}
return selectFolderHeader!!
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if ((holder is ArtistViewHolder) && (holder.coverArtId != null)) {
imageLoader.cancel(holder.coverArtId)
}
super.onViewRecycled(holder)
override fun setData(data: List<Artist>) {
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
super.notifyDataSetChanged()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ArtistViewHolder) {
if (holder is ViewHolder) {
val listPosition = if (selectFolderHeader != null) position - 1 else position
holder.textView.text = artistList[listPosition].name
holder.textView.text = itemList[listPosition].name
holder.section.text = getSectionForArtist(listPosition)
holder.layout.setOnClickListener { onArtistClick(artistList[listPosition]) }
holder.layout.setOnClickListener { onItemClick(itemList[listPosition]) }
holder.layout.setOnLongClickListener { view -> createPopupMenu(view, listPosition) }
holder.coverArtId = artistList[listPosition].coverArt
holder.coverArtId = itemList[listPosition].coverArt
if (Util.getShouldShowArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE
@ -111,15 +71,6 @@ class ArtistRowAdapter(
}
}
override fun getItemCount() = if (selectFolderHeader != null)
artistList.size + 1
else
artistList.size
override fun getItemViewType(position: Int): Int {
return if (position == 0 && selectFolderHeader != null) TYPE_HEADER else TYPE_ITEM
}
override fun getSectionName(position: Int): String {
var listPosition = if (selectFolderHeader != null) position - 1 else position
@ -127,18 +78,18 @@ class ArtistRowAdapter(
// scrolled up to the "Select Folder" row
if (listPosition < 0) listPosition = 0
return getSectionFromName(artistList[listPosition].name ?: " ")
return getSectionFromName(itemList[listPosition].name ?: " ")
}
private fun getSectionForArtist(artistPosition: Int): String {
if (artistPosition == 0)
return getSectionFromName(artistList[artistPosition].name ?: " ")
return getSectionFromName(itemList[artistPosition].name ?: " ")
val previousArtistSection = getSectionFromName(
artistList[artistPosition - 1].name ?: " "
itemList[artistPosition - 1].name ?: " "
)
val currentArtistSection = getSectionFromName(
artistList[artistPosition].name ?: " "
itemList[artistPosition].name ?: " "
)
return if (previousArtistSection == currentArtistSection) "" else currentArtistSection
@ -150,23 +101,10 @@ class ArtistRowAdapter(
return section.toString()
}
private fun createPopupMenu(view: View, position: Int): Boolean {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.select_artist_context, popup.menu)
val downloadMenuItem = popup.menu.findItem(R.id.artist_menu_download)
downloadMenuItem?.isVisible = !isOffline()
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick(menuItem, artistList[position])
}
popup.show()
return true
}
companion object {
private const val TYPE_HEADER = 0
private const val TYPE_ITEM = 1
/**
* Creates an instance of our ViewHolder class
*/
override fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
}
}

View File

@ -0,0 +1,126 @@
package org.moire.ultrasonic.fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
/*
* An abstract ScrollListener, which can be extended to provide endless scrolling capabilities
*/
abstract class EndlessScrollListener : RecyclerView.OnScrollListener {
// The minimum amount of items to have below your current scroll position
// before loading more.
private var treshold = VISIBLE_TRESHOLD
// The current offset index of data you have loaded
private var currentPage = 0
// The total number of items in the dataset after the last load
private var previousTotalItemCount = 0
// True if we are still waiting for the last set of data to load.
private var loading = true
// Sets the starting page index
private val startingPageIndex = 0
var thisManager: RecyclerView.LayoutManager
constructor(layoutManager: LinearLayoutManager) {
thisManager = layoutManager
}
@Suppress("Unused")
constructor(layoutManager: GridLayoutManager) {
thisManager = layoutManager
treshold *= layoutManager.spanCount
}
@Suppress("Unused")
constructor(layoutManager: StaggeredGridLayoutManager) {
thisManager = layoutManager
treshold *= layoutManager.spanCount
}
private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
var maxSize = 0
for (i in lastVisibleItemPositions.indices) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i]
} else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i]
}
}
return maxSize
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
var lastVisibleItemPosition = 0
val thisManager: RecyclerView.LayoutManager = thisManager
val totalItemCount = thisManager.itemCount
when (thisManager) {
is StaggeredGridLayoutManager -> {
val lastVisibleItemPositions =
thisManager.findLastVisibleItemPositions(null)
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
}
is GridLayoutManager -> {
lastVisibleItemPosition =
thisManager.findLastVisibleItemPosition()
}
is LinearLayoutManager -> {
lastVisibleItemPosition =
thisManager.findLastVisibleItemPosition()
}
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
currentPage = startingPageIndex
previousTotalItemCount = totalItemCount
if (totalItemCount == 0) {
loading = true
}
}
// If its still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
// If it isnt currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && lastVisibleItemPosition + treshold > totalItemCount) {
currentPage++
onLoadMore(currentPage, totalItemCount, view)
loading = true
}
}
// Call this method whenever performing new searches
fun resetState() {
currentPage = startingPageIndex
previousTotalItemCount = 0
loading = true
}
// Defines the process for actually loading more data based on page
abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
companion object {
// The minimum amount of items to have below your current scroll position
// before loading more.
const val VISIBLE_TRESHOLD = 7
}
}

View File

@ -0,0 +1,275 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SelectMusicFolderView
/**
* 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 TA: The Adapter to use (must extend GenericRowAdapter)
*/
@KoinApiExtension
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel()
internal val imageLoaderProvider: ImageLoaderProvider by inject()
protected val downloadHandler: DownloadHandler by inject()
protected var refreshListView: SwipeRefreshLayout? = null
internal var listView: RecyclerView? = null
internal lateinit var viewManager: LinearLayoutManager
internal var selectFolderHeader: SelectMusicFolderView? = null
/**
* The Adapter for the RecyclerView
* Recommendation: Implement this as a lazy delegate
*/
internal abstract val viewAdapter: TA
/**
* The ViewModel to use to get the data
*/
open val listModel: GenericListModel by viewModels()
/**
* The LiveData containing the list provided by the model
* Implement this as a getter
*/
internal lateinit var liveDataItems: LiveData<List<T>>
/**
* The central function to pass a query to the model and return a LiveData object
*/
abstract fun getLiveData(args: Bundle? = null): LiveData<List<T>>
/**
* The id of the target in the navigation graph where we should go,
* after the user has clicked on an item
*/
protected abstract val itemClickTarget: Int
/**
* The id of the RecyclerView
*/
protected abstract val recyclerViewId: Int
/**
* The id of the main layout
*/
abstract val mainLayout: Int
/**
* The id of the refresh view
*/
abstract val refreshListId: Int
/**
* The observer to be called if the available music folders have changed
*/
@Suppress("CommentOverPrivateProperty")
private val musicFolderObserver = { folders: List<MusicFolder> ->
viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
Unit
}
/**
* What to do when the user has modified the folder filter
*/
val onMusicFolderUpdate = { selectedFolderId: String? ->
if (!listModel.isOffline()) {
val currentSetting = listModel.activeServer
currentSetting.musicFolderId = selectedFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.notifyDataSetChanged()
listModel.refresh(refreshListView!!, arguments)
}
/**
* Whether to show the folder selector
*/
fun showFolderHeader(): Boolean {
return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Util.getShouldUseId3Tags()
}
fun setTitle(title: String?) {
if (title == null) {
FragmentTitle.setTitle(
this,
if (listModel.isOffline())
R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
FragmentTitle.setTitle(this, title)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set the title if available
setTitle(arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE))
// Setup refresh handler
refreshListView = view.findViewById(refreshListId)
refreshListView?.setOnRefreshListener {
listModel.refresh(refreshListView!!, arguments)
}
// Populate the LiveData. This starts an API request in most cases
liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes
liveDataItems.observe(viewLifecycleOwner, { newItems -> viewAdapter.setData(newItems) })
// Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
// Hook up the view with the manager and the adapter
listView = view.findViewById<RecyclerView>(recyclerViewId).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
// Configure whether to show the folder header
viewAdapter.folderHeaderEnabled = showFolderHeader()
}
@Override
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(mainLayout, container, false)
}
@Suppress("LongMethod")
fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
val isArtist = (item is Artist)
when (menuItem.itemId) {
R.id.menu_play_now ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_next ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = true,
shuffle = true,
background = false,
playNext = true,
unpin = false,
isArtist = isArtist
)
R.id.menu_play_last ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_pin ->
downloadHandler.downloadRecursively(
this,
item.id,
save = true,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = isArtist
)
R.id.menu_unpin ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = true,
isArtist = isArtist
)
R.id.menu_download ->
downloadHandler.downloadRecursively(
this,
item.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false,
isArtist = isArtist
)
}
return true
}
open fun onItemClick(item: T) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, item.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, item.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, item.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
findNavController().navigate(itemClickTarget, bundle)
}
}

View File

@ -0,0 +1,124 @@
package org.moire.ultrasonic.fragment
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.net.ConnectException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinApiExtension
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Util
/**
* An abstract Model, which can be extended to retrieve a list of items from the API
*/
@KoinApiExtension
open class GenericListModel(application: Application) :
AndroidViewModel(application), KoinComponent {
val activeServerProvider: ActiveServerProvider by inject()
val activeServer: ServerSetting
get() = activeServerProvider.getActiveServer()
val context: Context
get() = getApplication<Application>().applicationContext
var currentListIsSortable = true
var showHeader = true
@Suppress("UNUSED_PARAMETER")
open fun showSelectFolderHeader(args: Bundle?): Boolean {
return true
}
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
/**
* Helper function to check online status
*/
fun isOffline(): Boolean {
return ActiveServerProvider.isOffline()
}
/**
* Refreshes the cached items from the server
*/
fun refresh(swipe: SwipeRefreshLayout, bundle: Bundle?) {
backgroundLoadFromServer(true, swipe, bundle ?: Bundle())
}
/**
* Trigger a load() and notify the UI that we are loading
*/
fun backgroundLoadFromServer(
refresh: Boolean,
swipe: SwipeRefreshLayout,
bundle: Bundle = Bundle()
) {
viewModelScope.launch {
swipe.isRefreshing = true
loadFromServer(refresh, swipe, bundle)
swipe.isRefreshing = false
}
}
/**
* Calls the load() function with error handling
*/
suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout, bundle: Bundle) =
withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService()
val isOffline = ActiveServerProvider.isOffline()
val useId3Tags = Util.getShouldUseId3Tags()
try {
load(isOffline, useId3Tags, musicService, refresh, bundle)
} catch (exception: ConnectException) {
Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, swipe.context)
}
}
}
/**
* This is the central function you need to implement if you want to extend this class
*/
open fun load(
isOffline: Boolean,
useId3Tags: Boolean,
musicService: MusicService,
refresh: Boolean,
args: Bundle
) {
// Update the list of available folders if enabled
if (showSelectFolderHeader(args) && !isOffline && !useId3Tags) {
musicFolders.postValue(
musicService.getMusicFolders(refresh)
)
}
}
/**
* Retrieves the available Music Folders in a LiveData
*/
fun getMusicFolders(): LiveData<List<MusicFolder>> {
return musicFolders
}
}

View File

@ -0,0 +1,149 @@
/*
* GenericRowAdapter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.view.LayoutInflater
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.view.SelectMusicFolderView
/*
* An abstract Adapter, which can be extended to display a List of <T> in a RecyclerView
*/
abstract class GenericRowAdapter<T>(
val onItemClick: (T) -> Unit,
val onContextMenuClick: (MenuItem, T) -> Boolean,
private val imageLoader: ImageLoader,
private val onMusicFolderUpdate: (String?) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
open var itemList: List<T> = listOf()
protected abstract val layout: Int
protected abstract val contextMenuLayout: Int
var folderHeaderEnabled: Boolean = true
var selectFolderHeader: SelectMusicFolderView? = null
var musicFolders: List<MusicFolder> = listOf()
var selectedFolder: String? = null
/**
* Sets the data to be displayed in the RecyclerView
*/
open fun setData(data: List<T>) {
itemList = data
notifyDataSetChanged()
}
/**
* Sets the content and state of the music folder selector row
*/
fun setFolderList(changedFolders: List<MusicFolder>, selectedId: String?) {
musicFolders = changedFolders
selectedFolder = selectedId
selectFolderHeader?.setData(
selectedFolder,
musicFolders
)
notifyDataSetChanged()
}
open fun newViewHolder(view: View): RecyclerView.ViewHolder {
return ViewHolder(view)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
if (viewType == TYPE_ITEM) {
val row = LayoutInflater.from(parent.context)
.inflate(layout, parent, false)
return newViewHolder(row)
} else {
val row = LayoutInflater.from(parent.context)
.inflate(
R.layout.select_folder_header, parent, false
)
selectFolderHeader = SelectMusicFolderView(parent.context, row, onMusicFolderUpdate)
if (musicFolders.isNotEmpty()) {
selectFolderHeader?.setData(
selectedFolder,
musicFolders
)
}
return selectFolderHeader!!
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if ((holder is ViewHolder) && (holder.coverArtId != null)) {
imageLoader.cancel(holder.coverArtId)
}
super.onViewRecycled(holder)
}
abstract override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
override fun getItemCount(): Int {
if (selectFolderHeader != null)
return itemList.size + 1
else
return itemList.size
}
override fun getItemViewType(position: Int): Int {
return if (position == 0 && folderHeaderEnabled) TYPE_HEADER else TYPE_ITEM
}
internal fun createPopupMenu(view: View, position: Int): Boolean {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(contextMenuLayout, popup.menu)
val downloadMenuItem = popup.menu.findItem(R.id.menu_download)
downloadMenuItem?.isVisible = !ActiveServerProvider.isOffline()
popup.setOnMenuItemClickListener { menuItem ->
onContextMenuClick(menuItem, itemList[position])
}
popup.show()
return true
}
/**
* Holds the view properties of an Item row
*/
class ViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
var section: TextView = itemView.findViewById(R.id.row_section)
var textView: TextView = itemView.findViewById(R.id.row_artist_name)
var layout: RelativeLayout = itemView.findViewById(R.id.row_artist_layout)
var coverArt: ImageView = itemView.findViewById(R.id.artist_coverart)
var coverArtId: String? = null
}
companion object {
internal const val TYPE_HEADER = 0
internal const val TYPE_ITEM = 1
}
}

View File

@ -1,222 +0,0 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.SelectMusicFolderView
/**
* Displays the list of Artists from the media library
*/
class SelectArtistFragment : Fragment() {
private val activeServerProvider: ActiveServerProvider by inject()
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val artistListModel: ArtistListModel by viewModel()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val downloadHandler: DownloadHandler by inject()
private var refreshArtistListView: SwipeRefreshLayout? = null
private var artistListView: RecyclerView? = null
private lateinit var viewManager: RecyclerView.LayoutManager
private lateinit var viewAdapter: ArtistRowAdapter
private var selectFolderHeader: SelectMusicFolderView? = null
@Override
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.select_artist, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
refreshArtistListView = view.findViewById(R.id.select_artist_refresh)
refreshArtistListView!!.setOnRefreshListener {
artistListModel.refresh(refreshArtistListView!!)
}
if (!ActiveServerProvider.isOffline() &&
!Util.getShouldUseId3Tags()
) {
selectFolderHeader = SelectMusicFolderView(
requireContext(), view as ViewGroup,
{ selectedFolderId ->
if (!ActiveServerProvider.isOffline()) {
val currentSetting = activeServerProvider.getActiveServer()
currentSetting.musicFolderId = selectedFolderId
serverSettingsModel.updateItem(currentSetting)
}
viewAdapter.notifyDataSetChanged()
artistListModel.refresh(refreshArtistListView!!)
}
)
}
val title = arguments?.getString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE)
if (title == null) {
setTitle(
this,
if (ActiveServerProvider.isOffline())
R.string.music_library_label_offline
else R.string.music_library_label
)
} else {
setTitle(this, title)
}
val refresh = arguments?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
artistListModel.getMusicFolders()
.observe(
viewLifecycleOwner,
Observer { changedFolders ->
if (changedFolders != null) {
viewAdapter.notifyDataSetChanged()
selectFolderHeader!!.setData(
activeServerProvider.getActiveServer().musicFolderId,
changedFolders
)
}
}
)
val artists = artistListModel.getArtists(refresh, refreshArtistListView!!)
artists.observe(
viewLifecycleOwner, Observer { changedArtists -> viewAdapter.setData(changedArtists) }
)
viewManager = LinearLayoutManager(this.context)
viewAdapter = ArtistRowAdapter(
artists.value ?: listOf(),
selectFolderHeader,
{ artist -> onItemClick(artist) },
{ menuItem, artist -> onArtistMenuItemSelected(menuItem, artist) },
imageLoaderProvider.getImageLoader()
)
artistListView = view.findViewById<RecyclerView>(R.id.select_artist_list).apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
super.onViewCreated(view, savedInstanceState)
}
private fun onItemClick(artist: Artist) {
val bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, artist.id)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, artist.name)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, artist.id)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true)
findNavController().navigate(R.id.selectArtistToSelectAlbum, bundle)
}
private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean {
when (menuItem.itemId) {
R.id.artist_menu_play_now ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = false,
append = false,
autoPlay = true,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = true
)
R.id.artist_menu_play_next ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = false,
append = false,
autoPlay = true,
shuffle = true,
background = false,
playNext = true,
unpin = false,
isArtist = true
)
R.id.artist_menu_play_last ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = false,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = true
)
R.id.artist_menu_pin ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = true,
append = true,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = false,
isArtist = true
)
R.id.artist_menu_unpin ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = false,
playNext = false,
unpin = true,
isArtist = true
)
R.id.artist_menu_download ->
downloadHandler.downloadRecursively(
this,
artist.id,
save = false,
append = false,
autoPlay = false,
shuffle = false,
background = true,
playNext = false,
unpin = false,
isArtist = true
)
}
return true
}
}

View File

@ -223,7 +223,7 @@ class ServerSettingsModel(
/**
* Checks if there are any missing indexes in the ServerSetting list
* For displaying the Server Settings in a ListView, it is mandatory that their indexes
* are'nt missing. Ideally the indexes are continuous, but some circumstances (e.g.
* aren't missing. Ideally the indexes are continuous, but some circumstances (e.g.
* concurrency or migration errors) may get them out of order.
* This would make the List Adapter crash, so it is best to prepare and check the list.
*/

View File

@ -1,5 +1,5 @@
/*
* SelectAlbumFragment.kt
* TrackCollectionFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
@ -8,6 +8,8 @@
package org.moire.ultrasonic.fragment
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.LayoutInflater
@ -29,17 +31,16 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.security.SecureRandom
import java.util.Collections
import java.util.Random
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@ -53,21 +54,19 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.AlbumView
import org.moire.ultrasonic.view.EntryAdapter
import org.moire.ultrasonic.view.SelectMusicFolderView
import org.moire.ultrasonic.view.SongView
import timber.log.Timber
/**
* Displays a group of playable media from the library, which can be an Album, a Playlist, etc.
* TODO: Break up this class into smaller more specific classes, extending a base class if necessary
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* TODO: Refactor this fragment and model to extend the GenericListFragment
*/
@KoinApiExtension
class SelectAlbumFragment : Fragment() {
class TrackCollectionFragment : Fragment() {
private var refreshAlbumListView: SwipeRefreshLayout? = null
private var albumListView: ListView? = null
private var header: View? = null
private var selectFolderHeader: SelectMusicFolderView? = null
private var albumButtons: View? = null
private var emptyView: TextView? = null
private var selectButton: ImageView? = null
@ -91,10 +90,8 @@ class SelectAlbumFragment : Fragment() {
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val shareHandler: ShareHandler by inject()
private var cancellationToken: CancellationToken? = null
private val activeServerProvider: ActiveServerProvider by inject()
private val model: SelectAlbumModel by viewModels()
private val model: TrackCollectionModel by viewModels()
private val random: Random = SecureRandom()
override fun onCreate(savedInstanceState: Bundle?) {
@ -128,22 +125,8 @@ class SelectAlbumFragment : Fragment() {
false
)
selectFolderHeader = SelectMusicFolderView(
requireContext(), view as ViewGroup
) { selectedFolderId ->
if (!isOffline()) {
val serverSettingsModel: ServerSettingsModel by viewModel()
val currentSetting = activeServerProvider.getActiveServer()
currentSetting.musicFolderId = selectedFolderId
serverSettingsModel.updateItem(currentSetting)
}
this.updateDisplay(true)
}
model.musicFolders.observe(viewLifecycleOwner, musicFolderObserver)
model.currentDirectory.observe(viewLifecycleOwner, defaultObserver)
model.songsForGenre.observe(viewLifecycleOwner, songsForGenreObserver)
model.albumList.observe(viewLifecycleOwner, albumListObserver)
albumListView!!.choiceMode = ListView.CHOICE_MODE_MULTIPLE
albumListView!!.setOnItemClickListener { parent, theView, position, _ ->
@ -156,7 +139,7 @@ class SelectAlbumFragment : Fragment() {
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.title)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
Navigation.findNavController(theView).navigate(
R.id.selectAlbumFragment,
R.id.trackCollectionFragment,
bundle
)
} else if (entry != null && entry.isVideo) {
@ -197,7 +180,7 @@ class SelectAlbumFragment : Fragment() {
}
playNextButton!!.setOnClickListener {
downloadHandler.download(
this@SelectAlbumFragment, append = true,
this@TrackCollectionFragment, append = true,
save = false, autoPlay = false, playNext = true, shuffle = false,
songs = getSelectedSongs(albumListView)
)
@ -229,6 +212,13 @@ class SelectAlbumFragment : Fragment() {
updateDisplay(false)
}
val handler = CoroutineExceptionHandler { _, exception ->
Handler(Looper.getMainLooper()).post {
context?.let { CommunicationErrorHandler.handleError(exception, it) }
}
refreshAlbumListView!!.isRefreshing = false
}
private fun updateDisplay(refresh: Boolean) {
val args = requireArguments()
val id = args.getString(Constants.INTENT_EXTRA_NAME_ID)
@ -242,13 +232,8 @@ class SelectAlbumFragment : Fragment() {
val playlistName = args.getString(Constants.INTENT_EXTRA_NAME_PLAYLIST_NAME)
val shareId = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_ID)
val shareName = args.getString(Constants.INTENT_EXTRA_NAME_SHARE_NAME)
val albumListType = args.getString(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
)
val genreName = args.getString(Constants.INTENT_EXTRA_NAME_GENRE_NAME)
val albumListTitle = args.getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, 0
)
val getStarredTracks = args.getInt(Constants.INTENT_EXTRA_NAME_STARRED, 0)
val getVideos = args.getInt(Constants.INTENT_EXTRA_NAME_VIDEOS, 0)
val getRandomTracks = args.getInt(Constants.INTENT_EXTRA_NAME_RANDOM, 0)
@ -260,14 +245,14 @@ class SelectAlbumFragment : Fragment() {
)
fun setTitle(name: String?) {
setTitle(this@SelectAlbumFragment, name)
setTitle(this@TrackCollectionFragment, name)
}
fun setTitle(name: Int) {
setTitle(this@SelectAlbumFragment, name)
setTitle(this@TrackCollectionFragment, name)
}
model.viewModelScope.launch {
model.viewModelScope.launch(handler) {
refreshAlbumListView!!.isRefreshing = true
model.getMusicFolders(refresh)
@ -281,9 +266,6 @@ class SelectAlbumFragment : Fragment() {
} else if (shareId != null) {
setTitle(shareName)
model.getShare(shareId)
} else if (albumListType != null) {
setTitle(albumListTitle)
model.getAlbumList(albumListType, albumListSize, albumListOffset)
} else if (genreName != null) {
setTitle(genreName)
model.getSongsForGenre(genreName, albumListSize, albumListOffset)
@ -321,7 +303,7 @@ class SelectAlbumFragment : Fragment() {
if (entry != null && entry.isDirectory) {
val inflater = requireActivity().menuInflater
inflater.inflate(R.menu.select_album_context, menu)
inflater.inflate(R.menu.generic_context_menu, menu)
}
shareButton = menu.findItem(R.id.menu_item_share)
@ -330,7 +312,7 @@ class SelectAlbumFragment : Fragment() {
shareButton!!.isVisible = !isOffline()
}
val downloadMenuItem = menu.findItem(R.id.album_menu_download)
val downloadMenuItem = menu.findItem(R.id.menu_download)
if (downloadMenuItem != null) {
downloadMenuItem.isVisible = !isOffline()
}
@ -346,42 +328,42 @@ class SelectAlbumFragment : Fragment() {
val entryId = entry.id
when (menuItem.itemId) {
R.id.album_menu_play_now -> {
R.id.menu_play_now -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = true, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.album_menu_play_next -> {
R.id.menu_play_next -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = false,
playNext = true, unpin = false, isArtist = false
)
}
R.id.album_menu_play_last -> {
R.id.menu_play_last -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = true,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.album_menu_pin -> {
R.id.menu_pin -> {
downloadHandler.downloadRecursively(
this, entryId, save = true, append = true,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = false, isArtist = false
)
}
R.id.album_menu_unpin -> {
R.id.menu_unpin -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = false,
playNext = false, unpin = true, isArtist = false
)
}
R.id.album_menu_download -> {
R.id.menu_download -> {
downloadHandler.downloadRecursively(
this, entryId, save = false, append = false,
autoPlay = false, shuffle = false, background = true,
@ -389,6 +371,7 @@ class SelectAlbumFragment : Fragment() {
)
}
R.id.select_album_play_all -> {
// TODO: Why is this being handled here?!
playAll()
}
R.id.menu_item_share -> {
@ -620,63 +603,6 @@ class SelectAlbumFragment : Fragment() {
mediaPlayerController.unpin(songs)
}
private val musicFolderObserver = Observer<List<MusicFolder>> { changedFolders ->
if (changedFolders != null) {
selectFolderHeader!!.setData(
activeServerProvider.getActiveServer().musicFolderId,
changedFolders
)
}
}
private val albumListObserver = Observer<MusicDirectory> { musicDirectory ->
if (musicDirectory.getChildren().isNotEmpty()) {
pinButton!!.visibility = View.GONE
unpinButton!!.visibility = View.GONE
downloadButton!!.visibility = View.GONE
deleteButton!!.visibility = View.GONE
// Hide more button when results are less than album list size
if (musicDirectory.getChildren().size < requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
) {
moreButton!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
moreButton!!.setOnClickListener {
val theAlbumListTitle = requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, 0
)
val type = requireArguments().getString(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE
)
val theSize = requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
)
val theOffset = requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, 0
) + theSize
val bundle = Bundle()
bundle.putInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TITLE, theAlbumListTitle
)
bundle.putString(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_TYPE, type)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, theSize)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, theOffset)
Navigation.findNavController(requireView()).navigate(
R.id.selectAlbumFragment, bundle
)
}
}
} else {
moreButton!!.visibility = View.GONE
}
updateInterfaceWithEntries(musicDirectory)
}
private val songsForGenreObserver = Observer<MusicDirectory> { musicDirectory ->
// Hide more button when results are less than album list size
@ -699,7 +625,9 @@ class SelectAlbumFragment : Fragment() {
bundle.putString(Constants.INTENT_EXTRA_NAME_GENRE_NAME, theGenre)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, size)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, theOffset)
Navigation.findNavController(requireView()).navigate(R.id.selectAlbumFragment, bundle)
Navigation.findNavController(requireView())
.navigate(R.id.trackCollectionFragment, bundle)
}
updateInterfaceWithEntries(musicDirectory)
@ -710,7 +638,7 @@ class SelectAlbumFragment : Fragment() {
private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) {
val entries = musicDirectory.getChildren()
if (model.currentDirectoryIsSortable && Util.getShouldSortByDisc()) {
if (model.currentListIsSortable && Util.getShouldSortByDisc()) {
Collections.sort(entries, EntryByDiscAndTrackComparator())
}
@ -764,18 +692,15 @@ class SelectAlbumFragment : Fragment() {
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, listSize)
bundle.putInt(Constants.INTENT_EXTRA_NAME_ALBUM_LIST_OFFSET, offset)
Navigation.findNavController(requireView()).navigate(
R.id.selectAlbumFragment, bundle
R.id.trackCollectionFragment, bundle
)
}
}
}
} else {
if (model.showSelectFolderHeader) {
if (albumListView!!.headerViewsCount == 0) {
albumListView!!.addHeaderView(selectFolderHeader!!.itemView, null, false)
}
}
// TODO: This code path can be removed when getArtist has been moved to
// AlbumListFragment (getArtist returns the albums of an artist)
pinButton!!.visibility = View.GONE
unpinButton!!.visibility = View.GONE
downloadButton!!.visibility = View.GONE
@ -829,7 +754,7 @@ class SelectAlbumFragment : Fragment() {
)
}
model.currentDirectoryIsSortable = true
model.currentListIsSortable = true
}
private fun createHeader(
@ -847,7 +772,7 @@ class SelectAlbumFragment : Fragment() {
val albumHeader = AlbumHeader.processEntries(context, entries)
val titleView = header!!.findViewById<View>(R.id.select_album_title) as TextView
titleView.text = name ?: getTitle(this@SelectAlbumFragment) // getActionBarSubtitle());
titleView.text = name ?: getTitle(this@TrackCollectionFragment) // getActionBarSubtitle());
// Don't show a header if all entries are videos
if (albumHeader.isAllVideo) {

View File

@ -1,46 +1,40 @@
/*
* TrackCollectionModel.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import android.os.Bundle
import androidx.lifecycle.MutableLiveData
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinApiExtension
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Util
// TODO: Break up this class into smaller more specific classes, extending a base class if necessary
/*
* Model for retrieving different collections of tracks from the API
* TODO: Refactor this model to extend the GenericListModel
*/
@KoinApiExtension
class SelectAlbumModel(application: Application) : AndroidViewModel(application), KoinComponent {
private val context: Context
get() = getApplication<Application>().applicationContext
private val activeServerProvider: ActiveServerProvider by inject()
class TrackCollectionModel(application: Application) : GenericListModel(application) {
private val allSongsId = "-1"
val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
val albumList: MutableLiveData<MusicDirectory> = MutableLiveData()
val currentDirectory: MutableLiveData<MusicDirectory> = MutableLiveData()
val songsForGenre: MutableLiveData<MusicDirectory> = MutableLiveData()
var currentDirectoryIsSortable = true
var showHeader = true
var showSelectFolderHeader = false
suspend fun getMusicFolders(refresh: Boolean) {
withContext(Dispatchers.IO) {
if (!ActiveServerProvider.isOffline()) {
if (!isOffline()) {
val musicService = MusicServiceFactory.getMusicService()
musicFolders.postValue(musicService.getMusicFolders(refresh))
}
@ -124,6 +118,10 @@ class SelectAlbumModel(application: Application) : AndroidViewModel(application)
}
}
/*
* TODO: This method should be moved to AlbumListModel,
* since it displays a list of albums by a specified artist.
*/
suspend fun getArtist(refresh: Boolean, id: String?, name: String?) {
withContext(Dispatchers.IO) {
@ -164,7 +162,7 @@ class SelectAlbumModel(application: Application) : AndroidViewModel(application)
val musicDirectory: MusicDirectory
musicDirectory = if (allSongsId == id) {
if (allSongsId == id) {
val root = MusicDirectory()
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
@ -189,10 +187,11 @@ class SelectAlbumModel(application: Application) : AndroidViewModel(application)
root.addChild(song)
}
}
root
musicDirectory = root
} else {
service.getAlbum(id, name, refresh)
musicDirectory = service.getAlbum(id, name, refresh)
}
currentDirectory.postValue(musicDirectory)
}
}
@ -237,7 +236,7 @@ class SelectAlbumModel(application: Application) : AndroidViewModel(application)
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(size)
currentDirectoryIsSortable = false
currentListIsSortable = false
currentDirectory.postValue(musicDirectory)
}
}
@ -281,49 +280,18 @@ class SelectAlbumModel(application: Application) : AndroidViewModel(application)
}
}
suspend fun getAlbumList(albumListType: String, size: Int, offset: Int) {
showHeader = false
showSelectFolderHeader = !ActiveServerProvider.isOffline() &&
!Util.getShouldUseId3Tags() && (
(albumListType == AlbumListType.SORTED_BY_NAME.toString()) ||
(albumListType == AlbumListType.SORTED_BY_ARTIST.toString())
)
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory: MusicDirectory
val musicFolderId = if (showSelectFolderHeader) {
activeServerProvider.getActiveServer().musicFolderId
} else {
null
}
if (Util.getShouldUseId3Tags()) {
musicDirectory = service.getAlbumList2(
albumListType, size,
offset, musicFolderId
)
} else {
musicDirectory = service.getAlbumList(
albumListType, size,
offset, musicFolderId
)
}
currentDirectoryIsSortable = sortableCollection(albumListType)
albumList.postValue(musicDirectory)
}
}
private fun sortableCollection(albumListType: String): Boolean {
return albumListType != "newest" && albumListType != "random" &&
albumListType != "highest" && albumListType != "recent" &&
albumListType != "frequent"
}
// Returns true if the directory contains only folders
private fun hasOnlyFolders(musicDirectory: MusicDirectory) =
musicDirectory.getChildren(includeDirs = true, includeFiles = false).size ==
musicDirectory.getChildren(includeDirs = true, includeFiles = true).size
override fun load(
isOffline: Boolean,
useId3Tags: Boolean,
musicService: MusicService,
refresh: Boolean,
args: Bundle
) {
// See To_Do at the top
}
}

View File

@ -1,89 +1,87 @@
package org.moire.ultrasonic.view
import android.content.Context
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicFolder
/**
* This little view shows the currently selected Folder (or catalog) on the music server.
* When clicked it will drop down a list of all available Folders and allow you to
* select one. The intended usage is to supply a filter to lists of artists, albums, etc
*/
class SelectMusicFolderView(
private val context: Context,
root: ViewGroup,
private val onUpdate: (String?) -> Unit
) : RecyclerView.ViewHolder(
LayoutInflater.from(context).inflate(
R.layout.select_folder_header, root, false
)
) {
private var musicFolders: List<MusicFolder> = mutableListOf<MusicFolder>()
private var selectedFolderId: String? = null
private val folderName: TextView = itemView.findViewById(R.id.select_folder_name)
private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header)
private val MENU_GROUP_MUSIC_FOLDER = 10
init {
folderName.text = context.getString(R.string.select_artist_all_folders)
layout.setOnClickListener { onFolderClick() }
}
fun setData(selectedId: String?, folders: List<MusicFolder>) {
selectedFolderId = selectedId
musicFolders = folders
if (selectedFolderId != null) {
for ((id, name) in musicFolders) {
if (id == selectedFolderId) {
folderName.text = name
break
}
}
} else {
folderName.text = context.getString(R.string.select_artist_all_folders)
}
}
private fun onFolderClick() {
val popup = PopupMenu(context, layout)
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
)
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
menuItem.isChecked = true
}
musicFolders.forEachIndexed { i, musicFolder ->
val (id, name) = musicFolder
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
if (id == selectedFolderId) {
menuItem.isChecked = true
}
}
popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
popup.show()
}
private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId]
val musicFolderName = selectedFolder?.name
?: context.getString(R.string.select_artist_all_folders)
selectedFolderId = selectedFolder?.id
menuItem.isChecked = true
folderName.text = musicFolderName
onUpdate(selectedFolderId)
return true
}
}
package org.moire.ultrasonic.view
import android.content.Context
import android.view.MenuItem
import android.view.View
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicFolder
/**
* This little view shows the currently selected Folder (or catalog) on the music server.
* When clicked it will drop down a list of all available Folders and allow you to
* select one. The intended usage is to supply a filter to lists of artists, albums, etc
*/
class SelectMusicFolderView(
private val context: Context,
view: View,
private val onUpdate: (String?) -> Unit
) : RecyclerView.ViewHolder(view) {
private var musicFolders: List<MusicFolder> = mutableListOf()
private var selectedFolderId: String? = null
private val folderName: TextView = itemView.findViewById(R.id.select_folder_name)
private val layout: LinearLayout = itemView.findViewById(R.id.select_folder_header)
init {
folderName.text = context.getString(R.string.select_artist_all_folders)
layout.setOnClickListener { onFolderClick() }
}
fun setData(selectedId: String?, folders: List<MusicFolder>) {
selectedFolderId = selectedId
musicFolders = folders
if (selectedFolderId != null) {
for ((id, name) in musicFolders) {
if (id == selectedFolderId) {
folderName.text = name
break
}
}
} else {
folderName.text = context.getString(R.string.select_artist_all_folders)
}
}
private fun onFolderClick() {
val popup = PopupMenu(context, layout)
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
)
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
menuItem.isChecked = true
}
musicFolders.forEachIndexed { i, musicFolder ->
val (id, name) = musicFolder
menuItem = popup.menu.add(MENU_GROUP_MUSIC_FOLDER, i, i + 1, name)
if (id == selectedFolderId) {
menuItem.isChecked = true
}
}
popup.menu.setGroupCheckable(MENU_GROUP_MUSIC_FOLDER, true, true)
popup.setOnMenuItemClickListener { item -> onFolderMenuItemSelected(item) }
popup.show()
}
private fun onFolderMenuItemSelected(menuItem: MenuItem): Boolean {
val selectedFolder = if (menuItem.itemId == -1) null else musicFolders[menuItem.itemId]
val musicFolderName = selectedFolder?.name
?: context.getString(R.string.select_artist_all_folders)
selectedFolderId = selectedFolder?.id
menuItem.isChecked = true
folderName.text = musicFolderName
onUpdate(selectedFolderId)
return true
}
companion object {
const val MENU_GROUP_MUSIC_FOLDER = 10
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,12 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M0 0 L24 0 L24 24 L0 24 Z"/>
<path
android:fillColor="#FFF"
android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
android:width="200dp"
android:height="200dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M0,0h100v100h-100z"
android:fillColor="#1a1a1a"/>
<path
android:pathData="M42.415,84.787C49.463,84.53 55.166,77.789 54.75,70.704 55.614,58.35 56.479,45.979 57.342,33.633c0.513,-0.518 1.407,0.72 1.903,0.815 5.393,3.785 9.987,9.62 9.845,16.528 -0.003,5.402 -1.991,10.554 -4.162,15.413C71.552,59.26 74.281,48.374 70.666,39.149 68.858,33.816 64.197,30.278 61.795,25.279A26.452,26.452 0,0 1,58.5 14.397c-0.343,-0.816 -1.323,-0.945 -2.094,-0.999l-0.017,-0.001c-2.434,-0.17 -2.216,1.472 -2.331,3.117l-3.274,46.814c0,0 -1.255,-0.207 -2.188,-0.272 -7.098,-0.965 -15.202,3.666 -16.437,11.095 -1.246,5.877 4.638,11.171 10.257,10.635z"
android:strokeWidth="0.85"
android:fillColor="#ffffff"/>
</vector>

View File

@ -1,51 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="horizontal"
a:layout_width="fill_parent"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
a:id="@+id/row_artist_layout"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:minHeight="?android:attr/listPreferredItemHeight">
a:background="?android:attr/selectableItemBackground"
a:clickable="true"
a:focusable="true">
<ImageView
<com.google.android.material.imageview.ShapeableImageView
a:id="@+id/album_coverart"
a:layout_width="64dp"
a:layout_height="64dp"
a:layout_gravity="left|center_vertical"
a:paddingLeft="3dip" />
a:layout_gravity="center_horizontal|center_vertical"
a:layout_marginStart="6dp"
a:layout_marginLeft="6dp"
a:layout_marginTop="6dp"
a:scaleType="fitCenter"
a:src="@drawable/unknown_album"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/largeRoundedImageView" />
<LinearLayout
a:id="@+id/row_album_details"
a:layout_width="0dp"
a:layout_height="74dp"
a:layout_marginStart="10dp"
a:layout_marginLeft="10dp"
a:drawablePadding="6dip"
a:gravity="center_vertical"
a:minHeight="56dip"
a:orientation="vertical"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:layout_gravity="left|center_vertical"
a:paddingLeft="6dip"
a:paddingRight="3dip">
a:paddingLeft="3dip"
a:paddingRight="3dip"
a:textAppearance="?android:attr/textAppearanceMedium"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintLeft_toRightOf="@+id/album_coverart"
app:layout_constraintStart_toEndOf="@+id/album_coverart"
app:layout_constraintTop_toTopOf="parent">
<TextView
a:id="@+id/album_title"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceMedium"
a:ellipsize="marquee"
a:singleLine="true"
a:ellipsize="marquee" />
a:textAppearance="?android:attr/textAppearanceMedium"
tools:text="TITLE" />
<TextView
a:id="@+id/album_artist"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceSmall"
a:singleLine="true" />
tools:text="ARTIST" />
</LinearLayout>
<ImageView
a:id="@+id/album_star"
a:layout_width="38dp"
a:layout_height="fill_parent"
a:gravity="center_vertical"
a:layout_height="38dp"
a:layout_marginStart="16dp"
a:layout_marginLeft="16dp"
a:layout_marginTop="16dp"
a:background="@android:color/transparent"
a:src="?attr/star_hollow"
a:focusable="false"
a:paddingRight="3dip" />
a:gravity="center_horizontal"
a:paddingRight="3dip"
a:src="?attr/star_hollow"
app:layout_constraintLeft_toRightOf="@+id/row_album_details"
app:layout_constraintStart_toEndOf="@+id/row_album_details"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_star_hollow_dark"
a:paddingEnd="3dip" />
</LinearLayout>
<androidx.constraintlayout.widget.Guideline
a:id="@+id/guideline"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:orientation="vertical"
app:layout_constraintGuide_begin="76dp" />
<androidx.constraintlayout.widget.Guideline
a:id="@+id/guideline2"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:orientation="vertical"
app:layout_constraintGuide_begin="346dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:orientation="horizontal"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:minHeight="?android:attr/listPreferredItemHeight">
<ImageView
a:id="@+id/album_coverart"
a:layout_width="64dp"
a:layout_height="64dp"
a:layout_gravity="left|center_vertical"
a:paddingLeft="3dip" />
<LinearLayout
a:orientation="vertical"
a:layout_width="0dip"
a:layout_height="wrap_content"
a:layout_weight="1"
a:layout_gravity="left|center_vertical"
a:paddingLeft="6dip"
a:paddingRight="3dip">
<TextView
a:id="@+id/album_title"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceMedium"
a:singleLine="true"
a:ellipsize="marquee" />
<TextView
a:id="@+id/album_artist"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:textAppearance="?android:attr/textAppearanceSmall"
a:singleLine="true" />
</LinearLayout>
<ImageView
a:id="@+id/album_star"
a:layout_width="38dp"
a:layout_height="fill_parent"
a:gravity="center_vertical"
a:background="@android:color/transparent"
a:src="?attr/star_hollow"
a:focusable="false"
a:paddingRight="3dip" />
</LinearLayout>

View File

@ -6,13 +6,13 @@
a:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
a:id="@+id/select_artist_refresh"
a:id="@+id/generic_list_refresh"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1.0">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
a:id="@+id/select_artist_list"
a:id="@+id/generic_list_recycler"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:paddingTop="8dp"

View File

@ -19,8 +19,10 @@
android:id="@+id/now_playing_image"
android:layout_width="64.0dip"
android:layout_height="64.0dip"
android:layout_marginLeft="6dp"
android:focusable="true"
android:gravity="center" />
android:gravity="center"
android:layout_marginStart="6dp" />
<LinearLayout
android:layout_width="0.0dp"
@ -28,7 +30,8 @@
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:orientation="vertical"
android:paddingLeft="11.0dip">
android:paddingLeft="11.0dip"
android:paddingStart="11.0dip">
<TextView
android:id="@+id/now_playing_trackname"

View File

@ -2,22 +2,22 @@
<menu xmlns:a="http://schemas.android.com/apk/res/android" >
<item
a:id="@+id/artist_menu_play_now"
a:id="@+id/menu_play_now"
a:title="@string/common.play_now"/>
<item
a:id="@+id/artist_menu_play_next"
a:id="@+id/menu_play_next"
a:title="@string/common.play_next"/>
<item
a:id="@+id/artist_menu_play_last"
a:id="@+id/menu_play_last"
a:title="@string/common.play_last"/>
<item
a:id="@+id/artist_menu_pin"
a:id="@+id/menu_pin"
a:title="@string/common.pin"/>
<item
a:id="@+id/artist_menu_unpin"
a:id="@+id/menu_unpin"
a:title="@string/common.unpin"/>
<item
a:id="@+id/artist_menu_download"
a:id="@+id/menu_download"
a:title="@string/common.download"/>
</menu>

View File

@ -2,22 +2,22 @@
<menu xmlns:a="http://schemas.android.com/apk/res/android" >
<item
a:id="@+id/album_menu_play_now"
a:id="@+id/menu_play_now"
a:title="@string/common.play_now"/>
<item
a:id="@+id/album_menu_play_next"
a:title="@string/common.play_next"/>
<item
a:id="@+id/album_menu_play_last"
a:id="@+id/menu_play_last"
a:title="@string/common.play_last"/>
<item
a:id="@+id/album_menu_pin"
a:id="@+id/menu_pin"
a:title="@string/common.pin"/>
<item
a:id="@+id/album_menu_unpin"
a:id="@+id/menu_unpin"
a:title="@string/common.unpin"/>
<item
a:id="@+id/album_menu_download"
a:id="@+id/menu_download"
a:title="@string/common.download"/>
<item
a:id="@+id/menu_item_share"

View File

@ -11,7 +11,7 @@
a:icon="?attr/home"
a:title="@string/button_bar.home" />
<item
a:id="@+id/selectArtistFragment"
a:id="@+id/mediaLibraryFragment"
a:checkable="true"
a:icon="?attr/browse"
a:title="@string/button_bar.browse" />

View File

@ -8,8 +8,14 @@
android:name="org.moire.ultrasonic.fragment.MainFragment"
android:label="@string/common.appname" >
<action
android:id="@+id/mainToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
android:id="@+id/mainToTrackCollection"
app:destination="@id/trackCollectionFragment" />
<action
android:id="@+id/mainToAlbumList"
app:destination="@id/albumListFragment" />
<action
android:id="@+id/mainToArtistList"
app:destination="@id/artistListFragment" />
<action
android:id="@+id/mainToSelectGenre"
app:destination="@id/selectGenreFragment" />
@ -18,37 +24,48 @@
app:destination="@id/serverSelectorFragment" />
</fragment>
<fragment
android:id="@+id/selectArtistFragment"
android:name="org.moire.ultrasonic.fragment.SelectArtistFragment"
android:id="@+id/mediaLibraryFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment"
android:label="@string/music_library.label" >
<action
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/selectAlbumFragment"
android:name="org.moire.ultrasonic.fragment.SelectAlbumFragment" >
android:id="@+id/artistListFragment"
android:name="org.moire.ultrasonic.fragment.ArtistListFragment" >
<action
android:id="@+id/selectArtistToSelectAlbum"
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/trackCollectionFragment"
android:name="org.moire.ultrasonic.fragment.TrackCollectionFragment" >
</fragment>
<fragment
android:id="@+id/albumListFragment"
android:name="org.moire.ultrasonic.fragment.AlbumListFragment" >
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="org.moire.ultrasonic.fragment.SearchFragment" >
<action
android:id="@+id/searchToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/playlistsFragment"
android:name="org.moire.ultrasonic.fragment.PlaylistsFragment" >
<action
android:id="@+id/playlistsToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/sharesFragment"
android:name="org.moire.ultrasonic.fragment.SharesFragment" >
<action
android:id="@+id/sharesToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/bookmarksFragment"
@ -61,7 +78,7 @@
android:name="org.moire.ultrasonic.fragment.PodcastFragment" >
<action
android:id="@+id/podcastToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
</fragment>
<fragment
android:id="@+id/settingsFragment"
@ -81,7 +98,7 @@
android:name="org.moire.ultrasonic.fragment.PlayerFragment" >
<action
android:id="@+id/playerToSelectAlbum"
app:destination="@id/selectAlbumFragment" />
app:destination="@id/trackCollectionFragment" />
<action
android:id="@+id/playerToLyrics"
app:destination="@id/lyricsFragment" />

View File

@ -25,6 +25,11 @@
<item name="cornerSize">8dp</item>
</style>
<style name="largeRoundedImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">2dp</item>
</style>
<style name="ThemeOverlay.AppCompat.navTheme">
<item name="colorPrimary">?attr/color_menu_selected</item>
<item name="colorControlHighlight">?attr/color_selected</item>