diff --git a/.github/workflows/build-and-publish-kjs.yml b/.github/workflows/build-and-publish-kjs.yml new file mode 100644 index 00000000..a0e243a8 --- /dev/null +++ b/.github/workflows/build-and-publish-kjs.yml @@ -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 diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt new file mode 100644 index 00000000..cb9407b1 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt @@ -0,0 +1,7 @@ +package com.shabinder.common.models + +sealed class AllPlatforms{ + object Js:AllPlatforms() + object Jvm:AllPlatforms() + object Native:AllPlatforms() +} diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt new file mode 100644 index 00000000..70baacfb --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt @@ -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() diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt index e08c0164..0f9d23d8 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt @@ -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 diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt index 4f3c9218..5a6da405 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt @@ -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, fetcher: FetchPlatformQueryResult, diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt index 57db44ee..8d490484 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt @@ -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 * diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index c99708c0..d49a2a32 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -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() } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt index 4cd87a71..74a0bfeb 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt @@ -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 + } } \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index 10b3531e..a55c9d25 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -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") diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt index c4e94bf1..b98451e4 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt @@ -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 { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt index a01a48ba..16440679 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt @@ -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) diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index 42f8cc25..df6ce47f 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -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 diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt index 520e106d..255e5087 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt @@ -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> = MutableSharedFlow(1) +val allTracksStatus: HashMap = hashMapOf() actual suspend fun downloadTracks( list: List, @@ -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) } } } diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt index e9d66437..90004b11 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt @@ -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){} diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 72f453c8..9168cf49 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -42,6 +42,7 @@ interface SpotiFlyerMain { val dir: Dir val showPopUpMessage:(String)->Unit } + sealed class Output { data class Search(val link: String) : Output() } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/callbacks/SpotiFlyerRootCallBacks.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/callbacks/SpotiFlyerRootCallBacks.kt index db0250b3..fb3933cb 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/callbacks/SpotiFlyerRootCallBacks.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/callbacks/SpotiFlyerRootCallBacks.kt @@ -2,4 +2,5 @@ package com.shabinder.common.root.callbacks interface SpotiFlyerRootCallBacks { fun searchLink(link:String) + fun popBackToHomeScreen() } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index 22515ff3..a1577d25 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -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 = diff --git a/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/ExtractorTest.kt b/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/ExtractorTest.kt index 1cee74a2..9446c2e2 100644 --- a/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/ExtractorTest.kt +++ b/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/ExtractorTest.kt @@ -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 diff --git a/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/FuzzyWuzzyTest.kt b/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/FuzzyWuzzyTest.kt index d860bdd1..e34eff1a 100644 --- a/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/FuzzyWuzzyTest.kt +++ b/fuzzywuzzy/app/src/test/kotlin/com/willowtree/fuzzywuzzy/FuzzyWuzzyTest.kt @@ -1,4 +1,4 @@ -package com.willowtreeapps.fuzzywuzzy +package kotlin.com.willowtree.fuzzywuzzy import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch diff --git a/web-app/build/distributions/temp.txt b/web-app/build/distributions/temp.txt new file mode 100644 index 00000000..9c595a6f --- /dev/null +++ b/web-app/build/distributions/temp.txt @@ -0,0 +1 @@ +temp diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt index 3b3ffaa6..e56fff15 100644 --- a/web-app/src/main/kotlin/App.kt +++ b/web-app/src/main/kotlin/App.kt @@ -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(props) { override val directories = dependencies.directories override val database: Database? = directories.db override val showPopUpMessage: (String) -> Unit = {}//TODO - override val downloadProgressReport: MutableSharedFlow> - = MutableSharedFlow(1) + override val downloadProgressReport = DownloadProgressFlow } ) diff --git a/web-app/src/main/kotlin/Styles.kt b/web-app/src/main/kotlin/Styles.kt index 8150645a..e4b6fd96 100644 --- a/web-app/src/main/kotlin/Styles.kt +++ b/web-app/src/main/kotlin/Styles.kt @@ -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 diff --git a/web-app/src/main/kotlin/home/HomeScreen.kt b/web-app/src/main/kotlin/home/HomeScreen.kt index a51ab5d3..1fc2c616 100644 --- a/web-app/src/main/kotlin/home/HomeScreen.kt +++ b/web-app/src/main/kotlin/home/HomeScreen.kt @@ -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 = 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" diff --git a/web-app/src/main/kotlin/home/IconList.kt b/web-app/src/main/kotlin/home/IconList.kt index e496fab2..7f591eaa 100644 --- a/web-app/src/main/kotlin/home/IconList.kt +++ b/web-app/src/main/kotlin/home/IconList.kt @@ -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 @@ -22,16 +24,26 @@ fun RBuilder.IconList(handler:IconListProps.() -> Unit): ReactElement { } private val iconList = functionalComponent("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){ + //
+ styledForm { + attrs{ + id = "razorpay-form" + } + } + } + styledA(href = platformLink,target="_blank"){ styledImg { attrs { src = icon diff --git a/web-app/src/main/kotlin/home/Message.kt b/web-app/src/main/kotlin/home/Message.kt index df5bc852..1460ccf0 100644 --- a/web-app/src/main/kotlin/home/Message.kt +++ b/web-app/src/main/kotlin/home/Message.kt @@ -25,7 +25,7 @@ private val message = functionalComponent("Message") { props-> + props.text css { classes = mutableListOf("headingTitle") - fontSize = 3.2.rem + fontSize = 2.6.em } } } diff --git a/web-app/src/main/kotlin/home/Searchbar.kt b/web-app/src/main/kotlin/home/Searchbar.kt index 126581e9..d27328f9 100644 --- a/web-app/src/main/kotlin/home/Searchbar.kt +++ b/web-app/src/main/kotlin/home/Searchbar.kt @@ -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("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("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 { diff --git a/web-app/src/main/kotlin/list/CircularProgressBar.kt b/web-app/src/main/kotlin/list/CircularProgressBar.kt new file mode 100644 index 00000000..26f7520e --- /dev/null +++ b/web-app/src/main/kotlin/list/CircularProgressBar.kt @@ -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("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 + } + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/DownloadAllButton.kt b/web-app/src/main/kotlin/list/DownloadAllButton.kt index 489760aa..7ef116ec 100644 --- a/web-app/src/main/kotlin/list/DownloadAllButton.kt +++ b/web-app/src/main/kotlin/list/DownloadAllButton.kt @@ -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("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 + } + } } } } diff --git a/web-app/src/main/kotlin/list/DownloadButton.kt b/web-app/src/main/kotlin/list/DownloadButton.kt new file mode 100644 index 00000000..fbca8036 --- /dev/null +++ b/web-app/src/main/kotlin/list/DownloadButton.kt @@ -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("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 + } + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/ListScreen.kt b/web-app/src/main/kotlin/list/ListScreen.kt index 8a5aa508..d43ded27 100644 --- a/web-app/src/main/kotlin/list/ListScreen.kt +++ b/web-app/src/main/kotlin/list/ListScreen.kt @@ -9,6 +9,7 @@ import kotlinx.html.id import react.RBuilder import styled.css import styled.styledDiv +import styled.styledSection class ListScreen( props: Props, @@ -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 } } } diff --git a/web-app/src/main/kotlin/list/LoadingAnim.kt b/web-app/src/main/kotlin/list/LoadingAnim.kt index dedab437..e701adfc 100644 --- a/web-app/src/main/kotlin/list/LoadingAnim.kt +++ b/web-app/src/main/kotlin/list/LoadingAnim.kt @@ -16,22 +16,28 @@ fun RBuilder.LoadingAnim(handler: RProps.() -> Unit): ReactElement { } private val loadingAnim = functionalComponent("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 + } } } } \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/LoadingSpinner.kt b/web-app/src/main/kotlin/list/LoadingSpinner.kt new file mode 100644 index 00000000..45b57f52 --- /dev/null +++ b/web-app/src/main/kotlin/list/LoadingSpinner.kt @@ -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("Loading-Spinner") { + styledDiv { + styledDiv{} + styledDiv{} + styledDiv{} + styledDiv{} + css{ + classes = mutableListOf("lds-ring") + width = 50.px + marginRight = 8.px + } + } +} \ No newline at end of file diff --git a/web-app/src/main/kotlin/list/TrackItem.kt b/web-app/src/main/kotlin/list/TrackItem.kt index a0ec5739..587b9789 100644 --- a/web-app/src/main/kotlin/list/TrackItem.kt +++ b/web-app/src/main/kotlin/list/TrackItem.kt @@ -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("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("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 } } } diff --git a/web-app/src/main/kotlin/navbar/NavBar.kt b/web-app/src/main/kotlin/navbar/NavBar.kt index 4d0c0d75..cc5b6a3b 100644 --- a/web-app/src/main/kotlin/navbar/NavBar.kt +++ b/web-app/src/main/kotlin/navbar/NavBar.kt @@ -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("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 } } diff --git a/web-app/src/main/kotlin/root/RootR.kt b/web-app/src/main/kotlin/root/RootR.kt index 279e4220..d0882e15 100644 --- a/web-app/src/main/kotlin/root/RootR.kt +++ b/web-app/src/main/kotlin/root/RootR.kt @@ -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) : RenderableRootComponent renderableChild(HomeScreen::class, (component as Child.Main).component) @@ -33,7 +35,7 @@ class RootR(props: Props) : RenderableRootComponent, + var routerState: RouterState<*, Child> ) : RState } diff --git a/web-app/src/main/resources/check.svg b/web-app/src/main/resources/check.svg new file mode 100644 index 00000000..65c47d51 --- /dev/null +++ b/web-app/src/main/resources/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-app/src/main/resources/css-circular-prog-bar.css b/web-app/src/main/resources/css-circular-prog-bar.css new file mode 100644 index 00000000..fc7fc4f9 --- /dev/null +++ b/web-app/src/main/resources/css-circular-prog-bar.css @@ -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); } \ No newline at end of file diff --git a/web-app/src/main/resources/error.svg b/web-app/src/main/resources/error.svg new file mode 100644 index 00000000..2204a4a4 --- /dev/null +++ b/web-app/src/main/resources/error.svg @@ -0,0 +1 @@ + diff --git a/web-app/src/main/resources/index.html b/web-app/src/main/resources/index.html index 56d661df..224f7c73 100644 --- a/web-app/src/main/resources/index.html +++ b/web-app/src/main/resources/index.html @@ -7,10 +7,20 @@ + + + + -
+
diff --git a/web-app/src/main/resources/styles.css b/web-app/src/main/resources/styles.css index 9ddd95ef..372053bc 100644 --- a/web-app/src/main/resources/styles.css +++ b/web-app/src/main/resources/styles.css @@ -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; } -} \ No newline at end of file + .search-icon { + filter: none; + } + .searchButton { + background: white; + color : #2f3640; + } +}