Refactor and Icons Added

This commit is contained in:
shabinder 2021-02-07 21:49:48 +05:30
parent 475e1834bf
commit 7d7e82b3b7
47 changed files with 909 additions and 988 deletions

View File

@ -91,6 +91,13 @@ dependencies {
implementation(fetch) implementation(fetch)
} }
implementation(MVIKotlin.mvikotlin)
implementation(MVIKotlin.mvikotlinMain)
implementation(MVIKotlin.mvikotlinLogging)
implementation(MVIKotlin.mvikotlinTimeTravel)
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
//Test //Test
testImplementation("junit:junit:4.13.1") testImplementation("junit:junit:4.13.1")
androidTestImplementation(Androidx.junit) androidTestImplementation(Androidx.junit)

View File

@ -1,16 +1,59 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.shabinder.android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.shabinder.android">
<queries>
<package android:name="com.gaana" />
<package android:name="com.spotify.music" />
<package android:name="com.google.android.youtube" />
<package android:name="com.google.android.apps.youtube.music" />
</queries>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application <application
android:allowBackup="false" android:name=".App"
android:supportsRtl="true" android:allowBackup="false"
android:usesCleartextTraffic="true" android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> android:usesCleartextTraffic="true"
<activity android:name=".MainActivity"> android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:forceDarkAllowed="true"
android:requestLegacyExternalStorage="true"
tools:targetApi="q">
<activity android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name"
android:hardwareAccelerated="true"
android:theme="@style/Theme.SpotiFlyer"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data
android:name="com.razorpay.ApiKey"
android:value="rzp_live_3ZQeoFYOxjmXye"
/>
</application> </application>
</manifest> </manifest>

View File

@ -2,21 +2,43 @@ package com.shabinder.android
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.setContent import androidx.compose.ui.platform.setContent
import com.shabinder.android.di.appModule import com.arkivanov.decompose.ComponentContext
import com.shabinder.common.database.appContext import com.arkivanov.decompose.extensions.compose.jetbrains.rootComponent
import com.shabinder.common.initKoin import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
import com.shabinder.common.ui.SpotiFlyerMain import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
import org.koin.android.ext.koin.androidLogger import com.shabinder.common.Dir
import com.shabinder.common.FetchPlatformQueryResult
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRootContent
import com.shabinder.common.ui.SpotiFlyerTheme
import com.shabinder.database.Database
import org.koin.android.ext.android.inject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
val database: Database by inject()
val fetcher: FetchPlatformQueryResult by inject()
val dir: Dir by inject()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val scope = rememberCoroutineScope() SpotiFlyerTheme {
SpotiFlyerMain() SpotiFlyerRootContent(rootComponent(::spotiFlyerRoot))
}
} }
} }
private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
SpotiFlyerRoot(
componentContext,
dependencies = object : SpotiFlyerRoot.Dependencies{
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
override val database = this@MainActivity.database
override val fetchPlatformQueryResult = this@MainActivity.fetcher
override val directories: Dir = this@MainActivity.dir
}
)
} }

View File

@ -1,122 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.android
import android.content.Intent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import co.touchlab.kermit.Kermit
import com.shabinder.common.DownloadStatus
import com.shabinder.common.TrackDetails
import com.shabinder.common.YoutubeProvider
import com.shabinder.common.providers.GaanaProvider
import com.shabinder.common.providers.SpotifyProvider
import com.shabinder.database.Database
import com.shabinder.spotiflyer.ui.colorPrimaryDark
import com.tonyodev.fetch2.Status
class SharedViewModel(
val database: Database,
val logger: Kermit,
val spotifyProvider: SpotifyProvider,
val gaanaProvider : GaanaProvider,
val youtubeProvider: YoutubeProvider
) : ViewModel() {
var isAuthenticated by mutableStateOf(false)
private set
fun authenticated(s:Boolean) {
isAuthenticated = s
}
/*
* Nav Gives Error on YT links with ? sign
* */
var link by mutableStateOf("")
private set
fun updateLink(s:String) {
link = s
}
val trackList = mutableStateListOf<TrackDetails>()
fun updateTrackList(list:List<TrackDetails>){
trackList.clear()
trackList.addAll(list)
}
fun updateTrackStatus(position:Int, status: DownloadStatus){
if(position != -1){
val track = trackList[position].apply { downloaded = status }
trackList[position] = track
}
}
fun updateTrackStatus(intent: Intent){
val trackDetails = intent.getSerializableExtra("track") as TrackDetails?
trackDetails?.let {
val position: Int =
trackList.map { trackState -> trackState.title }.indexOf(it.title)
logger.d{"$position, ${intent.action} , ${it.title}"}
if (position != -1) {
trackList.getOrNull(position)?.let{ track ->
when (intent.action) {
Status.QUEUED.name -> {
track.downloaded = DownloadStatus.Queued
}
Status.FAILED.name -> {
track.downloaded = DownloadStatus.Failed
}
Status.DOWNLOADING.name -> {
track.downloaded = DownloadStatus.Downloading
}
"Progress" -> {
//Progress Update
track.progress = intent.getIntExtra("progress", 0)
track.downloaded = DownloadStatus.Downloading
}
"Converting" -> {
//Progress Update
track.downloaded = DownloadStatus.Converting
}
"track_download_completed" -> {
track.downloaded = DownloadStatus.Downloaded
}
}
trackList[position] = track
logger.d{"TrackListUpdated"}
}
}
}
}
var gradientColor by mutableStateOf(Color.Transparent)
private set
fun updateGradientColor(color: Color) {
gradientColor = color
}
fun resetGradient() {
gradientColor = colorPrimaryDark
}
}

View File

@ -1,9 +1,7 @@
package com.shabinder.android.di package com.shabinder.android.di
import com.shabinder.android.SharedViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val appModule = module { val appModule = module {
viewModel { SharedViewModel(get(),get(),get(),get(),get()) }
} }

View File

@ -1,78 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.android.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.*
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.providers.GaanaProvider
import com.shabinder.spotiflyer.providers.SpotifyProvider
import com.shabinder.spotiflyer.providers.YoutubeProvider
import com.shabinder.common.ui.home.Home
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.sharedViewModel
@Composable
fun ComposeNavigation(
mainActivity: MainActivity,
navController: NavHostController,
spotifyProvider: SpotifyProvider,
gaanaProvider: GaanaProvider,
youtubeProvider: YoutubeProvider,
) {
NavHost(
navController = navController,
startDestination = "home"
) {
//HomeScreen - Starting Point
composable("home") {
Home(
navController = navController,
mainActivity,
)
}
//Spotify Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"track_list/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
TrackList(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController,
spotifyProvider,
gaanaProvider,
youtubeProvider
)
}
}
}
fun NavController.navigateToTrackList(link:String, singleInstance: Boolean = true, inclusive:Boolean = false) {
sharedViewModel.updateLink(link)
navigate("track_list/$link") {
launchSingleTop = singleInstance
popUpTo(route = "home") {
this.inclusive = inclusive
}
}
}

