Merge branch 'KMP' into imgbot

This commit is contained in:
Shabinder Singh 2021-03-17 12:38:48 +05:30 committed by GitHub
commit 9c1e3ecd84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 874 additions and 153 deletions

View File

@ -0,0 +1,33 @@
name: Build and Publish
on: [ push, pull_request ]
jobs:
build:
name: Test and Build
runs-on: ubuntu-latest
steps:
# Setup Java 1.8 environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.3.1
# Build application
- name: Test and Build
run: ./gradlew :web-app:build
# If main branch update, deploy to gh-pages
- name: Deploy
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/KMP'
uses: JamesIves/github-pages-deploy-action@4.1.0
with:
repository-name: Shabinder/SpotiFlyer
token: adea5c27d4c7ee42dc4010cb8adef92538f6e52d
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: web-app/build/distributions # The folder the action should deploy.
CLEAN: true # Automatically remove deleted files from the deploy branch

View File

@ -0,0 +1,7 @@
package com.shabinder.common.models
sealed class AllPlatforms{
object Js:AllPlatforms()
object Jvm:AllPlatforms()
object Native:AllPlatforms()
}

View File

@ -0,0 +1,31 @@
package com.shabinder.common.models
sealed class CorsProxy(open val url: String){
data class SelfHostedCorsProxy(override val url:String = "https://kind-grasshopper-73.telebit.io/cors/"):CorsProxy(url)
data class PublicProxyWithExtension(override val url:String = "https://cors.bridged.cc/"):CorsProxy(url)
fun toggle(mode:CorsProxy? = null):CorsProxy{
mode?.let {
corsProxy = mode
return corsProxy
}
corsProxy = when(corsProxy){
is SelfHostedCorsProxy -> PublicProxyWithExtension()
is PublicProxyWithExtension -> SelfHostedCorsProxy()
}
return corsProxy
}
fun extensionMode():Boolean{
return when(corsProxy){
is SelfHostedCorsProxy -> false
is PublicProxyWithExtension -> true
}
}
}
/*
* This Var Keeps Track for Cors Config in JS Platform
* Default Self Hosted, However ask user to use extension if possible.
* */
var corsProxy:CorsProxy = CorsProxy.SelfHostedCorsProxy()

View File

