ID3 Tags and File Save for web-app.

This commit is contained in:
shabinder 2021-03-13 01:48:03 +05:30
parent 601d8135db
commit a47d9f52a0
34 changed files with 733 additions and 146 deletions

View File

@ -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") {

View File

@ -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") {

View File

@ -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(

View File

@ -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)) {

View File

@ -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))
} }
} }
} }

View File

@ -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)

View File

@ -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()

View File

@ -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,

View File

@ -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 =

View File

@ -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"){

View File

@ -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")

View File

@ -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)
} }
} }
} }

View File

@ -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)

View File

@ -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()
}

View File

@ -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}")
}
}
}
} }

View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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)
} }

View File

@ -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)
}
} }
} }

View File

@ -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

View File

@ -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
} }

View File

@ -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
} }
} }
} }

View 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
}
}
}

View File

@ -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
} }

View 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
}
}
}

View File

@ -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
}
}
} }

View File

@ -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
}
} }
} }
} }

View File

@ -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)

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View 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

View File

@ -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;