mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-12-22 20:57:54 +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")
|
||||
android()
|
||||
//ios()
|
||||
js {
|
||||
js() {
|
||||
browser()
|
||||
nodejs()
|
||||
//nodejs()
|
||||
binaries.executable()
|
||||
}
|
||||
sourceSets {
|
||||
named("commonTest") {
|
||||
|
@ -14,9 +14,10 @@ plugins {
|
||||
kotlin {
|
||||
jvm("desktop")
|
||||
android()
|
||||
js {
|
||||
js() {
|
||||
browser()
|
||||
nodejs()
|
||||
//nodejs()
|
||||
binaries.executable()
|
||||
}
|
||||
sourceSets {
|
||||
named("commonMain") {
|
||||
|
@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.list.SpotiFlyerList
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
@ -67,7 +68,7 @@ fun SpotiFlyerListContent(
|
||||
fun TrackCard(
|
||||
track: TrackDetails,
|
||||
downloadTrack:()->Unit,
|
||||
loadImage:suspend (String)-> ImageBitmap?
|
||||
loadImage:suspend (String)-> Picture
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
||||
ImageLoad(
|
||||
@ -120,7 +121,7 @@ fun CoverImage(
|
||||
title: String,
|
||||
coverURL: String,
|
||||
scope: CoroutineScope,
|
||||
loadImage: suspend (String) -> ImageBitmap?,
|
||||
loadImage: suspend (String) -> Picture,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
@ -28,10 +28,10 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.shabinder.common.di.Picture
|
||||
import com.shabinder.common.di.giveDonation
|
||||
import com.shabinder.common.di.openPlatform
|
||||
import com.shabinder.common.di.shareApp
|
||||
import com.shabinder.common.di.showPopUpMessage
|
||||
import com.shabinder.common.main.SpotiFlyerMain
|
||||
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
||||
import com.shabinder.common.models.DownloadRecord
|
||||
@ -303,7 +303,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
||||
@Composable
|
||||
fun HistoryColumn(
|
||||
list: List<DownloadRecord>,
|
||||
loadImage:suspend (String)-> ImageBitmap?,
|
||||
loadImage:suspend (String)-> Picture,
|
||||
onItemClicked: (String) -> Unit
|
||||
) {
|
||||
Crossfade(list){
|
||||
@ -335,7 +335,7 @@ fun HistoryColumn(
|
||||
@Composable
|
||||
fun DownloadRecordItem(
|
||||
item: DownloadRecord,
|
||||
loadImage:suspend (String)-> ImageBitmap?,
|
||||
loadImage:suspend (String)-> Picture,
|
||||
onItemClicked:(String)->Unit
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
||||
|
@ -50,8 +50,11 @@ kotlin {
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation(Ktor.clientJs)
|
||||
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.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kiulian.downloader.model.YoutubeVideo
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.razorpay.Checkout
|
||||
import com.shabinder.common.database.activityContext
|
||||
import com.shabinder.common.database.appContext
|
||||
import com.shabinder.common.di.worker.ForegroundService
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -87,8 +84,8 @@ actual fun queryActiveTracks() {
|
||||
|
||||
actual suspend fun downloadTracks(
|
||||
list: List<TrackDetails>,
|
||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
dir: Dir
|
||||
){
|
||||
if(!list.isNullOrEmpty()){
|
||||
val serviceIntent = Intent(activityContext, ForegroundService::class.java)
|
||||
|
@ -15,8 +15,8 @@ expect val isInternetAvailable:Boolean
|
||||
|
||||
expect suspend fun downloadTracks(
|
||||
list: List<TrackDetails>,
|
||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
dir: Dir
|
||||
)
|
||||
|
||||
expect fun queryActiveTracks()
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class FetchPlatformQueryResult(
|
||||
private val gaanaProvider: GaanaProvider,
|
||||
private val spotifyProvider: SpotifyProvider,
|
||||
val spotifyProvider: SpotifyProvider,
|
||||
val youtubeProvider: YoutubeProvider,
|
||||
val youtubeMusic: YoutubeMusic,
|
||||
val youtubeMp3: YoutubeMp3,
|
||||
|
@ -19,6 +19,7 @@ package com.shabinder.common.di.providers
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.di.*
|
||||
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.TrackDetails
|
||||
import com.shabinder.common.models.spotify.Album
|
||||
@ -41,11 +42,13 @@ class SpotifyProvider(
|
||||
|
||||
init {
|
||||
logger.d { "Creating Spotify Provider" }
|
||||
//GlobalScope.launch(Dispatchers.Default) {authenticateSpotify()}
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
authenticateSpotifyClient()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun authenticateSpotify(): HttpClient?{
|
||||
val token = tokenStore.getToken()
|
||||
override suspend fun authenticateSpotifyClient(override:Boolean): HttpClient?{
|
||||
val token = if(override) authenticateSpotify() else tokenStore.getToken()
|
||||
return if(token == null) {
|
||||
logger.d{ "Please Check your Network Connection" }
|
||||
null
|
||||
@ -69,7 +72,7 @@ class SpotifyProvider(
|
||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||
|
||||
if(!this::httpClient.isInitialized){
|
||||
authenticateSpotify()
|
||||
authenticateSpotifyClient()
|
||||
}
|
||||
|
||||
var spotifyLink =
|
||||
|
@ -249,8 +249,7 @@ class YoutubeMusic constructor(
|
||||
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
||||
contentType(ContentType.Application.Json)
|
||||
headers{
|
||||
//append("Content-Type"," application/json")
|
||||
append("Referer"," https://music.youtube.com/search")
|
||||
append("referer","https://music.youtube.com/search")
|
||||
}
|
||||
body = buildJsonObject {
|
||||
putJsonObject("context"){
|
||||
|
@ -13,7 +13,7 @@ interface SpotifyRequests {
|
||||
|
||||
val httpClient:HttpClient
|
||||
|
||||
suspend fun authenticateSpotify():HttpClient?
|
||||
suspend fun authenticateSpotifyClient(override:Boolean = false):HttpClient?
|
||||
|
||||
suspend fun getPlaylist(playlistID: String): Playlist {
|
||||
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
||||
|
@ -1,9 +1,5 @@
|
||||
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.model.YoutubeVideo
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
@ -62,20 +58,20 @@ val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = Mu
|
||||
|
||||
actual suspend fun downloadTracks(
|
||||
list: List<TrackDetails>,
|
||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
dir: Dir
|
||||
){
|
||||
list.forEach {
|
||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||
downloadTrack(it.videoID!!, it,saveFileWithMetaData)
|
||||
downloadTrack(it.videoID!!, it,dir::saveFileWithMetadata)
|
||||
} else {
|
||||
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||
val videoId = getYTIDBestMatch(searchQuery,it)
|
||||
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
||||
if (videoId.isNullOrBlank()) {
|
||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
||||
) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) })
|
||||
} 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
|
||||
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
|
||||
actual fun openPlatform(packageID:String, platformLink:String){
|
||||
//TODO
|
||||
@ -50,13 +53,40 @@ val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = M
|
||||
|
||||
actual suspend fun downloadTracks(
|
||||
list: List<TrackDetails>,
|
||||
getYTIDBestMatch:suspend (String, TrackDetails)->String?,
|
||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
||||
){/*
|
||||
list.forEach {
|
||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||
} else {
|
||||
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
dir: Dir
|
||||
){
|
||||
withContext(Dispatchers.Default){
|
||||
list.forEach {
|
||||
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
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.models.DownloadResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
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.khronos.webgl.Int8Array
|
||||
|
||||
actual class Dir actual constructor(
|
||||
private val logger: Kermit,
|
||||
@ -13,9 +24,10 @@ actual class Dir actual constructor(
|
||||
/*init {
|
||||
createDirectories()
|
||||
}*/
|
||||
/*
|
||||
* TODO
|
||||
* */
|
||||
|
||||
/*
|
||||
* TODO
|
||||
* */
|
||||
actual fun fileSeparator(): String = "/"
|
||||
|
||||
actual fun imageCacheDir(): String = "TODO" +
|
||||
@ -26,12 +38,9 @@ actual class Dir actual constructor(
|
||||
|
||||
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) {}
|
||||
|
||||
@ -40,6 +49,41 @@ actual class Dir actual constructor(
|
||||
mp3ByteArray: ByteArray,
|
||||
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){}
|
||||
@ -48,14 +92,14 @@ actual class Dir actual constructor(
|
||||
return Picture(url)
|
||||
}
|
||||
|
||||
private fun loadCachedImage(cachePath: String): ImageBitmap? {
|
||||
return null
|
||||
}
|
||||
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
||||
|
||||
private suspend fun freshImage(url:String): ImageBitmap?{
|
||||
return null
|
||||
}
|
||||
private suspend fun freshImage(url:String): ImageBitmap? = null
|
||||
|
||||
actual val db: Database?
|
||||
get() = database
|
||||
}
|
||||
|
||||
fun ByteArray.toArrayBuffer():ArrayBuffer{
|
||||
return this.unsafeCast<Int8Array>().buffer
|
||||
}
|
@ -60,7 +60,7 @@ internal class SpotiFlyerListStoreProvider(
|
||||
val finalList =
|
||||
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
|
||||
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 {
|
||||
if (it.downloaded == DownloadStatus.NotDownloaded)
|
||||
@ -70,7 +70,7 @@ internal class SpotiFlyerListStoreProvider(
|
||||
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))
|
||||
}
|
||||
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)))
|
||||
}
|
||||
is Intent.RefreshTracksStatuses -> queryActiveTracks()
|
||||
|
@ -16,6 +16,7 @@ dependencies {
|
||||
implementation(kotlin("stdlib-js"))
|
||||
implementation(Decompose.decompose)
|
||||
implementation(Koin.core)
|
||||
implementation(Ktor.clientJs)
|
||||
implementation(MVIKotlin.mvikotlin)
|
||||
implementation(MVIKotlin.coroutines)
|
||||
implementation(MVIKotlin.mvikotlinMain)
|
||||
@ -33,7 +34,8 @@ dependencies {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
js {
|
||||
js() {
|
||||
//useCommonJs()
|
||||
browser {
|
||||
webpackTask {
|
||||
cssSupport.enabled = true
|
||||
|
@ -18,7 +18,8 @@ external interface AppProps : RProps {
|
||||
var dependencies: AppDependencies
|
||||
}
|
||||
|
||||
fun RBuilder.app(attrs: AppProps.() -> Unit): ReactElement {
|
||||
@Suppress("FunctionName")
|
||||
fun RBuilder.App(attrs: AppProps.() -> Unit): ReactElement {
|
||||
return child(App::class){
|
||||
this.attrs(attrs)
|
||||
}
|
||||
|
@ -5,15 +5,16 @@ import com.shabinder.common.di.initKoin
|
||||
import react.dom.render
|
||||
import kotlinx.browser.document
|
||||
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.get
|
||||
|
||||
fun main() {
|
||||
window.onload = {
|
||||
render(document.getElementById("root")) {
|
||||
navBar {}
|
||||
app {
|
||||
App {
|
||||
dependencies = AppDependencies
|
||||
}
|
||||
}
|
||||
@ -21,6 +22,7 @@ fun main() {
|
||||
}
|
||||
|
||||
object AppDependencies : KoinComponent {
|
||||
val appScope = CoroutineScope(Dispatchers.Default)
|
||||
val logger: Kermit
|
||||
val directories: Dir
|
||||
val fetchPlatformQueryResult: FetchPlatformQueryResult
|
||||
@ -29,5 +31,8 @@ object AppDependencies : KoinComponent {
|
||||
directories = get()
|
||||
logger = 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.State
|
||||
import extras.RenderableComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.css.*
|
||||
import react.*
|
||||
import styled.css
|
||||
@ -23,17 +18,6 @@ class HomeScreen(
|
||||
|
||||
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() {
|
||||
println("Rendering New State = \"${state.data}\" ")
|
||||
styledDiv{
|
||||
@ -50,7 +34,6 @@ class HomeScreen(
|
||||
}
|
||||
|
||||
SearchBar {
|
||||
println("Search Props ${state.data.link}")
|
||||
link = state.data.link
|
||||
search = model::onLinkSearch
|
||||
onLinkChange = model::onInputLinkChanged
|
||||
|
@ -33,7 +33,6 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
|
||||
onChangeFunction = {
|
||||
val target = it.target as HTMLInputElement
|
||||
props.onLinkChange(target.value)
|
||||
println(target.value)
|
||||
}
|
||||
value = props.link
|
||||
}
|
||||
|
@ -2,9 +2,7 @@ package list
|
||||
|
||||
import kotlinx.css.*
|
||||
import kotlinx.html.id
|
||||
import react.RProps
|
||||
import react.rFunction
|
||||
import react.useState
|
||||
import react.*
|
||||
import styled.css
|
||||
import styled.styledDiv
|
||||
import styled.styledH1
|
||||
@ -16,19 +14,25 @@ external interface CoverImageProps : RProps {
|
||||
var coverName: String
|
||||
}
|
||||
|
||||
val CoverImage = rFunction<CoverImageProps>("CoverImage"){ props ->
|
||||
val (coverURL,setCoverURL) = useState(props.coverImageURL)
|
||||
val (coverName,setCoverName) = useState(props.coverName)
|
||||
@Suppress("FunctionName")
|
||||
fun RBuilder.CoverImage(handler: CoverImageProps.() -> Unit): ReactElement {
|
||||
return child(coverImage){
|
||||
attrs {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val coverImage = functionalComponent<CoverImageProps>("CoverImage"){ props ->
|
||||
styledDiv {
|
||||
styledImg(src=coverURL){
|
||||
styledImg(src= props.coverImageURL){
|
||||
css {
|
||||
height = 300.px
|
||||
width = 300.px
|
||||
height = 220.px
|
||||
width = 220.px
|
||||
}
|
||||
}
|
||||
styledH1 {
|
||||
+coverName
|
||||
+props.coverName
|
||||
css {
|
||||
textAlign = TextAlign.center
|
||||
}
|
||||
@ -40,6 +44,7 @@ val CoverImage = rFunction<CoverImageProps>("CoverImage"){ props ->
|
||||
display = Display.flex
|
||||
alignItems = Align.center
|
||||
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
|
||||
|
||||
import com.shabinder.common.list.SpotiFlyerList
|
||||
import com.shabinder.common.list.SpotiFlyerList.State
|
||||
import extras.RenderableComponent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.css.*
|
||||
import kotlinx.html.id
|
||||
import react.RBuilder
|
||||
import react.RState
|
||||
import styled.css
|
||||
import styled.styledDiv
|
||||
|
||||
class ListScreen(
|
||||
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() {
|
||||
|
||||
val result = state.data.queryResult
|
||||
|
||||
styledDiv {
|
||||
attrs {
|
||||
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 {
|
||||
classes = mutableListOf("list-screen")
|
||||
display = Display.flex
|
||||
flexDirection = FlexDirection.column
|
||||
flexGrow = 1.0
|
||||
justifyContent = JustifyContent.center
|
||||
alignItems = Align.center
|
||||
backgroundColor = Color.white
|
||||
alignItems = Align.stretch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import react.RProps
|
||||
import react.rFunction
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import kotlinx.css.*
|
||||
import kotlinx.html.id
|
||||
import kotlinx.html.js.onClickFunction
|
||||
import react.*
|
||||
import styled.*
|
||||
|
||||
external interface TrackItemProps : RProps {
|
||||
var coverImageURL: String
|
||||
var coverName: String
|
||||
var details:TrackDetails
|
||||
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,51 +6,60 @@ import react.*
|
||||
import styled.*
|
||||
|
||||
|
||||
fun RBuilder.navBar(attrs: RProps.() -> Unit): ReactElement{
|
||||
return child(NavBar::class){
|
||||
this.attrs(attrs)
|
||||
@Suppress("FunctionName")
|
||||
fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
|
||||
return child(navBar){
|
||||
attrs {
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalJsExport::class)
|
||||
@JsExport
|
||||
class NavBar : RComponent<RProps, RState>() {
|
||||
external interface NavBarProps:RProps{
|
||||
var isBackVisible: Boolean
|
||||
}
|
||||
|
||||
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 {
|
||||
+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 {
|
||||
src = "spotiflyer.svg"
|
||||
}
|
||||
}
|
||||
styledImg(src = "spotiflyer.svg",alt = "Logo") {
|
||||
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 {
|
||||
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 {
|
||||
height = 42.px
|
||||
width = 42.px
|
||||
}
|
||||
}
|
||||
css {
|
||||
marginLeft = LinearDimension.auto
|
||||
}
|
||||
css {
|
||||
marginLeft = LinearDimension.auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,9 @@ import com.shabinder.common.root.SpotiFlyerRoot.*
|
||||
import extras.RenderableRootComponent
|
||||
import extras.renderableChild
|
||||
import home.HomeScreen
|
||||
import kotlinx.coroutines.launch
|
||||
import list.ListScreen
|
||||
import navbar.NavBar
|
||||
import react.RBuilder
|
||||
import react.RState
|
||||
|
||||
@ -18,6 +20,9 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
|
||||
get() = model.routerState.value.activeChild.component
|
||||
|
||||
override fun RBuilder.render() {
|
||||
NavBar {
|
||||
isBackVisible = (component is Child.List)
|
||||
}
|
||||
when(component){
|
||||
is Child.Main -> renderableChild(HomeScreen::class, (component as Child.Main).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;
|
||||
src: url("pristine_script.ttf");
|
||||
}
|
||||
html {
|
||||
background-image: url("header-dark.jpg");
|
||||
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
body, html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-attachment: fixed;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
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: url("header.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
#appName{
|
||||
font-family: pristine, cursive;
|
||||
@ -90,7 +92,105 @@ body, html {
|
||||
line-height: 40px;
|
||||
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) {
|
||||
.searchBox:hover > .searchInput {
|
||||
width: 150px;
|
||||
|
Loading…
Reference in New Issue
Block a user