0
0
Fork 0
mirror of https://github.com/YTVanced/VancedManager synced 2024-11-26 05:05:15 +00:00
This commit is contained in:
X1nto 2021-10-31 12:52:51 +04:00
parent 4075bd154f
commit 9d3529e8a5
91 changed files with 1305 additions and 1187 deletions

View file

@ -1,6 +1,7 @@
plugins {
id("com.android.application")
kotlin("android")
id("kotlin-parcelize")
}
android {
@ -78,7 +79,6 @@ val languages: String get() {
dependencies {
implementation(kotlin("reflect"))
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")

View file

@ -1,4 +1,4 @@
package com.vanced.manager.downloader.api
package com.vanced.manager.core.downloader.api
import okhttp3.ResponseBody
import retrofit2.Call

View file

@ -1,4 +1,4 @@
package com.vanced.manager.downloader.api
package com.vanced.manager.core.downloader.api
import okhttp3.ResponseBody
import retrofit2.Call

View file

@ -1,4 +1,4 @@
package com.vanced.manager.downloader.api
package com.vanced.manager.core.downloader.api
import okhttp3.ResponseBody
import retrofit2.Call

View file

@ -0,0 +1,85 @@
package com.vanced.manager.core.downloader.base
import com.vanced.manager.core.downloader.util.DownloadStatus
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.awaitResponse
import java.io.FileOutputStream
typealias DownloadCall = Call<ResponseBody>
abstract class AppDownloader {
data class DownloadFile(
val fileName: String,
val call: DownloadCall,
)
private lateinit var call: DownloadCall
abstract suspend fun download(
appVersions: List<String>,
onStatus: (DownloadStatus) -> Unit
)
abstract fun getSavedFilePath(): String
suspend fun downloadFiles(
downloadFiles: Array<DownloadFile>,
onFile: (String) -> Unit,
onProgress: (Float) -> Unit,
onError: (error: String, fileName: String) -> Unit,
onSuccess: () -> Unit
) {
for (downloadFile in downloadFiles) {
try {
this.call = downloadFile.call
onFile(downloadFile.fileName)
val response = call.awaitResponse()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
writeFile(body, downloadFile.fileName) { progress ->
onProgress(progress)
}
}
continue
}
val error = response.errorBody()?.toString()
if (error != null) {
onError(error, downloadFile.fileName)
return
}
} catch (e: Exception) {
onError(e.stackTraceToString(), downloadFile.fileName)
return
}
}
onSuccess()
}
private inline fun writeFile(
body: ResponseBody,
fileName: String,
onProgress: (Float) -> Unit
) {
val inputStream = body.byteStream()
val outputStream = FileOutputStream(getSavedFilePath() + "/$fileName")
val totalBytes = body.contentLength()
val fileReader = ByteArray(4096)
var downloadedBytes = 0L
var read: Int
while (inputStream.read(fileReader).also { read = it } != -1) {
outputStream.write(fileReader, 0, read)
downloadedBytes += read
onProgress((downloadedBytes * 100 / totalBytes).toFloat())
}
inputStream.close()
outputStream.close()
}
}

View file

@ -0,0 +1,53 @@
package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.MicrogAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import java.io.File
class MicrogDownloader(
private val microgAPI: MicrogAPI,
private val context: Context,
) : AppDownloader() {
override suspend fun download(
appVersions: List<String>,
onStatus: (DownloadStatus) -> Unit
) {
downloadFiles(
downloadFiles = arrayOf(
DownloadFile(
call = microgAPI.getFile(),
fileName = "microg.apk"
)
),
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
))
}
)
}
override fun getSavedFilePath(): String {
val directory =
File(context.getExternalFilesDir("microg")!!.path)
if (!directory.exists())
directory.mkdirs()
return directory.path
}
}

View file

@ -0,0 +1,64 @@
package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.MusicAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
class MusicDownloader(
private val musicAPI: MusicAPI,
private val context: Context,
) : AppDownloader() {
private val version by musicVersionPref
private val variant by managerVariantPref
private lateinit var correctVersion: String
override suspend fun download(
appVersions: List<String>,
onStatus: (DownloadStatus) -> Unit
) {
correctVersion = getLatestOrProvidedAppVersion(version, appVersions)
downloadFiles(
downloadFiles = arrayOf(DownloadFile(
call = musicAPI.getFiles(
version = correctVersion,
variant = variant,
),
fileName = "music.apk"
)),
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
))
}
)
}
override fun getSavedFilePath(): String {
val directory =
File(context.getExternalFilesDir("vancedmusic")!!.path + "$correctVersion/$variant")
if (!directory.exists())
directory.mkdirs()
return directory.path
}
}

View file

@ -0,0 +1,92 @@
package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.VancedAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.vancedLanguagesPref
import com.vanced.manager.core.preferences.holder.vancedThemePref
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.vanced.manager.core.util.arch
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
class VancedDownloader(
private val vancedAPI: VancedAPI,
private val context: Context,
) : AppDownloader() {
private val theme by vancedThemePref
private val version by vancedVersionPref
private val variant by managerVariantPref
private val languages by vancedLanguagesPref
private lateinit var correctVersion: String
override suspend fun download(
appVersions: List<String>,
onStatus: (DownloadStatus) -> Unit
) {
correctVersion = getLatestOrProvidedAppVersion(version, appVersions)
val files = arrayOf(
getFile(
type = "Theme",
apkName = "$theme.apk",
),
getFile(
type = "Arch",
apkName = "split_config.$arch.apk",
)
) + languages.map { language ->
getFile(
type = "Language",
apkName = "split_config.$language.apk",
)
}
downloadFiles(
downloadFiles = files,
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
))
}
)
}
override fun getSavedFilePath(): String {
val directory =
File(context.getExternalFilesDir("vanced")!!.path + "$correctVersion/$variant")
if (!directory.exists())
directory.mkdirs()
return directory.path
}
private fun getFile(
type: String,
apkName: String,
) = DownloadFile(
call = vancedAPI.getFiles(
version = correctVersion,
variant = variant,
type = type,
apkName = apkName
),
fileName = apkName
)
}

View file

@ -0,0 +1,16 @@
package com.vanced.manager.core.downloader.util
sealed class DownloadStatus {
object StartInstall : DownloadStatus()
data class File(val fileName: String) : DownloadStatus()
data class Progress(val progress: Float) : DownloadStatus()
data class Error(
val displayError: String,
val stacktrace: String,
) : DownloadStatus()
}

View file

@ -1,8 +1,8 @@
package com.vanced.manager.installer.base
package com.vanced.manager.core.installer.base
import android.content.Context
import androidx.annotation.CallSuper
import com.vanced.manager.util.log
import com.vanced.manager.core.util.log
import com.xinto.apkhelper.statusCallback
import com.xinto.apkhelper.statusCallbackBuilder
import org.koin.core.component.KoinComponent

View file

