Initial commit
This commit is contained in:
commit
6072b2ba29
844 changed files with 220532 additions and 0 deletions
510
app/src/main/java/com/aryan/reader/SharedComposables.kt
Normal file
510
app/src/main/java/com/aryan/reader/SharedComposables.kt
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
// SharedComposables.kt
|
||||
package com.aryan.reader
|
||||
|
||||
import android.net.Uri
|
||||
import timber.log.Timber
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.outlined.FileOpen
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aryan.reader.data.RecentFileItem
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.sp
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.core.net.toUri
|
||||
|
||||
internal const val PRIVACY_POLICY_URL = "https://aryan-raj3112.github.io/reader-policy/privacy-policy.html"
|
||||
internal const val TERMS_URL = "https://aryan-raj3112.github.io/reader-policy/terms-and-conditions.html"
|
||||
internal const val LICENSES_URL = "https://aryan-raj3112.github.io/reader-policy/licenses.html"
|
||||
|
||||
class CustomTabUriHandler(private val context: Context) : UriHandler {
|
||||
override fun openUri(uri: String) {
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, uri.toUri())
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to launch Custom Tab, falling back to browser.")
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, uri.toUri()).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LegalText(
|
||||
modifier: Modifier = Modifier,
|
||||
prefixText: String, // Changed from baseText
|
||||
textAlign: TextAlign = TextAlign.Center
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append("$prefixText you agree to our ")
|
||||
pushStringAnnotation(tag = "terms", annotation = TERMS_URL)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
) {
|
||||
append("Terms of Service")
|
||||
}
|
||||
pop()
|
||||
append(" and acknowledge you have read our ")
|
||||
pushStringAnnotation(tag = "privacy", annotation = PRIVACY_POLICY_URL)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
) {
|
||||
append("Privacy Policy")
|
||||
}
|
||||
pop()
|
||||
append(".")
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
textAlign = textAlign,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
lineHeight = 18.sp
|
||||
),
|
||||
modifier = modifier,
|
||||
onClick = { offset ->
|
||||
annotatedString.getStringAnnotations(tag = "terms", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
uriHandler.openUri(annotation.item)
|
||||
}
|
||||
annotatedString.getStringAnnotations(tag = "privacy", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
uriHandler.openUri(annotation.item)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberFilePickerLauncher(
|
||||
onFilesSelected: (List<Uri>) -> Unit
|
||||
): ManagedActivityResultLauncher<Array<String>, List<@JvmSuppressWildcards Uri>> {
|
||||
return rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenMultipleDocuments(),
|
||||
onResult = { uris: List<Uri> ->
|
||||
if (uris.isNotEmpty()) {
|
||||
Timber.d("${uris.size} file(s) selected.")
|
||||
onFilesSelected(uris)
|
||||
} else {
|
||||
Timber.d("File selection cancelled.")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextualTopAppBar(
|
||||
selectedItemCount: Int,
|
||||
onNavIconClick: () -> Unit,
|
||||
onInfoClick: (() -> Unit)? = null,
|
||||
onSelectAllClick: (() -> Unit)? = null,
|
||||
onDeleteClick: () -> Unit
|
||||
) {
|
||||
CustomTopAppBar(
|
||||
title = { Text("$selectedItemCount selected") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavIconClick) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Clear Selection")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (selectedItemCount == 1 && onInfoClick != null) {
|
||||
IconButton(onClick = onInfoClick) {
|
||||
Icon(Icons.Filled.Info, contentDescription = "Info")
|
||||
}
|
||||
}
|
||||
if (onSelectAllClick != null) {
|
||||
IconButton(onClick = onSelectAllClick) {
|
||||
Icon(Icons.Filled.SelectAll, contentDescription = "Select All")
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDeleteClick) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = "Delete")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable () -> Unit,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {}
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shadowElevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
navigationIcon()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 12.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.titleLarge) {
|
||||
title()
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteConfirmationDialog(count: Int, onConfirm: () -> Unit, onDismiss: () -> Unit, isPermanentDelete: Boolean = false) {
|
||||
val title = if (isPermanentDelete) "Delete File(s) Permanently" else "Remove from Recents"
|
||||
val text = if (isPermanentDelete) {
|
||||
"Do you want to permanently delete $count selected file(s) from your device? This action cannot be undone."
|
||||
} else {
|
||||
"Do you want to remove $count selected file(s) from the recent files list? It will reappear if you open it again from the library."
|
||||
}
|
||||
val confirmText = if (isPermanentDelete) "Delete" else "Remove"
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = { Text(text) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) { Text(confirmText) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit) {
|
||||
val formattedDate = remember(item.timestamp) {
|
||||
SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(item.timestamp))
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("File Information") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
val displayName = item.displayName
|
||||
val epubTitle = item.title
|
||||
|
||||
if (item.type != FileType.PDF && !epubTitle.isNullOrBlank()) {
|
||||
InfoRow("Title:", epubTitle, maxLines = 3)
|
||||
if (displayName != epubTitle) {
|
||||
InfoRow("File Name:", displayName, maxLines = 2)
|
||||
}
|
||||
} else {
|
||||
InfoRow("File Name:", displayName, maxLines = 2)
|
||||
}
|
||||
|
||||
item.author?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }?.let {
|
||||
InfoRow("Author:", it, maxLines = 2)
|
||||
}
|
||||
|
||||
InfoRow("File Type:", item.type.name)
|
||||
InfoRow("Date Added:", formattedDate)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text("OK") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun InfoRow(label: String, value: String?, maxLines: Int = 1) {
|
||||
if (value.isNullOrBlank()) return
|
||||
Row {
|
||||
Text(
|
||||
text = label,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.width(90.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = maxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomTopBanner(bannerMessage: BannerMessage?) {
|
||||
AnimatedVisibility(
|
||||
visible = bannerMessage != null,
|
||||
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding(),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
color = if (bannerMessage?.isError == true) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
shadowElevation = 8.dp
|
||||
) {
|
||||
Text(
|
||||
text = bannerMessage?.message ?: "",
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
color = if (bannerMessage?.isError == true) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
@Composable
|
||||
fun AboutDialog(onDismiss: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val isOss = BuildConfig.FLAVOR == "oss"
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("About Episteme") },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"Version: ${BuildConfig.VERSION_NAME} (Build: ${BuildConfig.VERSION_CODE})",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
// Only show legal links in the Pro version
|
||||
if (!isOss) {
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Privacy Policy",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { uriHandler.openUri(PRIVACY_POLICY_URL) }
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Terms of Service",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { uriHandler.openUri(TERMS_URL) }
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Licenses",
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { uriHandler.openUri(LICENSES_URL) }
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Close") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyState(
|
||||
title: String,
|
||||
message: String,
|
||||
onSelectFileClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FileOpen,
|
||||
contentDescription = "No files icon",
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
SelectFileButton(onClick = onSelectFileClick, text = "Select a File")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectFileButton(onClick: () -> Unit, text: String) {
|
||||
FilledTonalButton(
|
||||
onClick = onClick,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearCloudDataConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Clear All Synced Data?") },
|
||||
text = { Text("Are you sure you want to permanently delete all of your book data from the cloud? This will also wipe your local library to prevent re-syncing. This action cannot be undone.") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = onConfirm,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
Text("DELETE ALL DATA")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoSizeText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
maxLines: Int = 1,
|
||||
) {
|
||||
var scaledTextStyle by remember(text, style) { mutableStateOf(style) }
|
||||
var readyToDraw by remember(text, style) { mutableStateOf(false) }
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier.drawWithContent {
|
||||
if (readyToDraw) {
|
||||
drawContent()
|
||||
}
|
||||
},
|
||||
style = scaledTextStyle,
|
||||
maxLines = maxLines,
|
||||
softWrap = false,
|
||||
onTextLayout = { textLayoutResult ->
|
||||
if (textLayoutResult.hasVisualOverflow) {
|
||||
scaledTextStyle = scaledTextStyle.copy(
|
||||
fontSize = scaledTextStyle.fontSize * 0.95
|
||||
)
|
||||
} else {
|
||||
readyToDraw = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue