mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
ID3 Tags and File Save for web-app.
This commit is contained in:
parent
601d8135db
commit
a47d9f52a0
@ -7,9 +7,10 @@ kotlin {
|
|||||||
jvm("desktop")
|
jvm("desktop")
|
||||||
android()
|
android()
|
||||||
//ios()
|
//ios()
|
||||||
js {
|
js() {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
//nodejs()
|
||||||
|
binaries.executable()
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
named("commonTest") {
|
named("commonTest") {
|
||||||
|
@ -14,9 +14,10 @@ plugins {
|
|||||||
kotlin {
|
kotlin {
|
||||||
jvm("desktop")
|
jvm("desktop")
|
||||||
android()
|
android()
|
||||||
js {
|
js() {
|
||||||
browser()
|
browser()
|
||||||
nodejs()
|
//nodejs()
|
||||||
|
binaries.executable()
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
named("commonMain") {
|
named("commonMain") {
|
||||||
|
@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.shabinder.common.di.Picture
|
||||||
import com.shabinder.common.list.SpotiFlyerList
|
import com.shabinder.common.list.SpotiFlyerList
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
@ -67,7 +68,7 @@ fun SpotiFlyerListContent(
|
|||||||
fun TrackCard(
|
fun TrackCard(
|
||||||
track: TrackDetails,
|
track: TrackDetails,
|
||||||
downloadTrack:()->Unit,
|
downloadTrack:()->Unit,
|
||||||
loadImage:suspend (String)-> ImageBitmap?
|
loadImage:suspend (String)-> Picture
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
@ -120,7 +121,7 @@ fun CoverImage(
|
|||||||
title: String,
|
title: String,
|
||||||
coverURL: String,
|
coverURL: String,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
loadImage: suspend (String) -> ImageBitmap?,
|
loadImage: suspend (String) -> Picture,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -28,10 +28,10 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.shabinder.common.di.Picture
|
||||||
import com.shabinder.common.di.giveDonation
|
import com.shabinder.common.di.giveDonation
|
||||||
import com.shabinder.common.di.openPlatform
|
import com.shabinder.common.di.openPlatform
|
||||||
import com.shabinder.common.di.shareApp
|
import com.shabinder.common.di.shareApp
|
||||||
import com.shabinder.common.di.showPopUpMessage
|
|
||||||
import com.shabinder.common.main.SpotiFlyerMain
|
import com.shabinder.common.main.SpotiFlyerMain
|
||||||
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
||||||
import com.shabinder.common.models.DownloadRecord
|
import com.shabinder.common.models.DownloadRecord
|
||||||
@ -303,7 +303,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun HistoryColumn(
|
fun HistoryColumn(
|
||||||
list: List<DownloadRecord>,
|
list: List<DownloadRecord>,
|
||||||
loadImage:suspend (String)-> ImageBitmap?,
|
loadImage:suspend (String)-> Picture,
|
||||||
onItemClicked: (String) -> Unit
|
onItemClicked: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Crossfade(list){
|
Crossfade(list){
|
||||||
@ -335,7 +335,7 @@ fun HistoryColumn(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DownloadRecordItem(
|
fun DownloadRecordItem(
|
||||||
item: DownloadRecord,
|
item: DownloadRecord,
|
||||||
loadImage:suspend (String)-> ImageBitmap?,
|
loadImage:suspend (String)-> Picture,
|
||||||
onItemClicked:(String)->Unit
|
onItemClicked:(String)->Unit
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
||||||
|
@ -50,8 +50,11 @@ kotlin {
|
|||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(Ktor.clientJs)
|
|
||||||
implementation(project(":common:data-models"))
|
implementation(project(":common:data-models"))
|
||||||
|
implementation(Ktor.clientJs)
|
||||||
|
implementation(npm("browser-id3-writer","4.4.0"))
|
||||||
|
implementation(npm("file-saver","2.0.4"))
|
||||||
|
//implementation(npm("@types/file-saver","2.0.1",generateExternals = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,12 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.kiulian.downloader.model.YoutubeVideo
|
import com.github.kiulian.downloader.model.YoutubeVideo
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||||
import com.razorpay.Checkout
|
import com.razorpay.Checkout
|
||||||
import com.shabinder.common.database.activityContext
|
import com.shabinder.common.database.activityContext
|
||||||
import com.shabinder.common.database.appContext
|
|
||||||
import com.shabinder.common.di.worker.ForegroundService
|
import com.shabinder.common.di.worker.ForegroundService
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -87,8 +84,8 @@ actual fun queryActiveTracks() {
|
|||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
fetcher: FetchPlatformQueryResult,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
dir: Dir
|
||||||
){
|
){
|
||||||
if(!list.isNullOrEmpty()){
|
if(!list.isNullOrEmpty()){
|
||||||
val serviceIntent = Intent(activityContext, ForegroundService::class.java)
|
val serviceIntent = Intent(activityContext, ForegroundService::class.java)
|
||||||
|
@ -15,8 +15,8 @@ expect val isInternetAvailable:Boolean
|
|||||||
|
|
||||||
expect suspend fun downloadTracks(
|
expect suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
fetcher: FetchPlatformQueryResult,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
dir: Dir
|
||||||
)
|
)
|
||||||
|
|
||||||
expect fun queryActiveTracks()
|
expect fun queryActiveTracks()
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
class FetchPlatformQueryResult(
|
class FetchPlatformQueryResult(
|
||||||
private val gaanaProvider: GaanaProvider,
|
private val gaanaProvider: GaanaProvider,
|
||||||
private val spotifyProvider: SpotifyProvider,
|
val spotifyProvider: SpotifyProvider,
|
||||||
val youtubeProvider: YoutubeProvider,
|
val youtubeProvider: YoutubeProvider,
|
||||||
val youtubeMusic: YoutubeMusic,
|
val youtubeMusic: YoutubeMusic,
|
||||||
val youtubeMp3: YoutubeMp3,
|
val youtubeMp3: YoutubeMp3,
|
||||||
|
@ -19,6 +19,7 @@ package com.shabinder.common.di.providers
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.*
|
import com.shabinder.common.di.*
|
||||||
import com.shabinder.common.di.spotify.SpotifyRequests
|
import com.shabinder.common.di.spotify.SpotifyRequests
|
||||||
|
import com.shabinder.common.di.spotify.authenticateSpotify
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.spotify.Album
|
import com.shabinder.common.models.spotify.Album
|
||||||
@ -41,11 +42,13 @@ class SpotifyProvider(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
logger.d { "Creating Spotify Provider" }
|
logger.d { "Creating Spotify Provider" }
|
||||||
//GlobalScope.launch(Dispatchers.Default) {authenticateSpotify()}
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
|
authenticateSpotifyClient()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun authenticateSpotify(): HttpClient?{
|
override suspend fun authenticateSpotifyClient(override:Boolean): HttpClient?{
|
||||||
val token = tokenStore.getToken()
|
val token = if(override) authenticateSpotify() else tokenStore.getToken()
|
||||||
return if(token == null) {
|
return if(token == null) {
|
||||||
logger.d{ "Please Check your Network Connection" }
|
logger.d{ "Please Check your Network Connection" }
|
||||||
null
|
null
|
||||||
@ -69,7 +72,7 @@ class SpotifyProvider(
|
|||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||||
|
|
||||||
if(!this::httpClient.isInitialized){
|
if(!this::httpClient.isInitialized){
|
||||||
authenticateSpotify()
|
authenticateSpotifyClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
var spotifyLink =
|
var spotifyLink =
|
||||||
|
@ -249,8 +249,7 @@ class YoutubeMusic constructor(
|
|||||||
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
headers{
|
headers{
|
||||||
//append("Content-Type"," application/json")
|
append("referer","https://music.youtube.com/search")
|
||||||
append("Referer"," https://music.youtube.com/search")
|
|
||||||
}
|
}
|
||||||
body = buildJsonObject {
|
body = buildJsonObject {
|
||||||
putJsonObject("context"){
|
putJsonObject("context"){
|
||||||
|
@ -13,7 +13,7 @@ interface SpotifyRequests {
|
|||||||
|
|
||||||
val httpClient:HttpClient
|
val httpClient:HttpClient
|
||||||
|
|
||||||
suspend fun authenticateSpotify():HttpClient?
|
suspend fun authenticateSpotifyClient(override:Boolean = false):HttpClient?
|
||||||
|
|
||||||
suspend fun getPlaylist(playlistID: String): Playlist {
|
suspend fun getPlaylist(playlistID: String): Playlist {
|
||||||
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.github.kiulian.downloader.model.YoutubeVideo
|
import com.github.kiulian.downloader.model.YoutubeVideo
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
@ -62,20 +58,20 @@ val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = Mu
|
|||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
fetcher: FetchPlatformQueryResult,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
dir: Dir
|
||||||
){
|
){
|
||||||
list.forEach {
|
list.forEach {
|
||||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||||
downloadTrack(it.videoID!!, it,saveFileWithMetaData)
|
downloadTrack(it.videoID!!, it,dir::saveFileWithMetadata)
|
||||||
} else {
|
} else {
|
||||||
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
val videoId = getYTIDBestMatch(searchQuery,it)
|
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
||||||
if (videoId.isNullOrBlank()) {
|
if (videoId.isNullOrBlank()) {
|
||||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
||||||
) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) })
|
) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) })
|
||||||
} else {//Found Youtube Video ID
|
} else {//Found Youtube Video ID
|
||||||
downloadTrack(videoId, it,saveFileWithMetaData)
|
downloadTrack(videoId, it,dir::saveFileWithMetadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
@file:JsModule("file-saver")
|
||||||
|
@file:JsNonModule
|
||||||
|
|
||||||
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
|
||||||
|
external interface FileSaverOptions {
|
||||||
|
var autoBom: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun saveAs(data: Blob, filename: String = definedExternally, options: FileSaverOptions = definedExternally)
|
||||||
|
|
||||||
|
external fun saveAs(data: Blob)
|
||||||
|
|
||||||
|
external fun saveAs(data: Blob, filename: String = definedExternally)
|
||||||
|
|
||||||
|
external fun saveAs(data: String, filename: String = definedExternally, options: FileSaverOptions = definedExternally)
|
||||||
|
|
||||||
|
external fun saveAs(data: String)
|
||||||
|
|
||||||
|
external fun saveAs(data: String, filename: String = definedExternally)
|
||||||
|
|
||||||
|
external fun saveAs(data: Blob, filename: String = definedExternally, disableAutoBOM: Boolean = definedExternally)
|
||||||
|
|
||||||
|
external fun saveAs(data: String, filename: String = definedExternally, disableAutoBOM: Boolean = definedExternally)
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
|
import org.w3c.files.Blob
|
||||||
|
|
||||||
|
@JsModule("browser-id3-writer")
|
||||||
|
@JsNonModule
|
||||||
|
external class ID3Writer(a: ArrayBuffer) {
|
||||||
|
fun setFrame(frameName:String,frameValue:Any):ID3Writer
|
||||||
|
fun removeTag()
|
||||||
|
fun addTag():ArrayBuffer
|
||||||
|
fun getBlob():Blob
|
||||||
|
fun getURL():String
|
||||||
|
fun revokeURL()
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
|
|
||||||
actual fun openPlatform(packageID:String, platformLink:String){
|
actual fun openPlatform(packageID:String, platformLink:String){
|
||||||
//TODO
|
//TODO
|
||||||
@ -50,13 +53,40 @@ val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = M
|
|||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
getYTIDBestMatch:suspend (String, TrackDetails)->String?,
|
fetcher: FetchPlatformQueryResult,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
dir: Dir
|
||||||
){/*
|
){
|
||||||
list.forEach {
|
withContext(Dispatchers.Default){
|
||||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
list.forEach {
|
||||||
} else {
|
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||||
|
downloadTrack(it.videoID!!, it, fetcher, dir)
|
||||||
|
} else {
|
||||||
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
|
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
||||||
|
if (videoID.isNullOrBlank()) {
|
||||||
|
} else {//Found Youtube Video ID
|
||||||
|
downloadTrack(videoID, it, fetcher, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) {
|
||||||
|
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
||||||
|
if(url == null){
|
||||||
|
// TODO Handle
|
||||||
|
println("No URL to Download")
|
||||||
|
}else {
|
||||||
|
downloadFile(url).collect {
|
||||||
|
when(it){
|
||||||
|
is DownloadResult.Success -> {
|
||||||
|
println("Download Completed")
|
||||||
|
dir.saveFileWithMetadata(it.byteArray, track)
|
||||||
|
}
|
||||||
|
is DownloadResult.Error -> println("Download Error: ${track.title}")
|
||||||
|
is DownloadResult.Progress -> println("Download Progress: ${it.progress} : ${track.title}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
|
import kotlinext.js.Object
|
||||||
|
import kotlinext.js.asJsObject
|
||||||
|
import kotlinext.js.js
|
||||||
|
import kotlinext.js.jsObject
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import org.khronos.webgl.ArrayBuffer
|
||||||
import org.w3c.dom.ImageBitmap
|
import org.w3c.dom.ImageBitmap
|
||||||
|
import org.khronos.webgl.Int8Array
|
||||||
|
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
@ -13,9 +24,10 @@ actual class Dir actual constructor(
|
|||||||
/*init {
|
/*init {
|
||||||
createDirectories()
|
createDirectories()
|
||||||
}*/
|
}*/
|
||||||
/*
|
|
||||||
* TODO
|
/*
|
||||||
* */
|
* TODO
|
||||||
|
* */
|
||||||
actual fun fileSeparator(): String = "/"
|
actual fun fileSeparator(): String = "/"
|
||||||
|
|
||||||
actual fun imageCacheDir(): String = "TODO" +
|
actual fun imageCacheDir(): String = "TODO" +
|
||||||
@ -26,12 +38,9 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = false
|
actual fun isPresent(path: String): Boolean = false
|
||||||
|
|
||||||
actual fun createDirectory(dirPath:String){
|
actual fun createDirectory(dirPath:String){}
|
||||||
|
|
||||||
}
|
actual suspend fun clearCache() {}
|
||||||
|
|
||||||
actual suspend fun clearCache() {
|
|
||||||
}
|
|
||||||
|
|
||||||
actual suspend fun cacheImage(image: Any,path:String) {}
|
actual suspend fun cacheImage(image: Any,path:String) {}
|
||||||
|
|
||||||
@ -40,6 +49,41 @@ actual class Dir actual constructor(
|
|||||||
mp3ByteArray: ByteArray,
|
mp3ByteArray: ByteArray,
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
) {
|
) {
|
||||||
|
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
|
||||||
|
val albumArt = downloadFile(trackDetails.albumArtURL)
|
||||||
|
albumArt.collect {
|
||||||
|
when(it){
|
||||||
|
is DownloadResult.Success -> {
|
||||||
|
println("Album Art Downloaded Success")
|
||||||
|
val albumArtObj = js {
|
||||||
|
this["type"] = 3
|
||||||
|
this["data"] = it.byteArray.toArrayBuffer()
|
||||||
|
this["description"] = "Cover Art"
|
||||||
|
}
|
||||||
|
writeTagsAndSave(writer, albumArtObj as Object,trackDetails)
|
||||||
|
}
|
||||||
|
is DownloadResult.Error -> {
|
||||||
|
println("Album Art Downloading Error")
|
||||||
|
writeTagsAndSave(writer,null,trackDetails)
|
||||||
|
}
|
||||||
|
is DownloadResult.Progress -> println("Album Art Downloading: ${it.progress}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun writeTagsAndSave(writer:ID3Writer, albumArt:Object?, trackDetails: TrackDetails){
|
||||||
|
writer.apply {
|
||||||
|
setFrame("TIT2", trackDetails.title)
|
||||||
|
setFrame("TPE1", trackDetails.artists)
|
||||||
|
setFrame("TALB", trackDetails.albumName?:"")
|
||||||
|
try{trackDetails.year?.substring(0,4)?.toInt()?.let { setFrame("TYER", it) }} catch(e:Exception){}
|
||||||
|
setFrame("TPE2", trackDetails.artists.joinToString(","))
|
||||||
|
setFrame("WOAS", trackDetails.source.toString())
|
||||||
|
setFrame("TLEN", trackDetails.durationSec)
|
||||||
|
albumArt?.let { setFrame("APIC", it) }
|
||||||
|
}
|
||||||
|
writer.addTag()
|
||||||
|
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(path:String){}
|
actual fun addToLibrary(path:String){}
|
||||||
@ -48,14 +92,14 @@ actual class Dir actual constructor(
|
|||||||
return Picture(url)
|
return Picture(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadCachedImage(cachePath: String): ImageBitmap? {
|
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun freshImage(url:String): ImageBitmap?{
|
private suspend fun freshImage(url:String): ImageBitmap? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
actual val db: Database?
|
actual val db: Database?
|
||||||
get() = database
|
get() = database
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ByteArray.toArrayBuffer():ArrayBuffer{
|
||||||
|
return this.unsafeCast<Int8Array>().buffer
|
||||||
|
}
|
@ -60,7 +60,7 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
val finalList =
|
val finalList =
|
||||||
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
|
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
|
||||||
if (finalList.isNullOrEmpty()) showPopUpMessage("All Songs are Processed")
|
if (finalList.isNullOrEmpty()) showPopUpMessage("All Songs are Processed")
|
||||||
else downloadTracks(finalList,fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
|
else downloadTracks(finalList,fetchQuery,dir)
|
||||||
|
|
||||||
val list = intent.trackList.map {
|
val list = intent.trackList.map {
|
||||||
if (it.downloaded == DownloadStatus.NotDownloaded)
|
if (it.downloaded == DownloadStatus.NotDownloaded)
|
||||||
@ -70,7 +70,7 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))
|
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))
|
||||||
}
|
}
|
||||||
is Intent.StartDownload -> {
|
is Intent.StartDownload -> {
|
||||||
downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
|
downloadTracks(listOf(intent.track),fetchQuery,dir)
|
||||||
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
|
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
|
||||||
}
|
}
|
||||||
is Intent.RefreshTracksStatuses -> queryActiveTracks()
|
is Intent.RefreshTracksStatuses -> queryActiveTracks()
|
||||||
|
@ -16,6 +16,7 @@ dependencies {
|
|||||||
implementation(kotlin("stdlib-js"))
|
implementation(kotlin("stdlib-js"))
|
||||||
implementation(Decompose.decompose)
|
implementation(Decompose.decompose)
|
||||||
implementation(Koin.core)
|
implementation(Koin.core)
|
||||||
|
implementation(Ktor.clientJs)
|
||||||
implementation(MVIKotlin.mvikotlin)
|
implementation(MVIKotlin.mvikotlin)
|
||||||
implementation(MVIKotlin.coroutines)
|
implementation(MVIKotlin.coroutines)
|
||||||
implementation(MVIKotlin.mvikotlinMain)
|
implementation(MVIKotlin.mvikotlinMain)
|
||||||
@ -33,7 +34,8 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
js {
|
js() {
|
||||||
|
//useCommonJs()
|
||||||
browser {
|
browser {
|
||||||
webpackTask {
|
webpackTask {
|
||||||
cssSupport.enabled = true
|
cssSupport.enabled = true
|
||||||
|
@ -18,7 +18,8 @@ external interface AppProps : RProps {
|
|||||||
var dependencies: AppDependencies
|
var dependencies: AppDependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RBuilder.app(attrs: AppProps.() -> Unit): ReactElement {
|
@Suppress("FunctionName")
|
||||||
|
fun RBuilder.App(attrs: AppProps.() -> Unit): ReactElement {
|
||||||
return child(App::class){
|
return child(App::class){
|
||||||
this.attrs(attrs)
|
this.attrs(attrs)
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,16 @@ import com.shabinder.common.di.initKoin
|
|||||||
import react.dom.render
|
import react.dom.render
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
import navbar.navBar
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
window.onload = {
|
window.onload = {
|
||||||
render(document.getElementById("root")) {
|
render(document.getElementById("root")) {
|
||||||
navBar {}
|
App {
|
||||||
app {
|
|
||||||
dependencies = AppDependencies
|
dependencies = AppDependencies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,6 +22,7 @@ fun main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object AppDependencies : KoinComponent {
|
object AppDependencies : KoinComponent {
|
||||||
|
val appScope = CoroutineScope(Dispatchers.Default)
|
||||||
val logger: Kermit
|
val logger: Kermit
|
||||||
val directories: Dir
|
val directories: Dir
|
||||||
val fetchPlatformQueryResult: FetchPlatformQueryResult
|
val fetchPlatformQueryResult: FetchPlatformQueryResult
|
||||||
@ -29,5 +31,8 @@ object AppDependencies : KoinComponent {
|
|||||||
directories = get()
|
directories = get()
|
||||||
logger = get()
|
logger = get()
|
||||||
fetchPlatformQueryResult = get()
|
fetchPlatformQueryResult = get()
|
||||||
|
appScope.launch {
|
||||||
|
//fetchPlatformQueryResult.spotifyProvider.authenticateSpotifyClient(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,12 +3,7 @@ package home
|
|||||||
import com.shabinder.common.main.SpotiFlyerMain
|
import com.shabinder.common.main.SpotiFlyerMain
|
||||||
import com.shabinder.common.main.SpotiFlyerMain.State
|
import com.shabinder.common.main.SpotiFlyerMain.State
|
||||||
import extras.RenderableComponent
|
import extras.RenderableComponent
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.css.*
|
import kotlinx.css.*
|
||||||
import react.*
|
import react.*
|
||||||
import styled.css
|
import styled.css
|
||||||
@ -23,17 +18,6 @@ class HomeScreen(
|
|||||||
|
|
||||||
override val stateFlow: Flow<SpotiFlyerMain.State> = model.models
|
override val stateFlow: Flow<SpotiFlyerMain.State> = model.models
|
||||||
|
|
||||||
override fun componentDidMount() {
|
|
||||||
if(!scope.isActive)
|
|
||||||
scope = CoroutineScope(Dispatchers.Default)
|
|
||||||
scope.launch {
|
|
||||||
stateFlow.collect {
|
|
||||||
println("Updating State = $it")
|
|
||||||
setState { data = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun RBuilder.render() {
|
override fun RBuilder.render() {
|
||||||
println("Rendering New State = \"${state.data}\" ")
|
println("Rendering New State = \"${state.data}\" ")
|
||||||
styledDiv{
|
styledDiv{
|
||||||
@ -50,7 +34,6 @@ class HomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SearchBar {
|
SearchBar {
|
||||||
println("Search Props ${state.data.link}")
|
|
||||||
link = state.data.link
|
link = state.data.link
|
||||||
search = model::onLinkSearch
|
search = model::onLinkSearch
|
||||||
onLinkChange = model::onInputLinkChanged
|
onLinkChange = model::onInputLinkChanged
|
||||||
|
@ -33,7 +33,6 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
|
|||||||
onChangeFunction = {
|
onChangeFunction = {
|
||||||
val target = it.target as HTMLInputElement
|
val target = it.target as HTMLInputElement
|
||||||
props.onLinkChange(target.value)
|
props.onLinkChange(target.value)
|
||||||
println(target.value)
|
|
||||||
}
|
}
|
||||||
value = props.link
|
value = props.link
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@ package list
|
|||||||
|
|
||||||
import kotlinx.css.*
|
import kotlinx.css.*
|
||||||
import kotlinx.html.id
|
import kotlinx.html.id
|
||||||
import react.RProps
|
import react.*
|
||||||
import react.rFunction
|
|
||||||
import react.useState
|
|
||||||
import styled.css
|
import styled.css
|
||||||
import styled.styledDiv
|
import styled.styledDiv
|
||||||
import styled.styledH1
|
import styled.styledH1
|
||||||
@ -16,19 +14,25 @@ external interface CoverImageProps : RProps {
|
|||||||
var coverName: String
|
var coverName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
val CoverImage = rFunction<CoverImageProps>("CoverImage"){ props ->
|
@Suppress("FunctionName")
|
||||||
val (coverURL,setCoverURL) = useState(props.coverImageURL)
|
fun RBuilder.CoverImage(handler: CoverImageProps.() -> Unit): ReactElement {
|
||||||
val (coverName,setCoverName) = useState(props.coverName)
|
return child(coverImage){
|
||||||
|
attrs {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coverImage = functionalComponent<CoverImageProps>("CoverImage"){ props ->
|
||||||
styledDiv {
|
styledDiv {
|
||||||
styledImg(src=coverURL){
|
styledImg(src= props.coverImageURL){
|
||||||
css {
|
css {
|
||||||
height = 300.px
|
height = 220.px
|
||||||
width = 300.px
|
width = 220.px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styledH1 {
|
styledH1 {
|
||||||
+coverName
|
+props.coverName
|
||||||
css {
|
css {
|
||||||
textAlign = TextAlign.center
|
textAlign = TextAlign.center
|
||||||
}
|
}
|
||||||
@ -40,6 +44,7 @@ val CoverImage = rFunction<CoverImageProps>("CoverImage"){ props ->
|
|||||||
display = Display.flex
|
display = Display.flex
|
||||||
alignItems = Align.center
|
alignItems = Align.center
|
||||||
flexDirection = FlexDirection.column
|
flexDirection = FlexDirection.column
|
||||||
|
marginTop = 12.px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
58
web-app/src/main/kotlin/list/DownloadAllButton.kt
Normal file
58
web-app/src/main/kotlin/list/DownloadAllButton.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import kotlinx.css.*
|
||||||
|
import kotlinx.html.id
|
||||||
|
import react.*
|
||||||
|
import styled.css
|
||||||
|
import styled.styledDiv
|
||||||
|
import styled.styledH5
|
||||||
|
import styled.styledImg
|
||||||
|
|
||||||
|
external interface DownloadAllButtonProps : RProps {
|
||||||
|
var isActive:Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun RBuilder.DownloadAllButton(handler: DownloadAllButtonProps.() -> Unit): ReactElement {
|
||||||
|
return child(downloadAllButton){
|
||||||
|
attrs {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val downloadAllButton = functionalComponent<DownloadAllButtonProps>("DownloadAllButton") { props->
|
||||||
|
styledDiv {
|
||||||
|
styledDiv {
|
||||||
|
|
||||||
|
styledImg(src = "download.svg",alt = "Download All Button") {
|
||||||
|
css {
|
||||||
|
classes = mutableListOf("download-all-icon")
|
||||||
|
height = 32.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
styledH5 {
|
||||||
|
attrs {
|
||||||
|
id = "download-all-text"
|
||||||
|
}
|
||||||
|
+ "Download All"
|
||||||
|
css {
|
||||||
|
whiteSpace = WhiteSpace.nowrap
|
||||||
|
fontSize = 15.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
css {
|
||||||
|
classes = mutableListOf("download-icon")
|
||||||
|
display = Display.flex
|
||||||
|
alignItems = Align.center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
classes = mutableListOf("download-button")
|
||||||
|
display = if(props.isActive) Display.flex else Display.none
|
||||||
|
alignItems = Align.center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +1,58 @@
|
|||||||
package list
|
package list
|
||||||
|
|
||||||
import com.shabinder.common.list.SpotiFlyerList
|
import com.shabinder.common.list.SpotiFlyerList
|
||||||
|
import com.shabinder.common.list.SpotiFlyerList.State
|
||||||
import extras.RenderableComponent
|
import extras.RenderableComponent
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.css.*
|
import kotlinx.css.*
|
||||||
import kotlinx.html.id
|
import kotlinx.html.id
|
||||||
import react.RBuilder
|
import react.RBuilder
|
||||||
import react.RState
|
|
||||||
import styled.css
|
import styled.css
|
||||||
import styled.styledDiv
|
import styled.styledDiv
|
||||||
|
|
||||||
class ListScreen(
|
class ListScreen(
|
||||||
props: Props<SpotiFlyerList>,
|
props: Props<SpotiFlyerList>,
|
||||||
) : RenderableComponent<SpotiFlyerList, ListScreen.State>(props,initialState = State(SpotiFlyerList.State())) {
|
) : RenderableComponent<SpotiFlyerList, State>(props,initialState = State()) {
|
||||||
|
|
||||||
override val stateFlow: Flow<State> = model.models.map { State(it) }
|
override val stateFlow: Flow<SpotiFlyerList.State> = model.models
|
||||||
|
|
||||||
override fun RBuilder.render() {
|
override fun RBuilder.render() {
|
||||||
|
|
||||||
|
val result = state.data.queryResult
|
||||||
|
|
||||||
styledDiv {
|
styledDiv {
|
||||||
attrs {
|
attrs {
|
||||||
id = "list-screen-div"
|
id = "list-screen-div"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(result == null){
|
||||||
|
LoadingAnim { }
|
||||||
|
}else{
|
||||||
|
CoverImage {
|
||||||
|
coverImageURL = result.coverUrl
|
||||||
|
coverName = result.title
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadAllButton {
|
||||||
|
isActive = state.data.trackList.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.data.trackList.forEachIndexed{ index, trackDetails ->
|
||||||
|
TrackItem {
|
||||||
|
details = trackDetails
|
||||||
|
downloadTrack = model::onDownloadClicked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
css {
|
css {
|
||||||
classes = mutableListOf("list-screen")
|
classes = mutableListOf("list-screen")
|
||||||
display = Display.flex
|
display = Display.flex
|
||||||
flexDirection = FlexDirection.column
|
flexDirection = FlexDirection.column
|
||||||
flexGrow = 1.0
|
flexGrow = 1.0
|
||||||
justifyContent = JustifyContent.center
|
justifyContent = JustifyContent.center
|
||||||
alignItems = Align.center
|
alignItems = Align.stretch
|
||||||
backgroundColor = Color.white
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class State(
|
|
||||||
var data: SpotiFlyerList.State
|
|
||||||
):RState
|
|
||||||
}
|
}
|
37
web-app/src/main/kotlin/list/LoadingAnim.kt
Normal file
37
web-app/src/main/kotlin/list/LoadingAnim.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import kotlinx.css.*
|
||||||
|
import react.*
|
||||||
|
import styled.css
|
||||||
|
import styled.styledDiv
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun RBuilder.LoadingAnim(handler: RProps.() -> Unit): ReactElement {
|
||||||
|
return child(loadingAnim){
|
||||||
|
attrs {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loadingAnim = functionalComponent<RProps>("Loading Animation") {
|
||||||
|
styledDiv {
|
||||||
|
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube1") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube2") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube3") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube4") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube5") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube6") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube7") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube8") } }
|
||||||
|
styledDiv { css { classes = mutableListOf("sk-cube sk-cube9") } }
|
||||||
|
|
||||||
|
css {
|
||||||
|
classes = mutableListOf("sk-cube-grid")
|
||||||
|
height = 60.px
|
||||||
|
width = 60.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,107 @@
|
|||||||
package list
|
package list
|
||||||
|
|
||||||
import react.RProps
|
import com.shabinder.common.models.TrackDetails
|
||||||
import react.rFunction
|
import kotlinx.css.*
|
||||||
|
import kotlinx.html.id
|
||||||
|
import kotlinx.html.js.onClickFunction
|
||||||
|
import react.*
|
||||||
|
import styled.*
|
||||||
|
|
||||||
external interface TrackItemProps : RProps {
|
external interface TrackItemProps : RProps {
|
||||||
var coverImageURL: String
|
var details:TrackDetails
|
||||||
var coverName: String
|
var downloadTrack:(TrackDetails)->Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
val trackItem = rFunction<TrackItemProps>("Track-Item"){
|
@Suppress("FunctionName")
|
||||||
|
fun RBuilder.TrackItem(handler: TrackItemProps.() -> Unit): ReactElement {
|
||||||
|
return child(trackItem){
|
||||||
|
attrs {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props ->
|
||||||
|
val details = props.details
|
||||||
|
styledDiv {
|
||||||
|
|
||||||
|
styledImg(src = details.albumArtURL) {
|
||||||
|
css {
|
||||||
|
height = 90.px
|
||||||
|
width = 90.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
styledDiv {
|
||||||
|
attrs {
|
||||||
|
id = "text-details"
|
||||||
|
}
|
||||||
|
styledDiv {
|
||||||
|
styledH3 {
|
||||||
|
+ details.title
|
||||||
|
css {
|
||||||
|
padding(8.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
height = 40.px
|
||||||
|
display =Display.flex
|
||||||
|
alignItems = Align.center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styledDiv {
|
||||||
|
styledH4 {
|
||||||
|
+ details.artists.joinToString(",")
|
||||||
|
css {
|
||||||
|
flexGrow = 1.0
|
||||||
|
padding(8.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styledH4 {
|
||||||
|
+ "${details.durationSec} sec"
|
||||||
|
css {
|
||||||
|
flexGrow = 1.0
|
||||||
|
padding(8.px)
|
||||||
|
textAlign = TextAlign.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
height = 40.px
|
||||||
|
display =Display.flex
|
||||||
|
alignItems = Align.center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
display = Display.flex
|
||||||
|
flexGrow = 1.0
|
||||||
|
flexDirection = FlexDirection.column
|
||||||
|
margin(8.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styledDiv {
|
||||||
|
styledImg(src = "download-gradient.svg") {
|
||||||
|
attrs {
|
||||||
|
onClickFunction = {
|
||||||
|
props.downloadTrack(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
margin(8.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css {
|
||||||
|
classes = mutableListOf("glow-button")
|
||||||
|
borderRadius = 100.px
|
||||||
|
width = 65.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
css {
|
||||||
|
alignItems = Align.center
|
||||||
|
display =Display.flex
|
||||||
|
flexDirection = FlexDirection.row
|
||||||
|
flexGrow = 1.0
|
||||||
|
color = Color.white
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,50 +6,59 @@ import react.*
|
|||||||
import styled.*
|
import styled.*
|
||||||
|
|
||||||
|
|
||||||
fun RBuilder.navBar(attrs: RProps.() -> Unit): ReactElement{
|
@Suppress("FunctionName")
|
||||||
return child(NavBar::class){
|
fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
|
||||||
this.attrs(attrs)
|
return child(navBar){
|
||||||
|
attrs {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalJsExport::class)
|
external interface NavBarProps:RProps{
|
||||||
@JsExport
|
var isBackVisible: Boolean
|
||||||
class NavBar : RComponent<RProps, RState>() {
|
}
|
||||||
|
|
||||||
override fun RBuilder.render() {
|
|
||||||
styledNav {
|
private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
|
||||||
|
styledNav {
|
||||||
|
css {
|
||||||
|
+NavBarStyles.nav
|
||||||
|
}
|
||||||
|
styledImg(src = "left-arrow.svg",alt = "Back Arrow"){
|
||||||
css {
|
css {
|
||||||
+NavBarStyles.nav
|
height = 42.px
|
||||||
|
width = 42.px
|
||||||
|
display = if(props.isBackVisible) Display.inline else Display.none
|
||||||
|
filter = "invert(100)"
|
||||||
|
marginRight = 12.px
|
||||||
}
|
}
|
||||||
styledImg {
|
}
|
||||||
attrs {
|
styledImg(src = "spotiflyer.svg",alt = "Logo") {
|
||||||
src = "spotiflyer.svg"
|
css {
|
||||||
}
|
height = 42.px
|
||||||
|
width = 42.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styledH1 {
|
||||||
|
+"SpotiFlyer"
|
||||||
|
attrs {
|
||||||
|
id = "appName"
|
||||||
|
}
|
||||||
|
css{
|
||||||
|
fontSize = 46.px
|
||||||
|
margin(horizontal = 14.px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){
|
||||||
|
styledImg(src = "github.svg"){
|
||||||
css {
|
css {
|
||||||
height = 42.px
|
height = 42.px
|
||||||
width = 42.px
|
width = 42.px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styledH1 {
|
css {
|
||||||
+"SpotiFlyer"
|
marginLeft = LinearDimension.auto
|
||||||
attrs {
|
|
||||||
id = "appName"
|
|
||||||
}
|
|
||||||
css{
|
|
||||||
fontSize = 46.px
|
|
||||||
margin(horizontal = 14.px)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){
|
|
||||||
styledImg(src = "github.svg"){
|
|
||||||
css {
|
|
||||||
height = 42.px
|
|
||||||
width = 42.px
|
|
||||||
}
|
|
||||||
}
|
|
||||||
css {
|
|
||||||
marginLeft = LinearDimension.auto
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ import com.shabinder.common.root.SpotiFlyerRoot.*
|
|||||||
import extras.RenderableRootComponent
|
import extras.RenderableRootComponent
|
||||||
import extras.renderableChild
|
import extras.renderableChild
|
||||||
import home.HomeScreen
|
import home.HomeScreen
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import list.ListScreen
|
import list.ListScreen
|
||||||
|
import navbar.NavBar
|
||||||
import react.RBuilder
|
import react.RBuilder
|
||||||
import react.RState
|
import react.RState
|
||||||
|
|
||||||
@ -18,6 +20,9 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
|
|||||||
get() = model.routerState.value.activeChild.component
|
get() = model.routerState.value.activeChild.component
|
||||||
|
|
||||||
override fun RBuilder.render() {
|
override fun RBuilder.render() {
|
||||||
|
NavBar {
|
||||||
|
isBackVisible = (component is Child.List)
|
||||||
|
}
|
||||||
when(component){
|
when(component){
|
||||||
is Child.Main -> renderableChild(HomeScreen::class, (component as Child.Main).component)
|
is Child.Main -> renderableChild(HomeScreen::class, (component as Child.Main).component)
|
||||||
is Child.List -> renderableChild(ListScreen::class, (component as Child.List).component)
|
is Child.List -> renderableChild(ListScreen::class, (component as Child.List).component)
|
||||||
|
49
web-app/src/main/resources/download-gradient.svg
Normal file
49
web-app/src/main/resources/download-gradient.svg
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.525879e-005" y1="258" x2="512" y2="258" gradientTransform="matrix(1 0 0 -1 0 514)">
|
||||||
|
<stop offset="0" style="stop-color:#80D8FF"/>
|
||||||
|
<stop offset="0.16" style="stop-color:#88D1FF"/>
|
||||||
|
<stop offset="0.413" style="stop-color:#9FBEFE"/>
|
||||||
|
<stop offset="0.725" style="stop-color:#C4A0FD"/>
|
||||||
|
<stop offset="1" style="stop-color:#EA80FC"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_1_);" d="M462.622,512H49.378C22.151,512,0,489.85,0,462.623v-118.49c0-11.046,8.954-20,20-20
|
||||||
|
s20,8.954,20,20v118.49c0,5.171,4.207,9.377,9.378,9.377h413.244c5.171,0,9.378-4.207,9.378-9.377v-118.49c0-11.046,8.954-20,20-20
|
||||||
|
c11.046,0,20,8.954,20,20v118.49C512,489.85,489.849,512,462.622,512z M270.548,425.426l146.888-155.701
|
||||||
|
c5.479-5.807,6.979-14.316,3.816-21.646c-3.163-7.33-10.381-12.079-18.364-12.079h-68.133V20c0-11.046-8.954-20-20-20h-117.51
|
||||||
|
c-11.046,0-20,8.954-20,20v216h-68.133c-7.983,0-15.202,4.748-18.364,12.078s-1.662,15.839,3.816,21.646l146.888,155.701
|
||||||
|
c3.778,4.005,9.041,6.275,14.548,6.275C261.507,431.7,266.77,429.431,270.548,425.426z M197.245,276c11.046,0,20-8.954,20-20V40
|
||||||
|
h77.51v216c0,11.046,8.954,20,20,20h41.77L256,382.556L155.476,276H197.245z"/>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
45
web-app/src/main/resources/download.svg
Normal file
45
web-app/src/main/resources/download.svg
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 471.2 471.2" style="enable-background:new 0 0 471.2 471.2;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M457.7,230.15c-7.5,0-13.5,6-13.5,13.5v122.8c0,33.4-27.2,60.5-60.5,60.5H87.5c-33.4,0-60.5-27.2-60.5-60.5v-124.8
|
||||||
|
c0-7.5-6-13.5-13.5-13.5s-13.5,6-13.5,13.5v124.8c0,48.3,39.3,87.5,87.5,87.5h296.2c48.3,0,87.5-39.3,87.5-87.5v-122.8
|
||||||
|
C471.2,236.25,465.2,230.15,457.7,230.15z"/>
|
||||||
|
<path d="M226.1,346.75c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4l85.8-85.8c5.3-5.3,5.3-13.8,0-19.1c-5.3-5.3-13.8-5.3-19.1,0l-62.7,62.8
|
||||||
|
V30.75c0-7.5-6-13.5-13.5-13.5s-13.5,6-13.5,13.5v273.9l-62.8-62.8c-5.3-5.3-13.8-5.3-19.1,0c-5.3,5.3-5.3,13.8,0,19.1
|
||||||
|
L226.1,346.75z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
web-app/src/main/resources/header-dark.jpg
Normal file
BIN
web-app/src/main/resources/header-dark.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
61
web-app/src/main/resources/left-arrow.svg
Normal file
61
web-app/src/main/resources/left-arrow.svg
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M468.662,212.66H147.974l52.851-52.851c16.896-16.897,16.897-44.393,0-61.293c-8.187-8.186-19.07-12.694-30.647-12.694
|
||||||
|
c-11.576,0-22.46,4.508-30.646,12.694L12.697,225.351C4.509,233.536,0,244.42,0,256s4.509,22.465,12.695,30.647l126.837,126.839
|
||||||
|
c8.186,8.185,19.069,12.693,30.646,12.693c11.577,0,22.462-4.508,30.646-12.694c8.179-8.177,12.683-19.06,12.683-30.647
|
||||||
|
s-4.504-22.47-12.682-30.647l-52.851-52.851h320.687C492.557,299.341,512,279.898,512,256S492.558,212.659,468.662,212.66z
|
||||||
|
M468.659,278.942h-345.31c-4.126,0-7.844,2.486-9.422,6.296c-1.579,3.811-0.706,8.198,2.21,11.115l70.264,70.263
|
||||||
|
c4.324,4.324,6.706,10.086,6.706,16.222c0,6.136-2.382,11.897-6.706,16.222H186.4c-4.333,4.333-10.093,6.719-16.222,6.719
|
||||||
|
c-6.128,0-11.889-2.387-16.222-6.719L27.118,272.221c-4.333-4.332-6.719-10.092-6.719-16.221s2.387-11.889,6.721-16.222
|
||||||
|
L153.957,112.94c4.334-4.334,10.093-6.72,16.221-6.72s11.89,2.387,16.223,6.718c8.945,8.947,8.945,23.501,0,32.446l-70.263,70.263
|
||||||
|
c-2.916,2.917-3.789,7.304-2.21,11.115c1.578,3.81,5.296,6.296,9.422,6.296h345.311c12.649,0,22.941,10.291,22.941,22.942
|
||||||
|
S481.31,278.943,468.659,278.942z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M124.477,326.835l-2.022-2.023c-3.985-3.982-10.441-3.982-14.425,0c-3.983,3.984-3.983,10.442,0,14.425l2.023,2.023
|
||||||
|
c1.992,1.991,4.601,2.987,7.212,2.987c2.61,0,5.22-0.996,7.213-2.987C128.46,337.275,128.46,330.817,124.477,326.835z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M102.707,305.064L49.431,251.79c-3.985-3.982-10.441-3.982-14.425,0c-3.983,3.984-3.983,10.442,0,14.425l53.276,53.275
|
||||||
|
c1.992,1.991,4.601,2.987,7.212,2.987c2.61,0,5.22-0.995,7.213-2.987C106.69,315.505,106.69,309.047,102.707,305.064z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -2,19 +2,21 @@
|
|||||||
font-family: pristine;
|
font-family: pristine;
|
||||||
src: url("pristine_script.ttf");
|
src: url("pristine_script.ttf");
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
background-image: url("header-dark.jpg");
|
||||||
|
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||||
|
}
|
||||||
body, html {
|
body, html {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-attachment: fixed;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
caret-color: crimson;
|
caret-color: crimson;
|
||||||
background-color: black;
|
|
||||||
/*background-image: linear-gradient(180deg,rgba(221,0,221,0.50),rgba(221,0,221,0.32),rgba(221,0,221,0.16),rgba(221,0,221,0.08),rgba(221,0,221,0.04),rgba(0,0,0,0));*/
|
/*background-image: linear-gradient(180deg,rgba(221,0,221,0.50),rgba(221,0,221,0.32),rgba(221,0,221,0.16),rgba(221,0,221,0.08),rgba(221,0,221,0.04),rgba(0,0,0,0));*/
|
||||||
background-image: url("header.png");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
}
|
}
|
||||||
#appName{
|
#appName{
|
||||||
font-family: pristine, cursive;
|
font-family: pristine, cursive;
|
||||||
@ -90,7 +92,105 @@ body, html {
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
#download-all-text {
|
||||||
|
color: black;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.download-button {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 100px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 12px auto;
|
||||||
|
transition: 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover {
|
||||||
|
width: 150px;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
color: black;
|
||||||
|
transition: 0.3s;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover #download-all-text {
|
||||||
|
display: inline;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover .download-all-icon {
|
||||||
|
filter: none;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.download-button:not(hover) .download-all-icon {
|
||||||
|
filter: invert(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sk-cube-grid {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 100px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sk-cube-grid .sk-cube {
|
||||||
|
width: 33%;
|
||||||
|
height: 33%;
|
||||||
|
background-color: rgb(240, 90, 220);
|
||||||
|
float: left;
|
||||||
|
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
|
||||||
|
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.sk-cube-grid .sk-cube1 {
|
||||||
|
-webkit-animation-delay: 0.2s;
|
||||||
|
animation-delay: 0.2s; }
|
||||||
|
.sk-cube-grid .sk-cube2 {
|
||||||
|
-webkit-animation-delay: 0.3s;
|
||||||
|
animation-delay: 0.3s; }
|
||||||
|
.sk-cube-grid .sk-cube3 {
|
||||||
|
-webkit-animation-delay: 0.4s;
|
||||||
|
animation-delay: 0.4s; }
|
||||||
|
.sk-cube-grid .sk-cube4 {
|
||||||
|
-webkit-animation-delay: 0.1s;
|
||||||
|
animation-delay: 0.1s; }
|
||||||
|
.sk-cube-grid .sk-cube5 {
|
||||||
|
-webkit-animation-delay: 0.2s;
|
||||||
|
animation-delay: 0.2s; }
|
||||||
|
.sk-cube-grid .sk-cube6 {
|
||||||
|
-webkit-animation-delay: 0.3s;
|
||||||
|
animation-delay: 0.3s; }
|
||||||
|
.sk-cube-grid .sk-cube7 {
|
||||||
|
-webkit-animation-delay: 0s;
|
||||||
|
animation-delay: 0s; }
|
||||||
|
.sk-cube-grid .sk-cube8 {
|
||||||
|
-webkit-animation-delay: 0.1s;
|
||||||
|
animation-delay: 0.1s; }
|
||||||
|
.sk-cube-grid .sk-cube9 {
|
||||||
|
-webkit-animation-delay: 0.2s;
|
||||||
|
animation-delay: 0.2s; }
|
||||||
|
|
||||||
|
@-webkit-keyframes sk-cubeGridScaleDelay {
|
||||||
|
0%, 70%, 100% {
|
||||||
|
-webkit-transform: scale3D(1, 1, 1);
|
||||||
|
transform: scale3D(1, 1, 1);
|
||||||
|
} 35% {
|
||||||
|
-webkit-transform: scale3D(0, 0, 1);
|
||||||
|
transform: scale3D(0, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-cubeGridScaleDelay {
|
||||||
|
0%, 70%, 100% {
|
||||||
|
-webkit-transform: scale3D(1, 1, 1);
|
||||||
|
transform: scale3D(1, 1, 1);
|
||||||
|
} 35% {
|
||||||
|
-webkit-transform: scale3D(0, 0, 1);
|
||||||
|
transform: scale3D(0, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@media screen and (max-width: 620px) {
|
@media screen and (max-width: 620px) {
|
||||||
.searchBox:hover > .searchInput {
|
.searchBox:hover > .searchInput {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
Loading…
Reference in New Issue
Block a user