Skip to content

Commit

Permalink
Add page actions (deletion,...) and loading screen
Browse files Browse the repository at this point in the history
  • Loading branch information
Chrisimx committed Dec 24, 2024
1 parent f4d31ab commit 2e09805
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 10 deletions.
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
android:theme="@style/Theme.ScanBridge"
android:usesCleartextTraffic="true"
tools:targetApi="35">

<provider
android:authorities="${applicationId}.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity
android:name=".ScanningActivity"
android:exported="false"
Expand Down
217 changes: 207 additions & 10 deletions app/src/main/java/io/github/chrisimx/scanbridge/ScanningActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,33 @@ package io.github.chrisimx.scanbridge

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.pdf.PdfDocument
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -64,13 +74,15 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
Expand All @@ -88,7 +100,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import java.nio.file.Files
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.concurrent.thread
import kotlin.io.path.Path
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

Expand All @@ -97,7 +112,7 @@ data class ScanningActivityData(
val capabilities: ScannerCapabilities,
val scanSettingsMenuOpen: MutableState<Boolean> = mutableStateOf(false),
val scanJobRunning: MutableState<Boolean> = mutableStateOf(false),
val stateCurrentScans: SnapshotStateList<String> = mutableStateListOf()
val stateCurrentScans: SnapshotStateList<Pair<String, ScanSettings>> = mutableStateListOf()
) {
fun toImmutable() = ImmutableScanningActivityData(
scanSettingsVM,
Expand All @@ -113,7 +128,7 @@ data class ImmutableScanningActivityData(
val capabilities: ScannerCapabilities,
private val scanSettingsMenuOpenState: State<Boolean>,
private val scanJobRunningState: State<Boolean>,
val currentScansState: SnapshotStateList<String>,
val currentScansState: SnapshotStateList<Pair<String, ScanSettings>>,
) {
val scanSettingsMenuOpen by scanSettingsMenuOpenState
val scanJobRunning by scanJobRunningState
Expand All @@ -136,8 +151,27 @@ class ScanningViewModel(
_scanningActivityData.scanJobRunning.value = value
}

fun addScan(path: String) {
_scanningActivityData.stateCurrentScans.add(path)
fun addScan(path: String, settings: ScanSettings) {
_scanningActivityData.stateCurrentScans.add(Pair(path, settings))
}

fun swapTwoPages(index1: Int, index2: Int) {
if (index1 < 0 || index1 >= _scanningActivityData.stateCurrentScans.size
|| index2 < 0 || index2 >= _scanningActivityData.stateCurrentScans.size
) {
return
}
val tmp = _scanningActivityData.stateCurrentScans[index1]
_scanningActivityData.stateCurrentScans[index1] =
_scanningActivityData.stateCurrentScans[index2]
_scanningActivityData.stateCurrentScans[index2] = tmp
}

fun removeScanAtIndex(index: Int) {
if (index < 0 || index >= _scanningActivityData.stateCurrentScans.size) {
return
}
_scanningActivityData.stateCurrentScans.removeAt(index)
}
}

Expand Down Expand Up @@ -200,6 +234,7 @@ class ScanningActivity : ComponentActivity() {
}

viewModel.setScanJobRunning(true)

val job =
esclRequestClient.createJob(scanSettings)
Log.d(TAG, "Sent scan request to scanner. Result: $job")
Expand All @@ -225,7 +260,7 @@ class ScanningActivity : ComponentActivity() {
}
nextPage.page.use {
val uuid = "scan-"+Uuid.random().toString()
val filePath = File(filesDir, uuid.toString()).toPath()
val filePath = File(filesDir, uuid).toPath()

Log.d(TAG, "File created: $filePath")

Expand All @@ -237,7 +272,7 @@ class ScanningActivity : ComponentActivity() {
filePath.toFile().delete()
return@thread
}
viewModel.addScan(filePath.toString())
viewModel.addScan(filePath.toString(), scanSettings)
}
}
}
Expand Down Expand Up @@ -344,6 +379,69 @@ class ScanningActivity : ComponentActivity() {
}) {
Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.settings))
}
IconButton(onClick = {
thread {
PdfDocument().apply {
for (scan in scanningViewModel.scanningActivityData.currentScansState) {
val bitmap =
BitmapFactory.decodeFile(scan.first)
val scanRegion =
scan.second.scanRegions!!.regions.first()
val width72thInches =
scanRegion.width.toInches().value * 72.0
val height72thInches =
scanRegion.height.toInches().value * 72.0
val pageInfo = PdfDocument.PageInfo.Builder(
width72thInches.toInt(),
height72thInches.toInt(),
1
).create()
val page = startPage(pageInfo)

page.canvas.drawBitmap(
bitmap,
null,
android.graphics.Rect(
0,
0,
width72thInches.toInt(),
height72thInches.toInt()
),
null
)
this.finishPage(page)
}
val parentDir = File(filesDir, "exports")
if (!parentDir.exists()) {
parentDir.mkdir()
}
val pdfFile = File(
parentDir, "pdfexport-${
LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH_mm_ss_SSS"))
}.pdf"
)
writeTo(pdfFile.outputStream())
val share = Intent(Intent.ACTION_SEND)
share.type = "application/pdf"
share.putExtra(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
this@ScanningActivity,
"${this@ScanningActivity.packageName}.fileprovider",
pdfFile
)
)

startActivity(share)
}
}
}) {
Icon(
Icons.Filled.Share,
contentDescription = stringResource(R.string.export)
)
}
},
floatingActionButton = {
ExtendedFloatingActionButton (onClick = {
Expand All @@ -369,28 +467,127 @@ class ScanningActivity : ComponentActivity() {

},
) { innerPadding ->

val pagerState = rememberPagerState(pageCount = {
scanningViewModel.scanningActivityData.currentScansState.size
scanningViewModel.scanningActivityData.currentScansState.size + if (scanningViewModel.scanningActivityData.scanJobRunning) 1 else 0
}
)
val scrollScope = rememberCoroutineScope()

if (!scanningViewModel.scanningActivityData.currentScansState.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(
R.string.page_x_of_y,
pagerState.currentPage + 1,
scanningViewModel.scanningActivityData.currentScansState.size + if (scanningViewModel.scanningActivityData.scanJobRunning) 1 else 0
)
)

if (scanningViewModel.scanningActivityData.currentScansState.size > pagerState.currentPage) {
Text(scanningViewModel.scanningActivityData.currentScansState[pagerState.currentPage].second.inputSource.toString())
}
}
}

HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
state = pagerState
) { page ->
val zoomState = rememberZoomableState(zoomSpec = ZoomSpec(5f))
if (page == scanningViewModel.scanningActivityData.currentScansState.size) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Text(stringResource(R.string.retrieving_page))
}
return@HorizontalPager
} else {
val zoomState = rememberZoomableState(zoomSpec = ZoomSpec(5f))

Column {
AsyncImage(
model = scanningViewModel.scanningActivityData.currentScansState[page],
model = scanningViewModel.scanningActivityData.currentScansState[page].first,
contentDescription = stringResource(R.string.desc_scanned_page),
modifier = Modifier
.zoomable(zoomState)
.padding(vertical = 5.dp),
)
}
}

Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding), contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.padding(10.dp)
.clip(
RoundedCornerShape(16.dp)
)
.background(MaterialTheme.colorScheme.inverseOnSurface),
) {
Row {
IconButton(onClick = {
if (scanningViewModel.scanningActivityData.currentScansState.size <= pagerState.currentPage) {
return@IconButton
}
Files.delete(Path(scanningViewModel.scanningActivityData.currentScansState[pagerState.currentPage].first))
scanningViewModel.removeScanAtIndex(pagerState.currentPage)
}) {
Icon(
Icons.Outlined.Delete,
contentDescription = stringResource(
R.string.delete_current_page
)
)
}
IconButton(onClick = {
scanningViewModel.swapTwoPages(
pagerState.currentPage,
pagerState.currentPage - 1
)
scrollScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}) {
Icon(
Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = stringResource(
R.string.swap_with_previous_page
)
)
}
IconButton(onClick = {
scanningViewModel.swapTwoPages(
pagerState.currentPage,
pagerState.currentPage + 1
)
scrollScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}) {
Icon(
Icons.AutoMirrored.Outlined.ArrowForward,
contentDescription = stringResource(
R.string.swap_with_next_page
)
)
}
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@
<string name="job_still_pending">The job is still pending</string>
<string name="job_pages_still_processed">The pages are still processed</string>
<string name="job_state_cannot_be_retrieved">The job state can\'t be retrieved</string>
<string name="delete_current_page">Delete current page</string>
<string name="swap_with_previous_page">Swap with previous</string>
<string name="swap_with_next_page">Swap with next</string>
<string name="page_x_of_y">Page %1$s of %2$s</string>
<string name="export">Export</string>
<string name="retrieving_page">Retrieving page</string>
</resources>
6 changes: 6 additions & 0 deletions app/src/main/res/xml/provider_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="exports"
path="exports/" />
</paths>

0 comments on commit 2e09805

Please sign in to comment.