all: reformat code
Simultaniously reformat code using ktlint and ktfmt.
This commit is contained in:
parent
28d28287fe
commit
09823d7829
55 changed files with 385 additions and 236 deletions
|
@ -120,7 +120,14 @@ dependencies {
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
target "src/**/*.kt"
|
target "src/**/*.kt"
|
||||||
ktfmt("0.37").dropboxStyle()
|
|
||||||
|
// ktlint does checking, while ktfmt actually does formatting
|
||||||
|
ktlint()
|
||||||
|
ktfmt().dropboxStyle()
|
||||||
licenseHeaderFile("NOTICE")
|
licenseHeaderFile("NOTICE")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
preDebugBuild.dependsOn spotlessApply
|
||||||
|
}
|
|
@ -21,51 +21,70 @@ package org.oxycblt.auxio
|
||||||
object IntegerTable {
|
object IntegerTable {
|
||||||
/** SongViewHolder */
|
/** SongViewHolder */
|
||||||
const val VIEW_TYPE_SONG = 0xA000
|
const val VIEW_TYPE_SONG = 0xA000
|
||||||
|
|
||||||
/** AlbumViewHolder */
|
/** AlbumViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM = 0xA001
|
const val VIEW_TYPE_ALBUM = 0xA001
|
||||||
|
|
||||||
/** ArtistViewHolder */
|
/** ArtistViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST = 0xA002
|
const val VIEW_TYPE_ARTIST = 0xA002
|
||||||
|
|
||||||
/** GenreViewHolder */
|
/** GenreViewHolder */
|
||||||
const val VIEW_TYPE_GENRE = 0xA003
|
const val VIEW_TYPE_GENRE = 0xA003
|
||||||
|
|
||||||
/** HeaderViewHolder */
|
/** HeaderViewHolder */
|
||||||
const val VIEW_TYPE_HEADER = 0xA004
|
const val VIEW_TYPE_HEADER = 0xA004
|
||||||
|
|
||||||
/** SortHeaderViewHolder */
|
/** SortHeaderViewHolder */
|
||||||
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
const val VIEW_TYPE_SORT_HEADER = 0xA005
|
||||||
|
|
||||||
/** AlbumDetailViewHolder */
|
/** AlbumDetailViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
|
const val VIEW_TYPE_ALBUM_DETAIL = 0xA006
|
||||||
|
|
||||||
/** AlbumSongViewHolder */
|
/** AlbumSongViewHolder */
|
||||||
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
const val VIEW_TYPE_ALBUM_SONG = 0xA007
|
||||||
|
|
||||||
/** ArtistDetailViewHolder */
|
/** ArtistDetailViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
|
const val VIEW_TYPE_ARTIST_DETAIL = 0xA008
|
||||||
|
|
||||||
/** ArtistAlbumViewHolder */
|
/** ArtistAlbumViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
const val VIEW_TYPE_ARTIST_ALBUM = 0xA009
|
||||||
|
|
||||||
/** ArtistSongViewHolder */
|
/** ArtistSongViewHolder */
|
||||||
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
const val VIEW_TYPE_ARTIST_SONG = 0xA00A
|
||||||
|
|
||||||
/** GenreDetailViewHolder */
|
/** GenreDetailViewHolder */
|
||||||
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
|
const val VIEW_TYPE_GENRE_DETAIL = 0xA00B
|
||||||
|
|
||||||
/** DiscHeaderViewHolder */
|
/** DiscHeaderViewHolder */
|
||||||
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
const val VIEW_TYPE_DISC_HEADER = 0xA00C
|
||||||
|
|
||||||
/** "Music playback" notification code */
|
/** "Music playback" notification code */
|
||||||
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
|
const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0
|
||||||
|
|
||||||
/** "Music loading" notification code */
|
/** "Music loading" notification code */
|
||||||
const val INDEXER_NOTIFICATION_CODE = 0xA0A1
|
const val INDEXER_NOTIFICATION_CODE = 0xA0A1
|
||||||
|
|
||||||
/** Intent request code */
|
/** Intent request code */
|
||||||
const val REQUEST_CODE = 0xA0C0
|
const val REQUEST_CODE = 0xA0C0
|
||||||
|
|
||||||
/** RepeatMode.NONE */
|
/** RepeatMode.NONE */
|
||||||
const val REPEAT_MODE_NONE = 0xA100
|
const val REPEAT_MODE_NONE = 0xA100
|
||||||
|
|
||||||
/** RepeatMode.ALL */
|
/** RepeatMode.ALL */
|
||||||
const val REPEAT_MODE_ALL = 0xA101
|
const val REPEAT_MODE_ALL = 0xA101
|
||||||
|
|
||||||
/** RepeatMode.TRACK */
|
/** RepeatMode.TRACK */
|
||||||
const val REPEAT_MODE_TRACK = 0xA102
|
const val REPEAT_MODE_TRACK = 0xA102
|
||||||
|
|
||||||
/** PlaybackMode.IN_GENRE */
|
/** PlaybackMode.IN_GENRE */
|
||||||
const val PLAYBACK_MODE_IN_GENRE = 0xA103
|
const val PLAYBACK_MODE_IN_GENRE = 0xA103
|
||||||
|
|
||||||
/** PlaybackMode.IN_ARTIST */
|
/** PlaybackMode.IN_ARTIST */
|
||||||
const val PLAYBACK_MODE_IN_ARTIST = 0xA104
|
const val PLAYBACK_MODE_IN_ARTIST = 0xA104
|
||||||
|
|
||||||
/** PlaybackMode.IN_ALBUM */
|
/** PlaybackMode.IN_ALBUM */
|
||||||
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
const val PLAYBACK_MODE_IN_ALBUM = 0xA105
|
||||||
|
|
||||||
/** PlaybackMode.ALL_SONGS */
|
/** PlaybackMode.ALL_SONGS */
|
||||||
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
const val PLAYBACK_MODE_ALL_SONGS = 0xA106
|
||||||
|
|
||||||
|
@ -73,10 +92,13 @@ object IntegerTable {
|
||||||
// const val DISPLAY_MODE_NONE = 0xA107
|
// const val DISPLAY_MODE_NONE = 0xA107
|
||||||
/** DisplayMode.SHOW_GENRES */
|
/** DisplayMode.SHOW_GENRES */
|
||||||
const val DISPLAY_MODE_SHOW_GENRES = 0xA108
|
const val DISPLAY_MODE_SHOW_GENRES = 0xA108
|
||||||
|
|
||||||
/** DisplayMode.SHOW_ARTISTS */
|
/** DisplayMode.SHOW_ARTISTS */
|
||||||
const val DISPLAY_MODE_SHOW_ARTISTS = 0xA109
|
const val DISPLAY_MODE_SHOW_ARTISTS = 0xA109
|
||||||
|
|
||||||
/** DisplayMode.SHOW_ALBUMS */
|
/** DisplayMode.SHOW_ALBUMS */
|
||||||
const val DISPLAY_MODE_SHOW_ALBUMS = 0xA10A
|
const val DISPLAY_MODE_SHOW_ALBUMS = 0xA10A
|
||||||
|
|
||||||
/** DisplayMode.SHOW_SONGS */
|
/** DisplayMode.SHOW_SONGS */
|
||||||
const val DISPLAY_MODE_SHOW_SONGS = 0xA10B
|
const val DISPLAY_MODE_SHOW_SONGS = 0xA10B
|
||||||
|
|
||||||
|
@ -85,20 +107,28 @@ object IntegerTable {
|
||||||
|
|
||||||
/** Sort.ByName */
|
/** Sort.ByName */
|
||||||
const val SORT_BY_NAME = 0xA10C
|
const val SORT_BY_NAME = 0xA10C
|
||||||
|
|
||||||
/** Sort.ByArtist */
|
/** Sort.ByArtist */
|
||||||
const val SORT_BY_ARTIST = 0xA10D
|
const val SORT_BY_ARTIST = 0xA10D
|
||||||
|
|
||||||
/** Sort.ByAlbum */
|
/** Sort.ByAlbum */
|
||||||
const val SORT_BY_ALBUM = 0xA10E
|
const val SORT_BY_ALBUM = 0xA10E
|
||||||
|
|
||||||
/** Sort.ByYear */
|
/** Sort.ByYear */
|
||||||
const val SORT_BY_YEAR = 0xA10F
|
const val SORT_BY_YEAR = 0xA10F
|
||||||
|
|
||||||
/** Sort.ByDuration */
|
/** Sort.ByDuration */
|
||||||
const val SORT_BY_DURATION = 0xA114
|
const val SORT_BY_DURATION = 0xA114
|
||||||
|
|
||||||
/** Sort.ByCount */
|
/** Sort.ByCount */
|
||||||
const val SORT_BY_COUNT = 0xA115
|
const val SORT_BY_COUNT = 0xA115
|
||||||
|
|
||||||
/** Sort.ByDisc */
|
/** Sort.ByDisc */
|
||||||
const val SORT_BY_DISC = 0xA116
|
const val SORT_BY_DISC = 0xA116
|
||||||
|
|
||||||
/** Sort.ByTrack */
|
/** Sort.ByTrack */
|
||||||
const val SORT_BY_TRACK = 0xA117
|
const val SORT_BY_TRACK = 0xA117
|
||||||
|
|
||||||
/** Sort.ByDateAdded */
|
/** Sort.ByDateAdded */
|
||||||
const val SORT_BY_DATE_ADDED = 0xA118
|
const val SORT_BY_DATE_ADDED = 0xA118
|
||||||
|
|
||||||
|
@ -106,15 +136,19 @@ object IntegerTable {
|
||||||
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
// const val REPLAY_GAIN_MODE_OFF = 0xA110
|
||||||
/** ReplayGainMode.Track */
|
/** ReplayGainMode.Track */
|
||||||
const val REPLAY_GAIN_MODE_TRACK = 0xA111
|
const val REPLAY_GAIN_MODE_TRACK = 0xA111
|
||||||
|
|
||||||
/** ReplayGainMode.Album */
|
/** ReplayGainMode.Album */
|
||||||
const val REPLAY_GAIN_MODE_ALBUM = 0xA112
|
const val REPLAY_GAIN_MODE_ALBUM = 0xA112
|
||||||
|
|
||||||
/** ReplayGainMode.Dynamic */
|
/** ReplayGainMode.Dynamic */
|
||||||
const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113
|
const val REPLAY_GAIN_MODE_DYNAMIC = 0xA113
|
||||||
|
|
||||||
/** BarAction.Next */
|
/** BarAction.Next */
|
||||||
const val BAR_ACTION_NEXT = 0xA119
|
const val BAR_ACTION_NEXT = 0xA119
|
||||||
|
|
||||||
/** BarAction.Repeat */
|
/** BarAction.Repeat */
|
||||||
const val BAR_ACTION_REPEAT = 0xA11A
|
const val BAR_ACTION_REPEAT = 0xA11A
|
||||||
|
|
||||||
/** BarAction.Shuffle */
|
/** BarAction.Shuffle */
|
||||||
const val BAR_ACTION_SHUFFLE = 0xA11B
|
const val BAR_ACTION_SHUFFLE = 0xA11B
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,15 @@ import org.oxycblt.auxio.playback.queue.QueueSheetBehavior
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
|
import org.oxycblt.auxio.util.collect
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.coordinatorLayoutBehavior
|
||||||
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
|
import org.oxycblt.auxio.util.getDimen
|
||||||
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
|
|
|
@ -28,7 +28,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.MimeType
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.music.ReleaseType
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
|
|
|
@ -26,10 +26,10 @@ import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A dialog displayed when "View properties" is selected on a song, showing more information about
|
* A dialog displayed when "View properties" is selected on a song, showing more information about
|
||||||
|
|
|
@ -29,12 +29,12 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||||
import org.oxycblt.auxio.detail.DiscHeader
|
import org.oxycblt.auxio.detail.DiscHeader
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.util.getPlural
|
import org.oxycblt.auxio.util.getPlural
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
|
|
|
@ -143,10 +143,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArtistAlbumViewHolder
|
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
private constructor(
|
IndicatorAdapter.ViewHolder(binding.root) {
|
||||||
private val binding: ItemParentBinding,
|
|
||||||
) : IndicatorAdapter.ViewHolder(binding.root) {
|
|
||||||
fun bind(item: Album, listener: MenuItemListener) {
|
fun bind(item: Album, listener: MenuItemListener) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
|
@ -178,10 +176,8 @@ private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArtistSongViewHolder
|
private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
private constructor(
|
IndicatorAdapter.ViewHolder(binding.root) {
|
||||||
private val binding: ItemSongBinding,
|
|
||||||
) : IndicatorAdapter.ViewHolder(binding.root) {
|
|
||||||
fun bind(item: Song, listener: MenuItemListener) {
|
fun bind(item: Song, listener: MenuItemListener) {
|
||||||
binding.songAlbumCover.bind(item)
|
binding.songAlbumCover.bind(item)
|
||||||
binding.songName.text = item.resolveName(binding.context)
|
binding.songName.text = item.resolveName(binding.context)
|
||||||
|
|
|
@ -83,8 +83,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
return item is Header || item is SortHeader
|
return item is Header || item is SortHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LeakingThis")
|
@Suppress("LeakingThis") protected val differ = AsyncListDiffer(this, diffCallback)
|
||||||
protected val differ = AsyncListDiffer(this, diffCallback)
|
|
||||||
|
|
||||||
override val currentList: List<Item>
|
override val currentList: List<Item>
|
||||||
get() = differ.currentList
|
get() = differ.currentList
|
||||||
|
|
|
@ -25,11 +25,11 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.util.getPlural
|
import org.oxycblt.auxio.util.getPlural
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,12 @@ import org.oxycblt.auxio.home.list.AlbumListFragment
|
||||||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||||
import org.oxycblt.auxio.home.list.SongListFragment
|
import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.system.Indexer
|
import org.oxycblt.auxio.music.system.Indexer
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -53,7 +58,12 @@ import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
|
import org.oxycblt.auxio.util.collect
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
|
import org.oxycblt.auxio.util.lazyReflectedField
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
||||||
|
|
|
@ -21,11 +21,13 @@ import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import java.util.*
|
import java.util.Formatter
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
|
import org.oxycblt.auxio.music.secsToMs
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
|
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
|
||||||
|
@ -34,8 +36,6 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.music.secsToMs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Album]s.
|
* A [HomeListFragment] for showing a list of [Album]s.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||||
|
@ -32,7 +33,6 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Artist]s.
|
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
||||||
|
@ -32,7 +33,6 @@ import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Genre]s.
|
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||||
|
|
|
@ -26,6 +26,8 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
|
import org.oxycblt.auxio.music.secsToMs
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
|
@ -36,8 +38,6 @@ import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.music.secsToMs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HomeListFragment] for showing a list of [Song]s.
|
* A [HomeListFragment] for showing a list of [Song]s.
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.home.tabs
|
package org.oxycblt.auxio.home.tabs
|
||||||
|
|
||||||
|
import org.oxycblt.auxio.home.tabs.Tab.Companion.fromSequence
|
||||||
|
import org.oxycblt.auxio.home.tabs.Tab.Companion.toSequence
|
||||||
|
import org.oxycblt.auxio.home.tabs.Tab.Invisible
|
||||||
|
import org.oxycblt.auxio.home.tabs.Tab.Visible
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
|
@ -50,6 +54,7 @@ sealed class Tab(open val mode: DisplayMode) {
|
||||||
companion object {
|
companion object {
|
||||||
/** The length a well-formed tab sequence should be */
|
/** The length a well-formed tab sequence should be */
|
||||||
private const val SEQUENCE_LEN = 4
|
private const val SEQUENCE_LEN = 4
|
||||||
|
|
||||||
/** The default tab sequence, represented in integer form */
|
/** The default tab sequence, represented in integer form */
|
||||||
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ class ArtistImageFetcher
|
||||||
private constructor(
|
private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
private val artist: Artist,
|
private val artist: Artist
|
||||||
) : BaseFetcher() {
|
) : BaseFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
val albums = Sort(Sort.Mode.ByName, true).albums(artist.albums)
|
val albums = Sort(Sort.Mode.ByName, true).albums(artist.albums)
|
||||||
|
@ -104,7 +104,7 @@ class GenreImageFetcher
|
||||||
private constructor(
|
private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val size: Size,
|
private val size: Size,
|
||||||
private val genre: Genre,
|
private val genre: Genre
|
||||||
) : BaseFetcher() {
|
) : BaseFetcher() {
|
||||||
override suspend fun fetch(): FetchResult? {
|
override suspend fun fetch(): FetchResult? {
|
||||||
// Genre logic is the most complicated, as we want to ensure album cover variation (i.e
|
// Genre logic is the most complicated, as we want to ensure album cover variation (i.e
|
||||||
|
|
|
@ -30,6 +30,7 @@ import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.music.Date.Companion.from
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.util.inRangeOrNull
|
import org.oxycblt.auxio.util.inRangeOrNull
|
||||||
|
@ -53,7 +54,9 @@ sealed class Music : Item {
|
||||||
* fast-scrolling.
|
* fast-scrolling.
|
||||||
*/
|
*/
|
||||||
val sortName: String?
|
val sortName: String?
|
||||||
get() = rawSortName ?: rawName?.run {
|
get() =
|
||||||
|
rawSortName
|
||||||
|
?: rawName?.run {
|
||||||
when {
|
when {
|
||||||
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
length > 5 && startsWith("the ", ignoreCase = true) -> substring(4)
|
||||||
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
length > 4 && startsWith("an ", ignoreCase = true) -> substring(3)
|
||||||
|
@ -185,6 +188,7 @@ class Song constructor(raw: Raw) : Music() {
|
||||||
val disc = raw.disc
|
val disc = raw.disc
|
||||||
|
|
||||||
private var _album: Album? = null
|
private var _album: Album? = null
|
||||||
|
|
||||||
/** The album of this song. */
|
/** The album of this song. */
|
||||||
val album: Album
|
val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
@ -212,6 +216,7 @@ class Song constructor(raw: Raw) : Music() {
|
||||||
artistName ?: album.artist.resolveName(context)
|
artistName ?: album.artist.resolveName(context)
|
||||||
|
|
||||||
private val _genres: MutableList<Genre> = mutableListOf()
|
private val _genres: MutableList<Genre> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The genres of this song. Most often one, but there could be multiple. There will always be at
|
* The genres of this song. Most often one, but there could be multiple. There will always be at
|
||||||
* least one genre, even if it is an "unknown genre" instance.
|
* least one genre, even if it is an "unknown genre" instance.
|
||||||
|
@ -327,6 +332,7 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
|
||||||
val durationMs = songs.sumOf { it.durationMs }
|
val durationMs = songs.sumOf { it.durationMs }
|
||||||
|
|
||||||
private var _artist: Artist? = null
|
private var _artist: Artist? = null
|
||||||
|
|
||||||
/** The parent artist of this album. */
|
/** The parent artist of this album. */
|
||||||
val artist: Artist
|
val artist: Artist
|
||||||
get() = unlikelyToBeNull(_artist)
|
get() = unlikelyToBeNull(_artist)
|
||||||
|
@ -634,9 +640,8 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
||||||
fun from(timestamp: String): Date? {
|
fun from(timestamp: String): Date? {
|
||||||
val groups =
|
val groups =
|
||||||
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
|
(ISO8601_REGEX.matchEntire(timestamp) ?: return null)
|
||||||
.groupValues.mapIndexedNotNull { index, s ->
|
.groupValues
|
||||||
if (index % 2 != 0) s.toIntOrNull() else null
|
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
|
||||||
}
|
|
||||||
|
|
||||||
return fromTokens(groups)
|
return fromTokens(groups)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.music
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import org.oxycblt.auxio.music.MusicStore.Callback
|
||||||
|
import org.oxycblt.auxio.music.MusicStore.Library
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,12 +101,16 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(song: Song) = find<Song>(song.uid)
|
fun sanitize(song: Song) = find<Song>(song.uid)
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
|
fun sanitize(songs: List<Song>) = songs.mapNotNull { sanitize(it) }
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(album: Album) = find<Album>(album.uid)
|
fun sanitize(album: Album) = find<Album>(album.uid)
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
|
fun sanitize(artist: Artist) = find<Artist>(artist.uid)
|
||||||
|
|
||||||
/** Sanitize an old item to find the corresponding item in a new library. */
|
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||||
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
|
fun sanitize(genre: Genre) = find<Genre>(genre.uid)
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import java.util.UUID
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
|
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
|
||||||
fun ContentResolver.queryCursor(
|
fun ContentResolver.queryCursor(
|
||||||
|
@ -59,13 +59,17 @@ val Long.audioUri: Uri
|
||||||
val Long.albumCoverUri: Uri
|
val Long.albumCoverUri: Uri
|
||||||
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
|
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
|
||||||
|
|
||||||
|
|
||||||
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
|
/** Shortcut to resolve a year from a nullable date. Will return "No Date" if it is null. */
|
||||||
fun Date?.resolveYear(context: Context) =
|
fun Date?.resolveYear(context: Context) =
|
||||||
this?.resolveYear(context) ?: context.getString(R.string.def_date)
|
this?.resolveYear(context) ?: context.getString(R.string.def_date)
|
||||||
|
|
||||||
/** Converts this string to a UUID, or returns null if it is not valid. */
|
/** Converts this string to a UUID, or returns null if it is not valid. */
|
||||||
fun String.toUuid() = try { UUID.fromString(this) } catch (e: IllegalArgumentException) { null }
|
fun String.toUuid() =
|
||||||
|
try {
|
||||||
|
UUID.fromString(this)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
/** Converts a long in milliseconds to a long in deci-seconds */
|
/** Converts a long in milliseconds to a long in deci-seconds */
|
||||||
fun Long.msToDs() = floorDiv(100)
|
fun Long.msToDs() = floorDiv(100)
|
||||||
|
|
|
@ -30,10 +30,12 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.getInstance()
|
||||||
|
|
||||||
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
|
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
|
||||||
|
|
||||||
/** The current music indexing state. */
|
/** The current music indexing state. */
|
||||||
val indexerState: StateFlow<Indexer.State?> = _indexerState
|
val indexerState: StateFlow<Indexer.State?> = _indexerState
|
||||||
|
|
||||||
private val _libraryExists = MutableStateFlow(false)
|
private val _libraryExists = MutableStateFlow(false)
|
||||||
|
|
||||||
/** Whether a music library has successfully been loaded. */
|
/** Whether a music library has successfully been loaded. */
|
||||||
val libraryExists: StateFlow<Boolean> = _libraryExists
|
val libraryExists: StateFlow<Boolean> = _libraryExists
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ import java.lang.reflect.Method
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
|
|
||||||
|
|
||||||
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
|
/** A path to a file. [name] is the stripped file name, [parent] is the parent path. */
|
||||||
data class Path(val name: String, val parent: Directory)
|
data class Path(val name: String, val parent: Directory)
|
||||||
|
|
||||||
|
@ -48,9 +47,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
|
||||||
// "primary" actually corresponds to the internal storage, not the primary volume.
|
// "primary" actually corresponds to the internal storage, not the primary volume.
|
||||||
// Removable storage is represented with the UUID.
|
// Removable storage is represented with the UUID.
|
||||||
if (volume.isInternalCompat) {
|
if (volume.isInternalCompat) {
|
||||||
"${DOCUMENT_URI_PRIMARY_NAME}:${relativePath}"
|
"$DOCUMENT_URI_PRIMARY_NAME:$relativePath"
|
||||||
} else {
|
} else {
|
||||||
volume.uuidCompat?.let { uuid -> "${uuid}:${relativePath}" }
|
volume.uuidCompat?.let { uuid -> "$uuid:$relativePath" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
/**
|
/** TODO: Stub class, not implemented yet */
|
||||||
* TODO: Stub class, not implemented yet
|
|
||||||
*/
|
|
||||||
class CacheLayer {
|
class CacheLayer {
|
||||||
fun init() {
|
fun init() {
|
||||||
// STUB: Add cache database
|
// STUB: Add cache database
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -9,6 +26,7 @@ import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Date
|
import org.oxycblt.auxio.music.Date
|
||||||
import org.oxycblt.auxio.music.Directory
|
import org.oxycblt.auxio.music.Directory
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
@ -20,7 +38,6 @@ import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This file acts as the base for most the black magic required to get a remotely sensible music
|
* This file acts as the base for most the black magic required to get a remotely sensible music
|
||||||
|
@ -81,8 +98,8 @@ import java.io.File
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layer that loads music from the MediaStore database. This is an intermediate step in
|
* The layer that loads music from the MediaStore database. This is an intermediate step in the
|
||||||
* the music loading process.
|
* music loading process.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheLayer) {
|
abstract class MediaStoreLayer(private val context: Context, private val cacheLayer: CacheLayer) {
|
||||||
|
@ -105,11 +122,10 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
private val settings = Settings(context)
|
private val settings = Settings(context)
|
||||||
|
|
||||||
private val _volumes = mutableListOf<StorageVolume>()
|
private val _volumes = mutableListOf<StorageVolume>()
|
||||||
protected val volumes: List<StorageVolume> get() = _volumes
|
protected val volumes: List<StorageVolume>
|
||||||
|
get() = _volumes
|
||||||
|
|
||||||
/**
|
/** Initialize this instance by making a query over the media database. */
|
||||||
* Initialize this instance by making a query over the media database.
|
|
||||||
*/
|
|
||||||
open fun init(): Cursor {
|
open fun init(): Cursor {
|
||||||
cacheLayer.init()
|
cacheLayer.init()
|
||||||
|
|
||||||
|
@ -149,7 +165,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
|
|
||||||
logD("Starting query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
logD("Starting query [proj: ${projection.toList()}, selector: $selector, args: $args]")
|
||||||
|
|
||||||
val cursor = requireNotNull(
|
val cursor =
|
||||||
|
requireNotNull(
|
||||||
context.contentResolverSafe.queryCursor(
|
context.contentResolverSafe.queryCursor(
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
|
@ -159,12 +176,12 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
|
|
||||||
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
|
||||||
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
|
||||||
displayNameIndex =
|
displayNameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
|
|
||||||
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.MIME_TYPE)
|
||||||
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
sizeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.SIZE)
|
||||||
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
dateAddedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_ADDED)
|
||||||
dateModifiedIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED)
|
dateModifiedIndex =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATE_MODIFIED)
|
||||||
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
|
||||||
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
|
||||||
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
|
||||||
|
@ -175,9 +192,7 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Finalize this instance by closing the cursor and finalizing the cache. */
|
||||||
* Finalize this instance by closing the cursor and finalizing the cache.
|
|
||||||
*/
|
|
||||||
fun finalize(rawSongs: List<Song.Raw>) {
|
fun finalize(rawSongs: List<Song.Raw>) {
|
||||||
cursor?.close()
|
cursor?.close()
|
||||||
cursor = null
|
cursor = null
|
||||||
|
@ -281,7 +296,8 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
}
|
}
|
||||||
|
|
||||||
// The album artist field is nullable and never has placeholder values.
|
// The album artist field is nullable and never has placeholder values.
|
||||||
raw.albumArtistNames = cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings)
|
raw.albumArtistNames =
|
||||||
|
cursor.getStringOrNull(albumArtistIndex)?.maybeParseSeparators(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -303,7 +319,6 @@ abstract class MediaStoreLayer(private val context: Context, private val cacheLa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
||||||
// speed, we only want to add redundancy on known issues, not with possible issues.
|
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||||
|
|
||||||
|
@ -377,7 +392,8 @@ class Api21MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : MediaStoreLayer(context, cacheLayer) {
|
open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
|
MediaStoreLayer(context, cacheLayer) {
|
||||||
private var volumeIndex = -1
|
private var volumeIndex = -1
|
||||||
private var relativePathIndex = -1
|
private var relativePathIndex = -1
|
||||||
|
|
||||||
|
@ -431,7 +447,8 @@ open class BaseApi29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) {
|
open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
|
BaseApi29MediaStoreLayer(context, cacheLayer) {
|
||||||
private var trackIndex = -1
|
private var trackIndex = -1
|
||||||
|
|
||||||
override fun init(): Cursor {
|
override fun init(): Cursor {
|
||||||
|
@ -462,7 +479,8 @@ open class Api29MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : Base
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) : BaseApi29MediaStoreLayer(context, cacheLayer) {
|
class Api30MediaStoreLayer(context: Context, cacheLayer: CacheLayer) :
|
||||||
|
BaseApi29MediaStoreLayer(context, cacheLayer) {
|
||||||
private var trackIndex: Int = -1
|
private var trackIndex: Int = -1
|
||||||
private var discIndex: Int = -1
|
private var discIndex: Int = -1
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata
|
||||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
|
||||||
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
||||||
|
import org.oxycblt.auxio.music.Date
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.audioUri
|
import org.oxycblt.auxio.music.audioUri
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import com.google.android.exoplayer2.metadata.Metadata
|
|
||||||
import org.oxycblt.auxio.music.Date
|
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The layer that leverages ExoPlayer's metadata retrieval system to index metadata.
|
* The layer that leverages ExoPlayer's metadata retrieval system to index metadata.
|
||||||
*
|
*
|
||||||
|
@ -32,14 +48,10 @@ class MetadataLayer(private val context: Context, private val mediaStoreLayer: M
|
||||||
private val settings = Settings(context)
|
private val settings = Settings(context)
|
||||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||||
|
|
||||||
/**
|
/** Initialize the sub-layers that this layer relies on. */
|
||||||
* Initialize the sub-layers that this layer relies on.
|
|
||||||
*/
|
|
||||||
fun init() = mediaStoreLayer.init().count
|
fun init() = mediaStoreLayer.init().count
|
||||||
|
|
||||||
/**
|
/** Finalize the sub-layers that this layer relies on. */
|
||||||
* Finalize the sub-layers that this layer relies on.
|
|
||||||
*/
|
|
||||||
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreLayer.finalize(rawSongs)
|
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreLayer.finalize(rawSongs)
|
||||||
|
|
||||||
fun parse(emit: (Song.Raw) -> Unit) {
|
fun parse(emit: (Song.Raw) -> Unit) {
|
||||||
|
@ -90,7 +102,6 @@ class MetadataLayer(private val context: Context, private val mediaStoreLayer: M
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The amount of tasks this backend can run efficiently at once. */
|
/** The amount of tasks this backend can run efficiently at once. */
|
||||||
private const val TASK_CAPACITY = 8
|
private const val TASK_CAPACITY = 8
|
||||||
|
@ -191,7 +202,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
tags["TRCK"]?.run { get(0).parsePositionNum() }?.let { raw.track = it }
|
tags["TRCK"]?.run { get(0).parsePositionNum() }?.let { raw.track = it }
|
||||||
|
|
||||||
// Disc, as NN/TT
|
// Disc, as NN/TT
|
||||||
tags["TPOS"]?.run { get(0).parsePositionNum() } ?.let { raw.disc = it }
|
tags["TPOS"]?.run { get(0).parsePositionNum() }?.let { raw.disc = it }
|
||||||
|
|
||||||
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
||||||
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
||||||
|
@ -204,8 +215,7 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
// 5. ID3v2.3 Release Year, as it is the most common date type
|
// 5. ID3v2.3 Release Year, as it is the most common date type
|
||||||
(tags["TDOR"]?.run { get(0).parseTimestamp() }
|
(tags["TDOR"]?.run { get(0).parseTimestamp() }
|
||||||
?: tags["TDRC"]?.run { get(0).parseTimestamp() }
|
?: tags["TDRC"]?.run { get(0).parseTimestamp() }
|
||||||
?: tags["TDRL"]?.run { get(0).parseTimestamp() }
|
?: tags["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(tags))
|
||||||
?: parseId3v23Date(tags))
|
|
||||||
?.let { raw.date = it }
|
?.let { raw.date = it }
|
||||||
|
|
||||||
// (Sort) Album
|
// (Sort) Album
|
||||||
|
@ -230,7 +240,9 @@ class Task(context: Context, private val settings: Settings, private val raw: So
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseId3v23Date(tags: Map<String, List<String>>): Date? {
|
private fun parseId3v23Date(tags: Map<String, List<String>>): Date? {
|
||||||
val year = tags["TORY"]?.run { get(0).toIntOrNull() } ?: tags["TYER"]?.run { get(0).toIntOrNull() } ?: return null
|
val year =
|
||||||
|
tags["TORY"]?.run { get(0).toIntOrNull() }
|
||||||
|
?: tags["TYER"]?.run { get(0).toIntOrNull() } ?: return null
|
||||||
|
|
||||||
val tdat = tags["TDAT"]
|
val tdat = tags["TDAT"]
|
||||||
return if (tdat != null && tdat[0].length == 4 && tdat[0].isDigitsOnly()) {
|
return if (tdat != null && tdat[0].length == 4 && tdat[0].isDigitsOnly()) {
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.extractor
|
package org.oxycblt.auxio.music.extractor
|
||||||
|
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
|
@ -50,20 +67,22 @@ fun List<String>.parseMultiValue(settings: Settings) =
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maybe a single tag into multi values with the user-preferred separators. If not enabled,
|
* Maybe a single tag into multi values with the user-preferred separators. If not enabled, the
|
||||||
* the plain string will be returned.
|
* plain string will be returned.
|
||||||
*/
|
*/
|
||||||
fun String.maybeParseSeparators(settings: Settings): List<String> {
|
fun String.maybeParseSeparators(settings: Settings): List<String> {
|
||||||
// Get the separators the user desires. If null, we don't parse any.
|
// Get the separators the user desires. If null, we don't parse any.
|
||||||
val separators = settings.separators ?: return listOf(this)
|
val separators = settings.separators ?: return listOf(this)
|
||||||
|
|
||||||
// Try to cache compiled regexes for particular separator combinations.
|
// Try to cache compiled regexes for particular separator combinations.
|
||||||
val regex = synchronized(SEPARATOR_REGEX_CACHE) {
|
val regex =
|
||||||
|
synchronized(SEPARATOR_REGEX_CACHE) {
|
||||||
SEPARATOR_REGEX_CACHE.getOrPut(separators) { Regex("[^\\\\][$separators]") }
|
SEPARATOR_REGEX_CACHE.getOrPut(separators) { Regex("[^\\\\][$separators]") }
|
||||||
}
|
}
|
||||||
|
|
||||||
val escape = synchronized(ESCAPE_REGEX_CACHE) {
|
val escape =
|
||||||
ESCAPE_REGEX_CACHE.getOrPut(separators) { Regex("\\\\[$separators]")}
|
synchronized(ESCAPE_REGEX_CACHE) {
|
||||||
|
ESCAPE_REGEX_CACHE.getOrPut(separators) { Regex("\\\\[$separators]") }
|
||||||
}
|
}
|
||||||
|
|
||||||
return regex.split(this).map { value ->
|
return regex.split(this).map { value ->
|
||||||
|
@ -72,15 +91,13 @@ fun String.maybeParseSeparators(settings: Settings): List<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Parse a multi-value tag into a [ReleaseType], handling separators in the process. */
|
||||||
* Parse a multi-value tag into a [ReleaseType], handling separators in the process.
|
|
||||||
*/
|
|
||||||
fun List<String>.parseReleaseType(settings: Settings) = ReleaseType.parse(parseMultiValue(settings))
|
fun List<String>.parseReleaseType(settings: Settings) = ReleaseType.parse(parseMultiValue(settings))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3
|
* Parse a multi-value genre name using ID3v2 rules. If there is one value, the ID3v2.3 rules will
|
||||||
* rules will be used, followed by separator parsing. Otherwise, each value will be iterated
|
* be used, followed by separator parsing. Otherwise, each value will be iterated through, and
|
||||||
* through, and numeric values transformed into string values.
|
* numeric values transformed into string values.
|
||||||
*/
|
*/
|
||||||
fun List<String>.parseId3GenreNames(settings: Settings) =
|
fun List<String>.parseId3GenreNames(settings: Settings) =
|
||||||
if (size == 1) {
|
if (size == 1) {
|
||||||
|
@ -89,13 +106,9 @@ fun List<String>.parseId3GenreNames(settings: Settings) =
|
||||||
map { it.parseId3v1Genre() ?: it }
|
map { it.parseId3v1Genre() ?: it }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Parse a single genre name using ID3v2.3 rules. */
|
||||||
* Parse a single genre name using ID3v2.3 rules.
|
|
||||||
*/
|
|
||||||
fun String.parseId3GenreNames(settings: Settings) =
|
fun String.parseId3GenreNames(settings: Settings) =
|
||||||
parseId3v1Genre()?.let { listOf(it) } ?:
|
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseSeparators(settings)
|
||||||
parseId3v2Genre() ?:
|
|
||||||
maybeParseSeparators(settings)
|
|
||||||
|
|
||||||
private fun String.parseId3v1Genre(): String? =
|
private fun String.parseId3v1Genre(): String? =
|
||||||
when {
|
when {
|
||||||
|
|
|
@ -26,7 +26,11 @@ import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.Album
|
||||||
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer
|
import org.oxycblt.auxio.music.extractor.Api21MediaStoreLayer
|
||||||
import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer
|
import org.oxycblt.auxio.music.extractor.Api29MediaStoreLayer
|
||||||
import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
|
import org.oxycblt.auxio.music.extractor.Api30MediaStoreLayer
|
||||||
|
@ -50,8 +54,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
* 3. Using the songs to build the library, which primarily involves linking up all data objects
|
* 3. Using the songs to build the library, which primarily involves linking up all data objects
|
||||||
* with their corresponding parents/children.
|
* with their corresponding parents/children.
|
||||||
*
|
*
|
||||||
* This class in particular handles 3 primarily. For the code that handles 1 and 2, see the
|
* This class in particular handles 3 primarily. For the code that handles 1 and 2, see the layer
|
||||||
* layer implementations.
|
* implementations.
|
||||||
*
|
*
|
||||||
* This class also fulfills the role of maintaining the current music loading state, which seems
|
* This class also fulfills the role of maintaining the current music loading state, which seems
|
||||||
* like a job for [MusicStore] but in practice is only really leveraged by the components that
|
* like a job for [MusicStore] but in practice is only really leveraged by the components that
|
||||||
|
@ -205,8 +209,10 @@ class Indexer {
|
||||||
|
|
||||||
val mediaStoreLayer =
|
val mediaStoreLayer =
|
||||||
when {
|
when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreLayer(context, cacheLayer)
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreLayer(context, cacheLayer)
|
Api30MediaStoreLayer(context, cacheLayer)
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
|
||||||
|
Api29MediaStoreLayer(context, cacheLayer)
|
||||||
else -> Api21MediaStoreLayer(context, cacheLayer)
|
else -> Api21MediaStoreLayer(context, cacheLayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,8 +240,8 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the initial query over the song database using [metadataLayer]. The songs returned by this
|
* Does the initial query over the song database using [metadataLayer]. The songs returned by
|
||||||
* function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
|
* this function are **not** well-formed. The companion [buildAlbums], [buildArtists], and
|
||||||
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
||||||
* linked up.
|
* linked up.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -20,7 +20,10 @@ package org.oxycblt.auxio.music.system
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.os.*
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.PowerManager
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -218,7 +221,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
when (key) {
|
when (key) {
|
||||||
getString(R.string.set_key_music_dirs),
|
getString(R.string.set_key_music_dirs),
|
||||||
getString(R.string.set_key_music_dirs_include)-> onStartIndexing()
|
getString(R.string.set_key_music_dirs_include) -> onStartIndexing()
|
||||||
getString(R.string.set_key_observing) -> {
|
getString(R.string.set_key_observing) -> {
|
||||||
if (!indexer.isIndexing) {
|
if (!indexer.isIndexing) {
|
||||||
updateIdleSession()
|
updateIdleSession()
|
||||||
|
|
|
@ -41,7 +41,7 @@ constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0,
|
defStyleAttr: Int = 0,
|
||||||
defStyleRes: Int = 0,
|
defStyleRes: Int = 0
|
||||||
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
|
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
override fun onFinishInflate() {
|
override fun onFinishInflate() {
|
||||||
super.onFinishInflate()
|
super.onFinishInflate()
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.msToDs
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
|
@ -33,7 +34,6 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
import org.oxycblt.auxio.util.getColorCompat
|
import org.oxycblt.auxio.util.getColorCompat
|
||||||
import org.oxycblt.auxio.music.msToDs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fragment showing the current playback state in a compact manner. Used as the bar for the
|
* A fragment showing the current playback state in a compact manner. Used as the bar for the
|
||||||
|
|
|
@ -26,10 +26,13 @@ import org.oxycblt.auxio.IntegerTable
|
||||||
enum class PlaybackMode {
|
enum class PlaybackMode {
|
||||||
/** Construct the queue from the genre's songs */
|
/** Construct the queue from the genre's songs */
|
||||||
ALL_SONGS,
|
ALL_SONGS,
|
||||||
|
|
||||||
/** Construct the queue from the artist's songs */
|
/** Construct the queue from the artist's songs */
|
||||||
IN_ALBUM,
|
IN_ALBUM,
|
||||||
|
|
||||||
/** Construct the queue from the album's songs */
|
/** Construct the queue from the album's songs */
|
||||||
IN_ARTIST,
|
IN_ARTIST,
|
||||||
|
|
||||||
/** Construct the queue from all songs */
|
/** Construct the queue from all songs */
|
||||||
IN_GENRE;
|
IN_GENRE;
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,11 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.msToDs
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.msToDs
|
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
|
|
|
@ -30,15 +30,15 @@ import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.dsToMs
|
||||||
|
import org.oxycblt.auxio.music.msToDs
|
||||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.music.dsToMs
|
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.music.msToDs
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
||||||
|
@ -54,21 +54,25 @@ class PlaybackViewModel(application: Application) :
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
private val _song = MutableStateFlow<Song?>(null)
|
private val _song = MutableStateFlow<Song?>(null)
|
||||||
|
|
||||||
/** The current song. */
|
/** The current song. */
|
||||||
val song: StateFlow<Song?>
|
val song: StateFlow<Song?>
|
||||||
get() = _song
|
get() = _song
|
||||||
private val _parent = MutableStateFlow<MusicParent?>(null)
|
private val _parent = MutableStateFlow<MusicParent?>(null)
|
||||||
|
|
||||||
/** The current model that is being played from, such as an [Album] or [Artist] */
|
/** The current model that is being played from, such as an [Album] or [Artist] */
|
||||||
val parent: StateFlow<MusicParent?> = _parent
|
val parent: StateFlow<MusicParent?> = _parent
|
||||||
private val _isPlaying = MutableStateFlow(false)
|
private val _isPlaying = MutableStateFlow(false)
|
||||||
val isPlaying: StateFlow<Boolean>
|
val isPlaying: StateFlow<Boolean>
|
||||||
get() = _isPlaying
|
get() = _isPlaying
|
||||||
private val _positionDs = MutableStateFlow(0L)
|
private val _positionDs = MutableStateFlow(0L)
|
||||||
|
|
||||||
/** The current playback position, in *deci-seconds* */
|
/** The current playback position, in *deci-seconds* */
|
||||||
val positionDs: StateFlow<Long>
|
val positionDs: StateFlow<Long>
|
||||||
get() = _positionDs
|
get() = _positionDs
|
||||||
|
|
||||||
private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
|
private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
|
||||||
|
|
||||||
/** The current repeat mode, see [RepeatMode] for more information */
|
/** The current repeat mode, see [RepeatMode] for more information */
|
||||||
val repeatMode: StateFlow<RepeatMode>
|
val repeatMode: StateFlow<RepeatMode>
|
||||||
get() = _repeatMode
|
get() = _repeatMode
|
||||||
|
|
|
@ -43,11 +43,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
class StyledSeekBar
|
class StyledSeekBar
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(
|
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
) :
|
|
||||||
ForcedLTRFrameLayout(context, attrs, defStyleAttr),
|
ForcedLTRFrameLayout(context, attrs, defStyleAttr),
|
||||||
Slider.OnSliderTouchListener,
|
Slider.OnSliderTouchListener,
|
||||||
Slider.OnChangeListener {
|
Slider.OnChangeListener {
|
||||||
|
|
|
@ -28,8 +28,13 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.recycler.*
|
import org.oxycblt.auxio.ui.recycler.IndicatorAdapter
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
|
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||||
|
import org.oxycblt.auxio.util.context
|
||||||
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
|
import org.oxycblt.auxio.util.getDimen
|
||||||
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
class QueueAdapter(private val listener: QueueItemListener) :
|
class QueueAdapter(private val listener: QueueItemListener) :
|
||||||
RecyclerView.Adapter<QueueSongViewHolder>() {
|
RecyclerView.Adapter<QueueSongViewHolder>() {
|
||||||
|
@ -104,10 +109,8 @@ interface QueueItemListener {
|
||||||
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueueSongViewHolder
|
class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) :
|
||||||
private constructor(
|
IndicatorAdapter.ViewHolder(binding.root) {
|
||||||
private val binding: ItemQueueSongBinding,
|
|
||||||
) : IndicatorAdapter.ViewHolder(binding.root) {
|
|
||||||
val bodyView: View
|
val bodyView: View
|
||||||
get() = binding.body
|
get() = binding.body
|
||||||
val backgroundView: View
|
val backgroundView: View
|
||||||
|
|
|
@ -25,7 +25,11 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.ui.AuxioSheetBehavior
|
import org.oxycblt.auxio.ui.AuxioSheetBehavior
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||||
|
import org.oxycblt.auxio.util.getDimen
|
||||||
|
import org.oxycblt.auxio.util.getDimenSize
|
||||||
|
import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat
|
||||||
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bottom sheet behavior designed for the queue in particular.
|
* The bottom sheet behavior designed for the queue in particular.
|
||||||
|
|
|
@ -23,8 +23,10 @@ import org.oxycblt.auxio.IntegerTable
|
||||||
enum class ReplayGainMode {
|
enum class ReplayGainMode {
|
||||||
/** Apply the track gain, falling back to the album gain if the track gain is not found. */
|
/** Apply the track gain, falling back to the album gain if the track gain is not found. */
|
||||||
TRACK,
|
TRACK,
|
||||||
|
|
||||||
/** Apply the album gain, falling back to the track gain if the album gain is not found. */
|
/** Apply the album gain, falling back to the track gain if the album gain is not found. */
|
||||||
ALBUM,
|
ALBUM,
|
||||||
|
|
||||||
/** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */
|
/** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */
|
||||||
DYNAMIC;
|
DYNAMIC;
|
||||||
|
|
||||||
|
@ -46,5 +48,5 @@ data class ReplayGainPreAmp(
|
||||||
/** The value to use when ReplayGain tags are present. */
|
/** The value to use when ReplayGain tags are present. */
|
||||||
val with: Float,
|
val with: Float,
|
||||||
/** The value to use when ReplayGain tags are not present. */
|
/** The value to use when ReplayGain tags are not present. */
|
||||||
val without: Float,
|
val without: Float
|
||||||
)
|
)
|
||||||
|
|
|
@ -116,8 +116,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
queue = queue,
|
queue = queue,
|
||||||
positionMs = rawState.positionMs,
|
positionMs = rawState.positionMs,
|
||||||
repeatMode = rawState.repeatMode,
|
repeatMode = rawState.repeatMode,
|
||||||
isShuffled = rawState.isShuffled,
|
isShuffled = rawState.isShuffled)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readRawState(): RawState? {
|
private fun readRawState(): RawState? {
|
||||||
|
@ -258,7 +257,7 @@ class PlaybackStateDatabase private constructor(context: Context) :
|
||||||
val parent: MusicParent?,
|
val parent: MusicParent?,
|
||||||
val positionMs: Long,
|
val positionMs: Long,
|
||||||
val repeatMode: RepeatMode,
|
val repeatMode: RepeatMode,
|
||||||
val isShuffled: Boolean,
|
val isShuffled: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class RawState(
|
private data class RawState(
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
@ -58,13 +59,16 @@ class PlaybackStateManager private constructor() {
|
||||||
/** The currently playing song. Null if there isn't one */
|
/** The currently playing song. Null if there isn't one */
|
||||||
val song
|
val song
|
||||||
get() = queue.getOrNull(index)
|
get() = queue.getOrNull(index)
|
||||||
|
|
||||||
/** The parent the queue is based on, null if all songs */
|
/** The parent the queue is based on, null if all songs */
|
||||||
var parent: MusicParent? = null
|
var parent: MusicParent? = null
|
||||||
private set
|
private set
|
||||||
private var _queue = mutableListOf<Song>()
|
private var _queue = mutableListOf<Song>()
|
||||||
|
|
||||||
/** The current queue determined by [parent] */
|
/** The current queue determined by [parent] */
|
||||||
val queue
|
val queue
|
||||||
get() = _queue
|
get() = _queue
|
||||||
|
|
||||||
/** The current position in the queue */
|
/** The current position in the queue */
|
||||||
var index = -1
|
var index = -1
|
||||||
private set
|
private set
|
||||||
|
@ -79,6 +83,7 @@ class PlaybackStateManager private constructor() {
|
||||||
field = value
|
field = value
|
||||||
notifyRepeatModeChanged()
|
notifyRepeatModeChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the queue is shuffled */
|
/** Whether the queue is shuffled */
|
||||||
var isShuffled = false
|
var isShuffled = false
|
||||||
private set
|
private set
|
||||||
|
|
|
@ -53,6 +53,7 @@ class SearchViewModel(application: Application) :
|
||||||
private val settings = Settings(application)
|
private val settings = Settings(application)
|
||||||
|
|
||||||
private val _searchResults = MutableStateFlow(listOf<Item>())
|
private val _searchResults = MutableStateFlow(listOf<Item>())
|
||||||
|
|
||||||
/** Current search results from the last [search] call. */
|
/** Current search results from the last [search] call. */
|
||||||
val searchResults: StateFlow<List<Item>>
|
val searchResults: StateFlow<List<Item>>
|
||||||
get() = _searchResults
|
get() = _searchResults
|
||||||
|
|
|
@ -36,10 +36,10 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.formatDurationMs
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.music.formatDurationMs
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
|
|
|
@ -19,7 +19,9 @@ package org.oxycblt.auxio.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Build
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -34,7 +36,6 @@ import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.accent.Accent
|
import org.oxycblt.auxio.ui.accent.Accent
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -215,13 +216,12 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The list of separators the user wants to parse by. */
|
||||||
* The list of separators the user wants to parse by.
|
|
||||||
*/
|
|
||||||
var separators: String?
|
var separators: String?
|
||||||
// Differ from convention and store a string of separator characters instead of an int
|
// Differ from convention and store a string of separator characters instead of an int
|
||||||
// code. This makes it easier to use in Regexes and makes it more extendable.
|
// code. This makes it easier to use in Regexes and makes it more extendable.
|
||||||
get() = inner.getString(context.getString(R.string.set_key_separators), null)?.ifEmpty { null }
|
get() =
|
||||||
|
inner.getString(context.getString(R.string.set_key_separators), null)?.ifEmpty { null }
|
||||||
set(value) {
|
set(value) {
|
||||||
inner.edit {
|
inner.edit {
|
||||||
putString(context.getString(R.string.set_key_separators), value)
|
putString(context.getString(R.string.set_key_separators), value)
|
||||||
|
@ -344,3 +344,34 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- COMPAT ---
|
||||||
|
|
||||||
|
fun handleAccentCompat(context: Context, prefs: SharedPreferences): Accent {
|
||||||
|
val currentKey = context.getString(R.string.set_key_accent)
|
||||||
|
|
||||||
|
if (prefs.contains(OldKeys.KEY_ACCENT3)) {
|
||||||
|
Log.d("Auxio.SettingsCompat", "Migrating ${OldKeys.KEY_ACCENT3}")
|
||||||
|
|
||||||
|
var accent = prefs.getInt(OldKeys.KEY_ACCENT3, 5)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// Accents were previously frozen as soon as the OS was updated to android twelve,
|
||||||
|
// as dynamic colors were enabled by default. This is no longer the case, so we need
|
||||||
|
// to re-update the setting to dynamic colors here.
|
||||||
|
accent = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putInt(currentKey, accent)
|
||||||
|
remove(OldKeys.KEY_ACCENT3)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Accent.from(prefs.getInt(currentKey, Accent.DEFAULT))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache of the old keys used in Auxio. */
|
||||||
|
private object OldKeys {
|
||||||
|
const val KEY_ACCENT3 = "auxio_accent"
|
||||||
|
}
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Auxio Project
|
|
||||||
*
|
|
||||||
* This program 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.
|
|
||||||
*
|
|
||||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.settings
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.ui.accent.Accent
|
|
||||||
|
|
||||||
// A couple of utils for migrating from old settings values to the new formats.
|
|
||||||
// Usually, these will last for 6 months before being removed.
|
|
||||||
|
|
||||||
fun handleAccentCompat(context: Context, prefs: SharedPreferences): Accent {
|
|
||||||
val currentKey = context.getString(R.string.set_key_accent)
|
|
||||||
|
|
||||||
if (prefs.contains(OldKeys.KEY_ACCENT3)) {
|
|
||||||
Log.d("Auxio.SettingsCompat", "Migrating ${OldKeys.KEY_ACCENT3}")
|
|
||||||
|
|
||||||
var accent = prefs.getInt(OldKeys.KEY_ACCENT3, 5)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// Accents were previously frozen as soon as the OS was updated to android twelve,
|
|
||||||
// as dynamic colors were enabled by default. This is no longer the case, so we need
|
|
||||||
// to re-update the setting to dynamic colors here.
|
|
||||||
accent = 16
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit {
|
|
||||||
putInt(currentKey, accent)
|
|
||||||
remove(OldKeys.KEY_ACCENT3)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Accent.from(prefs.getInt(currentKey, Accent.DEFAULT))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cache of the old keys used in Auxio. */
|
|
||||||
private object OldKeys {
|
|
||||||
const val KEY_ACCENT3 = "auxio_accent"
|
|
||||||
}
|
|
|
@ -26,7 +26,8 @@ import android.view.WindowInsets
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.getDimen
|
||||||
|
import org.oxycblt.auxio.util.systemGestureInsetsCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a reasonable enough skeleton around BottomSheetBehavior (Excluding auxio extensions in
|
* Implements a reasonable enough skeleton around BottomSheetBehavior (Excluding auxio extensions in
|
||||||
|
|
|
@ -30,11 +30,13 @@ import org.oxycblt.auxio.util.logD
|
||||||
*/
|
*/
|
||||||
class NavigationViewModel : ViewModel() {
|
class NavigationViewModel : ViewModel() {
|
||||||
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
|
private val _mainNavigationAction = MutableStateFlow<MainNavigationAction?>(null)
|
||||||
|
|
||||||
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
/** Flag for main fragment navigation. Intended for MainFragment use only. */
|
||||||
val mainNavigationAction: StateFlow<MainNavigationAction?>
|
val mainNavigationAction: StateFlow<MainNavigationAction?>
|
||||||
get() = _mainNavigationAction
|
get() = _mainNavigationAction
|
||||||
|
|
||||||
private val _exploreNavigationItem = MutableStateFlow<Music?>(null)
|
private val _exploreNavigationItem = MutableStateFlow<Music?>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
* Flag for navigation within the explore fragments. Observe this to coordinate navigation to an
|
||||||
* item's UI.
|
* item's UI.
|
||||||
|
@ -85,12 +87,16 @@ class NavigationViewModel : ViewModel() {
|
||||||
sealed class MainNavigationAction {
|
sealed class MainNavigationAction {
|
||||||
/** Expand the playback panel. */
|
/** Expand the playback panel. */
|
||||||
object Expand : MainNavigationAction()
|
object Expand : MainNavigationAction()
|
||||||
|
|
||||||
/** Collapse the playback panel. */
|
/** Collapse the playback panel. */
|
||||||
object Collapse : MainNavigationAction()
|
object Collapse : MainNavigationAction()
|
||||||
|
|
||||||
/** Go to settings. */
|
/** Go to settings. */
|
||||||
object Settings : MainNavigationAction()
|
object Settings : MainNavigationAction()
|
||||||
|
|
||||||
/** Go to the about page. */
|
/** Go to the about page. */
|
||||||
object About : MainNavigationAction()
|
object About : MainNavigationAction()
|
||||||
|
|
||||||
/** Show song details. */
|
/** Show song details. */
|
||||||
data class SongDetails(val song: Song) : MainNavigationAction()
|
data class SongDetails(val song: Song) : MainNavigationAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import kotlin.UnsupportedOperationException
|
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -27,6 +26,7 @@ import org.oxycblt.auxio.music.Date
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.ui.Sort.Mode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the sort modes used in Auxio.
|
* Represents the sort modes used in Auxio.
|
||||||
|
|
|
@ -349,7 +349,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
if (!dragging &&
|
if (!dragging &&
|
||||||
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
thumbView.isUnder(downX, thumbView.top.toFloat(), minTouchTargetSize) &&
|
||||||
abs(eventY - downY) > touchSlop) {
|
abs(eventY - downY) > touchSlop) {
|
||||||
|
|
||||||
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
if (thumbView.isUnder(downX, downY, minTouchTargetSize)) {
|
||||||
dragStartY = lastY
|
dragStartY = lastY
|
||||||
dragStartThumbOffset = thumbOffset
|
dragStartThumbOffset = thumbOffset
|
||||||
|
|
|
@ -73,10 +73,8 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
* The Shared ViewHolder for a [Album].
|
* The Shared ViewHolder for a [Album].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumViewHolder
|
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
private constructor(
|
IndicatorAdapter.ViewHolder(binding.root) {
|
||||||
private val binding: ItemParentBinding,
|
|
||||||
) : IndicatorAdapter.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(item: Album, listener: MenuItemListener) {
|
fun bind(item: Album, listener: MenuItemListener) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
|
@ -157,10 +155,8 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
||||||
* The Shared ViewHolder for a [Genre].
|
* The Shared ViewHolder for a [Genre].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreViewHolder
|
class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
private constructor(
|
IndicatorAdapter.ViewHolder(binding.root) {
|
||||||
private val binding: ItemParentBinding,
|
|
||||||
) : IndicatorAdapter.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(item: Genre, listener: MenuItemListener) {
|
fun bind(item: Genre, listener: MenuItemListener) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
|
|
|
@ -48,7 +48,6 @@ fun Int.nonZeroOrNull() = if (this > 0) this else null
|
||||||
/** Returns null if this value is not in [range]. */
|
/** Returns null if this value is not in [range]. */
|
||||||
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
|
fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null
|
||||||
|
|
||||||
|
|
||||||
/** Lazily reflect to retrieve a [Field]. */
|
/** Lazily reflect to retrieve a [Field]. */
|
||||||
fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy {
|
||||||
clazz.java.getDeclaredField(field).also { it.isAccessible = true }
|
clazz.java.getDeclaredField(field).also { it.isAccessible = true }
|
||||||
|
|
|
@ -44,6 +44,7 @@ fun createThinWidget(context: Context, state: WidgetComponent.WidgetState) =
|
||||||
.applyRoundingToBackground(context)
|
.applyRoundingToBackground(context)
|
||||||
.applyMeta(context, state)
|
.applyMeta(context, state)
|
||||||
.applyBasicControls(context, state)
|
.applyBasicControls(context, state)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is
|
* The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is
|
||||||
* generally because a Medium widget is too large for this widget size and a text-only widget is too
|
* generally because a Medium widget is too large for this widget size and a text-only widget is too
|
||||||
|
|
|
@ -160,6 +160,6 @@ class WidgetComponent(private val context: Context) :
|
||||||
val cover: Bitmap?,
|
val cover: Bitmap?,
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
val repeatMode: RepeatMode,
|
val repeatMode: RepeatMode,
|
||||||
val isShuffled: Boolean,
|
val isShuffled: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
classpath 'com.android.tools.build:gradle:7.4.0-alpha10'
|
classpath 'com.android.tools.build:gradle:7.4.0-alpha10'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.6.1"
|
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.10.0"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -205,6 +205,12 @@ set -- \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
# Use "xargs" to parse quoted args.
|
||||||
#
|
#
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
|
14
gradlew.bat
vendored
14
gradlew.bat
vendored
|
@ -14,7 +14,7 @@
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem Gradle startup script for Windows
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
rem the _cmd.exe /c_ return code!
|
rem the _cmd.exe /c_ return code!
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
exit /b 1
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
:mainEnd
|
:mainEnd
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
Loading…
Reference in a new issue