ManageReposSection.kt (8194B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages
      2 
      3 import androidx.compose.foundation.layout.*
      4 import androidx.compose.foundation.lazy.LazyColumn
      5 import androidx.compose.foundation.lazy.items
      6 import androidx.compose.material.icons.Icons
      7 import androidx.compose.material.icons.filled.Public
      8 import androidx.compose.material3.*
      9 import androidx.compose.runtime.*
     10 import androidx.compose.ui.Modifier
     11 import androidx.compose.ui.focus.FocusRequester
     12 import androidx.compose.ui.focus.focusRequester
     13 import androidx.compose.ui.layout.onGloballyPositioned
     14 import androidx.compose.ui.text.font.FontWeight
     15 import androidx.compose.ui.text.style.TextAlign
     16 import androidx.compose.ui.text.style.TextOverflow
     17 import androidx.compose.ui.unit.dp
     18 import androidx.compose.ui.unit.sp
     19 import androidx.core.net.toUri
     20 import androidx.navigation.NavBackStackEntry
     21 import kotlinx.coroutines.Dispatchers
     22 import kotlinx.coroutines.launch
     23 import me.rhunk.snapenhance.common.data.RepositoryIndex
     24 import me.rhunk.snapenhance.common.ui.AsyncUpdateDispatcher
     25 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList
     26 import me.rhunk.snapenhance.common.util.ktx.copyToClipboard
     27 import me.rhunk.snapenhance.common.util.ktx.getUrlFromClipboard
     28 import me.rhunk.snapenhance.storage.addRepo
     29 import me.rhunk.snapenhance.storage.getRepositories
     30 import me.rhunk.snapenhance.storage.removeRepo
     31 import me.rhunk.snapenhance.ui.manager.Routes
     32 import okhttp3.OkHttpClient
     33 
     34 class ManageReposSection: Routes.Route() {
     35     private val updateDispatcher = AsyncUpdateDispatcher()
     36     private val okHttpClient by lazy { OkHttpClient() }
     37 
     38     override val floatingActionButton: @Composable () -> Unit = {
     39         var showAddDialog by remember { mutableStateOf(false) }
     40         ExtendedFloatingActionButton(onClick = {
     41             showAddDialog = true
     42         }) {
     43             Text("Add Repository")
     44         }
     45 
     46         if (showAddDialog) {
     47             val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
     48 
     49             suspend fun addRepo(url: String) {
     50                 var modifiedUrl = url;
     51 
     52                 if (url.startsWith("https://github.com/")) {
     53                     val splitUrl = modifiedUrl.removePrefix("https://github.com/").split("/")
     54                     val repoName = splitUrl[0] + "/" + splitUrl[1]
     55                     // fetch default branch
     56                     okHttpClient.newCall(
     57                         okhttp3.Request.Builder().url("https://api.github.com/repos/$repoName").build()
     58                     ).execute().use { response ->
     59                         if (!response.isSuccessful) {
     60                             throw Exception("Failed to fetch default branch: ${response.code}")
     61                         }
     62                         val json = response.body.string()
     63                         val defaultBranch = context.gson.fromJson(json, Map::class.java)["default_branch"] as String
     64                         context.log.info("Default branch for $repoName is $defaultBranch")
     65                         modifiedUrl = "https://raw.githubusercontent.com/$repoName/$defaultBranch/"
     66                     }
     67                 }
     68 
     69                 val indexUri = modifiedUrl.toUri().buildUpon().appendPath("index.json").build()
     70                 okHttpClient.newCall(
     71                     okhttp3.Request.Builder().url(indexUri.toString()).build()
     72                 ).execute().use { response ->
     73                     if (!response.isSuccessful) {
     74                         throw Exception("Failed to fetch index from $indexUri: ${response.code}")
     75                     }
     76                     runCatching {
     77                         val repoIndex = context.gson.fromJson(response.body.charStream(), RepositoryIndex::class.java).also {
     78                             context.log.info("repository index: $it")
     79                         }
     80 
     81                         context.database.addRepo(modifiedUrl)
     82                         context.shortToast("Repository added successfully! $repoIndex")
     83                         showAddDialog = false
     84                         updateDispatcher.dispatch()
     85                     }.onFailure {
     86                         throw Exception("Failed to parse index from $indexUri")
     87                     }
     88                 }
     89             }
     90 
     91             var url by remember { mutableStateOf("") }
     92             var loading by remember { mutableStateOf(false) }
     93 
     94             AlertDialog(onDismissRequest = {
     95                 showAddDialog = false
     96             }, title = {
     97                 Text("Add Repository URL")
     98             }, text = {
     99                 val focusRequester = remember { FocusRequester() }
    100                 OutlinedTextField(
    101                     modifier = Modifier
    102                         .fillMaxWidth()
    103                         .focusRequester(focusRequester)
    104                         .onGloballyPositioned {
    105                             focusRequester.requestFocus()
    106                         },
    107                     value = url,
    108                     onValueChange = {
    109                         url = it
    110                     }, label = {
    111                         Text("Repository URL")
    112                     }
    113                 )
    114                 LaunchedEffect(Unit) {
    115                     context.androidContext.getUrlFromClipboard()?.let {
    116                         url = it
    117                     }
    118                 }
    119             }, confirmButton = {
    120                 Button(
    121                     enabled = !loading,
    122                     onClick = {
    123                         loading = true;
    124                         coroutineScope.launch {
    125                             runCatching {
    126                                 addRepo(url)
    127                             }.onFailure {
    128                                 context.log.error("Failed to add repository", it)
    129                                 context.shortToast("Failed to add repository: ${it.message}")
    130                             }
    131                             loading = false
    132                         }
    133                     }
    134                 ) {
    135                     if (loading) {
    136                         CircularProgressIndicator(modifier = Modifier.size(24.dp))
    137                     } else {
    138                         Text("Add")
    139                     }
    140                 }
    141             })
    142         }
    143     }
    144 
    145     override val content: @Composable (NavBackStackEntry) -> Unit = {
    146         val coroutineScope = rememberCoroutineScope()
    147         val repositories = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = updateDispatcher) {
    148             context.database.getRepositories()
    149         }
    150 
    151         LazyColumn(
    152             modifier = Modifier.fillMaxSize(),
    153             contentPadding = PaddingValues(8.dp),
    154         ) {
    155             item {
    156                 if (repositories.isEmpty()) {
    157                     Text("No repositories added", modifier = Modifier
    158                         .padding(16.dp)
    159                         .fillMaxWidth(), fontSize = 15.sp, fontWeight = FontWeight.Light, textAlign = TextAlign.Center)
    160                 }
    161             }
    162             items(repositories) { url ->
    163                 ElevatedCard(onClick = {
    164                     context.androidContext.copyToClipboard(url)
    165                 }) {
    166                     Row(
    167                         modifier = Modifier
    168                             .fillMaxWidth()
    169                             .padding(8.dp),
    170                         horizontalArrangement = Arrangement.spacedBy(4.dp),
    171                         verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
    172                     ) {
    173                         Icon(Icons.Default.Public, contentDescription = null)
    174                         Text(text = url, modifier = Modifier.weight(1f), overflow = TextOverflow.Ellipsis, maxLines = 4, fontSize = 15.sp, lineHeight = 15.sp)
    175                         Button(
    176                             onClick = {
    177                                 context.database.removeRepo(url)
    178                                 coroutineScope.launch {
    179                                     updateDispatcher.dispatch()
    180                                 }
    181                             }
    182                         ) {
    183                             Text("Remove")
    184                         }
    185                     }
    186                 }
    187             }
    188         }
    189     }
    190 }