diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index 9da865e5..466d4832 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt new file mode 100644 index 00000000..194408e6 --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt @@ -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 +} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index 530bb49b..7523dd12 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -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() } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt index 9a62688b..1c23e86c 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt @@ -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() diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt index aed96150..fa91d9b9 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Playlist.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt index 9f5ce4c2..a589877e 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/PodcastsChannel.kt @@ -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 } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt index 8912ea94..f6b8987a 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Share.kt @@ -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 = mutableListOf() -) : Serializable { - val name: String? +) : Serializable, GenericEntry() { + override val name: String? get() = url?.let { urlPattern.matcher(url).replaceFirst("$1") } fun getEntries(): List { diff --git a/detekt-baseline-debug.xml b/detekt-baseline-debug.xml index 3d56395b..7edc104a 100644 --- a/detekt-baseline-debug.xml +++ b/detekt-baseline-debug.xml @@ -24,25 +24,11 @@ ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType() !== VideoPlayerType.FLASH - ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() - ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean - ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean - ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) - ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) - ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) - ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() - ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) - ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) - ComplexMethod:SongView.kt$SongView$public override fun update() + ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() + ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ } EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$() EmptyFunctionBlock:SongView.kt$SongView${} @@ -61,94 +47,23 @@ ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) - LargeClass:DownloadFile.kt$DownloadFile - LargeClass:DownloadHandler.kt$DownloadHandler - LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler - LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter - LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer LargeClass:MediaPlayerService.kt$MediaPlayerService : Service - LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity LargeClass:RESTMusicService.kt$RESTMusicService : MusicService - LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment - LargeClass:SelectAlbumModel.kt$SelectAlbumModel : AndroidViewModelKoinComponent - LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment - LargeClass:ServerSettingsModel.kt$ServerSettingsModel : AndroidViewModel - LargeClass:SongView.kt$SongView : UpdateViewCheckable - LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry - LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting - LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) - LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) - LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String - LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File) + LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment + LongMethod:ArtistListFragment.kt$ArtistListFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:ArtistListFragment.kt$ArtistListFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() - LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> ) - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry> - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>) - LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() - LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean - LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle) LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?) - LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean - LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) - LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder() LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem> - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem> - LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context) - LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState) LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init() - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release() - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer) - LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification - LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying() - LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) - 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?) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) - LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying() - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, playlist: MusicDirectory ) - LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View? - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbumList(albumListType: String, size: Int, offset: Int) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getArtist(refresh: Boolean, id: String?, name: String?) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getMusicDirectory( refresh: Boolean, id: String?, name: String?, parentId: String? ) - LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) - LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean - LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) - LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry) LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) - LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) - LongMethod:SongView.kt$SongView$public override fun update() - LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable) - LongMethod:UApp.kt$UApp$override fun onCreate() - LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List<Artist>, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -> Unit, val onContextMenuClick: (MenuItem, Artist) -> Boolean, private val imageLoader: ImageLoader ) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) 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 ) LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean ) @@ -188,11 +103,11 @@ MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L MagicNumber:RESTMusicService.kt$RESTMusicService$206 MagicNumber:RESTMusicService.kt$RESTMusicService$5 - MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10 MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10 MagicNumber:SongView.kt$SongView$3 MagicNumber:SongView.kt$SongView$4 MagicNumber:SongView.kt$SongView$60 + MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10 NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() 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 ) NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() @@ -202,9 +117,9 @@ ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions) SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) } SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) } @@ -231,7 +146,7 @@ TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService - TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt" UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder" UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty() diff --git a/detekt-baseline-main.xml b/detekt-baseline-main.xml index b56454f8..b43d9b0e 100644 --- a/detekt-baseline-main.xml +++ b/detekt-baseline-main.xml @@ -2,14 +2,8 @@ - ComplexMethod:AlbumListType.kt$AlbumListType.Companion$@JvmStatic fun fromName(typeName: String): AlbumListType ComplexMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions - ComplexMethod:SubsonicError.kt$SubsonicError.Companion$fun getError(code: Int, message: String) EmptyFunctionBlock:SubsonicAPIClient.kt$SubsonicAPIClient.<no name provided>${} - LargeClass:ApiVersionCheckWrapper.kt$ApiVersionCheckWrapper : SubsonicAPIDefinition - LargeClass:SubsonicAPIDefinition.kt$SubsonicAPIDefinition - LongMethod:SubsonicAPIClient.kt$SubsonicAPIClient$private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse - LongMethod:SubsonicAPIVersions.kt$SubsonicAPIVersions.Companion$@JvmStatic @Throws(IllegalArgumentException::class) fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions MagicNumber:PasswordExt.kt$0xFF MagicNumber:PasswordExt.kt$4 MagicNumber:PasswordMD5Interceptor.kt$PasswordMD5Interceptor$16 diff --git a/detekt-baseline-release.xml b/detekt-baseline-release.xml index 3d56395b..7edc104a 100644 --- a/detekt-baseline-release.xml +++ b/detekt-baseline-release.xml @@ -24,25 +24,11 @@ ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo && Util.getVideoPlayerType() !== VideoPlayerType.FLASH - ComplexMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() - ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun areFieldsChanged(): Boolean - ComplexMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean - ComplexMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) - ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) - ComplexMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) - ComplexMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() - ComplexMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) - ComplexMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - ComplexMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - ComplexMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) - ComplexMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) - ComplexMethod:SongView.kt$SongView$public override fun update() + ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() + ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ } EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$() EmptyFunctionBlock:SongView.kt$SongView${} @@ -61,94 +47,23 @@ ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) - LargeClass:DownloadFile.kt$DownloadFile - LargeClass:DownloadHandler.kt$DownloadHandler - LargeClass:EditServerFragment.kt$EditServerFragment : FragmentOnBackPressedHandler - LargeClass:FilePickerAdapter.kt$FilePickerAdapter : Adapter - LargeClass:LocalMediaPlayer.kt$LocalMediaPlayer LargeClass:MediaPlayerService.kt$MediaPlayerService : Service - LargeClass:NavigationActivity.kt$NavigationActivity : AppCompatActivity LargeClass:RESTMusicService.kt$RESTMusicService : MusicService - LargeClass:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment - LargeClass:SelectAlbumModel.kt$SelectAlbumModel : AndroidViewModelKoinComponent - LargeClass:SelectArtistFragment.kt$SelectArtistFragment : Fragment - LargeClass:ServerSettingsModel.kt$ServerSettingsModel : AndroidViewModel - LargeClass:SongView.kt$SongView : UpdateViewCheckable - LongMethod:APIMusicDirectoryConverter.kt$fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry - LongMethod:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting - LongMethod:ArtistListModel.kt$ArtistListModel$private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) - LongMethod:ArtistRowAdapter.kt$ArtistRowAdapter$override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) - LongMethod:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String - LongMethod:DownloadFile.kt$DownloadFile$private fun updateModificationDate(file: File) + LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment + LongMethod:ArtistListFragment.kt$ArtistListFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:ArtistListFragment.kt$ArtistListFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() - LongMethod:DownloadHandler.kt$DownloadHandler$fun download( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Exception::class) private fun getSongsRecursively( parent: MusicDirectory, songs: MutableList<MusicDirectory.Entry> ) - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): List<MusicDirectory.Entry> - LongMethod:DownloadHandler.kt$DownloadHandler.<no name provided>$override fun done(songs: List<MusicDirectory.Entry>) - LongMethod:EditServerFragment.kt$EditServerFragment$ private fun finishActivity() - LongMethod:EditServerFragment.kt$EditServerFragment$ private fun getFields(): Boolean - LongMethod:EditServerFragment.kt$EditServerFragment$override fun onSaveInstanceState(savedInstanceState: Bundle) LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewStateRestored(savedInstanceState: Bundle?) - LongMethod:EditServerFragment.kt$EditServerFragment.<no name provided>$@Throws(Throwable::class) override fun doInBackground(): Boolean - LongMethod:FileLoggerTree.kt$FileLoggerTree$ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) - LongMethod:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$fun createNewFolder() LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getKitKatStorageItems(storages: List<File>): LinkedList<FileListItem> - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun getStorageItems(): LinkedList<FileListItem> - LongMethod:FilePickerDialog.kt$FilePickerDialog$private fun initialize(context: Context) - LongMethod:ImageLoaderProvider.kt$ImageLoaderProvider$@Synchronized fun getImageLoader(): ImageLoader - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized fun setPlayerState(playerState: PlayerState) LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun setupNext(downloadFile: DownloadFile) - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun init() - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$fun release() - LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$override fun onCompletion(mediaPlayer: MediaPlayer) - LongMethod:MediaPlayerService.kt$MediaPlayerService$ private fun buildForegroundNotification( playerState: PlayerState, currentPlaying: DownloadFile? ): Notification - LongMethod:MediaPlayerService.kt$MediaPlayerService$@Synchronized fun setNextPlaying() - LongMethod:MediaPlayerService.kt$MediaPlayerService$override fun onCreate() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun addActions( context: Context, notificationBuilder: NotificationCompat.Builder, playerState: PlayerState, song: MusicDirectory.Entry? ): IntArray - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnCurrentPlayingChangedHandler() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnPlayerStateChangedHandler() - LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState) - 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?) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) - LongMethod:NavigationActivity.kt$NavigationActivity$private fun showNowPlaying() - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - LongMethod:RESTMusicService.kt$RESTMusicService$@Throws(IOException::class) private fun savePlaylist( name: String?, playlist: MusicDirectory ) - LongMethod:RestErrorMapper.kt$ fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun createHeader( entries: List<MusicDirectory.Entry>, name: CharSequence?, songCount: Int ): View? - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun downloadBackground(save: Boolean, songs: List<MusicDirectory.Entry?>) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun enableButtons() - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun playAll(shuffle: Boolean = false, append: Boolean = false) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateDisplay(refresh: Boolean) - LongMethod:SelectAlbumFragment.kt$SelectAlbumFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getAlbumList(albumListType: String, size: Int, offset: Int) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getArtist(refresh: Boolean, id: String?, name: String?) - LongMethod:SelectAlbumModel.kt$SelectAlbumModel$suspend fun getMusicDirectory( refresh: Boolean, id: String?, name: String?, parentId: String? ) - LongMethod:SelectArtistFragment.kt$SelectArtistFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:SelectArtistFragment.kt$SelectArtistFragment$private fun onArtistMenuItemSelected(menuItem: MenuItem, artist: Artist): Boolean - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean - LongMethod:ServerRowAdapter.kt$ServerRowAdapter$ private fun serverMenuClick(view: View, position: Int) - LongMethod:ServerSelectorFragment.kt$ServerSelectorFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ fun migrateFromPreferences(): Boolean - LongMethod:ServerSettingsModel.kt$ServerSettingsModel$ private fun loadServerSettingFromPreferences( preferenceId: Int, serverId: Int, settings: SharedPreferences ): ServerSetting? LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) - LongMethod:SongView.kt$SongView$fun setLayout(song: MusicDirectory.Entry) LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) - LongMethod:SongView.kt$SongView$private fun updateDownloadStatus(downloadFile: DownloadFile) - LongMethod:SongView.kt$SongView$public override fun update() - LongMethod:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$override fun uncaughtException(thread: Thread, throwable: Throwable) - LongMethod:UApp.kt$UApp$override fun onCreate() - LongParameterList:ArtistRowAdapter.kt$ArtistRowAdapter$( private var artistList: List<Artist>, private var selectFolderHeader: SelectMusicFolderView?, val onArtistClick: (Artist) -> Unit, val onContextMenuClick: (MenuItem, Artist) -> Boolean, private val imageLoader: ImageLoader ) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean) + LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, append: Boolean, save: Boolean, autoPlay: Boolean, playNext: Boolean, shuffle: Boolean, songs: List<MusicDirectory.Entry?> ) 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 ) LongParameterList:DownloadHandler.kt$DownloadHandler$( fragment: Fragment, id: String, name: String?, save: Boolean, append: Boolean, autoplay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean ) @@ -188,11 +103,11 @@ MagicNumber:MediaPlayerService.kt$MediaPlayerService.Companion$50L MagicNumber:RESTMusicService.kt$RESTMusicService$206 MagicNumber:RESTMusicService.kt$RESTMusicService$5 - MagicNumber:SelectAlbumFragment.kt$SelectAlbumFragment$10 MagicNumber:SelectMusicFolderView.kt$SelectMusicFolderView$10 MagicNumber:SongView.kt$SongView$3 MagicNumber:SongView.kt$SongView$4 MagicNumber:SongView.kt$SongView$60 + MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10 NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() 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 ) NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() @@ -202,9 +117,9 @@ ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? - ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - ReturnCount:SelectAlbumFragment.kt$SelectAlbumFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean + ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean + ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean SpreadOperator:MediaPlayerService.kt$MediaPlayerService$(*compactActions) SwallowedException:DownloadFile.kt$DownloadFile$catch (e: Exception) { Timber.w("Failed to set last-modified date on %s", file) } SwallowedException:DownloadFile.kt$DownloadFile$catch (ex: IOException) { Timber.w("Failed to rename file %s to %s", completeFile, saveFile) } @@ -231,7 +146,7 @@ TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService - TooManyFunctions:SelectAlbumFragment.kt$SelectAlbumFragment : Fragment + TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment TopLevelPropertyNaming:SubsonicUncaughtExceptionHandler.kt$private const val filename = "ultrasonic-stacktrace.txt" UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder" UselessCallOnNotNull:FileLoggerTree.kt$FileLoggerTree$fileList.isNullOrEmpty() diff --git a/detekt-config.yml b/detekt-config.yml index 855e94ac..f152b9ec 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -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 diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.java index 4c814320..144d012f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/MainFragment.java @@ -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() diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java index 9608ae9b..05306643 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/NowPlayingFragment.java @@ -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); }); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java index 8483f848..e98e5798 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PlaylistsFragment.java @@ -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) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java index d2913d87..eb068046 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/PodcastFragment.java @@ -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); } }); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java index ff8da2b0..a3cd9b4a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SearchFragment.java @@ -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 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) { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java index 171f039e..c2559df2 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SelectGenreFragment.java @@ -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); } } }); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java index 965db039..4da42549 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/SharesFragment.java @@ -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); diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java index 92660026..14806a76 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Constants.java @@ -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"; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java index 0b7fba7c..21ef70b5 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/view/AlbumView.java @@ -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); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 5aa200a7..6c8e5691 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index 91ab71a6..285e91be 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -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() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt new file mode 100644 index 00000000..f5b4b4e4 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -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() { + + /** + * 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> { + 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(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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt new file mode 100644 index 00000000..7c927044 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt @@ -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> = MutableLiveData() + private var loadedUntil: Int = 0 + + fun getAlbumList( + refresh: Boolean, + swipe: SwipeRefreshLayout, + args: Bundle + ): LiveData> { + + 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() + 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" + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt new file mode 100644 index 00000000..2a5f1c46 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -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, + onItemClick: (MusicDirectory.Entry) -> Unit, + onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, + private val imageLoader: ImageLoader, + onMusicFolderUpdate: (String?) -> Unit +) : GenericRowAdapter( + 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) { + 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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt new file mode 100644 index 00000000..ddda850b --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -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() { + + /** + * 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> { + 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 + ) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index ef6dcc5e..2611e3d2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -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> = MutableLiveData() +@KoinApiExtension +class ArtistListModel(application: Application) : GenericListModel(application) { private val artists: MutableLiveData> = MutableLiveData() /** - * Retrieves the available Artists in a LiveData + * Retrieves all available Artists in a LiveData */ - fun getArtists(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { + fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { backgroundLoadFromServer(refresh, swipe) return artists } - /** - * Retrieves the available Music Folders in a LiveData - */ - fun getMusicFolders(): LiveData> { - 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 = + 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 = - 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) - } - } - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index de7c2f8d..dfd86ef9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -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 . - - 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, - private var selectFolderHeader: SelectMusicFolderView?, - val onArtistClick: (Artist) -> Unit, - val onContextMenuClick: (MenuItem, Artist) -> Boolean, - private val imageLoader: ImageLoader -) : RecyclerView.Adapter(), SectionedAdapter { + artistList: List, + onItemClick: (Artist) -> Unit, + onContextMenuClick: (MenuItem, Artist) -> Boolean, + private val imageLoader: ImageLoader, + onMusicFolderUpdate: (String?) -> Unit +) : GenericRowAdapter( + 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) { - 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) { + 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) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt new file mode 100644 index 00000000..4e12491a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EndlessScrollListener.kt @@ -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 it’s 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 isn’t 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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt new file mode 100644 index 00000000..723193ad --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt @@ -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> : 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> + + /** + * The central function to pass a query to the model and return a LiveData object + */ + abstract fun getLiveData(args: Bundle? = null): LiveData> + + /** + * 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 -> + 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(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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt new file mode 100644 index 00000000..1fe45f3f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -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().applicationContext + + var currentListIsSortable = true + var showHeader = true + + @Suppress("UNUSED_PARAMETER") + open fun showSelectFolderHeader(args: Bundle?): Boolean { + return true + } + + internal val musicFolders: MutableLiveData> = 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> { + return musicFolders + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt new file mode 100644 index 00000000..414c76d0 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericRowAdapter.kt @@ -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 in a RecyclerView +*/ +abstract class GenericRowAdapter( + val onItemClick: (T) -> Unit, + val onContextMenuClick: (MenuItem, T) -> Boolean, + private val imageLoader: ImageLoader, + private val onMusicFolderUpdate: (String?) -> Unit +) : RecyclerView.Adapter() { + open var itemList: List = listOf() + protected abstract val layout: Int + protected abstract val contextMenuLayout: Int + + var folderHeaderEnabled: Boolean = true + var selectFolderHeader: SelectMusicFolderView? = null + var musicFolders: List = listOf() + var selectedFolder: String? = null + + /** + * Sets the data to be displayed in the RecyclerView + */ + open fun setData(data: List) { + itemList = data + notifyDataSetChanged() + } + + /** + * Sets the content and state of the music folder selector row + */ + fun setFolderList(changedFolders: List, 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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectArtistFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectArtistFragment.kt deleted file mode 100644 index b2c4414d..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectArtistFragment.kt +++ /dev/null @@ -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(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 - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt index 02386b5e..af33133c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt @@ -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. */ diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt similarity index 84% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumFragment.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 43d1915c..b5ecc21f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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> { changedFolders -> - if (changedFolders != null) { - selectFolderHeader!!.setData( - activeServerProvider.getActiveServer().musicFolderId, - changedFolders - ) - } - } - - private val albumListObserver = Observer { 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 -> // 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(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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt similarity index 76% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumModel.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt index 536d3690..f0d5e4e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectAlbumModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt @@ -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().applicationContext - - private val activeServerProvider: ActiveServerProvider by inject() +class TrackCollectionModel(application: Application) : GenericListModel(application) { private val allSongsId = "-1" - val musicFolders: MutableLiveData> = MutableLiveData() - val albumList: MutableLiveData = MutableLiveData() val currentDirectory: MutableLiveData = MutableLiveData() val songsForGenre: MutableLiveData = 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 = 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 + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt index 28d34b2d..3dcec9a4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SelectMusicFolderView.kt @@ -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 = 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) - 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) { - 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 = 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) { + 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 + } +} diff --git a/ultrasonic/src/main/res/drawable-hdpi/unknown_album.png b/ultrasonic/src/main/res/drawable-hdpi/unknown_album.png deleted file mode 100644 index e59b7868..00000000 Binary files a/ultrasonic/src/main/res/drawable-hdpi/unknown_album.png and /dev/null differ diff --git a/ultrasonic/src/main/res/drawable/unknown_album.xml b/ultrasonic/src/main/res/drawable/unknown_album.xml index eae63c63..54f39529 100644 --- a/ultrasonic/src/main/res/drawable/unknown_album.xml +++ b/ultrasonic/src/main/res/drawable/unknown_album.xml @@ -1,12 +1,13 @@ - - + android:width="200dp" + android:height="200dp" + android:viewportWidth="100" + android:viewportHeight="100"> + + diff --git a/ultrasonic/src/main/res/layout/album_list_item.xml b/ultrasonic/src/main/res/layout/album_list_item.xml index e4129ff1..7959aa3d 100644 --- a/ultrasonic/src/main/res/layout/album_list_item.xml +++ b/ultrasonic/src/main/res/layout/album_list_item.xml @@ -1,51 +1,95 @@ - + a:background="?android:attr/selectableItemBackground" + a:clickable="true" + a:focusable="true"> - + 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" /> + 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"> + a:textAppearance="?android:attr/textAppearanceMedium" + tools:text="TITLE" /> + tools:text="ARTIST" /> + 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" /> - + + + + + diff --git a/ultrasonic/src/main/res/layout/album_list_item_legacy.xml b/ultrasonic/src/main/res/layout/album_list_item_legacy.xml new file mode 100644 index 00000000..e4129ff1 --- /dev/null +++ b/ultrasonic/src/main/res/layout/album_list_item_legacy.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + diff --git a/ultrasonic/src/main/res/layout/select_artist.xml b/ultrasonic/src/main/res/layout/generic_list.xml similarity index 93% rename from ultrasonic/src/main/res/layout/select_artist.xml rename to ultrasonic/src/main/res/layout/generic_list.xml index a9c04a70..1cb9529d 100644 --- a/ultrasonic/src/main/res/layout/select_artist.xml +++ b/ultrasonic/src/main/res/layout/generic_list.xml @@ -6,13 +6,13 @@ a:orientation="vertical"> + android:gravity="center" + android:layout_marginStart="6dp" /> + android:paddingLeft="11.0dip" + android:paddingStart="11.0dip"> \ No newline at end of file diff --git a/ultrasonic/src/main/res/menu/select_album_context.xml b/ultrasonic/src/main/res/menu/generic_context_menu.xml similarity index 73% rename from ultrasonic/src/main/res/menu/select_album_context.xml rename to ultrasonic/src/main/res/menu/generic_context_menu.xml index 8e03b96d..553bf63e 100644 --- a/ultrasonic/src/main/res/menu/select_album_context.xml +++ b/ultrasonic/src/main/res/menu/generic_context_menu.xml @@ -2,22 +2,22 @@ diff --git a/ultrasonic/src/main/res/navigation/navigation_graph.xml b/ultrasonic/src/main/res/navigation/navigation_graph.xml index a33cf59a..dc877465 100644 --- a/ultrasonic/src/main/res/navigation/navigation_graph.xml +++ b/ultrasonic/src/main/res/navigation/navigation_graph.xml @@ -8,8 +8,14 @@ android:name="org.moire.ultrasonic.fragment.MainFragment" android:label="@string/common.appname" > + android:id="@+id/mainToTrackCollection" + app:destination="@id/trackCollectionFragment" /> + + @@ -18,37 +24,48 @@ app:destination="@id/serverSelectorFragment" /> + app:destination="@id/trackCollectionFragment" /> + android:id="@+id/artistListFragment" + android:name="org.moire.ultrasonic.fragment.ArtistListFragment" > + + + + + + app:destination="@id/trackCollectionFragment" /> + app:destination="@id/trackCollectionFragment" /> + app:destination="@id/trackCollectionFragment" /> + app:destination="@id/trackCollectionFragment" /> + app:destination="@id/trackCollectionFragment" /> diff --git a/ultrasonic/src/main/res/values/styles.xml b/ultrasonic/src/main/res/values/styles.xml index e3687b97..f4b776e8 100644 --- a/ultrasonic/src/main/res/values/styles.xml +++ b/ultrasonic/src/main/res/values/styles.xml @@ -25,6 +25,11 @@ 8dp + +