View File

@ -0,0 +1,75 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.10546875"
android:scaleY="0.10546875"
android:translateX="27"
android:translateY="27">
<path
android:pathData="M256,256m-256,0a256,256 0,1 1,512 0a256,256 0,1 1,-512 0">
<aapt:attr name="android:fillColor">
<gradient
android:startY="437.0193"
android:startX="74.9807"
android:endY="74.9807"
android:endX="437.0193"
android:type="linear">
<item android:offset="0" android:color="#FF736BFD"/>
<item android:offset="1" android:color="#FFF54187"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FF000000"
android:pathData="M377,356.7c-68.9,-45.4 -155.6,-56.4 -257.6,-32.7c-20.5,4.8 -13.6,35.8 7.3,31.2C290.7,317 351.6,386 368.2,386C384,386 390.2,365.4 377,356.7z"/>
<path
android:fillColor="#FF000000"
android:pathData="M112.1,275.1C203.9,253.4 308.1,266 384,308c18.5,10.2 34,-17.8 15.5,-28c-82.7,-45.7 -195.6,-59.5 -294.7,-36C84.2,248.8 91.5,280 112.1,275.1L112.1,275.1z"/>
<path
android:fillColor="#FF000000"
android:pathData="M100,191.9c96.6,-29.6 232.2,-13.4 308.7,36.9c17.6,11.5 35.3,-15.1 17.6,-26.7c-84.9,-55.8 -229.2,-73.3 -335.6,-40.8C70.4,167.5 79.9,198.1 100,191.9L100,191.9z"/>
<path
android:pathData="M507.8,438.2c-1.6,97.2 -141.9,97.1 -143.5,0C365.9,341 506.2,341 507.8,438.2z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="386.3741"
android:startX="487.8323"
android:endY="490.009"
android:endX="384.1974"
android:type="linear">
<item android:offset="0" android:color="#FF736BFD"/>
<item android:offset="1" android:color="#FFF54187"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FF000000"
android:pathData="M486.8,456.8c-0.6,-2.4 -6.9,-1 -8.5,-1.4c11.5,-82 -82.4,-86.7 -87.1,-22.2c0.3,1.8 -1,6.7 2.2,6.6c0,0 8.6,0 8.6,0c3.1,0.1 2,-4.7 2.2,-6.6c0.1,-23.3 35,-23.3 35.2,0c0,0 0,6.9 0,6.9c-0.1,2.8 4.4,2.8 4.3,0c5,-35.2 -43.8,-40.1 -43.8,-4.7h-4.3c-1.6,-53.7 77.2,-55.9 78.4,-2.2c0,0 0,24.4 0,24.4c-0.1,2.9 3.8,2.1 5.6,2.2l-20.7,21l-20.7,-21c1.8,-0.1 5.6,0.7 5.6,-2.2c0,0 0,-8.8 0,-8.8c0,-2.8 -4.4,-2.8 -4.3,0c0,0 0,6.6 0,6.6c-2.2,0.2 -11.3,-1.3 -8,3.7c0,0 25.9,26.3 25.9,26.3c0.8,0.9 2.2,0.9 3.1,0C460.6,484.4 489.4,458.3 486.8,456.8z"
android:strokeWidth="0.75"
android:strokeColor="#000000"/>
<path
android:pathData="M510,437.5c-1.7,96.2 -142.1,96.2 -143.8,0C367.9,341.3 508.4,341.3 510,437.5z"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#000000"/>
</group>
</vector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<color name="colorPrimary">#FC5C7D</color>
<color name="colorPrimaryDark">#CE1CFF</color>
<color name="colorAccent">#9AB3FF</color>
<color name="white">#FFFFFF</color>
<color name="grey">#99FFFFFF</color>
<color name="darkGrey">#E6333333</color>
<color name="black">#000000</color>
<color name="dark">#121212</color>
<color name="successGreen">#59C351</color>
<color name="errorRed">#FF9494</color>
</resources>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@ -0,0 +1,33 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="app_name">SpotiFlyer</string>
<string name="home_about">About</string>
<string name="home_history">History</string>
<string name="supported_platform">Supported Platforms</string>
<string name="support_development">Support Development</string>
<string name="github_star">Star / Fork the project on Github.</string>
<string name="github">GitHub</string>
<string name="translate">Translate</string>
<string name="help_us_translate">Help us translate this app in your local language.</string>
<string name="donate">Donate</string>
<string name="donate_subtitle">If you think I deserve to get paid for my work, you can leave me some money here.</string>
<string name="share">Share</string>
<string name="share_subtitle">Share this app with your friends and family.</string>
<string name="made_with_love">Made with</string>
<string name="in_india">in India</string>
</resources>

View File

@ -0,0 +1,29 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<!-- Base application theme. -->
<style name="Theme.SpotiFlyer" parent="Theme.AppCompat.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColor">@color/white</item>
<item name="android:background">@android:color/black</item>
<item name="android:backgroundTint">@android:color/black</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
</resources>

View File

