New Release!

[Android and PWA] Now the app supports playlists.
You can browse, edit, add and remove playlists in both the PWA and
Android versions. You can add and remove songs to a playlist and you can
reorder the songs within the playlist.
Support for Android TV is coming.
This commit is contained in:
Carlos Pérez
2022-06-24 13:47:21 -03:00
parent 30cd0a5531
commit ef181cd267
18 changed files with 502 additions and 131 deletions

View File

@@ -9,10 +9,7 @@ import android.net.Uri
import android.os.IBinder
import android.util.Log
import androidx.core.content.ContextCompat
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
import com.google.gson.Gson
import com.google.gson.GsonBuilder
@@ -363,7 +360,7 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
@PluginMethod
fun getAlbumArt(call: PluginCall) {
try {
val id = call.getString("id") ?: throw ParameterException("id")
val id = call.getString("id") ?: ""
if (getOfflineMode()) {
call.resolve(okResponse(subsonicClient!!.getLocalAlbumArt(id)))
} else {
@@ -660,21 +657,6 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
}
}
fun getTranscoding(call: PluginCall) {
val transcoding = KeyValueStorage.getTranscoding()
call.resolve(okResponse(transcoding))
}
fun setTranscoding(call: PluginCall) {
try {
val transcoding =
call.getString("transcoding") ?: throw ParameterException("transcoding")
KeyValueStorage.setTranscoding(transcoding)
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun qrLogin(call: PluginCall) {
try {
@@ -706,7 +688,7 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
val playlist = binder!!.getPlaylist()
val request = SetPlaylistAndPlayRequest(
playlist,
playlist.indexOf(binder!!.getCurrentState().currentTrack),
playlist.entry.indexOf(binder!!.getCurrentState().currentTrack),
binder!!.getCurrentState().position,
binder!!.getCurrentState().playing
)
@@ -755,6 +737,137 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
call.resolve(okResponse(""))
}
@PluginMethod
fun getCurrentPlaylist(call: PluginCall) {
if (mBound) {
call.resolve(okResponse(binder!!.getPlaylist()))
} else {
call.resolve(okResponse(MusicService.getDefaultPlaylist()))
}
}
@PluginMethod
fun getPlaylists(call: PluginCall) {
try {
call.resolve(okArrayResponse(subsonicClient!!.getPlaylists()))
}
catch(e: Exception){
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun getPlaylist(call: PluginCall) {
try {
val id = call.getString("id") ?: throw ParameterException("id")
val ret = subsonicClient!!.getPlaylist(id)
call.resolve(okResponse(ret))
}
catch(e: Exception){
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun removeFromPlaylist(call: PluginCall) {
try {
val id: String = call.getString("id") ?: throw ParameterException("id")
val track: Int = call.getInt("track") ?: throw ParameterException("track")
subsonicClient!!.removeFromPlaylist(id, track)
call.resolve(okResponse(""))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun addToPlaylist(call: PluginCall) {
try {
val id: String = call.getString("id") ?: ""
val songId: String = call.getString("songId") ?: throw ParameterException("songId")
if (id === "") {
subsonicClient!!.createPlaylist(listOf(songId), "New playlist")
} else {
subsonicClient!!.addToPlaylist(id, songId)
}
call.resolve(okResponse(""))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun createPlaylist(call: PluginCall) {
try {
val name: String = call.getString("name") ?: throw ParameterException("name")
val jsIds: JSArray = call.getArray("songId") ?: throw ParameterException("songId")
val ids: MutableList<String> = jsIds.toList()
val ret = subsonicClient!!.createPlaylist(ids, name)
call.resolve(okResponse(ret))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun updatePlaylist(call: PluginCall) {
try {
val playlist: Playlist = gson!!.fromJson(call.getObject("playlist").toString(), Playlist::class.java)
val ret = subsonicClient!!.updatePlaylist(playlist)
call.resolve(okResponse(ret))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun removePlaylist(call: PluginCall) {
try {
val id = call.getString("id") ?: throw ParameterException("id")
subsonicClient!!.removePlaylist(id)
call.resolve(okResponse(""))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun skipTo(call: PluginCall) {
try {
val track = call.getInt("track") ?: throw ParameterException("track")
if (mBound) {
binder!!.skipTo(track)
}
call.resolve(okResponse(""))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
@PluginMethod
fun playPlaylist(call: PluginCall) {
try {
val track = call.getInt("track") ?: throw ParameterException("track")
val id = call.getString("playlist") ?: throw ParameterException("playlist")
if (mBound) {
binder!!.playPlaylist(id, track)
} else {
val intent = Intent(App.context, MusicService::class.java)
intent.action = Constants.SERVICE_PLAY_PLAYLIST
intent.putExtra("id", id)
intent.putExtra("track", track)
App.context.startService(intent)
}
call.resolve(okResponse(""))
} catch (e: Exception) {
call.resolve(errorResponse(e.message))
}
}
private fun setWebsocketConnectionStatus(status: Boolean) {
val ret = JSObject()
ret.put("connected", status)

View File

@@ -7,6 +7,7 @@ class Constants {
const val SERVICE_PREV = "PREV"
const val SERVICE_PLAY_ALBUM = "PLAY_ALBUM"
const val SERVICE_PLAY_RADIO = "PLAY_RADIO"
const val SERVICE_PLAY_PLAYLIST = "PLAY_PLAYLIST"
const val SERVICE_PLAY_SEARCH = "PLAY_SEARCH"
const val SERVICE_PLAY_SEARCH_ALBUM = "PLAY_SEARCH_ALBUM"
const val SERVICE_PLAY_SEARCH_ARTIST = "PLAY_SEARCH_ARTIST"

View File

@@ -159,7 +159,7 @@ class MessageServer(port: Int) : WebSocketServer(InetSocketAddress(port)), IBroa
val request: SetPlaylistAndPlayRequest
try{
request = gson.fromJson(command.data, SetPlaylistAndPlayRequest::class.java)
if(request.track >= request.playlist.size){
if(request.track >= request.playlist.entry.size){
throw Exception("The track parameter was out of bounds")
}
}

View File

@@ -47,7 +47,7 @@ class SubsonicClient(var initialAccount: Account) {
}
val db: SoniclairDatabase
val connectivityManager: ConnectivityManager
private val connectivityManager: ConnectivityManager
init {
account = initialAccount
@@ -74,12 +74,12 @@ class SubsonicClient(var initialAccount: Account) {
BigInteger(1, md.digest(saltedPassword.toByteArray())).toString(16).padStart(32, '0')
return BasicParams(
account.username ?: "",
if(account.usePlaintext) null else hash,
if(account.usePlaintext) null else salt,
if (account.usePlaintext) null else hash,
if (account.usePlaintext) null else salt,
"1.16.1",
"soniclair",
"json",
if(account.usePlaintext) account.password else null
if (account.usePlaintext) account.password else null
)
}
@@ -349,7 +349,17 @@ class SubsonicClient(var initialAccount: Account) {
}
if (parameters != null) {
for (key in parameters.keys) {
uriBuilder.appendQueryParameter(key, parameters[key])
if(parameters[key] == null)
{
continue;
}
if (parameters[key]!!.contains(",")) {
parameters[key]!!.split(",").forEach {
uriBuilder.appendQueryParameter(key, it.trim())
}
} else {
uriBuilder.appendQueryParameter(key, parameters[key])
}
}
}
@@ -678,6 +688,86 @@ class SubsonicClient(var initialAccount: Account) {
)!!.song
}
fun getPlaylist(id: String): Playlist {
val params = getBasicParams().asMap()
params["id"] = id
return makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "getPlaylist"),
params
)!!.playlist
}
fun getPlaylists(): List<Playlist> {
val params = getBasicParams().asMap()
return makeSubsonicRequest<PlaylistsResponse>(
listOf("rest", "getPlaylists"),
params
)!!.playlists.playlist
}
fun removePlaylist(id: String) {
val params = getBasicParams().asMap()
params["id"] = id
makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "deletePlaylist"),
params,
true
)
}
fun removeFromPlaylist(id: String, track: Int) {
val params = getBasicParams().asMap()
params["playlistId"] = id
params["songIndexToRemove"] = track.toString()
makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "updatePlaylist"),
params,
true
)
}
fun addToPlaylist(id: String, songId: String) {
val params = getBasicParams().asMap()
params["playlistId"] = id
params["songIdToAdd"] = songId
makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "updatePlaylist"),
params,
true
)
}
fun updatePlaylist(playlist: Playlist): Playlist {
val params = getBasicParams().asMap()
params["name"] = playlist.name
params["comment"] = playlist.comment
params["public"] = if (playlist.public) "true" else "false"
params["playlistId"] = playlist.id
makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "updatePlaylist"),
params,
true
)
val songsParams = getBasicParams().asMap()
songsParams["playlistId"] = playlist.id
songsParams["songId"] = playlist.entry.joinToString(",") { it.id }
return makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "createPlaylist"),
songsParams
)!!.playlist
}
fun createPlaylist(ids: List<String>, name: String): Playlist {
val songsParams = getBasicParams().asMap()
songsParams["name"] = name
songsParams["songId"] = ids.joinToString(",")
return makeSubsonicRequest<PlaylistResponse>(
listOf("rest", "createPlaylist"),
songsParams
)!!.playlist
}
fun login(username: String, password: String, url: String, usePlaintext: Boolean): Account {
val salt = "abcd1234"
val saltedPassword = "${password}${salt}"
@@ -686,12 +776,12 @@ class SubsonicClient(var initialAccount: Account) {
BigInteger(1, md.digest(saltedPassword.toByteArray())).toString(16).padStart(32, '0')
val basicParams = BasicParams(
username,
if(usePlaintext) null else hash,
if(usePlaintext) null else salt,
if (usePlaintext) null else hash,
if (usePlaintext) null else salt,
"1.16.1",
"soniclair",
"json",
if(usePlaintext) password else null
if (usePlaintext) password else null
)
val uriBuilder = Uri.parse(url).buildUpon()
.appendPath("rest")
@@ -736,68 +826,78 @@ class SubsonicClient(var initialAccount: Account) {
downloadQueueForce[it.id] = force
}
if (!downloading) {
download()
download(true)
}
}
private fun download() {
private fun download(spawn: Boolean) {
if (downloadQueue.size > 0) {
downloading = true
CoroutineScope(IO).launch {
if (KeyValueStorage.getSettings().cacheSize > 0) {
val dir = File(
Helpers.constructPath(
listOf(
App.context.filesDir.path,
getSongsDirectory()
val index = if (spawn) 2 else 0;
for (i in 0..index) {
CoroutineScope(IO).launch {
Thread.sleep(i.toLong() * 500)
if(downloadQueue.size == 0){
return@launch
}
val song = downloadQueue[0]
val force = downloadQueueForce[song.id] ?: false
downloadQueueForce.remove(downloadQueue[0].id)
downloadQueue.removeAt(0)
if (KeyValueStorage.getSettings().cacheSize > 0) {
val dir = File(
Helpers.constructPath(
listOf(
App.context.filesDir.path,
getSongsDirectory()
)
)
)
)
if (dir.exists()) {
val files = dir.listFiles()?.toList()
?.sortedBy { file -> file.lastModified() }?.toMutableList()
?: mutableListOf<File>()
var size = files.sumOf { it.length() }
while (size > KeyValueStorage.getSettings().cacheSize * (1024 * 1024 * 1024)) {
files[0].delete()
files.remove(files[0])
unregisterSong(files[0].name)
size = files.sumOf { it.length() }
if (dir.exists()) {
val files = dir.listFiles()?.toList()
?.sortedBy { file -> file.lastModified() }?.toMutableList()
?: mutableListOf<File>()
var size = files.sumOf { it.length() }
while (size > KeyValueStorage.getSettings().cacheSize * (1024 * 1024 * 1024)) {
files[0].delete()
files.remove(files[0])
unregisterSong(files[0].name)
size = files.sumOf { it.length() }
}
}
}
if (!File(getLocalSongUri(song.id)).exists() || force) {
registerSong(song)
downloadSong(song.id)
}
download(false)
}
if (!File(getLocalSongUri(downloadQueue[0].id)).exists() || downloadQueueForce[downloadQueue[0].id] == true) {
registerSong(downloadQueue[0])
downloadSong(downloadQueue[0].id)
}
downloadQueueForce.remove(downloadQueue[0].id)
downloadQueue.removeAt(0)
download()
}
} else {
downloading = false
}
}
private fun saveImage(image: Bitmap, directory: String, path: String) {
val storageDir = File(directory)
var success = true
if (!storageDir.exists()) {
success = storageDir.mkdirs()
}
if (success) {
val imageFile = File(path)
try {
val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 100, fOut)
fOut.flush()
fOut.close()
Log.i("Image save", "image successfully saved")
} catch (e: Exception) {
Log.e("Image saver", e.message!!)
Globals.NotifyObservers("EX", e.message)
}
}
private fun saveImage(image: Bitmap, directory: String, path: String) {
val storageDir = File(directory)
var success = true
if (!storageDir.exists()) {
success = storageDir.mkdirs()
}
if (success) {
val imageFile = File(path)
try {
val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 100, fOut)
fOut.flush()
fOut.close()
Log.i("Image save", "image successfully saved")
} catch (e: Exception) {
Log.e("Image saver", e.message!!)
Globals.NotifyObservers("EX", e.message)
}
}
}

View File

@@ -43,7 +43,6 @@ class UDPServer(
// Send it and forget it
CoroutineScope(Dispatchers.IO).launch {
try {
val buffer = ByteArray(1500)
val packet = DatagramPacket("soniclairClient".toByteArray(Charsets.UTF_8), "soniclairClient".toByteArray(Charsets.UTF_8).size, broadcastAddress, 30002)
@Suppress("BlockingMethodInNonBlockingContext")
broadcastSocket!!.send(packet)

View File

@@ -1,3 +1,22 @@
package tech.logica10.soniclair.models
class AlbumResponse(val album: AlbumWithSongs) : SubsonicResponse()
class AlbumResponse(val album: AlbumWithSongs) : SubsonicResponse()
class PlaylistsResponse(val playlists: PlaylistsInnerResponse) : SubsonicResponse()
class PlaylistsInnerResponse(val playlist: List<Playlist>)
class PlaylistResponse(val playlist: Playlist) : SubsonicResponse()
class Playlist(
val id: String,
val name: String,
val comment: String,
val owner: String,
val public: Boolean,
val songCount: Int,
val duration: Int,
val created: String,
val coverArt: String,
val entry: List<Song>
)

View File

@@ -1,3 +1,3 @@
package tech.logica10.soniclair.models
class SetPlaylistAndPlayRequest(val playlist: List<Song>, val track: Int, val seek: Float, val playing: Boolean)
class SetPlaylistAndPlayRequest(val playlist: Playlist, val track: Int, val seek: Float, val playing: Boolean)

View File

@@ -15,4 +15,5 @@ class Song(
var album: String,
var albumId: String,
var coverArt: String
)
)

View File

@@ -36,6 +36,7 @@ import org.videolan.libvlc.Media
import org.videolan.libvlc.MediaPlayer
import tech.logica10.soniclair.*
import tech.logica10.soniclair.KeyValueStorage.Companion.getActiveAccount
import tech.logica10.soniclair.models.Playlist
import tech.logica10.soniclair.models.SearchType
import tech.logica10.soniclair.models.Song
import java.io.File
@@ -73,7 +74,7 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
.setName("SonicLair")
.setDescription("Currently playing notification")
.build()
private var playlist: MutableList<Song> = mutableListOf()
private var playlist: Playlist = getDefaultPlaylist()
private var prevAction: NotificationCompat.Action? = null
private var pauseAction: NotificationCompat.Action? = null
private var playAction: NotificationCompat.Action? = null
@@ -101,6 +102,13 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
mAudioManager.registerAudioDeviceCallback(DeviceCallback(), null)
}
companion object {
fun getDefaultPlaylist(): Playlist {
return Playlist("", "", "", "", false, 0, 0, "", "", listOf())
}
}
private fun getNotificationBuilder(): NotificationCompat.Builder {
return NotificationCompat.Builder(App.context, "soniclair")
.setSmallIcon(R.drawable.ic_stat_soniclair)
@@ -355,6 +363,13 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
playRadio(id)
}
}
Constants.SERVICE_PLAY_PLAYLIST -> {
val id = intent.extras?.getString("id")
val track = intent.extras?.getInt("track")
if (id != null) {
playPlaylist(id, track ?: 0)
}
}
Constants.SERVICE_PLAY_SEARCH -> {
val id = intent.extras?.getString("query")
if (id != null) {
@@ -471,19 +486,36 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
}
private fun getSongsDuration(songs: List<Song>): Int {
return songs.sumOf { s -> s.duration }
}
@Throws(Exception::class)
private fun playRadio(id: String) {
playlist.clear()
playlist = getDefaultPlaylist()
try {
playlist.addAll(subsonicClient.getSimilarSongs(id))
playlist.add(0, subsonicClient.getSong(id))
currentTrack = playlist[0]
val songs: MutableList<Song> = mutableListOf()
songs.addAll(subsonicClient.getSimilarSongs(id))
songs.add(0, subsonicClient.getSong(id))
playlist = Playlist(
"current",
"Internet radio based on ${songs[0].title}",
"by ${songs[0].artist}",
getActiveAccount().username!!,
false,
songs.size,
getSongsDuration(songs),
"",
songs[0].albumId,
songs
)
currentTrack = songs[0]
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
&& !KeyValueStorage.getOfflineMode()
&& !App.isTv
) {
subsonicClient.downloadPlaylist(playlist, false)
subsonicClient.downloadPlaylist(songs, false)
}
loadMedia()
play()
@@ -493,24 +525,76 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
}
}
private fun playAlbum(id: String, track: Int) {
playlist.clear()
fun skipTo(track: Int) {
try {
playlist.addAll(
if (KeyValueStorage.getOfflineMode()) {
subsonicClient.getLocalAlbumWithSongs(id)!!.song
} else {
subsonicClient.getAlbum(id).song
}
)
currentTrack = playlist[track]
if (track >= playlist.entry.size) {
throw Exception("Track does not exist on playlist")
}
currentTrack = playlist.entry[track]
loadMedia()
play()
} catch (e: Exception) {
Globals.NotifyObservers("EX", e.message)
// Nobody listening still
}
}
private fun playPlaylist(id: String, track: Int) {
playlist = getDefaultPlaylist()
try {
playlist = subsonicClient.getPlaylist(id)
currentTrack = playlist.entry[track]
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
&& !KeyValueStorage.getOfflineMode()
&& !App.isTv
) {
subsonicClient.downloadPlaylist(playlist, false)
subsonicClient.downloadPlaylist(playlist.entry, false)
}
loadMedia()
play()
} catch (e: Exception) {
Globals.NotifyObservers("EX", e.message)
// Nobody listening still
}
}
private fun playAlbum(id: String, track: Int) {
playlist = getDefaultPlaylist()
try {
val songs: MutableList<Song> = mutableListOf()
songs.addAll(
if (KeyValueStorage.getOfflineMode()) {
subsonicClient.getLocalAlbumWithSongs(id)!!.song
} else {
subsonicClient.getAlbum(id).song
}
)
playlist = Playlist(
"current",
songs[0].album,
"by ${songs[0].artist}",
getActiveAccount().username!!,
false,
songs.size,
getSongsDuration(songs),
"",
songs[0].albumId,
songs
)
currentTrack = songs[track]
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
&& !KeyValueStorage.getOfflineMode()
&& !App.isTv
) {
subsonicClient.downloadPlaylist(songs, false)
}
loadMedia()
play()
@@ -568,8 +652,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
@Throws(JSONException::class, ExecutionException::class, InterruptedException::class)
private fun next() {
if (playlist.indexOf(currentTrack) < playlist.size - 1) {
currentTrack = playlist[playlist.indexOf(currentTrack) + 1]
if (playlist.entry.indexOf(currentTrack) < playlist.entry.size - 1) {
currentTrack = playlist.entry[playlist.entry.indexOf(currentTrack) + 1]
try {
loadMedia()
play()
@@ -581,8 +665,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
@Throws(JSONException::class, ExecutionException::class, InterruptedException::class)
private fun prev() {
if (playlist.indexOf(currentTrack) > 0) {
currentTrack = playlist[playlist.indexOf(currentTrack) - 1]
if (playlist.entry.indexOf(currentTrack) > 0) {
currentTrack = playlist.entry[playlist.entry.indexOf(currentTrack) - 1]
try {
loadMedia()
play()
@@ -636,7 +720,7 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
private fun setPlaylistAndPlay(
playlist: List<Song>,
playlist: Playlist,
track: Int,
seek: Float,
playing: Boolean
@@ -646,8 +730,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
return
// Otherwise copy the phone's state and play it
this.playlist = playlist.toMutableList()
this.currentTrack = playlist[track]
this.playlist = playlist
this.currentTrack = playlist.entry[track]
loadMedia()
mMediaPlayer!!.position = seek
if (playing) {
@@ -773,11 +857,19 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
}
}
fun setPlaylistAndPlay(playlist: List<Song>, track: Int, seek: Float, playing: Boolean) {
fun playPlaylist(id: String, track: Int) {
this@MusicService.playPlaylist(id, track)
}
fun setPlaylistAndPlay(playlist: Playlist, track: Int, seek: Float, playing: Boolean){
this@MusicService.setPlaylistAndPlay(playlist, track, seek, playing)
}
fun getPlaylist(): List<Song> {
fun skipTo(track: Int) {
this@MusicService.skipTo(track)
}
fun getPlaylist(): Playlist {
return this@MusicService.playlist
}

View File

@@ -62,7 +62,10 @@
"react-app",
"react-app/jest",
"prettier"
]
],
"rules": {
"no-console": "warn"
}
},
"browserslist": {
"production": [

View File

@@ -54,7 +54,7 @@ export default function AudioControl() {
setAndroidTV((await AndroidTVPlugin.get()).value);
}
} catch (e: any) {
console.error("ERROR ANDROID TV", e);
}
};
fetch();

View File

@@ -21,8 +21,6 @@ export default function EditPlaylist() {
const { register, setValue, handleSubmit } = useForm<IPlaylistFormData>();
const [formVisible, setFormVisible] = useState<boolean>(false);
const hash = async (data: IPlaylistFormData) => {
console.log(playlist);
console.log({ ...playlist, ...data });
const ret = await VLC.updatePlaylist({
playlist: { ...playlist!, ...data },
});
@@ -65,7 +63,7 @@ export default function EditPlaylist() {
</div>
<button
className="btn btn-primary"
onClick={handleSubmit(hash,() => {console.log("onerror")})}
onClick={handleSubmit(hash)}
>
Save
</button>

View File

@@ -118,7 +118,7 @@ export default function Playlist() {
</button>
)}
<div className="list-group overflow-scroll scrollable w-100">
<div className="list-group overflow-scroll scrollable scrollable-hidden w-100">
{playlist?.entry.map((s) => (
<PlaylistEntry
key={s.id}

View File

@@ -1,4 +1,7 @@
import { PluginListenerHandle } from "@capacitor/core";
import { Toast } from "@capacitor/toast";
import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import classNames from "classnames";
import {
CSSProperties,
@@ -35,6 +38,9 @@ export function PlaylistEntry({
[]
);
const { setMenuContext } = useContext(MenuContext);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const vlcListener = useRef<PluginListenerHandle>();
const [cached, setCached] = useState<boolean>(false);
const [coverArt, setCoverArt] = useState<string>("");
useEffect(() => {
@@ -46,6 +52,27 @@ export function PlaylistEntry({
};
func();
}, [coverArt, item.albumId]);
useEffect(() => {
const f = async () => {
if (vlcListener.current) {
await vlcListener.current.remove();
}
vlcListener.current = await VLC.addListener(
`progress${item.id}`,
(info: any) => {
setDownloadProgress(info.progress);
if (info.progress >= 99) {
setCached(true);
}
}
);
const status = await VLC.getSongStatus({ id: item.id });
if (status.status === "ok") {
setCached(status.value!!);
}
};
f();
}, [item]);
const selfRef = useRef<HTMLDivElement>();
@@ -143,10 +170,30 @@ export function PlaylistEntry({
by {item.artist}, from {item.album}
</span>
</div>
{actionable && <div className="col-auto d-flex flex-row align-items-center justify-content-end">
{SecondsToHHSS(item.duration)}
</div>}
{actionable && (
<div className="col-auto d-flex flex-row align-items-center justify-content-end">
{SecondsToHHSS(item.duration)}
{cached && <FontAwesomeIcon icon={faArrowDown} className="p-2"></FontAwesomeIcon>}
</div>
)}
{downloadProgress > 0 && downloadProgress < 100 && (
<div className="col-12 py-2">
<div
className="progress"
style={{ height: "2px", margin: 0, padding: 0 }}
>
<div
className="progress-bar progress-bar-soniclair"
role="progressbar"
style={{
width: `${downloadProgress}%`,
margin: 0,
padding: 0,
}}
></div>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -17,10 +17,11 @@ export default function Playlists() {
if (current.status === "ok" && ret.status === "ok") {
setPlaylists([current.value!!, ...ret.value!!]);
}
},[]);
}, []);
useEffect(() => {
fetch();
if (playlists.length === 0) {
fetch();
}
}, [fetch, playlists]);
const areYouSure = useCallback(
@@ -47,7 +48,12 @@ export default function Playlists() {
<span className="w-100 text-center text-white">
{`Are you sure you want to delete the playlist "${item.name}"?`}
</span>
<button className="btn btn-danger" onClick={yesImSure}>Delete</button>
<button
className="btn btn-danger"
onClick={yesImSure}
>
Delete
</button>
</div>
),
});
@@ -56,7 +62,7 @@ export default function Playlists() {
[fetch, setMenuContext]
);
return (
<div className="d-flex flex-column w-100 h-100 align-items-start">
<div className="d-flex flex-column w-100 h-100 align-items-start playlist-container">
{playlists.length > 0 && playlists[0].name !== "" && (
<>
<div className="section-header text-white">

View File

@@ -11,7 +11,6 @@ export default function Search() {
const [searchValue, setSearchValue] = useState<string>("");
const [result, setResult] = useState<ISearchResult | null>(null);
const setValue = (ev: any) => {
console.log("changing");
setSearchValue(ev.target.value);
};
const [androidTv, setAndroidTv] = useState(false);
@@ -26,8 +25,6 @@ export default function Search() {
timeoutRef.current = setTimeout(async () => {
const result = await VLC.search({ query: searchValue });
if (result.status === "ok") {
console.log(searchValue);
console.log(result.value!);
setResult(result.value!);
}
}, 350);

View File

@@ -53,7 +53,6 @@ export default function SongItem({
vlcListener.current = await VLC.addListener(
`progress${item.id}`,
(info: any) => {
console.log(info);
setDownloadProgress(info.progress);
if (info.progress >= 99) {
setCached(true);

View File

@@ -274,10 +274,9 @@ export class Backend extends WebPlugin implements IBackendPlugin {
songId: [options.songId],
name: "New playlist",
});
if(r.status === "ok"){
if (r.status === "ok") {
return this.OKResponse("");
}
else{
} else {
return this.ErrorResponse(r.error);
}
}
@@ -333,7 +332,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
);
};
socket.onmessage = (message) => {
console.log(message);
if (message.data === "soniclair") {
socket.close();
}
@@ -376,7 +374,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
}
}
async setSettings(options: ISettings): Promise<IBackendResponse<String>> {
console.log("setSettings", options);
localStorage.setItem("settings", JSON.stringify(options));
return this.OKResponse("");
}
@@ -1205,7 +1202,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
}
seek(options: { time: number }): Promise<IBackendResponse<string>> {
console.log(options.time);
this.audio.currentTime = options.time * this.currentTrack.duration;
return Promise.resolve(this.OKResponse(""));
}