Skip to content
This repository has been archived by the owner on Dec 12, 2020. It is now read-only.

Releases Browser #78

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ repositories {
dependencies {
implementation(kotlin("reflect"))

implementation("com.github.Xerus2000.util", "javafx", "259dad93192584306dbf951721d3cf8e6bd25d1b")
implementation("com.github.defvs.util", "javafx", "36699cd09dfa0c8fd3991ea12533f06af5d51ec0")
implementation("org.controlsfx", "controlsfx", "8.40.+")

implementation("ch.qos.logback", "logback-classic", "1.2.+")
Expand Down
7 changes: 2 additions & 5 deletions src/main/xerus/monstercat/MonsterUtilities.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ import xerus.monstercat.api.Covers
import xerus.monstercat.api.DiscordRPC
import xerus.monstercat.api.Player
import xerus.monstercat.downloader.TabDownloader
import xerus.monstercat.tabs.BaseTab
import xerus.monstercat.tabs.TabCatalog
import xerus.monstercat.tabs.TabGenres
import xerus.monstercat.tabs.TabSettings
import xerus.monstercat.tabs.TabSound
import xerus.monstercat.tabs.*
import java.io.File
import java.net.URL
import java.net.UnknownHostException
Expand Down Expand Up @@ -105,6 +101,7 @@ class MonsterUtilities(checkForUpdate: Boolean): JFXMessageDisplay {
addTab(TabCatalog::class)
addTab(TabGenres::class)
addTab(TabDownloader::class)
addTab(TabReleases::class)
addTab(TabSound::class)
addTab(TabSettings::class)
if(currentVersion != Settings.LASTVERSION.get()) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/xerus/monstercat/api/Cache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import xerus.monstercat.downloader.CONNECTSID
import xerus.monstercat.globalDispatcher
import java.io.File

private const val cacheVersion = 5
private const val cacheVersion = 6

object Cache: Refresher() {
private val logger = KotlinLogging.logger { }
Expand Down
12 changes: 11 additions & 1 deletion src/main/xerus/monstercat/api/Covers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object Covers {
fun getCoverImage(coverUrl: String, size: Int = 1024, invalidate: Boolean = false): Image =
getCover(coverUrl, 1024, invalidate).use { createImage(it, size) }

private fun createImage(content: InputStream, size: Number) =
fun createImage(content: InputStream, size: Number) =
Image(content, size.toDouble(), size.toDouble(), false, false)

/**
Expand All @@ -55,6 +55,16 @@ object Covers {
return coverFile.inputStream()
}

fun getCachedCover(coverUrl: String, cachedSize: Int, imageSize: Int = cachedSize): Image? {
val coverFile = coverCacheFile(coverUrl, cachedSize)
return try {
val imageStream = coverFile.inputStream()
createImage(imageStream, imageSize)
} catch(e: Exception) {
null
}
}

/** Fetches the given [coverUrl] with an [APIConnection] in the requested [size].
* @param coverUrl the base url to fetch the cover
* @param size the size of the cover to be fetched from the api, with all powers of 2 being available.
Expand Down
1 change: 1 addition & 0 deletions src/main/xerus/monstercat/api/response/Release.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ data class Release(
@Key var renderedArtists: String = "",
@Key override var title: String = "",
@Key var coverUrl: String = "",
@Key("inEarlyAccess") var earlyAccess: Boolean = false,
@Key var downloadable: Boolean = false): MusicItem() {

@Key var isCollection: Boolean = false
Expand Down
9 changes: 9 additions & 0 deletions src/main/xerus/monstercat/tabs/BaseTab.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xerus.monstercat.tabs

import javafx.scene.control.Control
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.layout.VBox
import mu.KotlinLogging
import org.controlsfx.validation.decoration.GraphicValidationDecoration
Expand All @@ -26,3 +27,11 @@ abstract class VTab : VBox(), BaseTab {
styleClass.add("vtab")
}
}

abstract class StackTab : StackPane(), BaseTab {
protected val logger = KotlinLogging.logger(javaClass.name)

init {
styleClass.add("vtab")
}
}
251 changes: 251 additions & 0 deletions src/main/xerus/monstercat/tabs/TabReleases.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package xerus.monstercat.tabs

import javafx.animation.FadeTransition
import javafx.beans.property.SimpleBooleanProperty
import javafx.collections.FXCollections
import javafx.geometry.Insets
import javafx.geometry.Orientation
import javafx.geometry.Pos
import javafx.scene.Group
import javafx.scene.control.*
import javafx.scene.effect.GaussianBlur
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.text.Font
import javafx.util.Duration
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.controlsfx.control.GridCell
import org.controlsfx.control.GridView
import xerus.ktutil.javafx.*
import xerus.ktutil.javafx.properties.SimpleObservable
import xerus.ktutil.javafx.properties.addListener
import xerus.ktutil.javafx.properties.listen
import xerus.ktutil.nullIfEmpty
import xerus.monstercat.api.Cache
import xerus.monstercat.api.Covers
import xerus.monstercat.api.Player
import xerus.monstercat.api.response.Release
import xerus.monstercat.api.response.Track
import xerus.monstercat.monsterUtilities


class TabReleases: StackTab() {
private var cols = SimpleObservable(2)
val cellSize: Double
get() = monsterUtilities.window.width / cols.value - 16.0 * (cols.value + 1)

private val releases = FXCollections.observableArrayList<Release>()

private val gridView = GridView<Release>().apply {
setCellFactory {
ReleaseGridCell(this@TabReleases).apply { setOnMouseClicked { showRelease(this.item) } }
}
horizontalCellSpacing = 16.0
verticalCellSpacing = 16.0

fun setCellSize() {
cellWidth = cellSize
cellHeight = cellSize
}
setCellSize()
cols.listen { setCellSize() }
monsterUtilities.window.widthProperty().listen { setCellSize() }
}

private val listView = ListView<Release>().apply {
setCellFactory {
ReleaseListCell().apply { setOnMouseClicked { showRelease(this.item) } }
}
}

private val blurLowRes = SimpleBooleanProperty(false)

init {
gridView.items = releases
listView.items = releases
GlobalScope.launch {
val releases = Cache.getReleases()
onFx { [email protected](releases) }
}
val gridEditor = HBox(0.0,
createButton("-") { cols.value = (cols.value - 1).coerceAtLeast(2) },
createButton("+") { cols.value = (cols.value + 1).coerceAtMost(4) },
CheckBox("Blur low-res").bind(blurLowRes).tooltip("May affect performance while loading covers, but looks less pixelated")
)
val colEditor = Group(
VBox(
CheckBox("Grid View").apply { isSelected = false }.onClick {
gridEditor.isDisable = !isSelected
val tab = this@TabReleases
tab.children.removeAll(listView, gridView)
if(isSelected) {
tab.children.add(0, gridView)
}else{
tab.children.add(0, listView)
}
},
gridEditor
).apply {
background = Background(BackgroundFill(Color(0.0, 0.0, 0.0, 0.7), CornerRadii(8.0), Insets.EMPTY))
padding = Insets(8.0)
}
)

val placeholder = Group(HBox(ImageView(Image("img/loading-16.gif")), Label("Loading Releases...")))
add(placeholder)
setAlignment(placeholder, Pos.CENTER)

releases.listen {
onFx {
if(!it.isNullOrEmpty()) {
add(listView)
add(colEditor)
setAlignment(colEditor, Pos.BOTTOM_LEFT)
children.remove(placeholder)
} else {
children.removeAll(listView, gridView, colEditor)
add(placeholder)
setAlignment(placeholder, Pos.CENTER)
}
}
}

}

private fun showRelease(release: Release) {
val parent = VBox()

val tracks = FXCollections.observableArrayList(release.tracks)
val tracksView = ListView<Track>(tracks).apply { setCellFactory { TrackListCell() } }

val infoHeader = HBox(16.0,
ImageView(Covers.getThumbnailImage(release.coverUrl, 256)).apply {
effect = GaussianBlur(10.0)
val cachedCover = Covers.getCachedCover(release.coverUrl, 256, 256)
if(cachedCover != null) {
image = cachedCover
effect = null
} else {
image = Covers.getThumbnailImage(release.coverUrl, 256)
GlobalScope.launch {
val image = Covers.getCover(release.coverUrl, 256).use { Covers.createImage(it, 256) }
onFx { [email protected] = image; effect = null }
}
}
setOnMouseClicked { monsterUtilities.viewCover(release.coverUrl) }
},
Separator(Orientation.VERTICAL)
)
infoHeader.fill(VBox(
Label(release.title).apply { style += "-fx-font-size: 32px; -fx-font-weight: bold;" },
Label(release.renderedArtists.nullIfEmpty()?.let { "by $it" }
?: "Various Artists").apply { style += "-fx-font-size: 24px;" },
HBox(Label(release.releaseDate), Label("${release.tracks.size} tracks")).apply { style += "-fx-font-size: 16px;" },
HBox(
buttonWithId("play") { Player.play(release) },
buttonWithId("satin-add") { /* TODO : Playlist add when merged */ }, // TODO : Add icon
buttonWithId("satin-open") { /* TODO : Tick in Downloader tab */ }.tooltip("Show in downloader") // TODO : Save icon
).id("controls").apply { fill(pos = 0) }
).apply { fill(pos = 3) })

parent.style += "-fx-background-color: -fx-background;"
parent.add(infoHeader)
parent.add(Separator(Orientation.HORIZONTAL))
parent.fill(tracksView)
parent.addButton("Back") {
FadeTransition(Duration(300.0), parent).apply {
fromValue = 1.0
toValue = 0.0
setOnFinished { children.remove(parent) }
}.play()
}.apply { isCancelButton = true }

FadeTransition(Duration(300.0), parent).apply {
fromValue = 0.0
toValue = 1.0
}.play()
add(parent).toFront()
}