@ -34,3 +34,23 @@ actual fun DownloadImageArrow(modifier: Modifier){
@Composable @Composable
actual fun DownloadAllImage() = vectorResource(R.drawable.ic_download_arrow) actual fun DownloadAllImage() = vectorResource(R.drawable.ic_download_arrow)
@Composable
actual fun SpotiFlyerLogo() = vectorResource(R.drawable.ic_spotiflyer_logo)
@Composable
actual fun HeartIcon() = vectorResource(R.drawable.ic_heart)
@Composable
actual fun SpotifyLogo() = vectorResource(R.drawable.ic_spotify_logo)
@Composable
actual fun GaanaLogo() = vectorResource(R.drawable.ic_gaana)
@Composable
actual fun YoutubeLogo() = vectorResource(R.drawable.ic_youtube)
@Composable
actual fun YoutubeMusicLogo() = vectorResource(R.drawable.ic_youtube_music_logo)
@Composable
actual fun GithubLogo() = vectorResource(R.drawable.ic_github)

View File

@ -20,8 +20,8 @@ import com.shabinder.common.DownloadStatus
import com.shabinder.common.Picture import com.shabinder.common.Picture
import com.shabinder.common.TrackDetails import com.shabinder.common.TrackDetails
import com.shabinder.common.ui.* import com.shabinder.common.ui.*
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography import com.shabinder.common.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent import com.shabinder.common.ui.colorAccent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@Composable @Composable

View File

@ -2,7 +2,9 @@ package com.shabinder.common.main
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.Dir
import com.shabinder.common.DownloadRecord import com.shabinder.common.DownloadRecord
import com.shabinder.common.Picture
import com.shabinder.common.main.integration.SpotiFlyerMainImpl import com.shabinder.common.main.integration.SpotiFlyerMainImpl
import com.shabinder.common.utils.Consumer import com.shabinder.common.utils.Consumer
import com.shabinder.database.Database import com.shabinder.database.Database
@ -23,10 +25,21 @@ interface SpotiFlyerMain {
* */ * */
fun onInputLinkChanged(link: String) fun onInputLinkChanged(link: String)
/*
* change TabBar Selected Category
* */
fun selectCategory(category: HomeCategory)
/*
* Load Image from cache/Internet and cache it
* */
fun loadImage(url:String):Picture?
interface Dependencies { interface Dependencies {
fun mainOutput(searched: Output): Consumer<Output> fun mainOutput(searched: Output): Consumer<Output>
val storeFactory: StoreFactory val storeFactory: StoreFactory
val database: Database val database: Database
val dir: Dir
} }
sealed class Output { sealed class Output {
data class Search(val link: String) : Output() data class Search(val link: String) : Output()
@ -34,8 +47,12 @@ interface SpotiFlyerMain {
data class State( data class State(
val records: List<DownloadRecord> = emptyList(), val records: List<DownloadRecord> = emptyList(),
val link: String = "" val link: String = "",
val selectedCategory: HomeCategory = HomeCategory.About
) )
enum class HomeCategory {
About, History
}
} }
@Suppress("FunctionName") // Factory function @Suppress("FunctionName") // Factory function

View File

@ -1,10 +1,359 @@
package com.shabinder.common.main package com.shabinder.common.main
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.AmbientTextStyle
import androidx.compose.material.Icon
import androidx.compose.material.TabDefaults.tabIndicatorOffset
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DateRange
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.shabinder.common.DownloadRecord
import com.shabinder.common.Picture
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
import com.shabinder.common.openPlatform
import com.shabinder.common.ui.*
import com.shabinder.common.ui.SpotiFlyerTypography
@Composable @Composable
fun SpotiFlyerMainContent(component: SpotiFlyerMain){ fun SpotiFlyerMainContent(component: SpotiFlyerMain){
val model by component.models.collectAsState(SpotiFlyerMain.State()) val model by component.models.collectAsState(SpotiFlyerMain.State())
Column {
SearchPanel(
model.link,
component::onInputLinkChanged,
component::onLinkSearch
)
HomeTabBar(
model.selectedCategory,
HomeCategory.values(),
component::selectCategory
)
when(model.selectedCategory){
HomeCategory.About -> AboutColumn()
HomeCategory.History -> HistoryColumn(
model.records,
component::loadImage,
component::onLinkSearch
)
}
}
}
@Composable
fun HomeTabBar(
selectedCategory: HomeCategory,
categories: Array<HomeCategory>,
selectCategory: (HomeCategory) -> Unit,
modifier: Modifier = Modifier
) {
val selectedIndex =categories.indexOfFirst { it == selectedCategory }
val indicator = @Composable { tabPositions: List<TabPosition> ->
HomeCategoryTabIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedIndex])
)
}
@Suppress("USELESS_CAST")//Showing Error in Latest Android Studio Canary
TabRow(
selectedTabIndex = selectedIndex,
indicator = indicator as @Composable (List<TabPosition>) -> Unit,
modifier = modifier,
) {
categories.forEachIndexed { index, category ->
Tab(
selected = index == selectedIndex,
onClick = { selectCategory(category) },
text = {
Text(
text = when (category) {
HomeCategory.About -> "About"
HomeCategory.History -> "History"
},
style = MaterialTheme.typography.body2
)
},
icon = {
when (category) {
HomeCategory.About -> Icon(Icons.Outlined.Info)
HomeCategory.History -> Icon(Icons.Outlined.DateRange)
}
}
)
}
}
}
@Composable
fun SearchPanel(
link:String,
updateLink:(String) -> Unit,
onSearch:(String) -> Unit,
modifier: Modifier = Modifier
){
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(top = 16.dp)
){
TextField(
leadingIcon = {
Icon(Icons.Rounded.Edit,tint = Color.LightGray)
},
label = { Text(text = "Paste Link Here...",color = Color.LightGray) },
value = link,
onValueChange = { updateLink(it) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
modifier = Modifier.padding(12.dp).fillMaxWidth()
.border(
BorderStroke(2.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))),
RoundedCornerShape(30.dp)
),
backgroundColor = Color.Black,
textStyle = AmbientTextStyle.current.merge(TextStyle(fontSize = 18.sp,color = Color.White)),
shape = RoundedCornerShape(size = 30.dp),
activeColor = Color.Transparent,
inactiveColor = Color.Transparent
)
OutlinedButton(
modifier = Modifier.padding(12.dp).wrapContentWidth(),
onClick = {
if(link.isBlank()) showPopUpMessage("Enter A Link!")
else{
//TODO if(!isOnline(ctx)) showPopUpMessage("Check Your Internet Connection") else
onSearch(link)
}
},
border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent)))
){
Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp))
}
}
}
@Composable
fun AboutColumn(modifier: Modifier = Modifier) {
ScrollableColumn(modifier.fillMaxSize(),contentPadding = PaddingValues(16.dp)) {
Card(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(1.dp,Color.Gray)
) {
Column(modifier.padding(12.dp)) {
Text(
text = "Supported Platforms",
style = SpotiFlyerTypography.body1,
color = colorAccent
)
Spacer(modifier = Modifier.padding(top = 12.dp))
Row(horizontalArrangement = Arrangement.Center,modifier = modifier.fillMaxWidth()) {
Icon(
imageVector = SpotifyLogo(), tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.spotify.music","http://open.spotify.com") })
)
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(imageVector = GaanaLogo(),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.gaana","http://gaana.com") })
)
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(imageVector = YoutubeLogo(),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com") })
)
Spacer(modifier = modifier.padding(start = 12.dp))
Icon(imageVector = YoutubeMusicLogo(),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.google.android.apps.youtube.music","https://music.youtube.com/") })
)
}
}
}
Spacer(modifier = Modifier.padding(top = 8.dp))
Card(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(1.dp,Color.Gray)
) {
Column(modifier.padding(12.dp)) {
Text(
text = "Support Development",
style = SpotiFlyerTypography.body1,
color = colorAccent
)
Spacer(modifier = Modifier.padding(top = 6.dp))
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(
onClick = { openPlatform("","http://github.com/Shabinder/SpotiFlyer") })
.padding(vertical = 6.dp)
) {
Icon(imageVector = GithubLogo(),tint = Color.LightGray)
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = "GitHub",
style = SpotiFlyerTypography.h6
)
Text(
text = "Star / Fork the project on Github.",
style = SpotiFlyerTypography.subtitle2
)
}
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { openPlatform("","http://github.com/Shabinder/SpotiFlyer") }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Info.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = "Translate",
style = SpotiFlyerTypography.h6
)
Text(
text = "Help us translate this app in your local language.",
style = SpotiFlyerTypography.subtitle2
)
}
}
/*Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { startPayment(mainActivity) }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.MailOutline.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.donate),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.donate_subtitle),
style = SpotiFlyerTypography.subtitle2
)
}
}*/
/*Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ctx.startActivity(shareIntent)
}),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Share.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.share),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.share_subtitle),
style = SpotiFlyerTypography.subtitle2
)
}
}*/
}
}
}
}
@Composable
fun HistoryColumn(
list: List<DownloadRecord>,
loadImage:(String)->Picture?,
onItemClicked: (String) -> Unit
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(list) {
DownloadRecordItem(
item = it,
loadImage,
onItemClicked
)
}
},
modifier = Modifier.padding(top = 8.dp).fillMaxSize()
)
}
@Composable
fun DownloadRecordItem(
item: DownloadRecord,
loadImage:(String)->Picture?,
onItemClicked:(String)->Unit
) {
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
val pic = loadImage(item.coverUrl)
ImageLoad(
pic,
modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp)
)
Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) {
Text(item.name,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
){
Text(item.type,fontSize = 13.sp)
Text("Tracks: ${item.totalFiles}",fontSize = 13.sp)
}
}
Image(
imageVector = Icons.Rounded.Share,
modifier = Modifier.clickable(onClick = {
//if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
onItemClicked(item.link)
})
)
}
}
@Composable
fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.onSurface
) {
Spacer(
modifier.padding(horizontal = 24.dp)
.preferredHeight(4.dp)
.background(color, RoundedCornerShape(topLeftPercent = 100, topRightPercent = 100))
)
} }

