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 }