Web App Changes and Fixes

This commit is contained in:
shabinder 2021-03-16 16:10:11 +05:30
parent 90b99cbb51
commit d4d3835f8d
25 changed files with 328 additions and 77 deletions

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
import com.razorpay.Checkout
import com.shabinder.common.database.activityContext
import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.Dispatchers
import org.json.JSONObject
@ -30,6 +31,7 @@ actual fun openPlatform(packageID:String, platformLink:String){
}
}
actual val dispatcherIO = Dispatchers.IO
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
actual val isInternetAvailable:Boolean
get() = internetAvailability.value ?: true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
@ -18,6 +19,7 @@ import kotlinx.coroutines.withContext
actual fun openPlatform(packageID:String, platformLink:String){
//TODO
}
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
actual val dispatcherIO = Dispatchers.IO

View File

@ -1,5 +1,6 @@
package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
@ -8,6 +9,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
actual val currentPlatform:AllPlatforms = AllPlatforms.Js
actual fun openPlatform(packageID:String, platformLink:String){
//TODO
}
@ -87,7 +90,6 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPla
when(it){
is DownloadResult.Success -> {
println("Download Completed")
allTracksStatus[track.title] = DownloadStatus.Downloaded
dir.saveFileWithMetadata(it.byteArray, track)
}
is DownloadResult.Error -> {

View File

@ -1,7 +1,9 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinext.js.Object
@ -50,7 +52,7 @@ actual class Dir actual constructor(
trackDetails: TrackDetails
) {
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
val albumArt = downloadFile(trackDetails.albumArtURL)
val albumArt = downloadFile(corsApi+trackDetails.albumArtURL)
albumArt.collect {
when(it){
is DownloadResult.Success -> {
@ -83,7 +85,9 @@ actual class Dir actual constructor(
albumArt?.let { setFrame("APIC", it) }
}
writer.addTag()
allTracksStatus[trackDetails.title] = DownloadStatus.Downloaded
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
DownloadProgressFlow.emit(allTracksStatus)
}
actual fun addToLibrary(path:String){}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
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.coroutines.flow.Flow
import kotlinx.css.*
@ -19,7 +21,6 @@ class HomeScreen(
override val stateFlow: Flow<SpotiFlyerMain.State> = model.models
override fun RBuilder.render() {
println("Rendering New State = \"${state.data}\" ")
styledDiv{
css {
display = Display.flex

View File

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

View File

@ -1,7 +1,6 @@
package list
import kotlinx.css.px
import kotlinx.css.width
import kotlinx.css.*
import react.*
import styled.css
import styled.styledDiv
@ -31,8 +30,11 @@ private val circularProgressBar = functionalComponent<CircularProgressBarProps>(
styledDiv{ css { classes = mutableListOf("value-bar") } }
}
css{
display = Display.flex
justifyContent = JustifyContent.center
classes = mutableListOf("progress-circle","p${props.progress}").apply { if(props.progress>50) add("over50") }
width = 50.px
marginBottom = 65.px
}
}
}

View File

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

View File

@ -35,7 +35,11 @@ class ListScreen(
}
DownloadAllButton {
isActive = state.data.trackList.isNotEmpty()
isActive = state.data.trackList.size > 1
downloadAll = {
model.onDownloadAllClicked(state.data.trackList)
}
link = state.data.link
}
styledDiv{

View File

@ -22,9 +22,12 @@ fun RBuilder.TrackItem(handler: TrackItemProps.() -> Unit): ReactElement {
}
private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props ->
val (downloadStatus,setDownloadStatus) = useState(props.details.downloaded)
val details = props.details
useEffect(listOf(props.details)){
setDownloadStatus(props.details.downloaded)
}
styledDiv {
styledImg(src = details.albumArtURL) {
css {
height = 90.px
@ -89,20 +92,23 @@ private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props
whiteSpace = WhiteSpace.nowrap
overflow = Overflow.hidden
}
+ details.durationSec.toString()
+ "${details.durationSec/60} min, ${details.durationSec%60} sec"
}
}
}
when(details.downloaded){
when(downloadStatus){
is DownloadStatus.NotDownloaded ->{
DownloadButton {
onClick = { props.downloadTrack(details) }
status = details.downloaded
onClick = {
setDownloadStatus(DownloadStatus.Queued)
props.downloadTrack(details)
}
status = downloadStatus
}
}
is DownloadStatus.Downloading -> {
CircularProgressBar {
progress = (details.downloaded as DownloadStatus.Downloading).progress
progress = downloadStatus.progress
}
}
DownloadStatus.Queued -> {
@ -111,7 +117,7 @@ private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props
DownloadStatus.Downloaded -> {
DownloadButton {
onClick = {}
status = details.downloaded
status = downloadStatus
}
}
DownloadStatus.Converting -> {
@ -120,7 +126,7 @@ private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props
DownloadStatus.Failed -> {
DownloadButton {
onClick = {}
status = details.downloaded
status = downloadStatus
}
}
}
@ -128,7 +134,7 @@ private val trackItem = functionalComponent<TrackItemProps>("Track-Item"){ props
css {
alignItems = Align.center
display =Display.flex
flexGrow = 1.0
paddingRight = 16.px
}
}
}

View File

@ -2,10 +2,11 @@ package navbar
import kotlinx.css.*
import kotlinx.html.id
import kotlinx.html.js.onBlurFunction
import kotlinx.html.js.onClickFunction
import react.*
import styled.*
@Suppress("FunctionName")
fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
return child(navBar){
@ -17,14 +18,25 @@ fun RBuilder.NavBar(handler: NavBarProps.() -> Unit): ReactElement{
external interface NavBarProps:RProps{
var isBackVisible: Boolean
var popBackToHomeScreen: () -> Unit
}
private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
styledNav {
css {
+NavBarStyles.nav
}
styledDiv{
attrs {
onClickFunction = {
props.popBackToHomeScreen()
}
onBlurFunction = {
props.popBackToHomeScreen()
}
}
styledImg(src = "left-arrow.svg",alt = "Back Arrow"){
css {
height = 42.px
@ -34,6 +46,12 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
marginRight = 12.px
}
}
}
styledA(href = "TODO Website Link") {
css {
display = Display.flex
alignItems = Align.center
}
styledImg(src = "spotiflyer.svg",alt = "Logo") {
css {
height = 42.px
@ -50,6 +68,46 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
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 {
@ -57,7 +115,10 @@ private val navBar = functionalComponent<NavBarProps>("NavBar") { props ->
width = 42.px
}
}
}
css {
display = Display.flex
alignItems = Align.center
marginLeft = LinearDimension.auto
}
}

View File

@ -6,7 +6,6 @@ import com.shabinder.common.root.SpotiFlyerRoot.*
import extras.RenderableRootComponent
import extras.renderableChild
import home.HomeScreen
import kotlinx.coroutines.launch
import list.ListScreen
import navbar.NavBar
import react.RBuilder
@ -19,9 +18,12 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
private val component: Child
get() = model.routerState.value.activeChild.component
private val callBacks get() = model.callBacks
override fun RBuilder.render() {
NavBar {
isBackVisible = (component is Child.List)
popBackToHomeScreen = callBacks::popBackToHomeScreen
}
when(component){
is Child.Main -> renderableChild(HomeScreen::class, (component as Child.Main).component)
@ -33,7 +35,7 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
model.routerState.bindToState { routerState = it }
}
class State(
var routerState: RouterState<*, Child>,
var routerState: RouterState<*, Child>
) : RState
}

View File

@ -1,7 +1,5 @@
.progress-circle {
margin: 20px;
position: relative; /* so that children can be absolutely positioned */
line-height: 5em;
}
.progress-circle:after{
@ -14,6 +12,7 @@
height: 4.3em;
background-color: white;
content: " ";
margin-top: 0.35em;
}
/* Text inside the control */
.progress-circle span {

View File

@ -16,6 +16,10 @@ 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%;
@ -239,3 +243,56 @@ body, html {
transform: scale3D(0, 0, 1);
}
}
/*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;
}