View File

@ -2,6 +2,7 @@ package com.shabinder.common.main.integration
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.mvikotlin.extensions.coroutines.states import com.arkivanov.mvikotlin.extensions.coroutines.states
import com.shabinder.common.Picture
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.* import com.shabinder.common.main.SpotiFlyerMain.*
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
@ -31,4 +32,10 @@ internal class SpotiFlyerMainImpl(
override fun onInputLinkChanged(link: String) { override fun onInputLinkChanged(link: String) {
store.accept(Intent.SetLink(link)) store.accept(Intent.SetLink(link))
} }
override fun selectCategory(category: HomeCategory) {
store.accept(Intent.SelectCategory(category))
}
override fun loadImage(url: String): Picture? = dir.loadImage(url)
} }

View File

@ -9,6 +9,7 @@ internal interface SpotiFlyerMainStore: Store<Intent, SpotiFlyerMain.State, Noth
sealed class Intent { sealed class Intent {
data class OpenPlatform(val platformID:String,val platformLink:String):Intent() data class OpenPlatform(val platformID:String,val platformLink:String):Intent()
data class SetLink(val link:String):Intent() data class SetLink(val link:String):Intent()
data class SelectCategory(val category: SpotiFlyerMain.HomeCategory):Intent()
object GiveDonation : Intent() object GiveDonation : Intent()
object ShareApp: Intent() object ShareApp: Intent()
} }

View File

@ -7,6 +7,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.DownloadRecord import com.shabinder.common.DownloadRecord
import com.shabinder.common.giveDonation import com.shabinder.common.giveDonation
import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.State import com.shabinder.common.main.SpotiFlyerMain.State
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.openPlatform import com.shabinder.common.openPlatform
@ -49,6 +50,7 @@ internal class SpotiFlyerMainStoreProvider(
private sealed class Result { private sealed class Result {
data class ItemsLoaded(val items: List<DownloadRecord>) : Result() data class ItemsLoaded(val items: List<DownloadRecord>) : Result()
data class CategoryChanged(val category: SpotiFlyerMain.HomeCategory) : Result()
data class LinkChanged(val link: String) : Result() data class LinkChanged(val link: String) : Result()
} }
@ -65,6 +67,7 @@ internal class SpotiFlyerMainStoreProvider(
is Intent.GiveDonation -> giveDonation() is Intent.GiveDonation -> giveDonation()
is Intent.ShareApp -> shareApp() is Intent.ShareApp -> shareApp()
is Intent.SetLink -> dispatch(Result.LinkChanged(link = intent.link)) is Intent.SetLink -> dispatch(Result.LinkChanged(link = intent.link))
is Intent.SelectCategory -> dispatch(Result.CategoryChanged(intent.category))
} }
} }
} }
@ -74,6 +77,7 @@ internal class SpotiFlyerMainStoreProvider(
when (result) { when (result) {
is Result.ItemsLoaded -> copy(records = result.items) is Result.ItemsLoaded -> copy(records = result.items)
is Result.LinkChanged -> copy(link = result.link) is Result.LinkChanged -> copy(link = result.link)
} is Result.CategoryChanged -> copy(selectedCategory = result.category)
}
} }
} }

View File

@ -1,20 +1,80 @@
package com.shabinder.common.root package com.shabinder.common.root
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.extensions.compose.jetbrains.Children import com.arkivanov.decompose.extensions.compose.jetbrains.Children
import com.shabinder.common.ui.utils.verticalGradientScrim
import com.shabinder.common.list.SpotiFlyerListContent import com.shabinder.common.list.SpotiFlyerListContent
import com.shabinder.common.main.SpotiFlyerMainContent import com.shabinder.common.main.SpotiFlyerMainContent
import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Child
import com.shabinder.common.ui.SpotiFlyerLogo
import com.shabinder.common.ui.appNameStyle
@Composable @Composable
fun SpotiFlyerRootContent(component: SpotiFlyerRoot) { fun SpotiFlyerRootContent(component: SpotiFlyerRoot) {
Children( val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
routerState = component.routerState, Column(
//TODO animation = crossfade() modifier = Modifier.fillMaxSize().verticalGradientScrim(
) { child, _ -> //color = sharedViewModel.gradientColor.copy(alpha = 0.38f),
when (child) { color = appBarColor.copy(alpha = 0.38f),
is Child.Main -> SpotiFlyerMainContent(component = child.component) startYPercentage = 0.29f,
is Child.List -> SpotiFlyerListContent(component = child.component) endYPercentage = 0f,
)
) {
AppBar(
backgroundColor = appBarColor,
modifier = Modifier.fillMaxWidth()
)
Children(
routerState = component.routerState,
//TODO animation = crossfade()
) { child, _ ->
when (child) {
is Child.Main -> SpotiFlyerMainContent(component = child.component)
is Child.List -> SpotiFlyerListContent(component = child.component)
}
} }
} }
} }
@Composable
fun AppBar(
backgroundColor: Color,
modifier: Modifier = Modifier
) {
TopAppBar(
backgroundColor = backgroundColor,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
imageVector = SpotiFlyerLogo(),
Modifier.preferredSize(32.dp)
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
text = "SpotiFlyer",
style = appNameStyle
)
}
},
/*actions = {
Providers(AmbientContentAlpha provides ContentAlpha.medium) {
IconButton(
onClick = { *//* TODO: Open Preferences*//* }
) {
Icon(Icons.Filled.Settings, tint = Color.Gray)
}
}
},*/
modifier = modifier,
elevation = 0.dp
)
}

View File

