* Implement build profiles and feature policy for offline desktop builds * Introduce unified cross-platform Settings Hub * Refactor main settings into a hierarchical page-based navigation model * Refactor library projection to use shared multiplatform logic * Refactor UI state consumption by removing intermediate screen models * Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic * Refactor state management for tabs, selection, and pinning to use shared bridge logic * Refactor file type management and validation into a centralized shared module * Centralize file type resolution and improve handling of unknown types * Centralize book import logic with SharedImportPlanner * Refactor magnifier geometry logic and coordinate mapping * Properly handle orientation changes in scroll-locked PDF reader * Add screen orientation controls to EPUB and PDF readers * Implement right-to-left (RTL) pagination support and refactor reader menus * Separate right-to-left pagination settings for PDF and EPUB * Ensure PDF page data is scoped by document key for multi tab support * Implement theme-aware link styling for the epub reader * Implement jump history for back and forward navigation in the epub reader * Improve locator handling and navigation logic in paginated reader mode * Implement stable pagination navigation and location tracking * Centralize banner message management and auto-dismiss logic in MainViewModel * Implement zoom and pan state preservation for PDF pan lock mode * Enhance reader navigation UI and workspace layout management in desktop app * Refactor reader navigation sidebar and relocate search controls in desktop app * Enhance reader UI with redesigned selection menus and bottom sheet overlays * Implement custom highlight palettes and reader theme customization in desktop app * Implement cross-platform modal layer and refine reader UI styling * Improve highlight accuracy and implement metadata enrichment on book open in desktop app * Implement two-page spread layout for paginated reader on desktop * Implement persistent caching for book loading and pagination in desktop app * Implement persistent caching for book loading and pagination in desktop app * Optimize reader settings updates by separating layout and appearance changes in desktop app * Improve desktop window branding and native Windows styling * Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app * Refine selection handle positioning and interaction logic * Implement EPUB selection debug logging and improve handle targeting * Optimize desktop book loading performance and UI responsiveness * Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer. * Implement smooth zoom preview for the PDF reader in desktop app * Optimize PDF rendering performance and responsiveness in the desktop reader * Implement conditional diagnostic logging and update desktop build configuration * Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling * Added management options for annotations and highlights in the sidebar in desktop app * Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability. * Refined library filters and enhanced OPDS functionality in desktop app * Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app * Added PPTX support including document parsing, rendering, and indexing * Improved PPTX rendering and layout accuracy * Implemented text autofit support for PPTX rendering * Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity * Improved EPUB pagination accuracy and added layout telemetry in desktop app * Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app * Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling * Implemented billing account obfuscation * Implemented hierarchical folder navigation and improved library selection functionality in desktop app * Implemented platform-aware directory resolution and multi-platform native library support for desktop * Added full-screen mode for the reader workspace * Added PDF zoom indicator and interactive vertical scrollbar with page tooltips * Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management * Updated PDF indexing to replace existing page text and removed search result item keys * Implemented "preparing" foreground notification for TTS service * Optimized PDF rendering performance by pre-calculating page-specific annotations * Refactored desktop packaging tasks and improved distribution configuration * Optimized EPUB parser memory usage and added path traversal protection * Refactored WorkManager monitoring logic and added work pruning * Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks * Implemented bitmap size limits and scaling to prevent canvas rendering errors * Split long text paragraphs into multiple semantic blocks during HTML parsing * Implemented local ActionMode for text selection to prevent platform crashes * Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic * Added desktop startup splash screen and deferred WebView initialization * Reorganized settings hub and added separate PDF reader defaults * Implemented embedded cover extraction and metadata support for MOBI and FB2 formats * Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance. * Implemented procedurally generated book covers and replaced static placeholders * Redesigned search UI with a top bar and results overlay in desktop app * Added PDF page gap and overlay visibility options and implemented DesktopBookImporter * Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop * Implemented PDF viewport persistence for zoom and scroll positions in desktop app * Improved desktop fullscreen implementation and state restoration * Implemented desktop window state persistence * Implemented flavor-based branding and ProGuard configuration for desktop builds * Implemented precise reader positioning and improved highlight rendering logic in desktop app * Added support for user-editable book metadata * Enhanced book metadata support and integrated info/edit dialogs * Implemented embedded EPUB metadata editing * Improved highlight mapping and added custom scrollbar styling for the reader. * Reduced desktop WebView bundle size by excluding unused locales and runtime files * Added neutral pan mode as the default PDF interaction state. * Refactored library empty states and updated primary navigation tabs in desktop app * Implemented native paginated reader and unified content rendering architecture in desktop epub reader * Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing. * Improved pagination overflow detection in desktop * Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
747 lines
33 KiB
Kotlin
747 lines
33 KiB
Kotlin
/*
|
|
* Episteme Reader - A native Android document reader.
|
|
* Copyright (C) 2026 Episteme
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* mail: epistemereader@gmail.com
|
|
*/
|
|
package com.aryan.reader
|
|
|
|
import android.app.Activity
|
|
import timber.log.Timber
|
|
import android.widget.Toast
|
|
import androidx.compose.foundation.BorderStroke
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.border
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.pager.HorizontalPager
|
|
import androidx.compose.foundation.pager.rememberPagerState
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
import androidx.compose.material.icons.filled.Info
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableIntStateOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.SpanStyle
|
|
import androidx.compose.ui.text.buildAnnotatedString
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextDecoration
|
|
import androidx.compose.ui.text.withStyle
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import com.aryan.reader.data.ProductDetailsEntity
|
|
import kotlinx.coroutines.launch
|
|
import java.text.NumberFormat
|
|
import java.util.Currency
|
|
|
|
@Suppress("KotlinConstantConditions")
|
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun ProScreen(
|
|
viewModel: MainViewModel,
|
|
onNavigateBack: () -> Unit
|
|
) {
|
|
val context = LocalContext.current
|
|
val proUpgradeState by viewModel.proUpgradeState.collectAsState()
|
|
val uiState by viewModel.uiState.collectAsState()
|
|
var showExistingPurchaseDialog by remember { mutableStateOf(false) }
|
|
var showSignInRequiredDialog by remember { mutableStateOf(false) }
|
|
|
|
// Removed Free Tab, so tabCount is max 2
|
|
val tabCount = if (BuildConfig.FLAVOR == "pro") 2 else 1
|
|
val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabCount })
|
|
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
|
val scope = rememberCoroutineScope()
|
|
|
|
LaunchedEffect(pagerState.currentPage) {
|
|
selectedTabIndex = pagerState.currentPage
|
|
}
|
|
|
|
LaunchedEffect(selectedTabIndex) {
|
|
scope.launch {
|
|
pagerState.animateScrollToPage(selectedTabIndex)
|
|
}
|
|
}
|
|
|
|
// Default to the Credits tab if they already own Pro
|
|
LaunchedEffect(uiState.isProUser) {
|
|
if (uiState.isProUser && BuildConfig.FLAVOR == "pro") {
|
|
selectedTabIndex = 1
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(proUpgradeState.error) {
|
|
proUpgradeState.error?.let {
|
|
Toast.makeText(context, it, Toast.LENGTH_LONG).show()
|
|
viewModel.clearBillingError()
|
|
}
|
|
}
|
|
|
|
if (showExistingPurchaseDialog) {
|
|
ExistingPurchaseDialog(onDismiss = { showExistingPurchaseDialog = false })
|
|
}
|
|
|
|
if (showSignInRequiredDialog) {
|
|
SignInRequiredDialog(
|
|
onSignInClick = {
|
|
scope.launch {
|
|
context.findActivity()?.let { activity ->
|
|
viewModel.signIn(activity)
|
|
}
|
|
}
|
|
showSignInRequiredDialog = false
|
|
},
|
|
onDismiss = { showSignInRequiredDialog = false }
|
|
)
|
|
}
|
|
|
|
Scaffold(
|
|
topBar = {
|
|
TopAppBar(
|
|
title = { },
|
|
navigationIcon = {
|
|
IconButton(onClick = onNavigateBack) {
|
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
|
}
|
|
},
|
|
colors = TopAppBarDefaults.topAppBarColors(
|
|
containerColor = MaterialTheme.colorScheme.background
|
|
)
|
|
)
|
|
}
|
|
) { paddingValues ->
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(top = paddingValues.calculateTopPadding(), start = 16.dp, end = 16.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Top
|
|
) {
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
// Tabs
|
|
TabRow(
|
|
selectedTabIndex = selectedTabIndex,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clip(MaterialTheme.shapes.extraLarge)
|
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
|
indicator = {},
|
|
divider = {}
|
|
) {
|
|
Tab(
|
|
selected = selectedTabIndex == 0,
|
|
onClick = { selectedTabIndex = 0 },
|
|
modifier = Modifier
|
|
.height(56.dp)
|
|
.clip(CircleShape)
|
|
.background(
|
|
if (selectedTabIndex == 0) MaterialTheme.colorScheme.surface else Color.Transparent
|
|
)
|
|
.border(
|
|
width = if (selectedTabIndex == 0) 2.dp else 0.dp,
|
|
color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else Color.Transparent,
|
|
shape = CircleShape
|
|
),
|
|
text = {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.crown),
|
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
|
modifier = Modifier.size(16.dp),
|
|
tint = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
AutoSizeText(stringResource(R.string.drawer_pro_unlocked),
|
|
style = LocalTextStyle.current.copy(
|
|
color = if (selectedTabIndex == 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
)
|
|
}
|
|
},
|
|
selectedContentColor = MaterialTheme.colorScheme.primary,
|
|
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
if (BuildConfig.FLAVOR == "pro") {
|
|
Tab(
|
|
selected = selectedTabIndex == 1,
|
|
onClick = { selectedTabIndex = 1 },
|
|
modifier = Modifier
|
|
.height(56.dp)
|
|
.clip(CircleShape)
|
|
.background(if (selectedTabIndex == 1) MaterialTheme.colorScheme.surface else Color.Transparent)
|
|
.border(
|
|
width = if (selectedTabIndex == 1) 2.dp else 0.dp,
|
|
color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else Color.Transparent,
|
|
shape = CircleShape
|
|
),
|
|
text = {
|
|
AutoSizeText(stringResource(R.string.credits_tab),
|
|
style = LocalTextStyle.current.copy(
|
|
color = if (selectedTabIndex == 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
)
|
|
},
|
|
selectedContentColor = MaterialTheme.colorScheme.primary,
|
|
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
HorizontalPager(
|
|
state = pagerState,
|
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
|
userScrollEnabled = true
|
|
) { page ->
|
|
when (page) {
|
|
0 -> {
|
|
ProTierCard(
|
|
isProUser = uiState.isProUser,
|
|
isUserSignedIn = uiState.currentUser != null,
|
|
proUpgradeState = proUpgradeState,
|
|
onUpgradeClick = {
|
|
(context as? Activity)?.let {
|
|
viewModel.launchPurchaseFlow(it)
|
|
}
|
|
},
|
|
onShowExistingPurchaseDialog = { showExistingPurchaseDialog = true },
|
|
onSignInRequiredClick = { showSignInRequiredDialog = true })
|
|
}
|
|
|
|
1 -> {
|
|
if (BuildConfig.FLAVOR == "pro") {
|
|
CreditTierCard(
|
|
credits = uiState.credits,
|
|
creditProducts = proUpgradeState.creditProducts,
|
|
isVerifying = proUpgradeState.isVerifying,
|
|
isUserSignedIn = uiState.currentUser != null,
|
|
onSignInRequiredClick = { showSignInRequiredDialog = true },
|
|
onBuyCredits = { productId ->
|
|
(context as? Activity)?.let { viewModel.launchPurchaseFlow(it, productId) }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ProTierCard(
|
|
isProUser: Boolean,
|
|
isUserSignedIn: Boolean,
|
|
proUpgradeState: ProUpgradeState,
|
|
onUpgradeClick: () -> Unit,
|
|
onShowExistingPurchaseDialog: () -> Unit,
|
|
onSignInRequiredClick: () -> Unit
|
|
) {
|
|
val productDetails = proUpgradeState.productDetails
|
|
val billingClientReady = proUpgradeState.billingClientReady
|
|
val localPurchaseExistsForOtherAccount = !isProUser && proUpgradeState.hasAccountConflict
|
|
|
|
var originalFormattedPrice by remember { mutableStateOf("$9.99") }
|
|
|
|
LaunchedEffect(productDetails) {
|
|
productDetails?.let { details ->
|
|
val priceAmountMicros = details.priceAmountMicros
|
|
val priceCurrencyCode = details.currencyCode
|
|
|
|
val originalPriceMicros = priceAmountMicros * 2
|
|
|
|
val currencyFormatter = NumberFormat.getCurrencyInstance().apply {
|
|
try {
|
|
currency = Currency.getInstance(priceCurrencyCode)
|
|
} catch (e: IllegalArgumentException) {
|
|
Timber.e(e, "Invalid currency code: $priceCurrencyCode")
|
|
}
|
|
}
|
|
originalFormattedPrice = currencyFormatter.format(originalPriceMicros / 1_000_000.0)
|
|
}
|
|
}
|
|
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = MaterialTheme.colorScheme.surface
|
|
)
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(16.dp)
|
|
.fillMaxWidth()
|
|
.verticalScroll(rememberScrollState()),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.crown),
|
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
|
modifier = Modifier.size(24.dp),
|
|
tint = MaterialTheme.colorScheme.primary
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(stringResource(R.string.drawer_pro_unlocked),
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = MaterialTheme.colorScheme.primary,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
if (!isProUser) {
|
|
val formattedPrice = productDetails?.formattedPrice
|
|
|
|
if (formattedPrice != null) {
|
|
Text(
|
|
text = buildAnnotatedString {
|
|
withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
|
|
append(originalFormattedPrice)
|
|
}
|
|
append(" " + stringResource(R.string.pro_sale_off)) },
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
text = formattedPrice,
|
|
style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp),
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
} else {
|
|
Text(stringResource(R.string.loading_price),
|
|
style = MaterialTheme.typography.displaySmall.copy(fontSize = 32.sp),
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
}
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(stringResource(R.string.one_time_payment),
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Surface(
|
|
shape = MaterialTheme.shapes.extraSmall,
|
|
color = MaterialTheme.colorScheme.primaryContainer,
|
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
|
) {
|
|
Text(stringResource(R.string.lifetime_access),
|
|
style = MaterialTheme.typography.labelSmall,
|
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
)
|
|
}
|
|
}
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
}
|
|
|
|
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalAlignment = Alignment.Start
|
|
) {
|
|
Text(stringResource(R.string.pro_includes),
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
fontWeight = FontWeight.SemiBold,
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
)
|
|
FeatureListItem(iconRes = R.drawable.cloud_sync, text = stringResource(R.string.feature_cloud_sync))
|
|
Text(stringResource(R.string.feature_cloud_sync_desc),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
|
|
)
|
|
FeatureListItem(iconRes = R.drawable.summarize, text = stringResource(R.string.feature_summarize))
|
|
Text(stringResource(R.string.feature_summarize_desc),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
|
|
)
|
|
FeatureListItem(iconRes = R.drawable.dictionary, text = stringResource(R.string.feature_smart_dict))
|
|
Text(stringResource(R.string.feature_smart_dict_desc),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
|
|
)
|
|
FeatureListItem(iconRes = R.drawable.chat_bubble, text = stringResource(R.string.feature_priority))
|
|
Text(stringResource(R.string.feature_priority_desc),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.padding(start = 36.dp, bottom = 8.dp)
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
if (isProUser) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(48.dp)
|
|
.background(
|
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f),
|
|
MaterialTheme.shapes.medium
|
|
)
|
|
) {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.crown),
|
|
contentDescription = stringResource(R.string.pro_unlocked),
|
|
tint = MaterialTheme.colorScheme.primary
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(stringResource(R.string.pro_unlocked),
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
fontWeight = FontWeight.SemiBold,
|
|
color = MaterialTheme.colorScheme.primary
|
|
)
|
|
}
|
|
} else {
|
|
when {
|
|
!isUserSignedIn -> {
|
|
Button(
|
|
onClick = onSignInRequiredClick,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(48.dp),
|
|
shape = MaterialTheme.shapes.medium,
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
)
|
|
) {
|
|
AutoSizeText(stringResource(R.string.sign_in_required), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold))
|
|
}
|
|
}
|
|
proUpgradeState.isVerifying -> {
|
|
Button(
|
|
onClick = {},
|
|
enabled = false,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(48.dp),
|
|
shape = MaterialTheme.shapes.medium,
|
|
) {
|
|
CircularProgressIndicator(
|
|
modifier = Modifier.size(24.dp),
|
|
strokeWidth = 2.dp,
|
|
color = LocalContentColor.current
|
|
)
|
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
Text(stringResource(R.string.verifying_purchase))
|
|
}
|
|
}
|
|
productDetails != null -> {
|
|
Button(
|
|
onClick = {
|
|
Timber.d("Upgrade button clicked.")
|
|
onUpgradeClick()
|
|
},
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(48.dp),
|
|
shape = MaterialTheme.shapes.medium,
|
|
enabled = billingClientReady
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
painter = painterResource(id = R.drawable.crown),
|
|
contentDescription = stringResource(R.string.drawer_pro_unlocked),
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
AutoSizeText(stringResource(R.string.get_lifetime_access), style = LocalTextStyle.current.copy(fontSize = 16.sp, fontWeight = FontWeight.SemiBold))
|
|
}
|
|
}
|
|
}
|
|
!billingClientReady -> {
|
|
Box(modifier = Modifier.height(48.dp), contentAlignment = Alignment.Center) {
|
|
CircularProgressIndicator()
|
|
}
|
|
}
|
|
else -> {
|
|
Text(stringResource(R.string.upgrade_unavailable),
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
textAlign = TextAlign.Center,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.height(48.dp)
|
|
)
|
|
}
|
|
}
|
|
|
|
// Text area below button
|
|
Spacer(modifier = Modifier.height(16.dp)) // Increased spacing
|
|
when {
|
|
!isUserSignedIn -> {
|
|
Text(stringResource(R.string.sign_in_to_purchase),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
textAlign = TextAlign.Center,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
proUpgradeState.isVerifying -> {
|
|
Text(stringResource(R.string.verifying_purchase_desc),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
textAlign = TextAlign.Center
|
|
)
|
|
}
|
|
localPurchaseExistsForOtherAccount -> {
|
|
TextButton(onClick = onShowExistingPurchaseDialog) {
|
|
Icon(
|
|
imageVector = Icons.Default.Info,
|
|
contentDescription = stringResource(R.string.info),
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
|
AutoSizeText(stringResource(R.string.existing_purchase_found))
|
|
}
|
|
}
|
|
else -> {
|
|
LegalText(prefixText = stringResource(R.string.legal_by_purchasing))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun FeatureListItem(@androidx.annotation.DrawableRes iconRes: Int? = null, icon: ImageVector? = null, text: String) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 4.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
if (iconRes != null) {
|
|
Icon(
|
|
painter = painterResource(id = iconRes),
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
} else if (icon != null) {
|
|
Icon(
|
|
imageVector = icon,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
} else {
|
|
Icon(
|
|
imageVector = Icons.Default.Info,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ExistingPurchaseDialog(onDismiss: () -> Unit) {
|
|
AlertDialog(
|
|
onDismissRequest = onDismiss,
|
|
icon = { Icon(Icons.Default.Info, contentDescription = null) },
|
|
title = { Text(stringResource(R.string.existing_purchase_found)) },
|
|
text = { Text(stringResource(R.string.dialog_existing_purchase_desc)) },
|
|
confirmButton = {
|
|
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_ok)) }
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun SignInRequiredDialog(onSignInClick: () -> Unit, onDismiss: () -> Unit) {
|
|
AlertDialog(
|
|
onDismissRequest = onDismiss,
|
|
icon = { Icon(painter = painterResource(id = R.drawable.crown), contentDescription = null) },
|
|
title = { Text(stringResource(R.string.sign_in_required)) },
|
|
text = { Text(stringResource(R.string.dialog_sign_in_required_desc)) },
|
|
confirmButton = {
|
|
TextButton(onClick = onSignInClick) { Text(stringResource(R.string.drawer_sign_in)) }
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_not_now)) }
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
private fun CreditTierCard(
|
|
credits: Int,
|
|
creditProducts: List<ProductDetailsEntity>,
|
|
isVerifying: Boolean,
|
|
isUserSignedIn: Boolean,
|
|
onSignInRequiredClick: () -> Unit,
|
|
onBuyCredits: (String) -> Unit
|
|
) {
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(16.dp)
|
|
.fillMaxWidth()
|
|
.verticalScroll(rememberScrollState()),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
Text(stringResource(R.string.credits_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
text = "$credits",
|
|
style = MaterialTheme.typography.displaySmall.copy(fontSize = 48.sp),
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.primary
|
|
)
|
|
Text(stringResource(R.string.credits_available), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
if (isVerifying) {
|
|
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
|
|
Text(stringResource(R.string.verifying_purchase), style = MaterialTheme.typography.bodySmall)
|
|
} else if (creditProducts.isEmpty()) {
|
|
Text(stringResource(R.string.loading_price), modifier = Modifier.padding(16.dp))
|
|
} else {
|
|
creditProducts.forEach { product ->
|
|
OutlinedCard(
|
|
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
|
|
onClick = {
|
|
if (isUserSignedIn) onBuyCredits(product.productId)
|
|
else onSignInRequiredClick()
|
|
},
|
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
|
Text(product.name, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge)
|
|
if (product.description.isNotBlank()) {
|
|
Text(product.description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
}
|
|
}
|
|
Button(
|
|
onClick = {
|
|
if (isUserSignedIn) onBuyCredits(product.productId)
|
|
else onSignInRequiredClick()
|
|
},
|
|
modifier = Modifier.wrapContentWidth()
|
|
) {
|
|
Text(product.formattedPrice)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isUserSignedIn) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
stringResource(R.string.sign_in_to_purchase_credits),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.primary,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(32.dp))
|
|
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
Text(
|
|
stringResource(R.string.credits_estimated_cost_breakdown),
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.Bold,
|
|
modifier = Modifier.align(Alignment.Start)
|
|
)
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
CostBreakdownItem(
|
|
iconRes = R.drawable.text_to_speech,
|
|
title = stringResource(R.string.credits_cloud_tts_title),
|
|
description = stringResource(R.string.credits_cloud_tts_desc)
|
|
)
|
|
CostBreakdownItem(
|
|
iconRes = R.drawable.summarize,
|
|
title = stringResource(R.string.credits_ai_summaries_title),
|
|
description = stringResource(R.string.credits_ai_summaries_desc)
|
|
)
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun CostBreakdownItem(
|
|
@androidx.annotation.DrawableRes iconRes: Int,
|
|
title: String,
|
|
description: String
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 8.dp),
|
|
verticalAlignment = Alignment.Top
|
|
) {
|
|
Icon(
|
|
painter = painterResource(id = iconRes),
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(24.dp).padding(top = 2.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
Column {
|
|
Text(text = title, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold)
|
|
Spacer(modifier = Modifier.height(2.dp))
|
|
Text(
|
|
text = description,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
lineHeight = 18.sp
|
|
)
|
|
}
|
|
}
|
|
}
|