Skip to content

Commit

Permalink
Cleaning up
Browse files Browse the repository at this point in the history
Adding a custom url addition only if custom bridge is installed
  • Loading branch information
jakepurple13 committed Jan 10, 2024
1 parent 1129953 commit 7d812d9
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/push_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}

- uses: r0adkll/sign-android-release@v1
- uses: kevin-david/zipalign-sign-android-release@v1.1.1
name: Sign app APK
id: manga_sign_no_firebase
with:
Expand Down
15 changes: 14 additions & 1 deletion .idea/deploymentTargetDropDown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Models/src/main/java/com/programmersbox/models/ApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ interface ExternalApiServicesCatalog : ApiServicesCatalog {
fun shouldReload(packageName: String, packageInfo: PackageInfo): Boolean = false
}

interface ExternalCustomApiServicesCatalog : ApiServicesCatalog {

suspend fun initialize(app: Application)

fun getSources(): List<SourceInformation>
override fun createSources(): List<ApiService> = getSources().map { it.apiService }

val hasRemoteSources: Boolean

fun shouldReload(packageName: String, packageInfo: PackageInfo): Boolean = false

suspend fun getRemoteSources(customUrls: List<String>): List<RemoteSources> = emptyList()
}

data class RemoteSources(
val name: String,
val packageName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.InstallMobile
Expand All @@ -33,6 +38,8 @@ import androidx.compose.material.icons.filled.SendTimeExtension
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
Expand All @@ -43,28 +50,37 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
Expand All @@ -87,8 +103,10 @@ import com.programmersbox.uiviews.utils.DownloadAndInstaller
import com.programmersbox.uiviews.utils.InsetSmallTopAppBar
import com.programmersbox.uiviews.utils.LightAndDarkPreviews
import com.programmersbox.uiviews.utils.LocalCurrentSource
import com.programmersbox.uiviews.utils.LocalSettingsHandling
import com.programmersbox.uiviews.utils.LocalSourcesRepository
import com.programmersbox.uiviews.utils.PreviewTheme
import com.programmersbox.uiviews.utils.SettingsHandling
import com.programmersbox.uiviews.utils.components.OtakuScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand All @@ -101,7 +119,15 @@ fun ExtensionList(
sourceRepository: SourceRepository = LocalSourcesRepository.current,
otakuWorldCatalog: OtakuWorldCatalog = koinInject(),
sourceLoader: SourceLoader = koinInject(),
viewModel: ExtensionListViewModel = viewModel { ExtensionListViewModel(sourceRepository, sourceLoader, otakuWorldCatalog) },
settingsHandling: SettingsHandling = LocalSettingsHandling.current,
viewModel: ExtensionListViewModel = viewModel {
ExtensionListViewModel(
sourceRepository = sourceRepository,
sourceLoader = sourceLoader,
otakuWorldCatalog = otakuWorldCatalog,
settingsHandling = settingsHandling
)
},
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val scope = rememberCoroutineScope()
Expand Down Expand Up @@ -132,6 +158,16 @@ fun ExtensionList(
)
}

var showUrlDialog by remember { mutableStateOf(false) }
if (showUrlDialog) {
CustomUrlDialog(
onDismissRequest = { showUrlDialog = false },
onAddUrl = { scope.launch { settingsHandling.addCustomUrl(it) } },
onRemoveUrl = { scope.launch { settingsHandling.removeCustomUrl(it) } },
urls = settingsHandling.customUrls.collectAsStateWithLifecycle(initialValue = emptyList()).value
)
}

OtakuScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Expand Down Expand Up @@ -170,6 +206,17 @@ fun ExtensionList(
},
leadingIcon = { Icon(Icons.Default.Refresh, null) }
)