@ -11,6 +11,7 @@ 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.di.worker.ForegroundService import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.json.JSONObject import org.json.JSONObject
@ -30,6 +31,7 @@ actual fun openPlatform(packageID:String, platformLink:String){
} }
} }
actual val dispatcherIO = Dispatchers.IO actual val dispatcherIO = Dispatchers.IO
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
actual val isInternetAvailable:Boolean actual val isInternetAvailable:Boolean
get() = internetAvailability.value ?: true get() = internetAvailability.value ?: true

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -13,6 +14,8 @@ expect val dispatcherIO: CoroutineDispatcher
expect val isInternetAvailable:Boolean expect val isInternetAvailable:Boolean
expect val currentPlatform: AllPlatforms
expect suspend fun downloadTracks( expect suspend fun downloadTracks(
list: List<TrackDetails>, list: List<TrackDetails>,
fetcher: FetchPlatformQueryResult, fetcher: FetchPlatformQueryResult,

View File

@ -1,11 +1,19 @@
package com.shabinder.common.di.gaana package com.shabinder.common.di.gaana
import com.shabinder.common.di.currentPlatform
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.corsProxy
import com.shabinder.common.models.gaana.* import com.shabinder.common.models.gaana.*
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
val corsApi get() = if(currentPlatform is AllPlatforms.Js){
corsProxy.url
} // "https://spotiflyer-cors.azurewebsites.net/" //"https://spotiflyer-cors.herokuapp.com/"//"https://cors.bridged.cc/"
else ""
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990" private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
private const val BASE_URL = "https://api.gaana.com/" private val BASE_URL get() = "${corsApi}https://api.gaana.com"
interface GaanaRequests { interface GaanaRequests {
@ -76,6 +84,7 @@ interface GaanaRequests {
"$BASE_URL/?type=$type&subtype=$subtype&seokey=$seokey&token=$TOKEN&format=$format" "$BASE_URL/?type=$type&subtype=$subtype&seokey=$seokey&token=$TOKEN&format=$format"
) )
} }
/* /*
* Api Request: http://api.gaana.com/?type=artist&subtype=artist_track_listing&seokey=neha-kakkar&limit=50&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON * Api Request: http://api.gaana.com/?type=artist&subtype=artist_track_listing&seokey=neha-kakkar&limit=50&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
* *

View File

@ -20,6 +20,7 @@ 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.di.spotify.authenticateSpotify
import com.shabinder.common.models.AllPlatforms
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
@ -43,7 +44,9 @@ class SpotifyProvider(
init { init {
logger.d { "Creating Spotify Provider" } logger.d { "Creating Spotify Provider" }
GlobalScope.launch(Dispatchers.Default) { GlobalScope.launch(Dispatchers.Default) {
authenticateSpotifyClient() if(currentPlatform is AllPlatforms.Js){
authenticateSpotifyClient(override = true)
}else authenticateSpotifyClient()
} }
} }

View File

@ -2,7 +2,11 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.currentPlatform
import com.shabinder.common.di.youtubeMp3.Yt1sMp3 import com.shabinder.common.di.youtubeMp3.Yt1sMp3
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.CorsProxy
import com.shabinder.common.models.corsProxy
import com.shabinder.database.Database import com.shabinder.database.Database
import io.ktor.client.* import io.ktor.client.*
@ -11,5 +15,9 @@ class YoutubeMp3(
private val logger: Kermit, private val logger: Kermit,
private val dir: Dir, private val dir: Dir,
):Yt1sMp3 { ):Yt1sMp3 {
suspend fun getMp3DownloadLink(videoID:String):String? = getLinkFromYt1sMp3(videoID) suspend fun getMp3DownloadLink(videoID:String):String? = getLinkFromYt1sMp3(videoID)?.let{
println("Is Self Hosted"+(corsProxy is CorsProxy.SelfHostedCorsProxy))
if (currentPlatform is AllPlatforms.Js && corsProxy !is CorsProxy.PublicProxyWithExtension) "https://kind-grasshopper-73.telebit.io/cors/$it"
else it
}
} }

View File

@ -1,6 +1,7 @@
package com.shabinder.common.di.providers package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack import com.shabinder.common.models.YoutubeTrack
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
@ -246,7 +247,7 @@ class YoutubeMusic constructor(
} }
private suspend fun getYoutubeMusicResponse(query: String):String{ private suspend fun getYoutubeMusicResponse(query: String):String{
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){ return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
headers{ headers{
append("referer","https://music.youtube.com/search") append("referer","https://music.youtube.com/search")

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di.spotify package com.shabinder.common.di.spotify
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.spotify.Album import com.shabinder.common.models.spotify.Album
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
import com.shabinder.common.models.spotify.Playlist import com.shabinder.common.models.spotify.Playlist
@ -7,7 +8,7 @@ import com.shabinder.common.models.spotify.Track
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
private const val BASE_URL = "https://api.spotify.com/v1" private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
interface SpotifyRequests { interface SpotifyRequests {

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di.youtubeMp3 package com.shabinder.common.di.youtubeMp3
import com.shabinder.common.di.gaana.corsApi
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
@ -19,14 +20,14 @@ interface Yt1sMp3 {
* Downloadable Mp3 Link for YT videoID. * Downloadable Mp3 Link for YT videoID.
* */ * */
suspend fun getLinkFromYt1sMp3(videoID: String):String? = suspend fun getLinkFromYt1sMp3(videoID: String):String? =
getConvertedMp3Link(videoID,getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", ""); getConvertedMp3Link(videoID,getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "")
/* /*
* POST:https://yt1s.com/api/ajaxSearch/index * POST:https://yt1s.com/api/ajaxSearch/index
* Body Form= q:yt video link ,vt:format=mp3 * Body Form= q:yt video link ,vt:format=mp3
* */ * */
private suspend fun getKey(videoID:String):String{ private suspend fun getKey(videoID:String):String{
val response:JsonObject? = httpClient.post("https://yt1s.com/api/ajaxSearch/index"){ val response:JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index"){
body = FormDataContent(Parameters.build { body = FormDataContent(Parameters.build {
append("q","https://www.youtube.com/watch?v=$videoID") append("q","https://www.youtube.com/watch?v=$videoID")
append("vt","mp3") append("vt","mp3")
@ -36,7 +37,7 @@ interface Yt1sMp3 {
} }
private suspend fun getConvertedMp3Link(videoID: String,key:String):JsonObject?{ private suspend fun getConvertedMp3Link(videoID: String,key:String):JsonObject?{
return httpClient.post("https://yt1s.com/api/ajaxConvert/convert"){ return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert"){
body = FormDataContent(Parameters.build { body = FormDataContent(Parameters.build {
append("vid", videoID) append("vid", videoID)
append("k",key) append("k",key)

View File

@ -4,6 +4,7 @@ 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
import com.github.kiulian.downloader.model.quality.AudioQuality import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult 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
@ -18,6 +19,7 @@ import kotlinx.coroutines.withContext
actual fun openPlatform(packageID:String, platformLink:String){ actual fun openPlatform(packageID:String, platformLink:String){
//TODO //TODO
} }
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
actual val dispatcherIO = Dispatchers.IO actual val dispatcherIO = Dispatchers.IO

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult 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
@ -7,7 +8,8 @@ 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 kotlinx.coroutines.flow.collect
import org.khronos.webgl.ArrayBuffer
actual val currentPlatform:AllPlatforms = AllPlatforms.Js
actual fun openPlatform(packageID:String, platformLink:String){ actual fun openPlatform(packageID:String, platformLink:String){
//TODO //TODO
@ -43,13 +45,14 @@ private suspend fun isInternetAvailable(): Boolean {
actual val isInternetAvailable:Boolean actual val isInternetAvailable:Boolean
get(){ get(){
return true return true
var result = false /*var result = false
val job = GlobalScope.launch { result = isInternetAvailable() } val job = GlobalScope.launch { result = isInternetAvailable() }
while(job.isActive){} while(job.isActive){}
return result return result*/
} }
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1) val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
val allTracksStatus: HashMap<String, DownloadStatus> = hashMapOf()
actual suspend fun downloadTracks( actual suspend fun downloadTracks(
list: List<TrackDetails>, list: List<TrackDetails>,
@ -58,24 +61,29 @@ actual suspend fun downloadTracks(
){ ){
withContext(Dispatchers.Default){ withContext(Dispatchers.Default){
list.forEach { list.forEach {
allTracksStatus[it.title] = DownloadStatus.Queued
if (!it.videoID.isNullOrBlank()) {//Video ID already known! if (!it.videoID.isNullOrBlank()) {//Video ID already known!
downloadTrack(it.videoID!!, it, fetcher, dir) downloadTrack(it.videoID!!, it, fetcher, dir)
} else { } else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it) val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
if (videoID.isNullOrBlank()) { if (videoID.isNullOrBlank()) {
allTracksStatus[it.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
} else {//Found Youtube Video ID } else {//Found Youtube Video ID
downloadTrack(videoID, it, fetcher, dir) downloadTrack(videoID, it, fetcher, dir)
} }
} }
} }
DownloadProgressFlow.emit(allTracksStatus)
} }
} }
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) { suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID) val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if(url == null){ if(url == null){
// TODO Handle allTracksStatus[track.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
println("No URL to Download") println("No URL to Download")
}else { }else {
downloadFile(url).collect { downloadFile(url).collect {
@ -84,9 +92,16 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPla
println("Download Completed") println("Download Completed")
dir.saveFileWithMetadata(it.byteArray, track) dir.saveFileWithMetadata(it.byteArray, track)
} }
is DownloadResult.Error -> println("Download Error: ${track.title}") is DownloadResult.Error -> {
is DownloadResult.Progress -> println("Download Progress: ${it.progress} : ${track.title}") allTracksStatus[track.title] = DownloadStatus.Failed
} println("Download Error: ${track.title}")
}
is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
println("Download Progress: ${it.progress} : ${track.title}")
}
}
DownloadProgressFlow.emit(allTracksStatus)
} }
} }
} }

View File

@ -1,7 +1,9 @@
package com.shabinder.common.di package com.shabinder.common.di
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
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.Object
@ -50,7 +52,7 @@ actual class Dir actual constructor(
trackDetails: TrackDetails trackDetails: TrackDetails
) { ) {
val writer = ID3Writer(mp3ByteArray.toArrayBuffer()) val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
val albumArt = downloadFile(trackDetails.albumArtURL) val albumArt = downloadFile(corsApi+trackDetails.albumArtURL)
albumArt.collect { albumArt.collect {
when(it){ when(it){
is DownloadResult.Success -> { is DownloadResult.Success -> {
@ -74,7 +76,7 @@ actual class Dir actual constructor(
private suspend fun writeTagsAndSave(writer:ID3Writer, albumArt:Object?, trackDetails: TrackDetails){ private suspend fun writeTagsAndSave(writer:ID3Writer, albumArt:Object?, trackDetails: TrackDetails){
writer.apply { writer.apply {
setFrame("TIT2", trackDetails.title) setFrame("TIT2", trackDetails.title)
setFrame("TPE1", trackDetails.artists) setFrame("TPE1", trackDetails.artists.toTypedArray())
setFrame("TALB", trackDetails.albumName?:"") setFrame("TALB", trackDetails.albumName?:"")
try{trackDetails.year?.substring(0,4)?.toInt()?.let { setFrame("TYER", it) }} catch(e:Exception){} try{trackDetails.year?.substring(0,4)?.toInt()?.let { setFrame("TYER", it) }} catch(e:Exception){}
setFrame("TPE2", trackDetails.artists.joinToString(",")) setFrame("TPE2", trackDetails.artists.joinToString(","))
@ -83,7 +85,9 @@ actual class Dir actual constructor(
albumArt?.let { setFrame("APIC", it) } albumArt?.let { setFrame("APIC", it) }
} }
writer.addTag() writer.addTag()
allTracksStatus[trackDetails.title] = DownloadStatus.Downloaded
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3") saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
DownloadProgressFlow.emit(allTracksStatus)
} }
actual fun addToLibrary(path:String){} actual fun addToLibrary(path:String){}

View File

@ -42,6 +42,7 @@ interface SpotiFlyerMain {
val dir: Dir val dir: Dir
val showPopUpMessage:(String)->Unit val showPopUpMessage:(String)->Unit
} }
sealed class Output { sealed class Output {
data class Search(val link: String) : Output() data class Search(val link: String) : Output()
} }

View File

@ -2,4 +2,5 @@ package com.shabinder.common.root.callbacks
interface SpotiFlyerRootCallBacks { interface SpotiFlyerRootCallBacks {
fun searchLink(link:String) fun searchLink(link:String)
fun popBackToHomeScreen()
} }

View File

@ -29,6 +29,11 @@ internal class SpotiFlyerRootImpl(
override val callBacks = object : SpotiFlyerRootCallBacks{ override val callBacks = object : SpotiFlyerRootCallBacks{
override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link)) override fun searchLink(link: String) = onMainOutput(SpotiFlyerMain.Output.Search(link))
override fun popBackToHomeScreen() {
router.popWhile {
it !is Configuration.Main
}
}
} }
private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child = private fun createChild(configuration: Configuration, componentContext: ComponentContext): Child =

View File

@ -1,4 +1,4 @@
package com.willowtreeapps.fuzzywuzzy package kotlin.com.willowtree.fuzzywuzzy
import com.willowtreeapps.fuzzywuzzy.diffutils.Extractor import com.willowtreeapps.fuzzywuzzy.diffutils.Extractor
import com.willowtreeapps.fuzzywuzzy.diffutils.algorithms.WeightedRatio import com.willowtreeapps.fuzzywuzzy.diffutils.algorithms.WeightedRatio

View File

@ -1,4 +1,4 @@
package com.willowtreeapps.fuzzywuzzy package kotlin.com.willowtree.fuzzywuzzy
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch

View File

@ -0,0 +1 @@
temp

View File

@ -6,11 +6,10 @@ import com.arkivanov.decompose.lifecycle.resume
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.di.DownloadProgressFlow
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.database.Database import com.shabinder.database.Database
import extras.renderableChild import extras.renderableChild
import kotlinx.coroutines.flow.MutableSharedFlow
import react.* import react.*
import root.RootR import root.RootR
@ -40,8 +39,7 @@ class App(props: AppProps): RComponent<AppProps, RState>(props) {
override val directories = dependencies.directories override val directories = dependencies.directories
override val database: Database? = directories.db override val database: Database? = directories.db
override val showPopUpMessage: (String) -> Unit = {}//TODO override val showPopUpMessage: (String) -> Unit = {}//TODO
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> override val downloadProgressReport = DownloadProgressFlow
= MutableSharedFlow(1)
} }
) )

View File

@ -10,7 +10,6 @@ val colorOffWhite = Color("#E7E7E7")
object Styles: StyleSheet("Searchbar", isStatic = true) { object Styles: StyleSheet("Searchbar", isStatic = true) {
val makeRow by css { val makeRow by css {
display = Display.flex display = Display.flex
flexDirection = FlexDirection.row
alignItems = Align.center alignItems = Align.center
alignContent = Align.center alignContent = Align.center
justifyContent = JustifyContent.center justifyContent = JustifyContent.center

View File

@ -1,10 +1,14 @@
package home package home
import com.shabinder.common.di.currentPlatform
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 com.shabinder.common.models.AllPlatforms
import extras.RenderableComponent import extras.RenderableComponent
import kotlinx.browser.document
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.css.* import kotlinx.css.*
import kotlinx.dom.appendElement
import react.* import react.*
import styled.css import styled.css
import styled.styledDiv import styled.styledDiv
@ -15,11 +19,23 @@ class HomeScreen(
props, props,
initialState = State() initialState = State()
) { ) {
override fun componentDidMount() {
super.componentDidMount()
val form = document.getElementById("razorpay-form")!!
repeat(form.childNodes.length){
form.childNodes.item(0)?.let { it1 -> form.removeChild(it1) }
form.childNodes.item(it)?.let { it1 -> form.removeChild(it1) }
}
form.appendElement("script"){
this.setAttribute("src","https://checkout.razorpay.com/v1/payment-button.js")
this.setAttribute("async", true.toString())
this.setAttribute("data-payment_button_id", "pl_GnKuuDBdBu0ank")
}
}
override val stateFlow: Flow<SpotiFlyerMain.State> = model.models override val stateFlow: Flow<SpotiFlyerMain.State> = model.models
override fun RBuilder.render() { override fun RBuilder.render() {
println("Rendering New State = \"${state.data}\" ")
styledDiv{ styledDiv{
css { css {
display = Display.flex display = Display.flex
@ -55,8 +71,8 @@ class HomeScreen(
private val platformIconList = mapOf( private val platformIconList = mapOf(
"spotify.svg" to "https://open.spotify.com/", "spotify.svg" to "https://open.spotify.com/",
"gaana.svg" to "https://www.gaana.com/", "gaana.svg" to "https://www.gaana.com/",
"youtube.svg" to "https://www.youtube.com/", //"youtube.svg" to "https://www.youtube.com/",
"youtube_music.svg" to "https://music.youtube.com/" //"youtube_music.svg" to "https://music.youtube.com/"
) )
private val badges = mapOf( private val badges = mapOf(
"https://img.shields.io/github/v/release/Shabinder/SpotiFlyer?color=7885FF&label=SpotiFlyer&logo=android&style=for-the-badge" "https://img.shields.io/github/v/release/Shabinder/SpotiFlyer?color=7885FF&label=SpotiFlyer&logo=android&style=for-the-badge"

View File

@ -1,11 +1,13 @@
package home package home
import kotlinx.browser.document
import kotlinx.css.* import kotlinx.css.*
import kotlinx.dom.appendElement
import kotlinx.dom.createElement
import kotlinx.html.SCRIPT
import kotlinx.html.id
import react.* import react.*
import styled.css import styled.*
import styled.styledA
import styled.styledDiv
import styled.styledImg
external interface IconListProps : RProps { external interface IconListProps : RProps {
var iconsAndPlatforms: Map<String,String> var iconsAndPlatforms: Map<String,String>
@ -22,16 +24,26 @@ fun RBuilder.IconList(handler:IconListProps.() -> Unit): ReactElement {
} }
private val iconList = functionalComponent<IconListProps>("IconList") { props -> private val iconList = functionalComponent<IconListProps>("IconList") { props ->
styledDiv { styledDiv {
css { css {
+Styles.makeRow
margin(18.px) margin(18.px)
if(props.isBadge) { if(props.isBadge) {
alignItems = Align.end classes = mutableListOf("info-banners")
} }
+ Styles.makeRow
} }
val firstElem = props.iconsAndPlatforms.keys.elementAt(1)
for((icon,platformLink) in props.iconsAndPlatforms){ for((icon,platformLink) in props.iconsAndPlatforms){
styledA(href = platformLink){ if(icon == firstElem && props.isBadge){
//<form><script src="https://checkout.razorpay.com/v1/payment-button.js" data-payment_button_id="pl_GnKuuDBdBu0ank" async> </script> </form>
styledForm {
attrs{
id = "razorpay-form"
}
}
}
styledA(href = platformLink,target="_blank"){
styledImg { styledImg {
attrs { attrs {
src = icon src = icon

View File

@ -25,7 +25,7 @@ private val message = functionalComponent<MessageProps>("Message") { props->
+ props.text + props.text
css { css {
classes = mutableListOf("headingTitle") classes = mutableListOf("headingTitle")
fontSize = 3.2.rem fontSize = 2.6.em
} }
} }
} }

View File

@ -1,9 +1,12 @@
package home package home
import kotlinx.browser.window
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onKeyDownFunction
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Window
import react.* import react.*
import styled.* import styled.*
@ -34,6 +37,12 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
val target = it.target as HTMLInputElement val target = it.target as HTMLInputElement
props.onLinkChange(target.value) props.onLinkChange(target.value)
} }
this.onKeyDownFunction = {
if(it.asDynamic().key == "Enter") {
if(props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms")
else props.search(props.link)
}
}
value = props.link value = props.link
} }
css { css {
@ -43,7 +52,8 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
styledButton { styledButton {
attrs { attrs {
onClickFunction = { onClickFunction = {
props.search(props.link) if(props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms")
else props.search(props.link)
} }
} }
css { css {

View File

@ -0,0 +1,40 @@
package list
import kotlinx.css.*
import react.*
import styled.css
import styled.styledDiv
import styled.styledSpan
@Suppress("FunctionName")
fun RBuilder.CircularProgressBar(handler: CircularProgressBarProps.() -> Unit): ReactElement {
return child(circularProgressBar){
attrs {
handler()
}
}
}
external interface CircularProgressBarProps : RProps {
var progress:Int
}
private val circularProgressBar = functionalComponent<CircularProgressBarProps>("Circular-Progress-Bar") { props->
styledDiv {
styledSpan { +"${props.progress}%" }
styledDiv{
css {
classes = mutableListOf("left-half-clipper")
}
styledDiv{ css { classes = mutableListOf("first50-bar") } }
styledDiv{ css { classes = mutableListOf("value-bar") } }
}
css{
display = Display.flex
justifyContent = JustifyContent.center
classes = mutableListOf("progress-circle","p${props.progress}").apply { if(props.progress>50) add("over50") }
width = 50.px
marginBottom = 65.px
}
}
}

View File

@ -2,6 +2,7 @@ package list
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.id import kotlinx.html.id
import kotlinx.html.js.onClickFunction
import react.* import react.*
import styled.css import styled.css
import styled.styledDiv import styled.styledDiv
@ -10,6 +11,8 @@ import styled.styledImg
external interface DownloadAllButtonProps : RProps { external interface DownloadAllButtonProps : RProps {
var isActive:Boolean var isActive:Boolean
var link : String
var downloadAll:()->Unit
} }
@Suppress("FunctionName") @Suppress("FunctionName")
@ -22,7 +25,33 @@ fun RBuilder.DownloadAllButton(handler: DownloadAllButtonProps.() -> Unit): Reac
} }
private val downloadAllButton = functionalComponent<DownloadAllButtonProps>("DownloadAllButton") { props-> private val downloadAllButton = functionalComponent<DownloadAllButtonProps>("DownloadAllButton") { props->
val (isClicked,setClicked) = useState(false)
useEffect(mutableListOf(props.link)){
setClicked(false)
}
if(props.isActive){
if(isClicked) {
styledDiv{ styledDiv{
css {
display = Display.flex
alignItems = Align.center
justifyContent = JustifyContent.center
height = 52.px
}
LoadingSpinner { }
}
}
else{
styledDiv {
attrs {
onClickFunction = {
//props.downloadAll()
setClicked(true)
}
}
styledDiv { styledDiv {
styledImg(src = "download.svg",alt = "Download All Button") { styledImg(src = "download.svg",alt = "Download All Button") {
@ -51,8 +80,10 @@ private val downloadAllButton = functionalComponent<DownloadAllButtonProps>("Dow
} }
css { css {
classes = mutableListOf("download-button") classes = mutableListOf("download-button")
display = if(props.isActive) Display.flex else Display.none display = Display.flex
alignItems = Align.center alignItems = Align.center
} }
} }
} }
}
}

View File

@ -0,0 +1,49 @@
package list
import com.shabinder.common.models.DownloadStatus
import kotlinx.css.*
import kotlinx.html.js.onClickFunction
import react.*
import styled.css
import styled.styledDiv
import styled.styledImg
@Suppress("FunctionName")
fun RBuilder.DownloadButton(handler: DownloadButtonProps.() -> Unit): ReactElement {
return child(downloadButton){
attrs {
handler()
}
}
}
external interface DownloadButtonProps : RProps {
var onClick:()->Unit
var status :DownloadStatus
}
private val downloadButton = functionalComponent<DownloadButtonProps>("Circular-Progress-Bar") { props->
styledDiv {
val src = when(props.status){
is DownloadStatus.NotDownloaded -> "download-gradient.svg"
is DownloadStatus.Downloaded -> "check.svg"
is DownloadStatus.Failed -> "error.svg"
else -> ""
}
styledImg(src = src) {
attrs {
onClickFunction = {
props.onClick()
}
}
css {
width = (2.5).em
margin(8.px)
}
}
css {
classes = mutableListOf("glow-button")
borderRadius = 100.px
}
}
}

View File

@ -9,6 +9,7 @@ import kotlinx.html.id
import react.RBuilder import react.RBuilder
import styled.css import styled.css
import styled.styledDiv import styled.styledDiv
import styled.styledSection
class ListScreen( class ListScreen(
props: Props<SpotiFlyerList>, props: Props<SpotiFlyerList>,
@ -20,9 +21,9 @@ class ListScreen(
val result = state.data.queryResult val result = state.data.queryResult
styledDiv { styledSection {
attrs { attrs {
id = "list-screen-div" id = "list-screen"
} }
if(result == null){ if(result == null){
@ -34,9 +35,20 @@ class ListScreen(
} }
DownloadAllButton { DownloadAllButton {
isActive = state.data.trackList.isNotEmpty() isActive = state.data.trackList.size > 1
downloadAll = {
model.onDownloadAllClicked(state.data.trackList)
}
link = state.data.link
} }
styledDiv{
css {
display =Display.flex
flexGrow = 1.0
flexDirection = FlexDirection.column
color = Color.white
}
state.data.trackList.forEachIndexed{ index, trackDetails -> state.data.trackList.forEachIndexed{ index, trackDetails ->
TrackItem { TrackItem {
details = trackDetails details = trackDetails
@ -44,14 +56,14 @@ class ListScreen(
} }
} }
} }
}
css { css {
classes = mutableListOf("list-screen") classes = mutableListOf("list-screen")
display = Display.flex display = Display.flex
padding(8.px)
flexDirection = FlexDirection.column flexDirection = FlexDirection.column
flexGrow = 1.0 flexGrow = 1.0
justifyContent = JustifyContent.center
alignItems = Align.stretch
} }
} }
} }

View File

@ -17,7 +17,12 @@ fun RBuilder.LoadingAnim(handler: RProps.() -> Unit): ReactElement {
private val loadingAnim = functionalComponent<RProps>("Loading Animation") { private val loadingAnim = functionalComponent<RProps>("Loading Animation") {
styledDiv{ styledDiv{
css {
flexGrow = 1.0
display = Display.flex
alignItems = Align.center
}
styledDiv {
styledDiv { css { classes = mutableListOf("sk-cube sk-cube1") } } styledDiv { css { classes = mutableListOf("sk-cube sk-cube1") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube2") } } styledDiv { css { classes = mutableListOf("sk-cube sk-cube2") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube3") } } styledDiv { css { classes = mutableListOf("sk-cube sk-cube3") } }
@ -35,3 +40,4 @@ private val loadingAnim = functionalComponent<RProps>("Loading Animation") {
} }
} }
} }
}

View File

@ -0,0 +1,31 @@
package list
import kotlinx.css.marginRight
import kotlinx.css.px
import kotlinx.css.width
import react.*
import styled.css
import styled.styledDiv
@Suppress("FunctionName")
fun RBuilder.LoadingSpinner(handler: RProps.() -> Unit): ReactElement {
return child(loadingSpinner){
attrs {
handler()
}
}
}
private val loadingSpinner = functionalComponent<RProps>("Loading-Spinner") {
styledDiv {
styledDiv{}
styledDiv{}
styledDiv{}
styledDiv{}
css{
classes = mutableListOf("lds-ring")
width = 50.px
marginRight = 8.px
}
}
}

View File

@ -1,9 +1,9 @@
package list package list
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.id import kotlinx.html.id
import kotlinx.html.js.onClickFunction
import react.* import react.*
import styled.* import styled.*
@ -22,9 +22,12 @@ fun RBuilder.TrackItem(handler: TrackItemProps.() -> Unit): ReactElement {
} }
private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props -> private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props ->
val (downloadStatus,setDownloadStatus) = useState(props.details.downloaded)
val details = props.details val details = props.details
useEffect(listOf(props.details)){
setDownloadStatus(props.details.downloaded)
}
styledDiv { styledDiv {
styledImg(src = details.albumArtURL) { styledImg(src = details.albumArtURL) {
css { css {
height = 90.px height = 90.px
@ -36,72 +39,102 @@ private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props
attrs { attrs {
id = "text-details" id = "text-details"
} }
css {
flexGrow = 1.0
minWidth = 0.px
display = Display.flex
flexDirection = FlexDirection.column
margin(8.px)
}
styledDiv{ styledDiv{
css {
height = 40.px
alignItems = Align.center
display = Display.flex
}
styledH3 { styledH3 {
+ details.title + details.title
css { css {
padding(8.px) padding(8.px)
fontSize = 1.3.em
textOverflow = TextOverflow.ellipsis
whiteSpace = WhiteSpace.nowrap
overflow = Overflow.hidden
} }
} }
css {
height = 40.px
display =Display.flex
alignItems = Align.center
}
} }
styledDiv { styledDiv {
css {
height = 40.px
alignItems = Align.center
display = Display.flex
}
styledH4 { styledH4 {
+ details.artists.joinToString(",") + details.artists.joinToString(",")
css { css {
flexGrow = 1.0 flexGrow = 1.0
padding(8.px) padding(8.px)
minWidth = 4.em
fontSize = 1.1.em
textOverflow = TextOverflow.ellipsis
whiteSpace = WhiteSpace.nowrap
overflow = Overflow.hidden
} }
} }
styledH4 { styledH4 {
+ "${details.durationSec} sec"
css { css {
textAlign = TextAlign.end
flexGrow = 1.0 flexGrow = 1.0
padding(8.px) padding(8.px)
textAlign = TextAlign.right minWidth = 4.em
fontSize = 1.1.em
textOverflow = TextOverflow.ellipsis
whiteSpace = WhiteSpace.nowrap
overflow = Overflow.hidden
}
+ "${details.durationSec/60} min, ${details.durationSec%60} sec"
} }
} }
css {
height = 40.px
display =Display.flex
alignItems = Align.center
} }
} when(downloadStatus){
css { is DownloadStatus.NotDownloaded ->{
display = Display.flex DownloadButton {
flexGrow = 1.0 onClick = {
flexDirection = FlexDirection.column setDownloadStatus(DownloadStatus.Queued)
margin(8.px)
}
}
styledDiv {
styledImg(src = "download-gradient.svg") {
attrs {
onClickFunction = {
props.downloadTrack(details) props.downloadTrack(details)
} }
} status = downloadStatus
css {
margin(8.px)
} }
} }
css { is DownloadStatus.Downloading -> {
classes = mutableListOf("glow-button") CircularProgressBar {
borderRadius = 100.px progress = downloadStatus.progress
width = 65.px }
}
DownloadStatus.Queued -> {
LoadingSpinner {}
}
DownloadStatus.Downloaded -> {
DownloadButton {
onClick = {}
status = downloadStatus
}
}
DownloadStatus.Converting -> {
LoadingSpinner {}
}
DownloadStatus.Failed -> {
DownloadButton {
onClick = {}
status = downloadStatus
}
} }
} }
css { css {
alignItems = Align.center alignItems = Align.center
display =Display.flex display =Display.flex
flexDirection = FlexDirection.row paddingRight = 16.px
flexGrow = 1.0
color = Color.white
} }
} }
} }

View File

@ -2,10 +2,11 @@ package navbar
import kotlinx.css.* import kotlinx.css.*
import kotlinx.html.id import kotlinx.html.id
import kotlinx.html.js.onBlurFunction
import kotlinx.html.js.onClickFunction
import react.* import react.*
import styled.* import styled.*
@Suppress("FunctionName") @Suppress("FunctionName")
fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{ fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
return child(navBar){ return child(navBar){
@ -17,14 +18,25 @@ fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
external interface NavBarProps:RProps{ external interface NavBarProps:RProps{
var isBackVisible: Boolean var isBackVisible: Boolean
var popBackToHomeScreen: () -> Unit
} }
private val navBar = functionalComponent<NavBarProps>("NavBar") { props -> private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
styledNav { styledNav {
css { css {
+NavBarStyles.nav +NavBarStyles.nav
} }
styledDiv{
attrs {
onClickFunction = {
props.popBackToHomeScreen()
}
onBlurFunction = {
props.popBackToHomeScreen()
}
}
styledImg(src = "left-arrow.svg",alt = "Back Arrow"){ styledImg(src = "left-arrow.svg",alt = "Back Arrow"){
css { css {
height = 42.px height = 42.px
@ -34,6 +46,12 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
marginRight = 12.px marginRight = 12.px
} }
} }
}
styledA(href = "https://shabinder.github.io/SpotiFlyer/",target="_blank") {
css {
display = Display.flex
alignItems = Align.center
}
styledImg(src = "spotiflyer.svg",alt = "Logo") { styledImg(src = "spotiflyer.svg",alt = "Logo") {
css { css {
height = 42.px height = 42.px
@ -50,6 +68,46 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
margin(horizontal = 14.px) margin(horizontal = 14.px)
} }
} }
}
/*val (corsMode,setCorsMode) = useState(CorsProxy.SelfHostedCorsProxy() as CorsProxy)
useEffect {
setCorsMode(corsProxy)
}*/
styledDiv{
/*styledH4 { + "Extension" }
styledDiv {
styledInput(type = InputType.checkBox) {
attrs{
id = "cmn-toggle-4"
value = "Extension"
checked = corsMode.extensionMode()
onChangeFunction = {
val state = it.target as HTMLInputElement
if(state.checked){
setCorsMode(corsProxy.toggle(CorsProxy.PublicProxyWithExtension()))
} else{
setCorsMode(corsProxy.toggle(CorsProxy.SelfHostedCorsProxy()))
}
println("Active Proxy: ${corsProxy.url}")
}
}
css{
classes = mutableListOf("cmn-toggle","cmn-toggle-round-flat")
}
}
styledLabel { attrs { htmlFor = "cmn-toggle-4" } }
css{
classes = mutableListOf("switch")
marginLeft = 8.px
marginRight = 16.px
}
}*/
styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){ styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){
styledImg(src = "github.svg"){ styledImg(src = "github.svg"){
css { css {
@ -57,7 +115,10 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
width = 42.px width = 42.px
} }
} }
}
css { css {
display = Display.flex
alignItems = Align.center
marginLeft = LinearDimension.auto marginLeft = LinearDimension.auto
} }
} }

View File

@ -6,7 +6,6 @@ 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 navbar.NavBar
import react.RBuilder import react.RBuilder
@ -19,9 +18,12 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
private val component: Child private val component: Child
get() = model.routerState.value.activeChild.component get() = model.routerState.value.activeChild.component
private val callBacks get() = model.callBacks
override fun RBuilder.render() { override fun RBuilder.render() {
NavBar { NavBar {
isBackVisible = (component is Child.List) isBackVisible = (component is Child.List)
popBackToHomeScreen = callBacks::popBackToHomeScreen
} }
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)
@ -33,7 +35,7 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
model.routerState.bindToState { routerState = it } model.routerState.bindToState { routerState = it }
} }
class State( class State(
var routerState: RouterState<*, Child>, var routerState: RouterState<*, Child>
) : RState ) : RState
} }

View File

@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 -6826)" gradientUnits="userSpaceOnUse" x1="0" x2="512" y1="-7082" y2="-7082"><stop offset="0" stop-color="#31d8ff"/><stop offset="1" stop-color="#FC5C7D"/></linearGradient><path d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0" fill="url(#a)"/><path d="m175 395.246094c-4.035156 0-7.902344-1.628906-10.726562-4.511719l-81-82.832031c-5.789063-5.921875-5.683594-15.417969.238281-21.210938 5.921875-5.792968 15.417969-5.6875 21.210937.238282l70.273438 71.859374 232.277344-237.523437c5.792968-5.921875 15.289062-6.027344 21.210937-.234375 5.925781 5.789062 6.03125 15.289062.238281 21.210938l-243 248.492187c-2.820312 2.882813-6.6875 4.511719-10.722656 4.511719zm0 0" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 923 B

View File

@ -0,0 +1,167 @@
.progress-circle {
margin: 20px;
}
.progress-circle:after{
border: none;
position: absolute;
text-align: center;
display: block;
border-radius: 50%;
width: 4.3em;
height: 4.3em;
background-color: white;
content: " ";
margin-top: 0.35em;
}
/* Text inside the control */
.progress-circle span {
position: absolute;
line-height: 5em;
width: 5em;
text-align: center;
display: block;
color: #53777A;
z-index: 2;
}
.left-half-clipper {
/* a round circle */
border-radius: 50%;
width: 5em;
height: 5em;
position: absolute; /* needed for clipping */
clip: rect(0, 5em, 5em, 2.5em); /* clips the whole left half*/
}
/* when p>50, don't clip left half*/
.progress-circle.over50 .left-half-clipper {
clip: rect(auto,auto,auto,auto);
}
.value-bar {
/*This is an overlayed square, that is made round with the border radius,
then it is cut to display only the left half, then rotated clockwise
to escape the outer clipping path.*/
position: absolute; /*needed for clipping*/
clip: rect(0, 2.5em, 5em, 0);
width: 5em;
height: 5em;
border-radius: 50%;
border: 0.45em solid #53777A; /*The border is 0.35 but making it larger removes visual artifacts */
/*background-color: #4D642D;*/ /* for debug */
box-sizing: border-box;
}
/* Progress bar filling the whole right half for values above 50% */
.progress-circle.over50 .first50-bar {
/*Progress bar for the first 50%, filling the whole right half*/
position: absolute; /*needed for clipping*/
clip: rect(0, 5em, 5em, 2.5em);
background-color: #53777A;
border-radius: 50%;
width: 5em;
height: 5em;
}
.progress-circle:not(.over50) .first50-bar{ display: none; }
/* Progress bar rotation position */
.progress-circle.p0 .value-bar { display: none; }
.progress-circle.p1 .value-bar { transform: rotate(4deg); }
.progress-circle.p2 .value-bar { transform: rotate(7deg); }
.progress-circle.p3 .value-bar { transform: rotate(11deg); }
.progress-circle.p4 .value-bar { transform: rotate(14deg); }
.progress-circle.p5 .value-bar { transform: rotate(18deg); }
.progress-circle.p6 .value-bar { transform: rotate(22deg); }
.progress-circle.p7 .value-bar { transform: rotate(25deg); }
.progress-circle.p8 .value-bar { transform: rotate(29deg); }
.progress-circle.p9 .value-bar { transform: rotate(32deg); }
.progress-circle.p10 .value-bar { transform: rotate(36deg); }
.progress-circle.p11 .value-bar { transform: rotate(40deg); }
.progress-circle.p12 .value-bar { transform: rotate(43deg); }
.progress-circle.p13 .value-bar { transform: rotate(47deg); }
.progress-circle.p14 .value-bar { transform: rotate(50deg); }
.progress-circle.p15 .value-bar { transform: rotate(54deg); }
.progress-circle.p16 .value-bar { transform: rotate(58deg); }
.progress-circle.p17 .value-bar { transform: rotate(61deg); }
.progress-circle.p18 .value-bar { transform: rotate(65deg); }
.progress-circle.p19 .value-bar { transform: rotate(68deg); }
.progress-circle.p20 .value-bar { transform: rotate(72deg); }
.progress-circle.p21 .value-bar { transform: rotate(76deg); }
.progress-circle.p22 .value-bar { transform: rotate(79deg); }
.progress-circle.p23 .value-bar { transform: rotate(83deg); }
.progress-circle.p24 .value-bar { transform: rotate(86deg); }
.progress-circle.p25 .value-bar { transform: rotate(90deg); }
.progress-circle.p26 .value-bar { transform: rotate(94deg); }
.progress-circle.p27 .value-bar { transform: rotate(97deg); }
.progress-circle.p28 .value-bar { transform: rotate(101deg); }
.progress-circle.p29 .value-bar { transform: rotate(104deg); }
.progress-circle.p30 .value-bar { transform: rotate(108deg); }
.progress-circle.p31 .value-bar { transform: rotate(112deg); }
.progress-circle.p32 .value-bar { transform: rotate(115deg); }
.progress-circle.p33 .value-bar { transform: rotate(119deg); }
.progress-circle.p34 .value-bar { transform: rotate(122deg); }
.progress-circle.p35 .value-bar { transform: rotate(126deg); }
.progress-circle.p36 .value-bar { transform: rotate(130deg); }
.progress-circle.p37 .value-bar { transform: rotate(133deg); }
.progress-circle.p38 .value-bar { transform: rotate(137deg); }
.progress-circle.p39 .value-bar { transform: rotate(140deg); }
.progress-circle.p40 .value-bar { transform: rotate(144deg); }
.progress-circle.p41 .value-bar { transform: rotate(148deg); }
.progress-circle.p42 .value-bar { transform: rotate(151deg); }
.progress-circle.p43 .value-bar { transform: rotate(155deg); }
.progress-circle.p44 .value-bar { transform: rotate(158deg); }
.progress-circle.p45 .value-bar { transform: rotate(162deg); }
.progress-circle.p46 .value-bar { transform: rotate(166deg); }
.progress-circle.p47 .value-bar { transform: rotate(169deg); }
.progress-circle.p48 .value-bar { transform: rotate(173deg); }
.progress-circle.p49 .value-bar { transform: rotate(176deg); }
.progress-circle.p50 .value-bar { transform: rotate(180deg); }
.progress-circle.p51 .value-bar { transform: rotate(184deg); }
.progress-circle.p52 .value-bar { transform: rotate(187deg); }
.progress-circle.p53 .value-bar { transform: rotate(191deg); }
.progress-circle.p54 .value-bar { transform: rotate(194deg); }
.progress-circle.p55 .value-bar { transform: rotate(198deg); }
.progress-circle.p56 .value-bar { transform: rotate(202deg); }
.progress-circle.p57 .value-bar { transform: rotate(205deg); }
.progress-circle.p58 .value-bar { transform: rotate(209deg); }
.progress-circle.p59 .value-bar { transform: rotate(212deg); }
.progress-circle.p60 .value-bar { transform: rotate(216deg); }
.progress-circle.p61 .value-bar { transform: rotate(220deg); }
.progress-circle.p62 .value-bar { transform: rotate(223deg); }
.progress-circle.p63 .value-bar { transform: rotate(227deg); }
.progress-circle.p64 .value-bar { transform: rotate(230deg); }
.progress-circle.p65 .value-bar { transform: rotate(234deg); }
.progress-circle.p66 .value-bar { transform: rotate(238deg); }
.progress-circle.p67 .value-bar { transform: rotate(241deg); }
.progress-circle.p68 .value-bar { transform: rotate(245deg); }
.progress-circle.p69 .value-bar { transform: rotate(248deg); }
.progress-circle.p70 .value-bar { transform: rotate(252deg); }
.progress-circle.p71 .value-bar { transform: rotate(256deg); }
.progress-circle.p72 .value-bar { transform: rotate(259deg); }
.progress-circle.p73 .value-bar { transform: rotate(263deg); }
.progress-circle.p74 .value-bar { transform: rotate(266deg); }
.progress-circle.p75 .value-bar { transform: rotate(270deg); }
.progress-circle.p76 .value-bar { transform: rotate(274deg); }
.progress-circle.p77 .value-bar { transform: rotate(277deg); }
.progress-circle.p78 .value-bar { transform: rotate(281deg); }
.progress-circle.p79 .value-bar { transform: rotate(284deg); }
.progress-circle.p80 .value-bar { transform: rotate(288deg); }
.progress-circle.p81 .value-bar { transform: rotate(292deg); }
.progress-circle.p82 .value-bar { transform: rotate(295deg); }
.progress-circle.p83 .value-bar { transform: rotate(299deg); }
.progress-circle.p84 .value-bar { transform: rotate(302deg); }
.progress-circle.p85 .value-bar { transform: rotate(306deg); }
.progress-circle.p86 .value-bar { transform: rotate(310deg); }
.progress-circle.p87 .value-bar { transform: rotate(313deg); }
.progress-circle.p88 .value-bar { transform: rotate(317deg); }
.progress-circle.p89 .value-bar { transform: rotate(320deg); }
.progress-circle.p90 .value-bar { transform: rotate(324deg); }
.progress-circle.p91 .value-bar { transform: rotate(328deg); }
.progress-circle.p92 .value-bar { transform: rotate(331deg); }
.progress-circle.p93 .value-bar { transform: rotate(335deg); }
.progress-circle.p94 .value-bar { transform: rotate(338deg); }
.progress-circle.p95 .value-bar { transform: rotate(342deg); }
.progress-circle.p96 .value-bar { transform: rotate(346deg); }
.progress-circle.p97 .value-bar { transform: rotate(349deg); }
.progress-circle.p98 .value-bar { transform: rotate(353deg); }
.progress-circle.p99 .value-bar { transform: rotate(356deg); }
.progress-circle.p100 .value-bar { transform: rotate(360deg); }

View File

@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 -12146)" gradientUnits="userSpaceOnUse" x1="0" x2="512" y1="-12402" y2="-12402"><stop offset="0" stop-color="#9AB3FF"/><stop offset="1" stop-color="#FC5C7D"/></linearGradient><path d="m512 256c0 141.386719-114.613281 256-256 256s-256-114.613281-256-256 114.613281-256 256-256 256 114.613281 256 256zm0 0" fill="url(#a)"/><g fill="#fff"><path d="m256 56c-110.28125 0-200 89.71875-200 200s89.71875 200 200 200 200-89.71875 200-200-89.71875-200-200-200zm0 370c-93.738281 0-170-76.261719-170-170s76.261719-170 170-170 170 76.261719 170 170-76.261719 170-170 170zm0 0"/><path d="m324.179688 187.820312c-5.859376-5.855468-15.355469-5.855468-21.214844 0l-46.964844 46.964844-46.964844-46.964844c-5.859375-5.855468-15.355468-5.855468-21.214844 0-5.855468 5.859376-5.855468 15.355469 0 21.214844l46.964844 46.964844-46.964844 46.964844c-5.855468 5.859375-5.855468 15.355468 0 21.214844 2.929688 2.929687 6.769532 4.394531 10.605469 4.394531 3.839844 0 7.679688-1.464844 10.605469-4.394531l46.96875-46.964844 46.964844 46.964844c2.929687 2.929687 6.769531 4.394531 10.605468 4.394531 3.839844 0 7.679688-1.464844 10.609376-4.394531 5.855468-5.859376 5.855468-15.355469 0-21.214844l-46.964844-46.964844 46.964844-46.964844c5.855468-5.859375 5.855468-15.355468 0-21.214844zm0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -7,10 +7,20 @@
<script src="web-app.js"></script> <script src="web-app.js"></script>
<link rel="icon" href="spotiflyer.svg" type="image/icon type"> <link rel="icon" href="spotiflyer.svg" type="image/icon type">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link href="css-circular-prog-bar.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=RocknRoll+One&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=RocknRoll+One&display=swap" rel="stylesheet">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PGSYNZHSS7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-PGSYNZHSS7');
</script>
</head> </head>
<body> <body>
<div style="height: 100%;display: flex; align-items: center;flex-direction: column" id="root"></div> <div style="display: flex;flex-direction: column; height: inherit; color: white;" id="root"></div>
</body> </body>
</html> </html>

View File

@ -2,10 +2,15 @@
font-family: pristine; font-family: pristine;
src: url("pristine_script.ttf"); src: url("pristine_script.ttf");
} }
html { html {
background-image: url("header-dark.jpg"); background-image: url("header-dark.jpg");
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif; font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
} }
a:link, a:visited {
color: white;
text-decoration: none;
}
body, html { body, html {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -20,16 +25,59 @@ body, html {
} }
#appName{ #appName{
font-family: pristine, cursive; font-family: pristine, cursive;
font-weight: 100;
text-shadow: 0.3px 0.5px #ffffff;
} }
.headingTitle{ .headingTitle{
text-align: center; text-align: center;
margin: 10px; margin: 10px;
font-family: 'RocknRoll One', sans-serif; font-family: 'RocknRoll One', sans-serif;
} }
.glow-button:hover { .glow-button, .PaymentButton {
transition: all .2s ease-in-out;
}
.glow-button:hover, .PaymentButton:hover {
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
box-shadow: 0 5px 15px rgb(105, 44, 143); box-shadow: 0 5px 15px rgb(105, 44, 143);
transform: scale(1.1);
} }
/*Loading Spinner*/
.lds-ring {
align-items: center;
justify-content: center;
display: flex;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 3.5em;
height: 3.5em;
border: 8px solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.button { .button {
text-decoration: none; text-decoration: none;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
@ -50,7 +98,7 @@ body, html {
} }
.searchBox:hover > .searchInput { .searchBox:hover > .searchInput {
width: 30vw; width: 35vw;
padding: 0 6px; padding: 0 6px;
} }
@ -92,6 +140,8 @@ body, html {
line-height: 40px; line-height: 40px;
width: 0px; width: 0px;
} }
/*Download All Button*/
#download-all-text { #download-all-text {
color: black; color: black;
display: none; display: none;
@ -191,9 +241,73 @@ body, html {
transform: scale3D(0, 0, 1); transform: scale3D(0, 0, 1);
} }
} }
@media screen and (max-width: 620px) {
.searchBox:hover > .searchInput { /*Extension Switch*/
width: 150px; .cmn-toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.cmn-toggle + label {
display: block;
position: relative;
cursor: pointer;
outline: none;
user-select: none;
}
input.cmn-toggle-round-flat + label {
/* width = 2*height or 2*border-radius */
padding: 2px;
width: 40px;
height: 20px;
border: 3px solid #dddddd;
border-radius: 60px;
transition: border-color 0.3s;
}
input.cmn-toggle-round-flat + label:before,
input.cmn-toggle-round-flat + label:after {
display: block;
position: absolute;
content: "";
}
input.cmn-toggle-round-flat + label:after {
/* width = 2*border-radius */
top: 4px;
left: 4px;
bottom: 4px;
width: 16px;
background-color: #dddddd;
border-radius: 52px;
transition: margin 0.3s, background 0.3s;
}
input.cmn-toggle-round-flat:checked + label {
border-color: #8ce196;
}
input.cmn-toggle-round-flat:checked + label:after {
/* margin-left = border-radius from 'input.cmn-toggle-round-flat + label' */
margin-left: 20px;
background-color: #8ce196;
}
@media screen and (max-width: 600px) {
/* CSS HERE ONLY ON PHONE */
.info-banners {
flex-direction: column;
}
.searchInput {
width: 60vw;
padding: 0 6px; padding: 0 6px;
} }
.search-icon {
filter: none;
}
.searchButton {
background: white;
color : #2f3640;
}
} }