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

View File

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

View File

@ -1,11 +1,19 @@
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 io.ktor.client.*
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 BASE_URL = "https://api.gaana.com/"
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
interface GaanaRequests {
@ -76,6 +84,7 @@ interface GaanaRequests {
"$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
*

View File

@ -20,6 +20,7 @@ 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.AllPlatforms
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.spotify.Album
@ -43,7 +44,9 @@ class SpotifyProvider(
init {
logger.d { "Creating Spotify Provider" }
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 com.shabinder.common.di.Dir
import com.shabinder.common.di.currentPlatform
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 io.ktor.client.*
@ -11,5 +15,9 @@ class YoutubeMp3(
private val logger: Kermit,
private val dir: Dir,
):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
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
@ -246,7 +247,7 @@ class YoutubeMusic constructor(
}
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)
headers{
append("referer","https://music.youtube.com/search")

View File

@ -1,5 +1,6 @@
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.PagingObjectPlaylistTrack
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.request.*
private const val BASE_URL = "https://api.spotify.com/v1"
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
interface SpotifyRequests {

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di.youtubeMp3
import com.shabinder.common.di.gaana.corsApi
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
@ -19,14 +20,14 @@ interface Yt1sMp3 {
* Downloadable Mp3 Link for YT videoID.
* */
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
* Body Form= q:yt video link ,vt:format=mp3
* */
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 {
append("q","https://www.youtube.com/watch?v=$videoID")
append("vt","mp3")
@ -36,7 +37,7 @@ interface Yt1sMp3 {
}
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 {
append("vid", videoID)
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.formats.Format
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.DownloadStatus
import com.shabinder.common.models.TrackDetails
@ -18,6 +19,7 @@ import kotlinx.coroutines.withContext
actual fun openPlatform(packageID:String, platformLink:String){
//TODO
}
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
actual val dispatcherIO = Dispatchers.IO

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
@ -7,7 +8,8 @@ import io.ktor.client.request.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
import org.khronos.webgl.ArrayBuffer
actual val currentPlatform:AllPlatforms = AllPlatforms.Js
actual fun openPlatform(packageID:String, platformLink:String){
//TODO
@ -43,13 +45,14 @@ private suspend fun isInternetAvailable(): Boolean {
actual val isInternetAvailable:Boolean
get(){
return true
var result = false
/*var result = false
val job = GlobalScope.launch { result = isInternetAvailable() }
while(job.isActive){}
return result
return result*/
}
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
val allTracksStatus: HashMap<String, DownloadStatus> = hashMapOf()
actual suspend fun downloadTracks(
list: List<TrackDetails>,
@ -58,24 +61,29 @@ actual suspend fun downloadTracks(
){
withContext(Dispatchers.Default){
list.forEach {
allTracksStatus[it.title] = DownloadStatus.Queued
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()) {
allTracksStatus[it.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
} else {//Found Youtube Video ID
downloadTrack(videoID, it, fetcher, dir)
}
}
}
DownloadProgressFlow.emit(allTracksStatus)
}
}
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if(url == null){
// TODO Handle
allTracksStatus[track.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
println("No URL to Download")
}else {
downloadFile(url).collect {
@ -84,9 +92,16 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPla
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}")
is DownloadResult.Error -> {
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
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinext.js.Object
@ -50,7 +52,7 @@ actual class Dir actual constructor(
trackDetails: TrackDetails
) {
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
val albumArt = downloadFile(trackDetails.albumArtURL)
val albumArt = downloadFile(corsApi+trackDetails.albumArtURL)
albumArt.collect {
when(it){
is DownloadResult.Success -> {
@ -74,7 +76,7 @@ actual class Dir actual constructor(
private suspend fun writeTagsAndSave(writer:ID3Writer, albumArt:Object?, trackDetails: TrackDetails){
writer.apply {
setFrame("TIT2", trackDetails.title)
setFrame("TPE1", trackDetails.artists)
setFrame("TPE1", trackDetails.artists.toTypedArray())
setFrame("TALB", trackDetails.albumName?:"")
try{trackDetails.year?.substring(0,4)?.toInt()?.let { setFrame("TYER", it) }} catch(e:Exception){}
setFrame("TPE2", trackDetails.artists.joinToString(","))
@ -83,7 +85,9 @@ actual class Dir actual constructor(
albumArt?.let { setFrame("APIC", it) }
}
writer.addTag()
allTracksStatus[trackDetails.title] = DownloadStatus.Downloaded
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
DownloadProgressFlow.emit(allTracksStatus)
}
actual fun addToLibrary(path:String){}

View File

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

View File

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

View File

@ -29,6 +29,11 @@ internal class SpotiFlyerRootImpl(
override val callBacks = object : SpotiFlyerRootCallBacks{
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 =

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.algorithms.WeightedRatio

View File

@ -1,4 +1,4 @@
package com.willowtreeapps.fuzzywuzzy
package kotlin.com.willowtree.fuzzywuzzy
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.logging.store.LoggingStoreFactory
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.database.Database
import extras.renderableChild
import kotlinx.coroutines.flow.MutableSharedFlow
import react.*
import root.RootR
@ -40,8 +39,7 @@ class App(props: AppProps): RComponent<AppProps, RState>(props) {
override val directories = dependencies.directories
override val database: Database? = directories.db
override val showPopUpMessage: (String) -> Unit = {}//TODO
override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>>
= MutableSharedFlow(1)
override val downloadProgressReport = DownloadProgressFlow
}
)

View File

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

View File

@ -1,10 +1,14 @@
package home
import com.shabinder.common.di.currentPlatform
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.State
import com.shabinder.common.models.AllPlatforms
import extras.RenderableComponent
import kotlinx.browser.document
import kotlinx.coroutines.flow.Flow
import kotlinx.css.*
import kotlinx.dom.appendElement
import react.*
import styled.css
import styled.styledDiv
@ -15,11 +19,23 @@ class HomeScreen(
props,
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 fun RBuilder.render() {
println("Rendering New State = \"${state.data}\" ")
styledDiv{
css {
display = Display.flex
@ -55,8 +71,8 @@ class HomeScreen(
private val platformIconList = mapOf(
"spotify.svg" to "https://open.spotify.com/",
"gaana.svg" to "https://www.gaana.com/",
"youtube.svg" to "https://www.youtube.com/",
"youtube_music.svg" to "https://music.youtube.com/"
//"youtube.svg" to "https://www.youtube.com/",
//"youtube_music.svg" to "https://music.youtube.com/"
)
private val badges = mapOf(
"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
import kotlinx.browser.document
import kotlinx.css.*
import kotlinx.dom.appendElement
import kotlinx.dom.createElement
import kotlinx.html.SCRIPT
import kotlinx.html.id
import react.*
import styled.css
import styled.styledA
import styled.styledDiv
import styled.styledImg
import styled.*
external interface IconListProps : RProps {
var iconsAndPlatforms: Map<String,String>
@ -22,16 +24,26 @@ fun RBuilder.IconList(handler:IconListProps.() -> Unit): ReactElement {
}
private val iconList = functionalComponent<IconListProps>("IconList") { props ->
styledDiv {
css {
+Styles.makeRow
margin(18.px)
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){
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 {
attrs {
src = icon

View File

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

View File

@ -1,9 +1,12 @@
package home
import kotlinx.browser.window
import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.html.js.onKeyDownFunction
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Window
import react.*
import styled.*
@ -34,6 +37,12 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
val target = it.target as HTMLInputElement
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
}
css {
@ -43,7 +52,8 @@ val searchbar = functionalComponent<SearchbarProps>("SearchBar"){ props ->
styledButton {
attrs {
onClickFunction = {
props.search(props.link)
if(props.link.isEmpty()) window.alert("Enter a Link from Supported Platforms")
else props.search(props.link)
}
}
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.html.id
import kotlinx.html.js.onClickFunction
import react.*
import styled.css
import styled.styledDiv
@ -10,6 +11,8 @@ import styled.styledImg
external interface DownloadAllButtonProps : RProps {
var isActive:Boolean
var link : String
var downloadAll:()->Unit
}
@Suppress("FunctionName")
@ -22,37 +25,65 @@ fun RBuilder.DownloadAllButton(handler: DownloadAllButtonProps.() -> Unit): Reac
}
private val downloadAllButton = functionalComponent<DownloadAllButtonProps>("DownloadAllButton") { props->
styledDiv {
styledDiv {
styledImg(src = "download.svg",alt = "Download All Button") {
val (isClicked,setClicked) = useState(false)
useEffect(mutableListOf(props.link)){
setClicked(false)
}
if(props.isActive){
if(isClicked) {
styledDiv{
css {
classes = mutableListOf("download-all-icon")
height = 32.px
display = Display.flex
alignItems = Align.center
justifyContent = JustifyContent.center
height = 52.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
LoadingSpinner { }
}
}
css {
classes = mutableListOf("download-button")
display = if(props.isActive) Display.flex else Display.none
alignItems = Align.center
else{
styledDiv {
attrs {
onClickFunction = {
//props.downloadAll()
setClicked(true)
}
}
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 = Display.flex
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 styled.css
import styled.styledDiv
import styled.styledSection
class ListScreen(
props: Props<SpotiFlyerList>,
@ -20,9 +21,9 @@ class ListScreen(
val result = state.data.queryResult
styledDiv {
styledSection {
attrs {
id = "list-screen-div"
id = "list-screen"
}
if(result == null){
@ -34,13 +35,25 @@ class ListScreen(
}
DownloadAllButton {
isActive = state.data.trackList.isNotEmpty()
isActive = state.data.trackList.size > 1
downloadAll = {
model.onDownloadAllClicked(state.data.trackList)
}
link = state.data.link
}
state.data.trackList.forEachIndexed{ index, trackDetails ->
TrackItem {
details = trackDetails
downloadTrack = model::onDownloadClicked
styledDiv{
css {
display =Display.flex
flexGrow = 1.0
flexDirection = FlexDirection.column
color = Color.white
}
state.data.trackList.forEachIndexed{ index, trackDetails ->
TrackItem {
details = trackDetails
downloadTrack = model::onDownloadClicked
}
}
}
}
@ -48,10 +61,9 @@ class ListScreen(
css {
classes = mutableListOf("list-screen")
display = Display.flex
padding(8.px)
flexDirection = FlexDirection.column
flexGrow = 1.0
justifyContent = JustifyContent.center
alignItems = Align.stretch
}
}
}

View File

@ -16,22 +16,28 @@ fun RBuilder.LoadingAnim(handler: RProps.() -> Unit): ReactElement {
}
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") } }
styledDiv{
css {
classes = mutableListOf("sk-cube-grid")
height = 60.px
width = 60.px
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-cube2") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube3") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube4") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube5") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube6") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube7") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube8") } }
styledDiv { css { classes = mutableListOf("sk-cube sk-cube9") } }
css {
classes = mutableListOf("sk-cube-grid")
height = 60.px
width = 60.px
}
}
}
}

View File

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

View File

@ -2,10 +2,11 @@ package navbar
import kotlinx.css.*
import kotlinx.html.id
import kotlinx.html.js.onBlurFunction
import kotlinx.html.js.onClickFunction
import react.*
import styled.*
@Suppress("FunctionName")
fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
return child(navBar){
@ -17,47 +18,107 @@ fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
external interface NavBarProps:RProps{
var isBackVisible: Boolean
var popBackToHomeScreen: () -> Unit
}
private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
styledNav {
css {
+NavBarStyles.nav
}
styledImg(src = "left-arrow.svg",alt = "Back Arrow"){
css {
height = 42.px
width = 42.px
display = if(props.isBackVisible) Display.inline else Display.none
filter = "invert(100)"
marginRight = 12.px
}
}
styledImg(src = "spotiflyer.svg",alt = "Logo") {
css {
height = 42.px
width = 42.px
}
}
styledH1 {
+"SpotiFlyer"
styledDiv{
attrs {
id = "appName"
onClickFunction = {
props.popBackToHomeScreen()
}
onBlurFunction = {
props.popBackToHomeScreen()
}
}
css{
fontSize = 46.px
margin(horizontal = 14.px)
styledImg(src = "left-arrow.svg",alt = "Back Arrow"){
css {
height = 42.px
width = 42.px
display = if(props.isBackVisible) Display.inline else Display.none
filter = "invert(100)"
marginRight = 12.px
}
}
}
styledA(href = "https://github.com/Shabinder/SpotiFlyer/"){
styledImg(src = "github.svg"){
styledA(href = "https://shabinder.github.io/SpotiFlyer/",target="_blank") {
css {
display = Display.flex
alignItems = Align.center
}
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)
}
}
}
/*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/"){
styledImg(src = "github.svg"){
css {
height = 42.px
width = 42.px
}
}
}
css {
display = Display.flex
alignItems = Align.center
marginLeft = LinearDimension.auto
}
}

View File

@ -6,7 +6,6 @@ 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
@ -19,9 +18,12 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
private val component: Child
get() = model.routerState.value.activeChild.component
private val callBacks get() = model.callBacks
override fun RBuilder.render() {
NavBar {
isBackVisible = (component is Child.List)
popBackToHomeScreen = callBacks::popBackToHomeScreen
}
when(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 }
}
class State(
var routerState: RouterState<*, Child>,
var routerState: RouterState<*, Child>
) : 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>
<link rel="icon" href="spotiflyer.svg" type="image/icon type">
<link rel="stylesheet" href="styles.css">
<link href="css-circular-prog-bar.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com">
<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>
<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>
</html>

View File

@ -2,10 +2,15 @@
font-family: pristine;
src: url("pristine_script.ttf");
}
html {
background-image: url("header-dark.jpg");
font-family: Lora,Helvetica Neue,Helvetica,Arial,sans-serif;
}
a:link, a:visited {
color: white;
text-decoration: none;
}
body, html {
width: 100%;
height: 100%;
@ -20,16 +25,59 @@ body, html {
}
#appName{
font-family: pristine, cursive;
font-weight: 100;
text-shadow: 0.3px 0.5px #ffffff;
}
.headingTitle{
text-align: center;
margin: 10px;
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);
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 {
text-decoration: none;
color: rgba(255, 255, 255, 0.8);
@ -50,7 +98,7 @@ body, html {
}
.searchBox:hover > .searchInput {
width: 30vw;
width: 35vw;
padding: 0 6px;
}
@ -92,6 +140,8 @@ body, html {
line-height: 40px;
width: 0px;
}
/*Download All Button*/
#download-all-text {
color: black;
display: none;
@ -191,9 +241,73 @@ body, html {
transform: scale3D(0, 0, 1);
}
}
@media screen and (max-width: 620px) {
.searchBox:hover > .searchInput {
width: 150px;
/*Extension Switch*/
.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;
}
.search-icon {
filter: none;
}
.searchButton {
background: white;
color : #2f3640;
}
}