@ -41,6 +41,7 @@ internal class SpotiFlyerRootImpl(
componentContext = componentContext, componentContext = componentContext,
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by this { dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by this {
override fun mainOutput(searched: SpotiFlyerMain.Output): Consumer<SpotiFlyerMain.Output> = Consumer(::onMainOutput) override fun mainOutput(searched: SpotiFlyerMain.Output): Consumer<SpotiFlyerMain.Output> = Consumer(::onMainOutput)
override val dir: Dir = directories
} }
) )

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui package com.shabinder.common.ui
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.material.darkColors import androidx.compose.material.darkColors

View File

@ -4,6 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.font
import androidx.compose.ui.text.font.fontFamily
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import com.shabinder.common.Picture import com.shabinder.common.Picture

View File

@ -10,6 +10,27 @@ expect fun DownloadImageTick(modifier: Modifier = Modifier)
@Composable @Composable
expect fun DownloadAllImage():ImageVector expect fun DownloadAllImage():ImageVector
@Composable
expect fun SpotiFlyerLogo():ImageVector
@Composable
expect fun SpotifyLogo():ImageVector
@Composable
expect fun YoutubeLogo():ImageVector
@Composable
expect fun GaanaLogo():ImageVector
@Composable
expect fun YoutubeMusicLogo():ImageVector
@Composable
expect fun GithubLogo():ImageVector
@Composable
expect fun HeartIcon():ImageVector
@Composable @Composable
expect fun DownloadImageError(modifier: Modifier = Modifier) expect fun DownloadImageError(modifier: Modifier = Modifier)

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui package com.shabinder.common.ui
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes import androidx.compose.material.Shapes

View File

@ -1,12 +0,0 @@
package com.shabinder.common.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Composable
fun SpotiFlyerMain(){
Column {
Text("Hello World")
}
}

View File