class ReleaseGridCell(private val context: TabReleases): GridCell<Release>() {
override fun updateItem(item: Release?, empty: Boolean) {
super.updateItem(item, empty)
if(empty || item == null) {
graphic = null
} else {
val lowRes = SimpleBooleanProperty(true)
val cover = ImageView()

val cachedCover = Covers.getCachedCover(item.coverUrl, 256, 256)
if(cachedCover != null) {
cover.image = cachedCover
lowRes.value = false
} else {
cover.image = Covers.getThumbnailImage(item.coverUrl, 256)
lowRes.value = true
GlobalScope.launch {
val image = Covers.getCover(item.coverUrl, 256).use { Covers.createImage(it, 256) }
lowRes.value = false
onFx { cover.image = image }
}
}

cover.fitHeight = context.cellSize
cover.fitWidth = context.cellSize
context.cols.listen {
cover.fitHeight = context.cellSize
cover.fitWidth = context.cellSize
}

cover.effect = if(context.blurLowRes.value && lowRes.value) GaussianBlur(5.0) else null
arrayOf(context.blurLowRes, lowRes).addListener {
cover.effect = if(context.blurLowRes.value && lowRes.value) GaussianBlur(5.0) else null
}

graphic = StackPane(cover,
Label(item.toString()).apply {
background = Background(BackgroundFill(Color(0.0, 0.0, 0.0, 0.7), CornerRadii(8.0), Insets.EMPTY))
padding = Insets(8.0)
translateY = -16.0
}
).apply {
alignment = Pos.BOTTOM_CENTER; tooltip = Tooltip(item.toString())
if(item.earlyAccess) style += "-fx-border-color: gold; -fx-border-width: 4px; -fx-border-radius: 8px"
}
}
}
}

class ReleaseListCell: ListCell<Release>() {
override fun updateItem(item: Release?, empty: Boolean) {
super.updateItem(item, empty)
if(empty || item == null) {
graphic = null
} else {
val cover = ImageView(Covers.getThumbnailImage(item.coverUrl, 256)).apply { fitHeight = 64.0; fitWidth = 64.0; }
graphic = HBox(cover, Label(item.toString()).apply { font = Font(14.0); padding = Insets(16.0) })
}
}
}

class TrackListCell: ListCell<Track>() {
override fun updateItem(item: Track?, empty: Boolean) {
super.updateItem(item, empty)
if(empty || item == null) graphic = null
else {
val parent = HBox()
parent.add(HBox(
buttonWithId("play") { Player.playTrack(item) },
buttonWithId("satin-add") { /* TODO : Add to playlist once the branch is merged */ }, // TODO : Add icon
buttonWithId("satin-open") { /* TODO : Tick in Downloader tab */ }.tooltip("Show in downloader") // TODO: Save icon
).id("controls").apply { alignment = Pos.CENTER_LEFT })
parent.fill(HBox(Label(item.toString()) /* TODO : Unlicensable alert */), 0)

graphic = parent
}
}
}
}