mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-26 02:24:31 +01:00
Merge branch 'KMP' into imgbot
This commit is contained in:
commit
9c1e3ecd84
33
.github/workflows/build-and-publish-kjs.yml
vendored
Normal file
33
.github/workflows/build-and-publish-kjs.yml
vendored
Normal 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
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
sealed class AllPlatforms{
|
||||||
|
object Js:AllPlatforms()
|
||||||
|
object Jvm:AllPlatforms()
|
||||||
|
object Native:AllPlatforms()
|
||||||
|
}
|
@ -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()
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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){}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,5 @@ package com.shabinder.common.root.callbacks
|
|||||||
|
|
||||||
interface SpotiFlyerRootCallBacks {
|
interface SpotiFlyerRootCallBacks {
|
||||||
fun searchLink(link:String)
|
fun searchLink(link:String)
|
||||||
|
fun popBackToHomeScreen()
|
||||||
}
|
}
|
||||||
|
@ -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 =
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
1
web-app/build/distributions/temp.txt
Normal file
1
web-app/build/distributions/temp.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
temp
|
@ -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)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
40
web-app/src/main/kotlin/list/CircularProgressBar.kt
Normal file
40
web-app/src/main/kotlin/list/CircularProgressBar.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
49
web-app/src/main/kotlin/list/DownloadButton.kt
Normal file
49
web-app/src/main/kotlin/list/DownloadButton.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
31
web-app/src/main/kotlin/list/LoadingSpinner.kt
Normal file
31
web-app/src/main/kotlin/list/LoadingSpinner.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
1
web-app/src/main/resources/check.svg
Normal file
1
web-app/src/main/resources/check.svg
Normal 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 |
167
web-app/src/main/resources/css-circular-prog-bar.css
Normal file
167
web-app/src/main/resources/css-circular-prog-bar.css
Normal 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); }
|
1
web-app/src/main/resources/error.svg
Normal file
1
web-app/src/main/resources/error.svg
Normal 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 |
@ -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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user