if (viewModel.hasCustomBridge) {
DropdownMenuItem(
text = { Text("Add Custom Tachiyomi Bridge") },
onClick = {
showUrlDialog = true
showDropDown = false
},
leadingIcon = { Icon(Icons.Default.AddCircleOutline, null) },
)
}
}
},
scrollBehavior = scrollBehavior,
Expand Down Expand Up @@ -521,6 +568,134 @@ private fun Modifier.rotateWithBoolean(shouldRotate: Boolean) = composed {
rotate(animateFloatAsState(targetValue = if (shouldRotate) 180f else 0f, label = "").value)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CustomUrlDialog(
onDismissRequest: () -> Unit,
onAddUrl: (String) -> Unit,
onRemoveUrl: (String) -> Unit,
urls: List<String>,
) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = rememberModalBottomSheetState(true)
) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
navigationIcon = {
IconButton(
onClick = onDismissRequest
) { Icon(Icons.Default.Close, null) }
},
title = { Text("Custom Tachiyomi Urls") },
actions = { Text(urls.size.toString()) }
)
},
bottomBar = {
var customUrl by rememberSaveable { mutableStateOf("") }

ElevatedCard(
shape = RoundedCornerShape(
topStart = 12.dp,
topEnd = 12.dp,
bottomEnd = 0.dp,
bottomStart = 0.dp
),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
OutlinedTextField(
value = customUrl,
label = { Text("Custom Url") },
onValueChange = { customUrl = it },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri
),
singleLine = true,
modifier = Modifier
.align(Alignment.CenterVertically)
.fillMaxWidth()
.weight(0.85f)
)
IconButton(
onClick = {
if (customUrl.isNotBlank()) {
onAddUrl(customUrl)
customUrl = ""
}
},
modifier = Modifier
.padding(start = 16.dp)
.align(Alignment.CenterVertically)
.fillMaxWidth()
.weight(0.15f)
) {
Icon(
Icons.Default.Add,
contentDescription = "Add",
)
}
}
}
}
) { padding ->
LazyColumn(
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.fillMaxSize()
) {
items(urls) {
var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Are you sure you want to delete this?") },
text = { Text(it) },
confirmButton = {
TextButton(
onClick = { onRemoveUrl(it) },
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Delete") }
},
dismissButton = {
TextButton(
onClick = { showDeleteDialog = false },
) { Text("No") }
}
)
}
val clipboard = LocalClipboardManager.current
OutlinedCard(
onClick = { clipboard.setText(buildAnnotatedString { append(it) }) }
) {
ListItem(
headlineContent = { Text(it) },
trailingContent = {
IconButton(
onClick = { showDeleteDialog = true }
) {
Icon(
Icons.Default.Delete,
null,
tint = MaterialTheme.colorScheme.error
)
}
}
)
}
}
}
}
}
}

@LightAndDarkPreviews
@Composable
private fun ExtensionListPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ import androidx.lifecycle.viewModelScope
import com.programmersbox.extensionloader.SourceLoader
import com.programmersbox.extensionloader.SourceRepository
import com.programmersbox.models.ExternalApiServicesCatalog
import com.programmersbox.models.ExternalCustomApiServicesCatalog
import com.programmersbox.models.RemoteSources
import com.programmersbox.models.SourceInformation
import com.programmersbox.uiviews.OtakuWorldCatalog
import com.programmersbox.uiviews.utils.SettingsHandling
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

class ExtensionListViewModel(
sourceRepository: SourceRepository,
private val sourceLoader: SourceLoader,
otakuWorldCatalog: OtakuWorldCatalog,
settingsHandling: SettingsHandling,
) : ViewModel() {
private val installedSources = mutableStateListOf<SourceInformation>()
val remoteSources = mutableStateMapOf<String, RemoteState>()
Expand All @@ -35,30 +39,46 @@ class ExtensionListViewModel(
remoteSources.values.filterIsInstance<RemoteViewState>().flatMap { it.sources }
}

val hasCustomBridge by derivedStateOf {
installedSources.any { it.catalog is ExternalCustomApiServicesCatalog && it.name == "Custom Tachiyomi Bridge" }
}

init {
sourceRepository.sources
.onEach {
installedSources.clear()
installedSources.addAll(it)
}
.onEach { sources ->
remoteSources.clear()
remoteSources["${otakuWorldCatalog.name}World"] = RemoteViewState(otakuWorldCatalog.getRemoteSources())
remoteSources.putAll(
sources.asSequence()
.map { it.catalog }
.filterIsInstance<ExternalApiServicesCatalog>()
.filter { it.hasRemoteSources }
.distinct()
.toList()
.associate { c ->
c.name to runCatching { c.getRemoteSources() }
.fold(
onSuccess = { RemoteViewState(it) },
onFailure = { RemoteErrorState() }
)
}
)
sources.asSequence()
.map { it.catalog }
.filterIsInstance<ExternalApiServicesCatalog>()
.filter { it.hasRemoteSources }
.distinct()
.toList()
.forEach { c ->
remoteSources[c.name] = runCatching { c.getRemoteSources() }
.fold(
onSuccess = { RemoteViewState(it) },
onFailure = { RemoteErrorState() }
)
}
}
.combine(settingsHandling.customUrls) { sources, urls ->
sources.asSequence()
.map { it.catalog }
.filterIsInstance<ExternalCustomApiServicesCatalog>()
.filter { it.hasRemoteSources }
.distinct()
.toList()
.forEach { c ->
remoteSources[c.name] = runCatching { c.getRemoteSources(urls) }
.fold(
onSuccess = { RemoteViewState(it) },
onFailure = { RemoteErrorState() }
)
}
}
.launchIn(viewModelScope)
}
Expand Down
Loading

0 comments on commit 7d812d9

Please sign in to comment.