@ -14,13 +14,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui package com.shabinder.common.ui
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@Composable @Composable
fun ComposeLearnTheme(content: @Composable() () -> Unit) { fun SpotiFlyerTheme(content: @Composable() () -> Unit) {
MaterialTheme( MaterialTheme(
colors = SpotiFlyerColors, colors = SpotiFlyerColors,
typography = SpotiFlyerTypography, typography = SpotiFlyerTypography,

View File

@ -14,63 +14,57 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui package com.shabinder.common.ui
import androidx.compose.material.Typography import androidx.compose.material.Typography
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.font
import androidx.compose.ui.text.font.fontFamily
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.shabinder.spotiflyer.R
private val Montserrat = fontFamily( /*private val Montserrat = fontFamily(
font(R.font.montserrat_light, FontWeight.Light), font(R.font.montserrat_light, FontWeight.Light),
font(R.font.montserrat_regular, FontWeight.Normal), font(R.font.montserrat_regular, FontWeight.Normal),
font(R.font.montserrat_medium, FontWeight.Medium), font(R.font.montserrat_medium, FontWeight.Medium),
font(R.font.montserrat_semibold, FontWeight.SemiBold), font(R.font.montserrat_semibold, FontWeight.SemiBold),
) )*/
val pristineFont = fontFamily(
font(R.font.pristine_script, FontWeight.Bold)
)
val SpotiFlyerTypography = Typography( val SpotiFlyerTypography = Typography(
h1 = TextStyle( h1 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 96.sp, fontSize = 96.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
lineHeight = 117.sp, lineHeight = 117.sp,
letterSpacing = (-1.5).sp letterSpacing = (-1.5).sp
), ),
h2 = TextStyle( h2 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 60.sp, fontSize = 60.sp,
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
lineHeight = 73.sp, lineHeight = 73.sp,
letterSpacing = (-0.5).sp letterSpacing = (-0.5).sp
), ),
h3 = TextStyle( h3 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 48.sp, fontSize = 48.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
lineHeight = 59.sp lineHeight = 59.sp
), ),
h4 = TextStyle( h4 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 30.sp, fontSize = 30.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 37.sp lineHeight = 37.sp
), ),
h5 = TextStyle( h5 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 24.sp, fontSize = 24.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 29.sp lineHeight = 29.sp
), ),
h6 = TextStyle( h6 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 18.sp, fontSize = 18.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
lineHeight = 26.sp, lineHeight = 26.sp,
@ -78,49 +72,49 @@ val SpotiFlyerTypography = Typography(
), ),
subtitle1 = TextStyle( subtitle1 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
), ),
subtitle2 = TextStyle( subtitle2 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
lineHeight = 17.sp, lineHeight = 17.sp,
letterSpacing = 0.1.sp letterSpacing = 0.1.sp
), ),
body1 = TextStyle( body1 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.15.sp, letterSpacing = 0.15.sp,
), ),
body2 = TextStyle( body2 = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.25.sp letterSpacing = 0.25.sp
), ),
button = TextStyle( button = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 1.25.sp letterSpacing = 1.25.sp
), ),
caption = TextStyle( caption = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
overline = TextStyle( overline = TextStyle(
fontFamily = Montserrat, //fontFamily = Montserrat,
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 16.sp, lineHeight = 16.sp,
@ -129,7 +123,7 @@ val SpotiFlyerTypography = Typography(
) )
val appNameStyle = TextStyle( val appNameStyle = TextStyle(
fontFamily = pristineFont, fontFamily = FontFamily.Cursive,
fontSize = 40.sp, fontSize = 40.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
lineHeight = 42.sp, lineHeight = 42.sp,

View File

@ -1,436 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.common.ui.home
import android.content.Intent
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.AmbientTextStyle
import androidx.compose.material.Icon
import androidx.compose.material.TabDefaults.tabIndicatorOffset
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.CardGiftcard
import androidx.compose.material.icons.rounded.Flag
import androidx.compose.material.icons.rounded.InsertLink
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.AmbientContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.viewModel
import androidx.core.net.toUri
import androidx.navigation.NavController
import com.razorpay.Checkout
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.navigation.navigateToTrackList
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent
import com.shabinder.spotiflyer.ui.colorPrimary
import com.shabinder.spotiflyer.ui.home.HomeCategory
import com.shabinder.spotiflyer.ui.home.HomeViewModel
import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.openPlatform
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import dev.chrisbanes.accompanist.coil.CoilImage
import org.json.JSONObject
@Composable
fun Home(
navController: NavController,
mainActivity: MainActivity,
modifier: Modifier = Modifier) {
val viewModel: HomeViewModel = viewModel()
Column(modifier = modifier) {
AuthenticationBanner(sharedViewModel.isAuthenticated,modifier)
SearchPanel(
sharedViewModel.link,
sharedViewModel::updateLink,
navController,
modifier
)
HomeTabBar(
viewModel.selectedCategory,
HomeCategory.values(),
viewModel::selectCategory,
modifier
)
when(viewModel.selectedCategory){
HomeCategory.About -> AboutColumn(mainActivity)
HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController)
}
}
//Update Download List
viewModel.getDownloadRecordList()
//reset Gradient
sharedViewModel.resetGradient()
}
@Composable
fun AboutColumn(mainActivity: MainActivity,modifier: Modifier = Modifier) {
val ctx = AmbientContext.current
ScrollableColumn(modifier.fillMaxSize(),contentPadding = PaddingValues(16.dp)) {
Card(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(1.dp,Color.Gray)
) {
Column(modifier.padding(12.dp)) {
Text(
text = stringResource(R.string.supported_platform),
style = SpotiFlyerTypography.body1,
color = colorAccent
)
Spacer(modifier = Modifier.padding(top = 12.dp))
Row(horizontalArrangement = Arrangement.Center,modifier = modifier.fillMaxWidth()) {
Icon(
imageVector = vectorResource(id = R.drawable.ic_spotify_logo), tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.spotify.music","http://open.spotify.com",ctx) })
)
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(imageVector = vectorResource(id = R.drawable.ic_gaana ),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.gaana","http://gaana.com",ctx) })
)
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(imageVector = vectorResource(id = R.drawable.ic_youtube),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com",ctx) })
)
Spacer(modifier = modifier.padding(start = 12.dp))
Icon(imageVector = vectorResource(id = R.drawable.ic_youtube_music_logo),tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { openPlatform("com.google.android.apps.youtube.music","https://music.youtube.com/",ctx) })
)
}
}
}
Spacer(modifier = Modifier.padding(top = 8.dp))
Card(
modifier = modifier.fillMaxWidth(),
border = BorderStroke(1.dp,Color.Gray)
) {
Column(modifier.padding(12.dp)) {
Text(
text = stringResource(R.string.support_development),
style = SpotiFlyerTypography.body1,
color = colorAccent
)
Spacer(modifier = Modifier.padding(top = 6.dp))
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(
onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer",ctx) })
.padding(vertical = 6.dp)
) {
Icon(imageVector = vectorResource(id = R.drawable.ic_github ),tint = Color.LightGray)
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.github),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.github_star),
style = SpotiFlyerTypography.subtitle2
)
}
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer", ctx) }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Flag.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.translate),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.help_us_translate),
style = SpotiFlyerTypography.subtitle2
)
}
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { startPayment(mainActivity) }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.CardGiftcard.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.donate),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.donate_subtitle),
style = SpotiFlyerTypography.subtitle2
)
}
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ctx.startActivity(shareIntent)
}),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Share.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.share),
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.share_subtitle),
style = SpotiFlyerTypography.subtitle2
)
}
}
}
}
}
}
@Composable
fun HistoryColumn(
list: List<DownloadRecord>,
navController: NavController
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
content = {
items(list) {
DownloadRecordItem(item = it,navController = navController)
}
},
modifier = Modifier.padding(top = 8.dp).fillMaxSize()
)
}
@Composable
fun DownloadRecordItem(item: DownloadRecord,navController: NavController) {
val ctx = AmbientContext.current
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
val imgUri = item.coverUrl.toUri().buildUpon().scheme("https").build()
CoilImage(
data = imgUri,
//Loading Placeholder Makes Scrolling very stuttery
// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) },
error = {Image(vectorResource(id = R.drawable.ic_musicplaceholder))},
contentScale = ContentScale.Inside,
// fadeIn = true,
modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp)
)
Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) {
Text(item.name,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
){
Text(item.type,fontSize = 13.sp)
Text("Tracks: ${item.totalFiles}",fontSize = 13.sp)
}
}
Image(
imageVector = vectorResource(id = R.drawable.ic_share_open),
modifier = Modifier.clickable(onClick = {
if(!isOnline(ctx)) showDialog("Check Your Internet Connection")
else navController.navigateToTrackList(item.link)
})
)
}
}
private fun startPayment(mainActivity: MainActivity) {
/*
* You need to pass current activity in order to let Razorpay create CheckoutActivity
* */
val co = Checkout().apply {
setKeyID("rzp_live_3ZQeoFYOxjmXye")
setImage(R.drawable.ic_launcher_foreground)
}
try {
val preFill = JSONObject()
val options = JSONObject().apply {
put("name","SpotiFlyer")
put("description","Thanks For the Donation!")
//You can omit the image option to fetch the image from dashboard
//put("image","https://github.com/Shabinder/SpotiFlyer/raw/master/app/SpotifyDownload.png")
put("currency","INR")
put("amount","4900")
put("prefill",preFill)
}
co.open(mainActivity,options)
}catch (e: Exception){
showDialog("Error in payment: "+ e.message)
e.printStackTrace()
}
}
@Composable
fun AuthenticationBanner(isAuthenticated: Boolean, modifier: Modifier) {
if (!isAuthenticated) {
// TODO show a progress indicator or similar
}
}
@Composable
fun HomeTabBar(
selectedCategory: HomeCategory,
categories: Array<HomeCategory>,
selectCategory: (HomeCategory) -> Unit,
modifier: Modifier = Modifier
) {
val selectedIndex =categories.indexOfFirst { it == selectedCategory }
val indicator = @Composable { tabPositions: List<TabPosition> ->
HomeCategoryTabIndicator(
Modifier.tabIndicatorOffset(tabPositions[selectedIndex])
)
}
TabRow(
selectedTabIndex = selectedIndex,
indicator = indicator,
modifier = modifier,
) {
categories.forEachIndexed { index, category ->
Tab(
selected = index == selectedIndex,
onClick = { selectCategory(category) },
text = {
Text(
text = when (category) {
HomeCategory.About -> stringResource(R.string.home_about)
HomeCategory.History -> stringResource(R.string.home_history)
},
style = MaterialTheme.typography.body2
)
},
icon = {
when (category) {
HomeCategory.About -> Icon(Icons.Outlined.Info)
HomeCategory.History -> Icon(Icons.Outlined.History)
}
}
)
}
}
}
@Composable
fun SearchPanel(
link:String,
updateLink:(s:String) -> Unit,
navController: NavController,
modifier: Modifier = Modifier
){
val ctx = AmbientContext.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(top = 16.dp)
){
TextField(
leadingIcon = {
Icon(Icons.Rounded.InsertLink,tint = Color.LightGray)
},
label = {Text(text = "Paste Link Here...",color = Color.LightGray)},
value = link,
onValueChange = { updateLink(it) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
modifier = Modifier.padding(12.dp).fillMaxWidth()
.border(
BorderStroke(2.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))),
RoundedCornerShape(30.dp)
),
backgroundColor = Color.Black,
textStyle = AmbientTextStyle.current.merge(TextStyle(fontSize = 18.sp,color = Color.White)),
shape = RoundedCornerShape(size = 30.dp),
activeColor = Color.Transparent,
inactiveColor = Color.Transparent
)
OutlinedButton(
modifier = Modifier.padding(12.dp).wrapContentWidth(),
onClick = {
if(link.isBlank()) showDialog("Enter A Link!")
else{
if(!isOnline(ctx)) showDialog("Check Your Internet Connection")
else navController.navigateToTrackList(link)
}
},
border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent)))
){
Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp))
}
}
}
@Composable
fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.onSurface
) {
Spacer(
modifier.padding(horizontal = 24.dp)
.preferredHeight(4.dp)
.background(color, RoundedCornerShape(topLeftPercent = 100, topRightPercent = 100))
)
}