@ -1,6 +1,6 @@
package com.vanced.manager.installer.impl
package com.vanced.manager.core.installer.impl
import com.vanced.manager.installer.base.AppInstaller
import com.vanced.manager.core.installer.base.AppInstaller
import com.xinto.apkhelper.installApk
class MicrogInstaller : AppInstaller() {

View file

@ -1,8 +1,8 @@
package com.vanced.manager.installer.impl
package com.vanced.manager.core.installer.impl
import com.vanced.manager.installer.base.AppInstaller
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.preferences.holder.musicVersionPref
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.xinto.apkhelper.installApk
class MusicInstaller : AppInstaller() {

View file

@ -1,8 +1,8 @@
package com.vanced.manager.installer.impl
package com.vanced.manager.core.installer.impl
import com.vanced.manager.installer.base.AppInstaller
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.preferences.holder.vancedVersionPref
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.xinto.apkhelper.installSplitApks
class VancedInstaller : AppInstaller() {

View file

@ -1,4 +1,4 @@
package com.vanced.manager.preferences
package com.vanced.manager.core.preferences
data class CheckboxPreference(
val title: String,

View file

@ -1,4 +1,4 @@
package com.vanced.manager.preferences
package com.vanced.manager.core.preferences
import android.content.SharedPreferences
import androidx.compose.runtime.State

View file

@ -1,4 +1,4 @@
package com.vanced.manager.preferences
package com.vanced.manager.core.preferences
data class RadioButtonPreference(
val title: String,

View file

@ -1,4 +1,4 @@
package com.vanced.manager.preferences.holder
package com.vanced.manager.core.preferences.holder
const val MANAGER_VARIANT_DEFAULT_VALUE = "nonroot"

View file

@ -1,9 +1,9 @@
package com.vanced.manager.preferences.holder
package com.vanced.manager.core.preferences.holder
import com.vanced.manager.preferences.managerBooleanPreference
import com.vanced.manager.preferences.managerLongPreference
import com.vanced.manager.preferences.managerStringPreference
import com.vanced.manager.preferences.managerStringSetPreference
import com.vanced.manager.core.preferences.managerBooleanPreference
import com.vanced.manager.core.preferences.managerLongPreference
import com.vanced.manager.core.preferences.managerStringPreference
import com.vanced.manager.core.preferences.managerStringSetPreference
import com.vanced.manager.ui.theme.defAccentColor
val useCustomTabsPref = managerBooleanPreference(USE_CUSTOM_TABS_KEY)

View file

@ -1,4 +1,4 @@
package com.vanced.manager.preferences.holder
package com.vanced.manager.core.preferences.holder
const val USE_CUSTOM_TABS_KEY = "use_custom_tabs"
const val MANAGER_VARIANT_KEY = "manager_variant"

View file

@ -0,0 +1,11 @@
package com.vanced.manager.core.util
fun getLatestOrProvidedAppVersion(
version: String,
appVersions: List<String>
): String {
if (appVersions.contains(version))
return version
return appVersions.last()
}

View file

@ -1,4 +1,4 @@
package com.vanced.manager.util
package com.vanced.manager.core.util
import com.vanced.manager.domain.model.NotificationPrefModel
import com.vanced.manager.network.util.MICROG_NAME

View file

@ -1,4 +1,4 @@
package com.vanced.manager.util
package com.vanced.manager.core.util
import android.os.Build

View file

@ -0,0 +1,9 @@
package com.vanced.manager.core.util
sealed class Message {
data class Success(val message: String) : Message()
data class Warning(val message: String) : Message()
data class Error(val message: String) : Message()
}
val downloadLogs = mutableListOf<Message>()

View file

@ -1,4 +1,4 @@
package com.vanced.manager.util
package com.vanced.manager.core.util
import android.util.Log
import androidx.compose.ui.graphics.Color

View file

@ -1,4 +1,4 @@
package com.vanced.manager.util
package com.vanced.manager.core.util
import com.vanced.manager.R
import com.vanced.manager.domain.model.Link

View file

@ -1,4 +1,4 @@
package com.vanced.manager.util
package com.vanced.manager.core.util
enum class Variant {

View file

@ -1,8 +1,8 @@
package com.vanced.manager.di
import com.vanced.manager.downloader.api.MicrogAPI
import com.vanced.manager.downloader.api.MusicAPI
import com.vanced.manager.downloader.api.VancedAPI
import com.vanced.manager.core.downloader.api.MicrogAPI
import com.vanced.manager.core.downloader.api.MusicAPI
import com.vanced.manager.core.downloader.api.VancedAPI
import com.vanced.manager.network.util.BASE
import okhttp3.OkHttpClient
import org.koin.dsl.module

View file

@ -1,34 +1,32 @@
package com.vanced.manager.di
import com.vanced.manager.downloader.api.MicrogAPI
import com.vanced.manager.downloader.api.MusicAPI
import com.vanced.manager.downloader.api.VancedAPI
import com.vanced.manager.downloader.impl.MicrogDownloader
import com.vanced.manager.downloader.impl.MusicDownloader
import com.vanced.manager.downloader.impl.VancedDownloader
import com.vanced.manager.installer.impl.MicrogInstaller
import com.vanced.manager.installer.impl.MusicInstaller
import com.vanced.manager.installer.impl.VancedInstaller
import android.content.Context
import com.vanced.manager.core.downloader.api.MicrogAPI
import com.vanced.manager.core.downloader.api.MusicAPI
import com.vanced.manager.core.downloader.api.VancedAPI
import com.vanced.manager.core.downloader.impl.MicrogDownloader
import com.vanced.manager.core.downloader.impl.MusicDownloader
import com.vanced.manager.core.downloader.impl.VancedDownloader
import org.koin.dsl.module
val downloaderModule = module {
fun provideVancedDownloader(
vancedInstaller: VancedInstaller,
vancedAPI: VancedAPI,
) = VancedDownloader(vancedInstaller, vancedAPI)
context: Context,
) = VancedDownloader(vancedAPI, context)
fun provideMusicDownloader(
musicInstaller: MusicInstaller,
musicAPI: MusicAPI,
) = MusicDownloader(musicInstaller, musicAPI)
context: Context,
) = MusicDownloader(musicAPI, context)
fun provideMicrogDownloader(
microgInstaller: MicrogInstaller,
microgAPI: MicrogAPI,
) = MicrogDownloader(microgInstaller, microgAPI)
context: Context,
) = MicrogDownloader(microgAPI, context)
single { provideVancedDownloader(get(), get()) }
single { provideMusicDownloader(get(), get()) }
single { provideMicrogDownloader(get(), get()) }
factory { provideVancedDownloader(get(), get()) }
factory { provideMusicDownloader(get(), get()) }
factory { provideMicrogDownloader(get(), get()) }
}

View file

@ -1,8 +1,8 @@
package com.vanced.manager.di
import com.vanced.manager.installer.impl.MicrogInstaller
import com.vanced.manager.installer.impl.MusicInstaller
import com.vanced.manager.installer.impl.VancedInstaller
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
import org.koin.dsl.module
val installerModule = module {

View file

@ -1,12 +1,17 @@
package com.vanced.manager.di
import android.content.Context
import com.vanced.manager.domain.datasource.PackageInformationDataSource
import com.vanced.manager.network.model.AppDtoMapper
import com.vanced.manager.network.model.JsonDtoMapper
import org.koin.dsl.module
val mapperModule = module {
fun provideAppMapper(): AppDtoMapper = AppDtoMapper()
fun provideAppMapper(
packageInformationDataSource: PackageInformationDataSource,
context: Context,
) = AppDtoMapper(packageInformationDataSource, context)
fun provideJsonMapper(
appDtoMapper: AppDtoMapper
@ -14,6 +19,6 @@ val mapperModule = module {
appDtoMapper = appDtoMapper
)
single { provideAppMapper() }
single { provideAppMapper(get(), get()) }
single { provideJsonMapper(get()) }
}

View file

@ -1,26 +1,22 @@
package com.vanced.manager.domain.model
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.ui.widget.screens.home.installation.InstallationOption
data class App(
val name: String? = null,
val remoteVersion: String? = null,
val remoteVersionCode: Int? = null,
val installedVersion: String? = null,
val installedVersionCode: Int? = null,
val installedVersionRoot: String? = null,
val installedVersionCodeRoot: Int? = null,
val iconUrl: String? = "",
val appStatus: AppStatus = AppStatus.Install,
val appStatusRoot: AppStatus = AppStatus.Install,
val packageName: String? = null,
val packageNameRoot: String? = null,
val changelog: String? = null,
val url: String? = null,
val versions: List<String>? = null,
val themes: List<String>? = null,
val languages: List<String>? = null,
val downloader: AppDownloader? = null,
val installationOptions: List<InstallationOption>? = null
val name: String,
val remoteVersion: String,
val remoteVersionCode: Int,
val installedVersion: String?,
val installedVersionCode: Int?,
val installedVersionRoot: String?,
val installedVersionCodeRoot: Int?,
val iconUrl: String?,
val appStatus: AppStatus,
val appStatusRoot: AppStatus,
val packageName: String,
val packageNameRoot: String?,
val changelog: String,
val url: String?,
val versions: List<String>?,
val themes: List<String>?,
val languages: List<String>?,
val installationOptions: List<InstallationOption>?
)

View file

@ -0,0 +1,34 @@
package com.vanced.manager.domain.model
import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.parcelize.Parcelize
sealed class InstallationOption(
@StringRes val itemTitleId: Int,
) : Parcelable {
@Parcelize
data class MultiSelect(
@StringRes val titleId: Int,
val items: List<InstallationOptionItem>,
val getOption: () -> Set<String>,
val addOption: (String) -> Unit,
val removeOption: (String) -> Unit
) : InstallationOption(titleId)
@Parcelize
data class SingleSelect(
@StringRes val titleId: Int,
val items: List<InstallationOptionItem>,
val getOption: () -> String,
val setOption: (String) -> Unit,
) : InstallationOption(titleId)
}
@Parcelize
data class InstallationOptionItem(
val displayText: String,
val key: String,
) : Parcelable

View file

@ -1,5 +1,6 @@
package com.vanced.manager.domain.pkg
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
@ -21,6 +22,7 @@ class PkgManagerImpl(
const val MAJOR_IGNORE = 0xFFFFFFFF
}
@SuppressLint("WrongConstant")
@Suppress("DEPRECATION")
@Throws(PackageManager.NameNotFoundException::class)
override suspend fun getVersionCode(packageName: String): Int {
@ -37,6 +39,7 @@ class PkgManagerImpl(
}
}
@SuppressLint("WrongConstant")
@Throws(PackageManager.NameNotFoundException::class)
override suspend fun getVersionName(packageName: String): String =
packageManager.getPackageInfo(packageName, PACKAGE_FLAG_ALL_OFF)

View file

@ -1,168 +0,0 @@
package com.vanced.manager.downloader.base
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.vanced.manager.domain.model.App
import com.vanced.manager.installer.base.AppInstaller
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.util.log
import okhttp3.ResponseBody
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Call
import retrofit2.awaitResponse
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
//TODO remove showDownloadScreen and viewModel references
abstract class AppDownloader(
val appName: String,
val appInstaller: AppInstaller
) : KoinComponent {
data class File(
val call: Call<ResponseBody>,
val fileName: String
)
var downloadProgress by mutableStateOf(0f)
private set
var downloadFile by mutableStateOf("")
private set
var installing by mutableStateOf(false)
var error by mutableStateOf(false)
val showDownloadScreen = mutableStateOf(false)
private var canceled = false
private lateinit var call: Call<ResponseBody>
private val tag = this::class.simpleName!!
private fun resetValues() {
showDownloadScreen.value = false
downloadProgress = 0f
downloadFile = ""
installing = false
error = false
canceled = false
}
val context: Context by inject()
abstract suspend fun download(
app: App,
viewModel: MainViewModel
)
private fun install(viewModel: MainViewModel) {
installing = true
appInstaller.install {
viewModel.fetch()
resetValues()
}
}
suspend fun downloadFile(
file: File,
viewModel: MainViewModel,
folderStructure: String,
onError: (error: String) -> Unit = {},
) {
downloadFiles(
files = listOf(file),
viewModel = viewModel,
folderStructure = folderStructure,
onError = onError
)
}
suspend fun downloadFiles(
files: List<File>,
viewModel: MainViewModel,
folderStructure: String,
onError: (error: String) -> Unit = {},
) {
showDownloadScreen.value = true
fun error(errorBody: String) {
error = true
onError(errorBody)
log(tag, errorBody)
}
try {
for (file in files) {
if (canceled) {
break
}
val fileName = file.fileName
downloadFile = fileName
this.call = file.call
val response = call.awaitResponse()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
if (!writeFile(body, fileName, folderStructure)) {
error("Failed to write file data")
}
}
} else {
val error = response.errorBody()?.toString()
if (error != null) {
error(error)
}
}
}
} catch (e: Exception) {
error(e.stackTraceToString())
}
if (canceled) {
resetValues()
return
}
install(viewModel)
}
fun cancelDownload() {
canceled = true
call.cancel()
}
private fun writeFile(
body: ResponseBody,
fileName: String,
folderStructure: String
): Boolean {
val folder = File("${context.getExternalFilesDir(appName)?.path}/$folderStructure")
folder.mkdirs()
val file = File("${folder.path}/$fileName")
val inputStream = body.byteStream()
val outputStream = FileOutputStream(file)
return try {
val totalBytes = body.contentLength()
val fileReader = ByteArray(4096)
var downloadedBytes = 0L
var read: Int
while (inputStream.read(fileReader).also { read = it } != -1) {
outputStream.write(fileReader, 0, read)
downloadedBytes += read
downloadProgress = (downloadedBytes * 100 / totalBytes).toFloat()
}
true
} catch (e: IOException) {
false
} finally {
inputStream.close()
outputStream.close()
}
}
}

View file

@ -1,31 +0,0 @@
package com.vanced.manager.downloader.impl
import com.vanced.manager.domain.model.App
import com.vanced.manager.downloader.api.MicrogAPI
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.installer.impl.MicrogInstaller
import com.vanced.manager.ui.viewmodel.MainViewModel
class MicrogDownloader(
microgInstaller: MicrogInstaller,
private val microgAPI: MicrogAPI,
) : AppDownloader(
appName = "microg",
appInstaller = microgInstaller
) {
override suspend fun download(
app: App,
viewModel: MainViewModel
) {
downloadFile(
file = File(
call = microgAPI.getFile(),
fileName = "microg.apk"
),
viewModel = viewModel,
folderStructure = ""
)
}
}

View file

@ -1,44 +0,0 @@
package com.vanced.manager.downloader.impl
import com.vanced.manager.domain.model.App
import com.vanced.manager.downloader.api.MusicAPI
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.installer.impl.MusicInstaller
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.preferences.holder.musicVersionPref
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.util.getLatestOrProvidedAppVersion
class MusicDownloader(
musicInstaller: MusicInstaller,
private val musicAPI: MusicAPI,
) : AppDownloader(
appName = "music",
appInstaller = musicInstaller
) {
private val version by musicVersionPref
private val variant by managerVariantPref
override suspend fun download(
app: App,
viewModel: MainViewModel
) {
val correctVersion = getLatestOrProvidedAppVersion(
version = version,
app = app
)
downloadFile(
file = File(
call = musicAPI.getFiles(
version = correctVersion,
variant = variant,
),
fileName = "music.apk"
),
viewModel = viewModel,
folderStructure = "$correctVersion/$variant"
)
}
}

View file

@ -1,79 +0,0 @@
package com.vanced.manager.downloader.impl
import com.vanced.manager.domain.model.App
import com.vanced.manager.downloader.api.VancedAPI
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.installer.impl.VancedInstaller
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.preferences.holder.vancedLanguagesPref
import com.vanced.manager.preferences.holder.vancedThemePref
import com.vanced.manager.preferences.holder.vancedVersionPref
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.util.arch
import com.vanced.manager.util.getLatestOrProvidedAppVersion
import com.vanced.manager.util.log
class VancedDownloader(
vancedInstaller: VancedInstaller,
private val vancedAPI: VancedAPI,
) : AppDownloader(
appName = "vanced",
appInstaller = vancedInstaller
) {
private val theme by vancedThemePref
private val version by vancedVersionPref
private val variant by managerVariantPref
private val languages by vancedLanguagesPref
override suspend fun download(
app: App,
viewModel: MainViewModel
) {
val correctVersion = getLatestOrProvidedAppVersion(
version = version,
app = app
)
val files = listOf(
getFile(
type = "Theme",
apkName = "$theme.apk",
version = correctVersion
),
getFile(
type = "Arch",
apkName = "split_config.$arch.apk",
version = correctVersion
)
) + languages.map { language ->
getFile(
type = "Language",
apkName = "split_config.$language.apk",
version = correctVersion
)
}
downloadFiles(
files = files,
viewModel,
folderStructure = "$correctVersion/$variant",
onError = {
log("error", it)
}
)
}
private fun getFile(
type: String,
apkName: String,
version: String,
) = File(
call = vancedAPI.getFiles(
version = version,
variant = variant,
type = type,
apkName = apkName
),
fileName = apkName
)
}

View file

@ -1,24 +0,0 @@
package com.vanced.manager.ext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
fun <T : Any> Call<T>.enqueue(
onResponse: (call: Call<T>, response: Response<T>) -> Unit,
onFailure: (call: Call<T>, t: Throwable) -> Unit
) {
enqueue(object : Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>
) = onResponse(call, response)
override fun onFailure(
call: Call<T>,
t: Throwable
) = onFailure(call, t)
})
}

View file

@ -3,15 +3,15 @@ package com.vanced.manager.network.model
import com.google.gson.annotations.SerializedName
data class AppDto(
@SerializedName("name") val name: String? = null,
@SerializedName("version") val version: String? = null,
@SerializedName("versionCode") val versionCode: Int? = null,
@SerializedName("name") val name: String,
@SerializedName("version") val version: String,
@SerializedName("versionCode") val versionCode: Int,
@SerializedName("changelog") val changelog: String,
@SerializedName("icon_url") val iconUrl: String? = null,
@SerializedName("package_name") val packageName: String,
@SerializedName("package_name_root") val packageNameRoot: String? = null,
@SerializedName("url") val url: String? = null,
@SerializedName("versions") val versions: List<String>? = null,
@SerializedName("themes") val themes: List<String>? = null,
@SerializedName("langs") val languages: List<String>? = null,
@SerializedName("package_name") val packageName: String? = null,
@SerializedName("package_name_root") val packageNameRoot: String? = null,
@SerializedName("changelog") val changelog: String? = null,
@SerializedName("icon_url") val iconUrl: String? = null,
@SerializedName("url") val url: String? = null,
)

View file

@ -2,48 +2,37 @@ package com.vanced.manager.network.model
import android.content.Context
import com.vanced.manager.R
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.vanced.manager.core.preferences.holder.vancedLanguagesPref
import com.vanced.manager.core.preferences.holder.vancedThemePref
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.vanced.manager.domain.datasource.PackageInformationDataSource
import com.vanced.manager.domain.model.App
import com.vanced.manager.domain.model.AppStatus
import com.vanced.manager.domain.model.InstallationOption
import com.vanced.manager.domain.model.InstallationOptionItem
import com.vanced.manager.domain.util.EntityMapper
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.downloader.impl.MicrogDownloader
import com.vanced.manager.downloader.impl.MusicDownloader
import com.vanced.manager.downloader.impl.VancedDownloader
import com.vanced.manager.network.util.MICROG_NAME
import com.vanced.manager.network.util.MUSIC_NAME
import com.vanced.manager.network.util.VANCED_NAME
import com.vanced.manager.preferences.CheckboxPreference
import com.vanced.manager.preferences.RadioButtonPreference
import com.vanced.manager.preferences.holder.musicVersionPref
import com.vanced.manager.preferences.holder.vancedLanguagesPref
import com.vanced.manager.preferences.holder.vancedThemePref
import com.vanced.manager.preferences.holder.vancedVersionPref
import com.vanced.manager.ui.widget.screens.home.installation.CheckboxInstallationOption
import com.vanced.manager.ui.widget.screens.home.installation.InstallationOption
import com.vanced.manager.ui.widget.screens.home.installation.RadiobuttonInstallationOption
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import java.util.*
class AppDtoMapper : EntityMapper<AppDto, App>, KoinComponent {
private val packageInformationDataSource: PackageInformationDataSource by inject()
private val context: Context by inject()
class AppDtoMapper(
private val packageInformationDataSource: PackageInformationDataSource,
context: Context
) : EntityMapper<AppDto, App> {
private val latestVersionRadioButton =
RadioButtonPreference(
title = context.getString(R.string.app_version_dialog_option_latest),
InstallationOptionItem(
displayText = context.getString(R.string.app_version_dialog_option_latest),
key = "latest"
)
override suspend fun mapToModel(entity: AppDto): App =
with(entity) {
val localVersionCode = packageInformationDataSource.getVersionCode(packageName ?: "")
val localVersionCode = packageInformationDataSource.getVersionCode(packageName)
val localVersionCodeRoot =
packageInformationDataSource.getVersionCode(packageNameRoot ?: "")
val localVersionName = packageInformationDataSource.getVersionName(packageName ?: "")
val localVersionName = packageInformationDataSource.getVersionName(packageName)
val localVersionNameRoot =
packageInformationDataSource.getVersionName(packageNameRoot ?: "")
App(
@ -64,12 +53,11 @@ class AppDtoMapper : EntityMapper<AppDto, App>, KoinComponent {
versions = versions,
themes = themes,
languages = languages,
downloader = getDownloader(name),
installationOptions = getInstallationOptions(entity)
installationOptions = getInstallationOptions(name, themes, versions, languages)
)
}
private fun compareVersionCodes(remote: Int?, local: Int?): AppStatus =
private fun compareVersionCodes(remote: Int?, local: Int?) =
if (local != null && remote != null) {
when {
remote > local -> AppStatus.Update
@ -80,60 +68,73 @@ class AppDtoMapper : EntityMapper<AppDto, App>, KoinComponent {
AppStatus.Install
}
private fun getInstallationOptions(app: AppDto): List<InstallationOption>? =
when (app.name) {
private fun getInstallationOptions(
appName: String,
appThemes: List<String>?,
appVersions: List<String>?,
appLanguages: List<String>?,
) = when (appName) {
VANCED_NAME -> listOf(
RadiobuttonInstallationOption(
InstallationOption.SingleSelect(
titleId = R.string.app_installation_options_theme,
preference = vancedThemePref,
buttons = app.themes?.map { version ->
RadioButtonPreference(
title = version.replaceFirstChar { it.titlecase(Locale.getDefault()) },
getOption = { vancedThemePref.value.value },
setOption = {
vancedThemePref.save(it)
},
items = appThemes?.map { theme ->
InstallationOptionItem(
displayText = theme.replaceFirstChar {
it.titlecase(Locale.getDefault())
},
key = theme
)
} ?: emptyList(),
),
InstallationOption.SingleSelect(
titleId = R.string.app_installation_options_version,
getOption = { vancedVersionPref.value.value },
setOption = {
vancedVersionPref.save(it)
},
items = appVersions?.map { version ->
InstallationOptionItem(
displayText = version,
key = version
)
} ?: emptyList()
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList(),
),
RadiobuttonInstallationOption(
titleId = R.string.app_installation_options_version,
preference = vancedVersionPref,
buttons = app.versions?.map {
RadioButtonPreference(
title = it,
key = it
)
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList()
),
CheckboxInstallationOption(
InstallationOption.MultiSelect(
titleId = R.string.app_installation_options_language,
preference = vancedLanguagesPref,
buttons = app.languages?.map {
CheckboxPreference(
title = Locale(it).displayName,
key = it
getOption = { vancedLanguagesPref.value.value },
addOption = {
vancedLanguagesPref.save(vancedLanguagesPref.value.value + it)
},
removeOption = {
vancedLanguagesPref.save(vancedLanguagesPref.value.value - it)
},
items = appLanguages?.map { version ->
InstallationOptionItem(
displayText = version,
key = version
)
} ?: emptyList()
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList(),
),
)
MUSIC_NAME -> listOf(
RadiobuttonInstallationOption(
InstallationOption.SingleSelect(
titleId = R.string.app_installation_options_version,
preference = musicVersionPref,
buttons = app.versions?.map {
RadioButtonPreference(
title = it,
key = it
getOption = { musicVersionPref.value.value },
setOption = {
musicVersionPref.save(it)
},
items = appVersions?.map { version ->
InstallationOptionItem(
displayText = version,
key = version
)
} ?: emptyList(),
)
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList()
),
)
else -> null
}
private fun getDownloader(app: String?): AppDownloader? =
when (app) {
VANCED_NAME -> get<VancedDownloader>()
MUSIC_NAME -> get<MusicDownloader>()
MICROG_NAME -> get<MicrogDownloader>()
else -> null
}
}

View file

@ -10,26 +10,25 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIos
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.vanced.manager.ui.component.color.managerAnimatedColor
import com.vanced.manager.ui.component.color.managerSurfaceColor
import com.vanced.manager.ui.component.color.managerTextColor
import com.vanced.manager.ui.component.menu.ManagerDropdownMenuItem
import com.vanced.manager.ui.component.text.ToolbarTitleText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.screens.*
import com.vanced.manager.ui.theme.ManagerTheme
import com.vanced.manager.ui.theme.isDark
import com.vanced.manager.ui.util.Screen
class MainActivity : ComponentActivity() {
@ -49,19 +48,23 @@ class MainActivity : ComponentActivity() {
val isMenuExpanded = remember { mutableStateOf(false) }
val surfaceColor = managerSurfaceColor()
val navController = rememberAnimatedNavController()
val screens = listOf(
Screen.Home,
Screen.Settings,
Screen.About,
Screen.Logs
val isDark = isDark()
val navController = rememberAnimatedNavController()
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setSystemBarsColor(
color = surfaceColor,
darkIcons = !isDark
)
}
Scaffold(
topBar = {
MainToolbar(
navController = navController,
screens = screens,
isMenuExpanded = isMenuExpanded
)
},
@ -91,12 +94,29 @@ class MainActivity : ComponentActivity() {
)
}
) {
screens.fastForEach { screen ->
composable(
route = screen.route,
) {
screen.content()
composable(Screen.Home.route) {
HomeLayout(navController)
}
composable(Screen.Settings.route) {
SettingsLayout()
}
composable(Screen.About.route) {
AboutLayout()
}
composable(Screen.InstallPreferences.route) {
val arguments = navController.previousBackStackEntry?.arguments
InstallPreferencesScreen(
installationOptions = arguments?.getParcelableArrayList("app")!!
)
}
composable(Screen.Install.route,) {
val arguments = navController.previousBackStackEntry?.arguments
InstallScreen(
appName = arguments?.getString("appName")!!,
appVersions = arguments.getParcelableArrayList("appVersions")!!
)
}
}
}
@ -105,7 +125,6 @@ class MainActivity : ComponentActivity() {
@Composable
fun MainToolbar(
navController: NavHostController,
screens: List<Screen>,
isMenuExpanded: MutableState<Boolean>
) {
val currentScreenRoute =
@ -115,7 +134,7 @@ class MainActivity : ComponentActivity() {
title = {
ToolbarTitleText(
text = managerString(
stringId = screens.find { it.route == currentScreenRoute }?.displayName
stringId = Screen.values().find { it.route == currentScreenRoute }?.displayName
)
)
},
@ -139,7 +158,7 @@ class MainActivity : ComponentActivity() {
},
modifier = Modifier.background(MaterialTheme.colors.surface),
) {
screens.filter { it.route != currentScreenRoute }.forEach { screen ->
for (screen in Screen.values()) {
ManagerDropdownMenuItem(
title = stringResource(id = screen.displayName)
) {

View file

@ -28,7 +28,7 @@ fun IconButton(
onClick = onClick,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 18.dp)
indication = rememberRipple(radius = buttonSize / 2)
)
.size(buttonSize),
contentAlignment = Alignment.Center

View file

@ -7,7 +7,7 @@ import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.vanced.manager.preferences.holder.useCustomTabsPref
import com.vanced.manager.core.preferences.holder.useCustomTabsPref
@Composable
fun ManagerLinkCard(

View file

@ -6,7 +6,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import com.vanced.manager.preferences.holder.managerAccentColorPref
import com.vanced.manager.core.preferences.holder.managerAccentColorPref
@Composable
fun contentColorForColor(color: Color) =

View file

@ -8,8 +8,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.window.Dialog
import com.vanced.manager.ui.component.card.ManagerCard
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
@Composable
fun ManagerDialog(
@ -46,8 +46,8 @@ fun ManagerDialog(
content = {
ManagerCard {
Column(
modifier = Modifier.padding(defaultContentPaddingHorizontal),
verticalArrangement = Arrangement.spacedBy(defaultContentPaddingVertical)
modifier = Modifier.padding(DefaultContentPaddingHorizontal),
verticalArrangement = Arrangement.spacedBy(DefaultContentPaddingVertical)
) {
title()
content()

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
@Composable
fun ManagerButtonColumn(
@ -14,7 +14,7 @@ fun ManagerButtonColumn(
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(defaultContentPaddingVertical),
verticalArrangement = Arrangement.spacedBy(DefaultContentPaddingVertical),
content = content
)
}

View file

@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun ManagerLazyColumn(
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
content: LazyListScope.() -> Unit
) {

View file

@ -11,17 +11,18 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
@Composable
fun ManagerScrollableColumn(
contentPaddingVertical: Dp = defaultContentPaddingVertical,
modifier: Modifier = Modifier,
contentPaddingVertical: Dp = DefaultContentPaddingVertical,
itemSpacing: Dp = 0.dp,
content: @Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
modifier = modifier
.verticalScroll(scrollState)
.padding(vertical = contentPaddingVertical),
horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -12,8 +12,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.preferences.CheckboxPreference
import com.vanced.manager.preferences.ManagerPreference
import com.vanced.manager.core.preferences.CheckboxPreference
import com.vanced.manager.core.preferences.ManagerPreference
import com.vanced.manager.ui.component.button.ManagerThemedTextButton
import com.vanced.manager.ui.widget.list.CheckboxItem
import kotlinx.coroutines.launch

View file

@ -3,7 +3,7 @@ package com.vanced.manager.ui.component.preference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.unit.dp
import com.vanced.manager.preferences.ManagerPreference
import com.vanced.manager.core.preferences.ManagerPreference
import com.vanced.manager.ui.widget.checkbox.ManagerAnimatedCheckbox
import kotlinx.coroutines.launch

View file

@ -11,7 +11,7 @@ import com.vanced.manager.ui.component.color.managerAnimatedColor
import com.vanced.manager.ui.component.list.ManagerListItem
import com.vanced.manager.ui.component.modifier.managerClickable
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
@Composable
fun Preference(
@ -43,7 +43,7 @@ fun Preference(
ManagerListItem(
modifier = Modifier
.managerClickable(onClick = onClick)
.padding(horizontal = defaultContentPaddingHorizontal),
.padding(horizontal = DefaultContentPaddingHorizontal),
title = {
CompositionLocalProvider(
LocalContentColor provides color,

View file

@ -6,8 +6,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vanced.manager.preferences.ManagerPreference
import com.vanced.manager.preferences.RadioButtonPreference
import com.vanced.manager.core.preferences.ManagerPreference
import com.vanced.manager.core.preferences.RadioButtonPreference
import com.vanced.manager.ui.widget.button.ManagerSaveButton
import com.vanced.manager.ui.widget.list.RadiobuttonItem
import kotlinx.coroutines.launch

View file

@ -2,12 +2,15 @@ package com.vanced.manager.ui.component.text
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun AppVersionText(
text: String
text: String,
modifier: Modifier = Modifier,
) {
ManagerText(
modifier = modifier,
text = text,
textStyle = MaterialTheme.typography.body2,
)

View file

@ -5,14 +5,14 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.vanced.manager.ui.component.color.managerAnimatedColor
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
@Composable
fun CategoryTitleText(
text: String
) {
ManagerText(
modifier = Modifier.padding(start = defaultContentPaddingHorizontal),
modifier = Modifier.padding(start = DefaultContentPaddingHorizontal),
text = text,
textStyle = MaterialTheme.typography.h2,
color = managerAnimatedColor(MaterialTheme.colors.onSurface)

View file

@ -11,11 +11,11 @@ import androidx.compose.ui.text.style.TextAlign
@Composable
fun ManagerText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
textStyle: TextStyle = LocalTextStyle.current,
textAlign: TextAlign? = null,
text: String,
) {
Text(
modifier = modifier,
@ -28,11 +28,11 @@ fun ManagerText(
@Composable
fun ManagerText(
text: AnnotatedString,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
textStyle: TextStyle = LocalTextStyle.current,
textAlign: TextAlign? = null,
text: AnnotatedString,
) {
Text(
modifier = modifier,

View file

@ -24,7 +24,7 @@ import com.vanced.manager.ui.component.layout.ScrollableItemRow
import com.vanced.manager.ui.component.list.ManagerListItem
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
import com.vanced.manager.ui.widget.layout.CategoryLayout
data class Person(
@ -171,7 +171,7 @@ fun AboutManagerCard() {
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(vertical = defaultContentPaddingVertical),
modifier = Modifier.padding(vertical = DefaultContentPaddingVertical),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {

View file

@ -1,58 +1,145 @@
package com.vanced.manager.ui.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import coil.compose.rememberImagePainter
import coil.request.CachePolicy
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vanced.manager.R
import com.vanced.manager.core.util.socialMedia
import com.vanced.manager.core.util.sponsors
import com.vanced.manager.ui.component.card.ManagerLinkCard
import com.vanced.manager.ui.component.dialog.ManagerDialog
import com.vanced.manager.ui.component.layout.ManagerScrollableColumn
import com.vanced.manager.ui.component.layout.ManagerSwipeRefresh
import com.vanced.manager.ui.component.layout.ScrollableItemRow
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
import com.vanced.manager.ui.util.Screen
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.ui.widget.app.AppCard
import com.vanced.manager.ui.widget.app.AppCardPlaceholder
import com.vanced.manager.ui.widget.button.ManagerCloseButton
import com.vanced.manager.ui.widget.layout.CategoryLayout
import com.vanced.manager.ui.widget.screens.home.apps.HomeAppsItem
import com.vanced.manager.ui.widget.screens.home.socialmedia.HomeSocialMediaItem
import com.vanced.manager.ui.widget.screens.home.sponsors.HomeSponsorsItem
import org.koin.androidx.compose.getViewModel
@ExperimentalAnimationApi
@Composable
@Preview
fun HomeLayout() {
fun HomeLayout(
navController: NavController
) {
val viewModel: MainViewModel = getViewModel()
val isFetching by viewModel.isFetching.collectAsState()
val refreshState = rememberSwipeRefreshState(isRefreshing = isFetching)
val appState by viewModel.appState.collectAsState()
val refreshState = rememberSwipeRefreshState(isRefreshing = appState is MainViewModel.AppState.Fetching)
ManagerSwipeRefresh(
refreshState = refreshState,
onRefresh = { viewModel.fetch() }
) {
ManagerScrollableColumn(
contentPaddingVertical = defaultContentPaddingVertical,
contentPaddingVertical = DefaultContentPaddingVertical,
itemSpacing = 18.dp
) {
CategoryLayout(
categoryName = managerString(
stringId = R.string.home_category_apps
),
categoryName = managerString(R.string.home_category_apps),
contentPaddingHorizontal = 0.dp
) {
HomeAppsItem(viewModel)
AnimatedContent(
targetState = appState,
transitionSpec = {
scaleIn(initialScale = 0.9f) + fadeIn() with
scaleOut(targetScale = 0.9f) + fadeOut()
}
CategoryLayout(
categoryName = managerString(
stringId = R.string.home_category_support_us
)
) { animatedAppState ->
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
HomeSponsorsItem()
when (animatedAppState) {
is MainViewModel.AppState.Success -> {
for (app in animatedAppState.apps) {
val appIcon = rememberImagePainter(app.iconUrl) {
diskCachePolicy(CachePolicy.ENABLED)
}
var showAppInfoDialog by rememberSaveable { mutableStateOf(false) }
AppCard(
appName = app.name,
appIcon = appIcon,
appInstalledVersion = app.installedVersion,
appRemoteVersion = app.remoteVersion,
onDownloadClick = {
if (app.installationOptions != null) {
navController.navigate(Screen.InstallPreferences.route)
} else {
navController.navigate(Screen.Install.route)
}
},
onUninstallClick = { /*TODO*/ },
onLaunchClick = { /*TODO*/ },
onInfoClick = {
showAppInfoDialog = true
}
CategoryLayout(
categoryName = managerString(
stringId = R.string.home_category_social_media
)
if (showAppInfoDialog) {
ManagerDialog(
title = managerString(R.string.app_info_title, app.name),
onDismissRequest = { showAppInfoDialog = false },
buttons = {
ManagerCloseButton(onClick = {
showAppInfoDialog = false
})
}
) {
HomeSocialMediaItem()
ManagerText(
modifier = Modifier.padding(top = 4.dp),
text = app.changelog,
textStyle = MaterialTheme.typography.subtitle1
)
}
}
}
}
is MainViewModel.AppState.Fetching -> {
for (i in 0 until animatedAppState.placeholderAppsCount) {
AppCardPlaceholder()
}
}
is MainViewModel.AppState.Error -> {
}
}
}
}
}
CategoryLayout(managerString(R.string.home_category_support_us)) {
ScrollableItemRow(items = sponsors) { sponsor ->
ManagerLinkCard(
icon = sponsor.icon,
title = sponsor.title,
link = sponsor.link
)
}
}
CategoryLayout( managerString(R.string.home_category_social_media)) {
ScrollableItemRow(items = socialMedia) { socialMedia ->
ManagerLinkCard(
icon = socialMedia.icon,
title = socialMedia.title,
link = socialMedia.link
)
}
}
}
}

View file

@ -0,0 +1,104 @@
package com.vanced.manager.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Scaffold
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEachIndexed
import com.vanced.manager.domain.model.InstallationOption
import com.vanced.manager.ui.component.card.ManagerCard
import com.vanced.manager.ui.component.layout.ManagerLazyColumn
import com.vanced.manager.ui.component.layout.ManagerScrollableColumn
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.widget.list.CheckboxItem
import com.vanced.manager.ui.widget.list.RadiobuttonItem
@Composable
fun InstallPreferencesScreen(
installationOptions: List<InstallationOption>
) {
var selectedOptionIndex by rememberSaveable { mutableStateOf(0) }
Scaffold(
floatingActionButton = {
}
) { paddingValues ->
ManagerScrollableColumn(
modifier = Modifier.padding(paddingValues)
) {
installationOptions.fastForEachIndexed { index, installationOption ->
ManagerCard(onClick = {
selectedOptionIndex = index
}) {
Column {
ManagerText(
text = managerString(installationOption.itemTitleId),
textStyle = TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold
)
)
AnimatedVisibility(
visible = index == selectedOptionIndex
) {
ManagerLazyColumn(
modifier = Modifier.sizeIn(
minHeight = 400.dp,
maxHeight = 400.dp
)
) {
when (installationOption) {
is InstallationOption.MultiSelect -> {
items(installationOption.items) { item ->
val preference = installationOption.getOption()
CheckboxItem(
modifier = Modifier.fillMaxWidth(),
text = item.displayText,
isChecked = preference.contains(item.key),
onCheck = {
if (it) {
installationOption.addOption(item.key)
} else {
installationOption.removeOption(item.key)
}
}
)
}
}
is InstallationOption.SingleSelect -> {
items(installationOption.items) { item ->
val preference = installationOption.getOption()
RadiobuttonItem(
modifier = Modifier.fillMaxWidth(),
text = item.displayText,
isSelected = preference == item.key,
onSelect = {
installationOption.setOption(item.key)
},
tag = item.key
)
}
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,190 @@
package com.vanced.manager.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vanced.manager.core.downloader.impl.MicrogDownloader
import com.vanced.manager.core.downloader.impl.MusicDownloader
import com.vanced.manager.core.downloader.impl.VancedDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
import com.vanced.manager.network.util.MICROG_NAME
import com.vanced.manager.network.util.MUSIC_NAME
import com.vanced.manager.network.util.VANCED_NAME
import com.vanced.manager.ui.component.layout.ManagerLazyColumn
import com.vanced.manager.ui.component.modifier.managerClickable
import com.vanced.manager.ui.component.progressindicator.ManagerProgressIndicator
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.viewmodel.MainViewModel
import org.koin.androidx.compose.get
import org.koin.androidx.compose.getViewModel
sealed class Log {
data class Info(val infoText: String) : Log()
data class Success(val successText: String) : Log()
data class Error(
val displayText: String,
val stacktrace: String,
) : Log()
}
@Composable
fun InstallScreen(
appName: String,
appVersions: List<String>
) {
val logs = rememberSaveable { mutableStateListOf<Log>() }
var progress by rememberSaveable { mutableStateOf(0f) }
var installing by rememberSaveable { mutableStateOf(false) }
val viewModel: MainViewModel = getViewModel()
val downloader = when (appName) {
VANCED_NAME -> get<VancedDownloader>()
MUSIC_NAME -> get<MusicDownloader>()
MICROG_NAME -> get<MicrogDownloader>()
else -> throw IllegalArgumentException("$appName is not a valid app")
}
val installer = when (appName) {
VANCED_NAME -> get<VancedInstaller>()
MUSIC_NAME -> get<MusicInstaller>()
MICROG_NAME -> get<MicrogInstaller>()
else -> throw IllegalArgumentException("$appName is not a valid app")
}
//FIXME this is absolutely bad, must move to WorkManager
LaunchedEffect(true) {
downloader.download(appVersions) { status ->
when (status) {
is DownloadStatus.File -> logs.add(Log.Info("Downloading ${status.fileName}"))
is DownloadStatus.Error -> logs.add(Log.Error(
displayText = status.displayError,
stacktrace = status.stacktrace
))
is DownloadStatus.Progress -> progress = status.progress
is DownloadStatus.StartInstall -> {
installing = true
installer.install {
viewModel.fetch()
}
}
}
}
}
Scaffold(
topBar = {
if (installing) {
ManagerProgressIndicator()
} else {
ManagerProgressIndicator(progress)
}
},
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
}
}
) { paddingValues ->
ManagerLazyColumn(
modifier = Modifier.padding(paddingValues)
) {
items(logs) { log ->
when (log) {
is Log.Success -> {
ManagerText(
text = log.successText,
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
),
)
}
is Log.Info -> {
ManagerText(
text = log.infoText,
textStyle = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
color = Color.Green
),
)
}
is Log.Error -> {
var visible by remember { mutableStateOf(false) }
val iconRotation by animateFloatAsState(if (visible) 0f else 90f)
Row(
modifier = Modifier.managerClickable {
visible = !visible
},
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
modifier = Modifier.rotate(iconRotation),
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = "expand",
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
ManagerText(
text = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colors.error)) {
append(log.displayText)
append(": ")
}
withStyle(SpanStyle(color = MaterialTheme.colors.error.copy(alpha = 0.7f))) {
append(log.stacktrace)
}
},
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
),
)
AnimatedVisibility(visible) {
ManagerText(
text = log.stacktrace,
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
),
)
}
}
}
}
}
}
}
}
}

View file

@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
import com.vanced.manager.ui.component.color.managerAccentColor
import com.vanced.manager.ui.component.color.managerSurfaceColor
import com.vanced.manager.ui.component.layout.ManagerLazyColumn
import com.vanced.manager.util.logs
import com.vanced.manager.core.util.logs
@Composable
fun LogLayout() {

View file

@ -5,14 +5,14 @@ import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.ui.component.layout.ManagerScrollableColumn
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
import com.vanced.manager.ui.widget.layout.SettingsCategoryLayout
import com.vanced.manager.ui.widget.screens.settings.*
@Composable
fun SettingsLayout() {
ManagerScrollableColumn(
contentPaddingVertical = defaultContentPaddingVertical,
contentPaddingVertical = DefaultContentPaddingVertical,
itemSpacing = 12.dp
) {
SettingsCategoryLayout(

View file

@ -5,7 +5,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import com.vanced.manager.preferences.holder.managerThemePref
import com.vanced.manager.core.preferences.holder.managerThemePref
const val defAccentColor = 0xFF0477E1

View file

@ -2,16 +2,5 @@ package com.vanced.manager.ui.util
import androidx.compose.ui.unit.dp
val defaultContentPaddingHorizontal = 16.dp
val defaultContentPaddingVertical = 12.dp
fun test(anotherFun: () -> Int) = anotherFun() + 5
fun test(anotherNum: Int) = anotherNum + 5
fun test2() {
test {
print("haha jonathan you are banging my number")
return@test 0
}
test(0)
}
val DefaultContentPaddingHorizontal = 16.dp
val DefaultContentPaddingVertical = 12.dp

View file

@ -8,41 +8,32 @@ import com.vanced.manager.ui.screens.HomeLayout
import com.vanced.manager.ui.screens.LogLayout
import com.vanced.manager.ui.screens.SettingsLayout
sealed class Screen(
enum class Screen(
val route: String,
@StringRes val displayName: Int,
val content: @Composable () -> Unit
) {
object Home : Screen(
Home(
route = "home",
displayName = R.string.app_name,
content = {
HomeLayout()
}
)
object Settings : Screen(
displayName = R.string.app_name
),
Settings(
route = "settings",
displayName = R.string.toolbar_settings,
content = {
SettingsLayout()
}
)
object About : Screen(
),
About(
route = "about",
displayName = R.string.toolbar_about,
content = {
AboutLayout()
}
)
object Logs : Screen(
),
Logs(
route = "logs",
displayName = R.string.toolbar_logs,
content = {
LogLayout()
}
)
),
InstallPreferences(
route = "installpreferences",
displayName = R.string.toolbar_installation_preferences
),
Install(
route = "install",
displayName = R.string.toolbar_install
),
}

View file

@ -1,13 +1,12 @@
package com.vanced.manager.ui.viewmodel
import android.util.Log
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vanced.manager.domain.model.App
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.preferences.holder.musicEnabled
import com.vanced.manager.preferences.holder.vancedEnabled
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicEnabled
import com.vanced.manager.core.preferences.holder.vancedEnabled
import com.vanced.manager.repository.JsonRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -18,40 +17,54 @@ class MainViewModel(
private val repository: JsonRepository
) : ViewModel() {
private val vanced = MutableStateFlow(App())
private val music = MutableStateFlow(App())
private val microg = MutableStateFlow(App())
private val manager = MutableStateFlow(App())
sealed class AppState {
private val _isFetching = MutableStateFlow(false)
val isFetching: StateFlow<Boolean> = _isFetching
data class Fetching(val placeholderAppsCount: Int) : AppState()
val apps = mutableListOf<StateFlow<App>>()
data class Success(val apps: List<App>) : AppState()
data class Error(val error: String) : AppState()
}
private val _appState = MutableStateFlow<AppState>(AppState.Fetching(3))
val appState: StateFlow<AppState> = _appState
fun fetch() {
viewModelScope.launch(Dispatchers.IO) {
_isFetching.value = true
val vancedEnabled = vancedEnabled.value.value
val musicEnabled = musicEnabled.value.value
val isNonroot = managerVariantPref.value.value == "nonroot"
var appsCount = 0
if (vancedEnabled) appsCount++
if (musicEnabled) appsCount++
if (isNonroot) appsCount++
_appState.value = AppState.Fetching(appsCount)
try {
with(repository.fetch()) {
this@MainViewModel.vanced.value = vanced
this@MainViewModel.music.value = music
this@MainViewModel.microg.value = microg
}
} catch (e: Exception) {
Log.d("HomeViewModel", "failed to fetch: $e")
val apps = mutableListOf<App>()
apps.apply {
if (vancedEnabled) add(vanced)
if (musicEnabled) add(music)
if (isNonroot) add(microg)
}
_isFetching.value = false
_appState.value = AppState.Success(apps)
}
} catch (e: Exception) {
val error = "failed to fetch: \n${e.stackTraceToString()}"
_appState.value = AppState.Error(error)
Log.d("MainViewModel", error)
}
}
}
init {
apps.apply {
if (vancedEnabled.value.value) add(vanced)
if (musicEnabled.value.value) add(music)
if (managerVariantPref.value.value == "nonroot") add(microg)
}
fetch()
}

View file

@ -0,0 +1,91 @@
package com.vanced.manager.ui.widget.app
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.DeleteForever
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Launch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.ImagePainter
import com.vanced.manager.R
import com.vanced.manager.ui.component.button.IconButton
import com.vanced.manager.ui.component.text.AppVersionText
import com.vanced.manager.ui.component.text.ManagerText
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppCard(
appName: String,
appIcon: ImagePainter,
appInstalledVersion: String?,
appRemoteVersion: String?,
onDownloadClick: () -> Unit,
onUninstallClick: () -> Unit,
onLaunchClick: () -> Unit,
onInfoClick: () -> Unit,
) {
BaseAppCard(
appTitle = {
ManagerText(
modifier = Modifier.fillMaxSize(),
text = appName,
textStyle = MaterialTheme.typography.h5
)
},
appIcon = {
Image(
modifier = Modifier.size(48.dp),
painter = appIcon,
contentDescription = "App Icon",
)
},
appVersionsColumn = {
AppVersionText(
text = stringResource(
id = R.string.app_version_latest,
appRemoteVersion ?: stringResource(
id = R.string.app_content_unavailable
)
)
)
AppVersionText(
text = stringResource(
id = R.string.app_version_installed,
appInstalledVersion ?: stringResource(
id = R.string.app_content_unavailable
)
)
)
},
appActionsRow = {
IconButton(
icon = Icons.Outlined.Info,
contentDescription = "App Info",
onClick = onInfoClick
)
IconButton(
icon = Icons.Rounded.DeleteForever,
contentDescription = "Uninstall",
onClick = onUninstallClick
)
IconButton(
icon = Icons.Rounded.Launch,
contentDescription = "Launch",
onClick = onLaunchClick
)
IconButton(
icon = Icons.Rounded.Download,
contentDescription = "Install",
onClick = onDownloadClick
)
}
)
}

View file

@ -0,0 +1,57 @@
package com.vanced.manager.ui.widget.app
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.vanced.manager.ui.component.modifier.managerPlaceholder
import com.vanced.manager.ui.component.text.AppVersionText
import com.vanced.manager.ui.component.text.ManagerText
@Composable
fun AppCardPlaceholder() {
BaseAppCard(
appTitle = {
ManagerText(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.managerPlaceholder(true),
text = " ".repeat(40),
textStyle = MaterialTheme.typography.h5
)
},
appIcon = {
Box(
Modifier
.clip(MaterialTheme.shapes.medium)
.managerPlaceholder(true)
.size(48.dp)
)
},
appVersionsColumn = {
AppVersionText(
modifier = Modifier
.managerPlaceholder(true)
.clip(MaterialTheme.shapes.small),
text = " ".repeat(30)
)
AppVersionText(
modifier = Modifier
.managerPlaceholder(true)
.clip(MaterialTheme.shapes.small),
text = " ".repeat(30)
)
},
appActionsRow = {
Box(
Modifier
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth(0.8f)
.height(36.dp)
.managerPlaceholder(true)
)
}
)
}

View file

@ -0,0 +1,69 @@
package com.vanced.manager.ui.widget.app
import androidx.compose.foundation.layout.*
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.ui.component.card.ManagerCard
import com.vanced.manager.ui.component.card.ManagerThemedCard
import com.vanced.manager.ui.component.list.ManagerListItem
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
@Composable
fun BaseAppCard(
appTitle: @Composable () -> Unit,
appIcon: @Composable () -> Unit,
appVersionsColumn: @Composable ColumnScope.() -> Unit,
appActionsRow: @Composable RowScope.() -> Unit,
) {
ManagerThemedCard {
Column {
ManagerCard {
ManagerListItem(
modifier = Modifier.padding(
horizontal = DefaultContentPaddingHorizontal,
vertical = DefaultContentPaddingVertical
),
title = appTitle,
icon = appIcon
)
}
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colors.onSurface
) {
Row(
modifier = Modifier.padding(
horizontal = DefaultContentPaddingHorizontal,
vertical = 8.dp
),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.Start),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
ManagerText(stringResource(id = R.string.app_versions))
appVersionsColumn()
}
Row(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.End),
content = appActionsRow
)
}
}
}
}
}

View file

@ -5,14 +5,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.vanced.manager.ui.component.text.CategoryTitleText
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
@Composable
fun CategoryLayout(
categoryName: String,
contentPaddingHorizontal: Dp = defaultContentPaddingHorizontal,
categoryNameSpacing: Dp = defaultContentPaddingVertical,
contentPaddingHorizontal: Dp = DefaultContentPaddingHorizontal,
categoryNameSpacing: Dp = DefaultContentPaddingVertical,
content: @Composable () -> Unit,
) {
Column(

View file

@ -1,6 +1,5 @@
package com.vanced.manager.ui.widget.list
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -16,13 +15,13 @@ import com.vanced.manager.ui.widget.checkbox.ManagerAnimatedCheckbox
fun CheckboxItem(
text: String,
isChecked: Boolean,
onCheck: (Boolean) -> Unit = {}
onCheck: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val toggle = { onCheck(!isChecked) }
ManagerSelectableListItem(
modifier = Modifier
.fillMaxWidth()
modifier = modifier
.managerClickable(onClick = toggle),
title = {
Text(

View file

@ -17,12 +17,12 @@ fun <T> RadiobuttonItem(
text: String,
tag: T,
isSelected: Boolean,
onSelect: (tag: T) -> Unit
onSelect: (tag: T) -> Unit,
modifier: Modifier = Modifier
) {
val onClick = { onSelect(tag) }
ManagerSelectableListItem(
modifier = Modifier
.fillMaxWidth()
.managerClickable(onClick = onClick),
title = {
Text(

View file

@ -1,25 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.ui.widget.screens.home.apps.card.AppCard
@Composable
fun HomeAppsItem(
viewModel: MainViewModel
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
viewModel.apps.fastForEach { app ->
val observedApp by app.collectAsState()
AppCard(observedApp, viewModel)
}
}
}

View file

@ -1,94 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps.card
import androidx.compose.foundation.layout.*
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.DeleteForever
import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Launch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.ui.component.button.IconButton
import com.vanced.manager.ui.component.text.AppVersionText
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
@Composable
fun AppActionCard(
appRemoteVersion: String?,
appInstalledVersion: String?,
onInfoClick: () -> Unit,
onUninstallClick: () -> Unit,
onLaunchClick: () -> Unit,
onDownloadClick: () -> Unit,
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onSurface) {
Row(
modifier = Modifier.padding(
horizontal = defaultContentPaddingHorizontal
),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.Start),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
ManagerText(
text = stringResource(id = R.string.app_versions)
)
AppVersionText(
text = stringResource(
id = R.string.app_version_latest,
appRemoteVersion ?: stringResource(
id = R.string.app_content_unavailable
)
)
)
AppVersionText(
text = stringResource(
id = R.string.app_version_installed,
appInstalledVersion ?: stringResource(
id = R.string.app_content_unavailable
)
)
)
}
Row(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.End)
) {
IconButton(
icon = Icons.Outlined.Info,
contentDescription = "App Info",
onClick = onInfoClick
)
IconButton(
icon = Icons.Rounded.DeleteForever,
contentDescription = "Uninstall",
onClick = onUninstallClick
)
IconButton(
icon = Icons.Rounded.Launch,
contentDescription = "Launch",
onClick = onLaunchClick
)
IconButton(
icon = Icons.Rounded.Download,
contentDescription = "Install",
onClick = onDownloadClick
)
}
}
}
}

View file

@ -1,139 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps.card
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import com.vanced.manager.domain.model.App
import com.vanced.manager.ui.component.card.ManagerThemedCard
import com.vanced.manager.ui.component.layout.ManagerButtonColumn
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
import com.vanced.manager.ui.util.defaultContentPaddingVertical
import com.vanced.manager.ui.viewmodel.MainViewModel
import com.vanced.manager.ui.widget.button.ManagerCancelButton
import com.vanced.manager.ui.widget.button.ManagerDownloadButton
import com.vanced.manager.ui.widget.screens.home.apps.dialog.AppChangelogDialog
import com.vanced.manager.ui.widget.screens.home.apps.dialog.AppDownloadDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppCard(
app: App,
viewModel: MainViewModel
) {
var showAppInfoDialog by rememberSaveable { mutableStateOf(false) }
var showInstallationOptions by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val icon = rememberImagePainter(
data = app.iconUrl
)
val hasInstallationOptions = app.installationOptions != null
val animationSpec = tween<IntSize>(400)
val downloader = app.downloader
val download: () -> Unit = {
showInstallationOptions = false
coroutineScope.launch {
downloader!!.download(app, viewModel)
}
}
Column {
ManagerThemedCard {
Column {
AppInfoCard(
appName = app.name,
icon = icon,
)
Column(
modifier = Modifier.padding(vertical = defaultContentPaddingVertical)
) {
AppActionCard(
appInstalledVersion = app.installedVersion,
appRemoteVersion = app.remoteVersion,
onDownloadClick = {
if (hasInstallationOptions) {
showInstallationOptions = true
} else {
download()
}
},
onInfoClick = {
showAppInfoDialog = true
},
onLaunchClick = {},
onUninstallClick = {}
)
}
}
}
if (hasInstallationOptions) {
AnimatedVisibility(
modifier = Modifier.fillMaxWidth(),
visible = showInstallationOptions,
enter = expandVertically(
animationSpec = animationSpec
),
exit = shrinkVertically(
animationSpec = animationSpec
)
) {
Column(
modifier = Modifier.padding(vertical = 4.dp)
) {
app.installationOptions?.forEach {
it.item()
}
ManagerButtonColumn(
modifier = Modifier
.padding(horizontal = defaultContentPaddingHorizontal)
.padding(top = defaultContentPaddingVertical)
) {
ManagerDownloadButton(
onClick = download
)
ManagerCancelButton {
showInstallationOptions = false
}
}
}
}
}
}
if (app.name != null && downloader != null && downloader.showDownloadScreen.value) {
AppDownloadDialog(
app = app.name,
downloader = downloader,
onCancelClick = {
downloader.cancelDownload()
}
)
}
if (app.name != null && app.changelog != null && showAppInfoDialog) {
AppChangelogDialog(
appName = app.name,
changelog = app.changelog,
onDismissRequest = {
showAppInfoDialog = false
}
)
}
}

View file

@ -1,48 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps.card
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil.annotation.ExperimentalCoilApi
import coil.compose.ImagePainter
import com.vanced.manager.ui.component.card.ManagerCard
import com.vanced.manager.ui.component.list.ManagerListItem
import com.vanced.manager.ui.component.modifier.managerPlaceholder
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.util.defaultContentPaddingHorizontal
@OptIn(ExperimentalCoilApi::class)
@Composable
fun AppInfoCard(
appName: String?,
icon: ImagePainter,
) {
ManagerCard {
ManagerListItem(
modifier = Modifier.padding(
horizontal = defaultContentPaddingHorizontal,
vertical = 12.dp
),
title = {
ManagerText(
modifier = Modifier.managerPlaceholder(appName == null),
text = appName ?: "",
textStyle = MaterialTheme.typography.h5
)
},
icon = {
Image(
modifier = Modifier
.size(48.dp)
.managerPlaceholder(icon.state is ImagePainter.State.Loading),
painter = icon,
contentDescription = "",
)
}
)
}
}

View file

@ -1,33 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps.dialog
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.ui.component.dialog.ManagerDialog
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.widget.button.ManagerCloseButton
@Composable
fun AppChangelogDialog(
appName: String,
changelog: String,
onDismissRequest: () -> Unit,
) {
ManagerDialog(
title = managerString(R.string.app_info_title, appName),
onDismissRequest = onDismissRequest,
buttons = {
ManagerCloseButton(onClick = onDismissRequest)
}
) {
ManagerText(
modifier = Modifier.padding(top = 4.dp),
text = changelog,
textStyle = MaterialTheme.typography.subtitle1
)
}
}

View file

@ -1,41 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.apps.dialog
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.vanced.manager.R
import com.vanced.manager.downloader.base.AppDownloader
import com.vanced.manager.ui.component.dialog.ManagerDialog
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.widget.button.ManagerCancelButton
import com.vanced.manager.ui.widget.screens.home.download.AppDownloadDialogProgress
@Composable
fun AppDownloadDialog(
app: String,
downloader: AppDownloader,
onCancelClick: () -> Unit,
) {
ManagerDialog(
title = app,
onDismissRequest = {},
buttons = {
ManagerCancelButton(onClick = onCancelClick)
}
) {
ManagerText(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.app_download_dialog_subtitle),
textStyle = MaterialTheme.typography.subtitle2,
textAlign = TextAlign.Center
)
AppDownloadDialogProgress(
progress = downloader.downloadProgress,
file = downloader.downloadFile,
installing = downloader.installing
)
}
}

View file

@ -1,45 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.download
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vanced.manager.R
import com.vanced.manager.ui.component.progressindicator.ManagerProgressIndicator
import com.vanced.manager.ui.resources.managerString
@Composable
fun AppDownloadDialogProgress(
progress: Float,
file: String,
installing: Boolean
) {
when (installing) {
true -> ManagerProgressIndicator()
false -> ManagerProgressIndicator(progress = progress / 100f)
}
val animatedProgress by animateIntAsState(targetValue = progress.toInt())
Row {
Text(
modifier = Modifier
.weight(1f)
.wrapContentWidth(Alignment.Start),
text = managerString(
stringId = R.string.app_download_dialog_downloading_file,
file
)
)
Text(
modifier = Modifier
.padding(start = 4.dp)
.wrapContentWidth(Alignment.End),
text = "$animatedProgress%"
)
}
}

View file

@ -1,21 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.installation
import androidx.annotation.StringRes
import com.vanced.manager.preferences.CheckboxPreference
import com.vanced.manager.preferences.ManagerPreference
import com.vanced.manager.ui.component.preference.CheckboxDialogPreference
import com.vanced.manager.ui.resources.managerString
data class CheckboxInstallationOption(
@StringRes val titleId: Int,
val preference: ManagerPreference<Set<String>>,
val buttons: List<CheckboxPreference>
) : InstallationOption(
item = {
CheckboxDialogPreference(
preferenceTitle = managerString(stringId = titleId),
preference = preference,
buttons = buttons
)
}
)

View file

@ -1,7 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.installation
import androidx.compose.runtime.Composable
open class InstallationOption(
val item: @Composable () -> Unit
)

View file

@ -1,21 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.installation
import androidx.annotation.StringRes
import com.vanced.manager.preferences.ManagerPreference
import com.vanced.manager.preferences.RadioButtonPreference
import com.vanced.manager.ui.component.preference.RadiobuttonDialogPreference
import com.vanced.manager.ui.resources.managerString
data class RadiobuttonInstallationOption(
@StringRes val titleId: Int,
val preference: ManagerPreference<String>,
val buttons: List<RadioButtonPreference>
) : InstallationOption(
item = {
RadiobuttonDialogPreference(
preferenceTitle = managerString(stringId = titleId),
preference = preference,
buttons = buttons
)
}
)

View file

@ -1,17 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.socialmedia
import androidx.compose.runtime.Composable
import com.vanced.manager.ui.component.card.ManagerLinkCard
import com.vanced.manager.ui.component.layout.ScrollableItemRow
import com.vanced.manager.util.socialMedia
@Composable
fun HomeSocialMediaItem() {
ScrollableItemRow(items = socialMedia) { socialMedia ->
ManagerLinkCard(
icon = socialMedia.icon,
title = socialMedia.title,
link = socialMedia.link
)
}
}

View file

@ -1,17 +0,0 @@
package com.vanced.manager.ui.widget.screens.home.sponsors
import androidx.compose.runtime.Composable
import com.vanced.manager.ui.component.card.ManagerLinkCard
import com.vanced.manager.ui.component.layout.ScrollableItemRow
import com.vanced.manager.util.sponsors
@Composable
fun HomeSponsorsItem() {
ScrollableItemRow(items = sponsors) { sponsor ->
ManagerLinkCard(
icon = sponsor.icon,
title = sponsor.title,
link = sponsor.link
)
}
}

View file

@ -3,7 +3,7 @@ package com.vanced.manager.ui.widget.screens.settings
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import com.vanced.manager.R
import com.vanced.manager.preferences.holder.managerAccentColorPref
import com.vanced.manager.core.preferences.holder.managerAccentColorPref
import com.vanced.manager.ui.component.color.ManagerColorPicker
import com.vanced.manager.ui.component.preference.DialogPreference
import com.vanced.manager.ui.resources.managerString

View file

@ -3,7 +3,7 @@ package com.vanced.manager.ui.widget.screens.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.vanced.manager.R
import com.vanced.manager.preferences.holder.useCustomTabsPref
import com.vanced.manager.core.preferences.holder.useCustomTabsPref
import com.vanced.manager.ui.component.preference.CheckboxPreference
@Composable

View file

@ -2,8 +2,8 @@ package com.vanced.manager.ui.widget.screens.settings
import androidx.compose.runtime.Composable
import com.vanced.manager.R
import com.vanced.manager.preferences.RadioButtonPreference
import com.vanced.manager.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.RadioButtonPreference
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.ui.component.preference.RadiobuttonDialogPreference
import com.vanced.manager.ui.resources.managerString

View file

@ -1,9 +1,9 @@
package com.vanced.manager.ui.widget.screens.settings
import androidx.compose.runtime.Composable
import com.vanced.manager.preferences.managerBooleanPreference
import com.vanced.manager.core.preferences.managerBooleanPreference
import com.vanced.manager.core.util.notificationApps
import com.vanced.manager.ui.component.preference.CheckboxPreference
import com.vanced.manager.util.notificationApps
@Composable
fun SettingsNotificationsItem() {

View file

@ -2,8 +2,8 @@ package com.vanced.manager.ui.widget.screens.settings
import androidx.compose.runtime.Composable
import com.vanced.manager.R
import com.vanced.manager.preferences.RadioButtonPreference
import com.vanced.manager.preferences.holder.managerThemePref
import com.vanced.manager.core.preferences.RadioButtonPreference
import com.vanced.manager.core.preferences.holder.managerThemePref
import com.vanced.manager.ui.component.preference.RadiobuttonDialogPreference
import com.vanced.manager.ui.resources.managerString

View file

@ -1,13 +0,0 @@
package com.vanced.manager.util
import com.vanced.manager.domain.model.App
fun getLatestOrProvidedAppVersion(
version: String,
app: App
): String {
if (version == "latest") {
return app.versions?.last() ?: version
}
return version
}

View file

@ -71,6 +71,8 @@
<string name="toolbar_home">Manager</string>
<string name="toolbar_logs">Logs</string>
<string name="toolbar_settings">Settings</string>
<string name="toolbar_install">Install</string>
<string name="toolbar_installation_preferences">Installation Preferences</string>
<string name="toolbar_update_manager">Update Manager</string>
<!-- App Card -->

View file

@ -4,9 +4,10 @@ buildscript {
mavenCentral()
}
val kotlinVersion = "1.5.31"
dependencies {
classpath("com.android.tools.build:gradle:7.0.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}