View File

@ -1,54 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.home
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.utils.sharedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HomeViewModel : ViewModel() {
var selectedCategory by mutableStateOf(HomeCategory.About)
private set
fun selectCategory(s:HomeCategory) {
selectedCategory = s
}
var downloadRecordList by mutableStateOf<List<DownloadRecord>>(listOf())
fun getDownloadRecordList() {
viewModelScope.launch {
withContext(Dispatchers.IO){
delay(100) //TEMP
downloadRecordList = sharedViewModel.databaseDAO.getRecord()
}
}
}
}
enum class HomeCategory {
About, History
}

View File

@ -1,111 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.example.jetcaster.util.verticalGradientScrim
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.navigation.ComposeNavigation
import com.shabinder.spotiflyer.ui.appNameStyle
import dev.chrisbanes.accompanist.insets.statusBarsHeight
@Composable
fun MainScreen(
modifier: Modifier,
mainActivity: MainActivity,
sharedViewModel: SharedViewModel,
navController: NavHostController,
topPadding: Dp = 0.dp
){
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
Column(
modifier = modifier.fillMaxSize().verticalGradientScrim(
color = sharedViewModel.gradientColor.copy(alpha = 0.38f),
startYPercentage = 0.29f,
endYPercentage = 0f,
)
) {
// Draw a scrim over the status bar which matches the app bar
Spacer(
Modifier.background(appBarColor).fillMaxWidth()
.statusBarsHeight()
)
AppBar(
backgroundColor = appBarColor,
modifier = Modifier.fillMaxWidth()
)
//Space for Animation
Spacer(Modifier.padding(top = topPadding))
ComposeNavigation(
mainActivity,
navController,
sharedViewModel.spotifyProvider,
sharedViewModel.gaanaProvider,
sharedViewModel.youtubeProvider
)
}
}
@Composable
fun AppBar(
backgroundColor: Color,
modifier: Modifier = Modifier
) {
TopAppBar(
backgroundColor = backgroundColor,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
imageVector = vectorResource(R.drawable.ic_spotiflyer_logo),
Modifier.preferredSize(32.dp)
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
text = "SpotiFlyer",
style = appNameStyle
)
}
},
/*actions = {
Providers(AmbientContentAlpha provides ContentAlpha.medium) {
IconButton(
onClick = { *//* TODO: Open Preferences*//* }
) {
Icon(Icons.Filled.Settings, tint = Color.Gray)
}
}
},*/
modifier = modifier,
elevation = 0.dp
)
}

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui.splash package com.shabinder.common.ui.splash
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -27,14 +27,9 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.shabinder.spotiflyer.R import com.shabinder.common.ui.*
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent
import com.shabinder.spotiflyer.ui.colorPrimary
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private const val SplashWaitTime: Long = 1100 private const val SplashWaitTime: Long = 1100
@ -49,7 +44,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
delay(SplashWaitTime) delay(SplashWaitTime)
currentOnTimeout() currentOnTimeout()
} }
Image(imageVector = vectorResource(id = R.drawable.ic_spotiflyer_logo)) Image(imageVector = SpotiFlyerLogo())
MadeInIndia(Modifier.align(Alignment.BottomCenter)) MadeInIndia(Modifier.align(Alignment.BottomCenter))
} }
} }
@ -67,15 +62,15 @@ fun MadeInIndia(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.made_with_love), text = "Made with ",
color = colorPrimary, color = colorPrimary,
fontSize = 22.sp fontSize = 22.sp
) )
Spacer(modifier = Modifier.padding(start = 4.dp)) Spacer(modifier = Modifier.padding(start = 4.dp))
Icon(vectorResource(id = R.drawable.ic_heart),tint = Color.Unspecified) Icon(HeartIcon(),tint = Color.Unspecified)
Spacer(modifier = Modifier.padding(start = 4.dp)) Spacer(modifier = Modifier.padding(start = 4.dp))
Text( Text(
text = stringResource(id = R.string.in_india), text = " in India",
color = colorPrimary, color = colorPrimary,
fontSize = 22.sp fontSize = 22.sp
) )

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui.utils package com.shabinder.common.ui.utils
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver

View File

@ -1,87 +0,0 @@
/*
* Copyright (c) 2021 Shabinder Singh
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.utils
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import coil.Coil
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.size.Scale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Immutable
data class DominantColors(val color: Color, val onColor: Color)
suspend fun calculateDominantColor(url: String,ctx:Context): DominantColors? {
// we calculate the swatches in the image, and return the first valid color
return calculateSwatchesInImage(ctx, url)
// First we want to sort the list by the color's population
.sortedByDescending { swatch -> swatch.population }
// Then we want to find the first valid color
.firstOrNull { swatch -> Color(swatch.rgb).contrastAgainst(Color.Black) >= 3f }
// If we found a valid swatch, wrap it in a [DominantColors]
?.let { swatch ->
DominantColors(
color = Color(swatch.rgb),
onColor = Color(swatch.bodyTextColor).copy(alpha = 1f)
)
}
}
/**
* Fetches the given [imageUrl] with [Coil], then uses [Palette] to calculate the dominant color.
*/
suspend fun calculateSwatchesInImage(
context: Context,
imageUrl: String
): List<Palette.Swatch> {
val r = ImageRequest.Builder(context)
.data(imageUrl)
// We scale the image to cover 128px x 128px (i.e. min dimension == 128px)
.size(128).scale(Scale.FILL)
// Disable hardware bitmaps, since Palette uses Bitmap.getPixels()
.allowHardware(false)
.build()
val bitmap = when (val result = Coil.execute(r)) {
is SuccessResult -> result.drawable.toBitmap()
else -> null
}
return bitmap?.let {
withContext(Dispatchers.Default) {
val palette = Palette.Builder(bitmap)
// Disable any bitmap resizing in Palette. We've already loaded an appropriately
// sized bitmap through Coil
.resizeBitmapArea(0)
// Clear any built-in filters. We want the unfiltered dominant color
.clearFilters()
// We reduce the maximum color count down to 8
.maximumColorCount(8)
.generate()
palette.swatches
}
} ?: emptyList()
}

View File

@ -14,9 +14,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.example.jetcaster.util package com.shabinder.common.ui.utils
import androidx.annotation.FloatRange
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -41,8 +40,10 @@ import kotlin.math.pow
*/ */
fun Modifier.verticalGradientScrim( fun Modifier.verticalGradientScrim(
color: Color, color: Color,
@FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f, //@FloatRange(from = 0.0, to = 1.0)
@FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f, startYPercentage: Float = 0f,
//@FloatRange(from = 0.0, to = 1.0)
endYPercentage: Float = 1f,
decay: Float = 1.0f, decay: Float = 1.0f,
numStops: Int = 16, numStops: Int = 16,
fixedHeight: Float? = null fixedHeight: Float? = null

View File

@ -32,3 +32,24 @@ actual fun DownloadImageArrow(modifier: Modifier){
@Composable @Composable
actual fun DownloadAllImage():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_download_arrow.xml") actual fun DownloadAllImage():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_download_arrow.xml")
@Composable
actual fun SpotiFlyerLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_spotiflyer_logo.xml")
@Composable
actual fun HeartIcon():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_heart.xml")
@Composable
actual fun SpotifyLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_spotify_logo.xml")
@Composable
actual fun YoutubeLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_youtube.xml")
@Composable
actual fun GaanaLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_gaana.xml")
@Composable
actual fun YoutubeMusicLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_youtube_music_logo.xml")
@Composable
actual fun GithubLogo():ImageVector = vectorXmlResource("common/compose-ui/src/main/res/drawable/ic_github.xml")

View File

@ -0,0 +1,21 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="38dp"
android:height="38dp" android:viewportWidth="512" android:viewportHeight="512">
<path android:fillColor="#4EA4FF" android:pathData="M255.968,5.329C114.624,5.329 0,120.401 0,262.353c0,113.536 73.344,209.856 175.104,243.872c12.8,2.368 17.472,-5.568 17.472,-12.384c0,-6.112 -0.224,-22.272 -0.352,-43.712c-71.2,15.52 -86.24,-34.464 -86.24,-34.464c-11.616,-29.696 -28.416,-37.6 -28.416,-37.6c-23.264,-15.936 1.728,-15.616 1.728,-15.616c25.696,1.824 39.2,26.496 39.2,26.496c22.848,39.264 59.936,27.936 74.528,21.344c2.304,-16.608 8.928,-27.936 16.256,-34.368c-56.832,-6.496 -116.608,-28.544 -116.608,-127.008c0,-28.064 9.984,-51.008 26.368,-68.992c-2.656,-6.496 -11.424,-32.64 2.496,-68c0,0 21.504,-6.912 70.4,26.336c20.416,-5.696 42.304,-8.544 64.096,-8.64c21.728,0.128 43.648,2.944 64.096,8.672c48.864,-33.248 70.336,-26.336 70.336,-26.336c13.952,35.392 5.184,61.504 2.56,68c16.416,17.984 26.304,40.928 26.304,68.992c0,98.72 -59.84,120.448 -116.864,126.816c9.184,7.936 17.376,23.616 17.376,47.584c0,34.368 -0.32,62.08 -0.32,70.496c0,6.88 4.608,14.88 17.6,12.352C438.72,472.145 512,375.857 512,262.353C512,120.401 397.376,5.329 255.968,5.329z"/>
</vector>

View File

@ -7,13 +7,16 @@ import android.os.Environment
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.database.appContext import com.shabinder.common.database.appContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.* import java.io.*
import java.lang.Exception import java.lang.Exception
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
actual open class Dir actual constructor( actual class Dir actual constructor(
private val logger: Kermit private val logger: Kermit
) { ) {
@ -53,7 +56,7 @@ actual open class Dir actual constructor(
File(imageCacheDir()).deleteRecursively() File(imageCacheDir()).deleteRecursively()
} }
actual fun cacheImage(picture: Picture) { actual suspend fun cacheImage(picture: Picture) {
try { try {
val path = imageCacheDir() + picture.name val path = imageCacheDir() + picture.name
FileOutputStream(path).use { out -> FileOutputStream(path).use { out ->
@ -92,9 +95,10 @@ actual open class Dir actual constructor(
.setId3v2TagsAndSaveFile(trackDetails,path) .setId3v2TagsAndSaveFile(trackDetails,path)
} }
actual fun loadImage(url: String, cachePath: String):Picture? { actual fun loadImage(url: String):Picture? {
val cachePath = imageCacheDir() + getNameURL(url)
var picture: Picture? = loadCachedImage(cachePath) var picture: Picture? = loadCachedImage(cachePath)
if (picture == null) picture = freshImage(url,cachePath) if (picture == null) picture = freshImage(url)
return picture return picture
} }
@ -129,7 +133,7 @@ actual open class Dir actual constructor(
null null
} }
} }
private fun freshImage(url:String,cachePath: String):Picture?{ private fun freshImage(url:String):Picture?{
return try { return try {
val source = URL(url) val source = URL(url)
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
@ -147,8 +151,9 @@ actual open class Dir actual constructor(
result.width, result.width,
result.height result.height
) )
GlobalScope.launch(Dispatchers.IO) {
cacheImage(picture) cacheImage(picture)
}
picture picture
} else null } else null
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -32,6 +32,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { SpotifyProvider(get(),get(),get(),get()) } single { SpotifyProvider(get(),get(),get(),get()) }
single { GaanaProvider(get(),get(),get(),get()) } single { GaanaProvider(get(),get(),get(),get()) }
single { YoutubeProvider(get(),get(),get(),get()) } single { YoutubeProvider(get(),get(),get(),get()) }
single { FetchPlatformQueryResult(get(),get(),get(),get()) }
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) } single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
} }

View File

@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt import kotlin.math.roundToInt
expect open class Dir( expect class Dir(
logger: Kermit, logger: Kermit,
) { ) {
fun isPresent(path:String):Boolean fun isPresent(path:String):Boolean
@ -18,7 +18,7 @@ expect open class Dir(
fun imageCacheDir(): String fun imageCacheDir(): String
fun createDirectory(dirPath:String) fun createDirectory(dirPath:String)
suspend fun cacheImage(picture: Picture) suspend fun cacheImage(picture: Picture)
fun loadImage(url:String, cachePath:String = imageCacheDir() + getNameURL(url)):Picture? fun loadImage(url:String):Picture?
suspend fun clearCache() suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails) suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails)
} }

View File

@ -13,7 +13,7 @@ import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.imageio.ImageIO import javax.imageio.ImageIO
actual open class Dir actual constructor(private val logger: Kermit) { actual class Dir actual constructor(private val logger: Kermit) {
actual fun fileSeparator(): String = File.separator actual fun fileSeparator(): String = File.separator
@ -85,7 +85,8 @@ actual open class Dir actual constructor(private val logger: Kermit) {
.setId3v2TagsAndSaveFile(trackDetails,path) .setId3v2TagsAndSaveFile(trackDetails,path)
} }
actual fun loadImage(url: String, cachePath: String):Picture? { actual fun loadImage(url: String):Picture? {
val cachePath = imageCacheDir() + getNameURL(url)
var picture: Picture? = loadCachedImage(cachePath) var picture: Picture? = loadCachedImage(cachePath)
if (picture == null) picture = freshImage(url,cachePath) if (picture == null) picture = freshImage(url,cachePath)
return picture return picture