Commit 152ff13e by Aleksandr Tamakov

Job list card

parent bfd003e0
......@@ -105,12 +105,15 @@ val huaweiImplementation by configurations
dependencies {
implementation(project(":feature:session"))
implementation(project(":feature:rendering"))
implementation(project(":feature:job"))
implementation(project(":feature:spot"))
// implementation(project((":feature:ms_office")))
implementation(project((":library:slider")))
implementation(project((":core:core")))
implementation(project((":ui:render_preview")))
implementation(project((":ui:compose_components")))
implementation(project((":ui:print_job")))
// google
implementation("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
......@@ -121,6 +124,7 @@ dependencies {
implementation("androidx.hilt:hilt-navigation-compose:${GoogleVersions.hiltNavigationCompose}")
implementation("androidx.preference:preference-ktx:${GoogleVersions.preferences}")
implementation("androidx.room:room-runtime:${GoogleVersions.room}")
implementation("androidx.hilt:hilt-work:${GoogleVersions.work_hilt}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
......
......@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "5310a974346e7b7c3f4fbbab21cba840",
"identityHash": "e7ecfb35b007c71647c63f54d2f787ba",
"entities": [
{
"tableName": "Session",
......@@ -199,12 +199,116 @@
}
],
"foreignKeys": []
},
{
"tableName": "PrintJob",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `cost` REAL NOT NULL, `spotId` TEXT NOT NULL, `profileId` TEXT NOT NULL, `printSize` INTEGER NOT NULL, `printOrientation` INTEGER NOT NULL, `copies` INTEGER NOT NULL, `status` INTEGER NOT NULL, `errorMessage` TEXT, `comment` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `accessCode` TEXT, `accessToken` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cost",
"columnName": "cost",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "spotId",
"columnName": "spotId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profileId",
"columnName": "profileId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "printSize",
"columnName": "printSize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "printOrientation",
"columnName": "printOrientation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "copies",
"columnName": "copies",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "errorMessage",
"columnName": "errorMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessCode",
"columnName": "accessCode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5310a974346e7b7c3f4fbbab21cba840')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e7ecfb35b007c71647c63f54d2f787ba')"
]
}
}
\ No newline at end of file
package com.isidroid.c23
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.isidroid.c23.data.source.settings.Settings
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class App: Application() {
class App : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
Settings.init(applicationContext)
Timber.plant(Timber.DebugTree())
}
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
\ No newline at end of file
......@@ -19,11 +19,13 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.isidroid.c23.data.worker.job.SendJobWorker
import com.isidroid.c23.ext.isEdgeToEdge
import com.isidroid.c23.ui.navigation.AppNavHost
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.core.ext.printCurrentDestination
import dagger.hilt.android.AndroidEntryPoint
import java.util.UUID
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
......
package com.isidroid.c23.data.source.local
import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.model.PrintJob
import com.isidroid.spot.data.source.local.dao.PrintProfileDao
import com.isidroid.spot.model.PrintProfile
import com.isidroid.session.data.source.local.SessionDao
......@@ -13,15 +16,16 @@ import com.isidroid.spot.model.Spot
import com.isidroid.spot.data.source.local.dao.SpotDao
@Database(
version = 1,
version = 2,
entities = [
Session::class,
PrintProfile::class,
Spot::class
Spot::class,
PrintJob::class
],
exportSchema = true,
autoMigrations = [
// AutoMigration(from = 1, to = 2),
AutoMigration(from = 1, to = 2),
]
)
@TypeConverters(RoomConverters::class)
......@@ -29,6 +33,7 @@ abstract class AppDataBase : RoomDatabase() {
abstract val sessionDao: SessionDao
abstract val printProfileDao: PrintProfileDao
abstract val spotDao: SpotDao
abstract val jobDao: JobDao
companion object {
@Volatile
......
......@@ -12,4 +12,7 @@ object RoomConverters {
@TypeConverter fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter fun dateToTimestamp(date: Date?): Long? = date?.time
@TypeConverter fun fromListString(value: List<String>?): String? = value?.joinToString(",")
@TypeConverter fun stringToListString(value: String?): List<String>? = value?.let { v -> v.split(",").map { it.trim() } }
}
\ No newline at end of file
package com.isidroid.c23.data.worker.job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class SendJobEventCollectorFlow @Inject constructor() {
private val _eventsFlow = MutableSharedFlow<State>()
val eventsFlow = _eventsFlow.asSharedFlow()
class State {
}
}
\ No newline at end of file
package com.isidroid.c23.data.worker.job
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltWorker
internal class SendJobWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
// @Inject private val collectorFlow: SendJobEventCollectorFlow
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
Result.success()
}
companion object {
fun create(context: Context) {
val workManager = WorkManager.getInstance(context)
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val builder = OneTimeWorkRequestBuilder<SendJobWorker>().keepResultsForAtLeast(1, TimeUnit.SECONDS)
val data = Data.Builder().build()
val requestBuilder = builder
.setConstraints(constraints)
.setInputData(data)
.setId(UUID.randomUUID())
// We use KEEP to avoid duplicate runs of SyncRepository
workManager.beginUniqueWork(SendJobWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, requestBuilder.build()).enqueue()
}
}
}
\ No newline at end of file
......@@ -20,4 +20,7 @@ object DatabaseModule {
@Provides @Singleton
fun providesSpotDao(appDataBase: AppDataBase) = appDataBase.spotDao
@Provides @Singleton
fun providesJobDao(appDataBase: AppDataBase) = appDataBase.jobDao
}
\ No newline at end of file
package com.isidroid.c23.domain.dto
import androidx.compose.ui.graphics.Color
import java.util.Date
data class PrintJobListItem(
val id: String,
val spotName: String,
val cost: Float,
val paperInfo: String,
val isColor: Boolean,
val copies: Int,
val comment: String?,
val statusName: String,
val statusColor: Color,
val cover: String?,
val accessCode: String,
val createdAt: Date
)
\ No newline at end of file
package com.isidroid.c23.domain.use_case
import androidx.compose.ui.graphics.Color
import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.core.FlowResult
import com.isidroid.job.constant.statusName
import com.isidroid.job.repository.JobRepository
import com.isidroid.rendering.constant.printSizeName
import com.isidroid.spot.repository.SpotRepository
import kotlinx.coroutines.flow.flow
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PrintJobsUseCase @Inject constructor(
private val repository: JobRepository,
private val spotRepository: SpotRepository
) {
fun load() = flow {
emit(FlowResult.Loading)
val jobList = repository.readLocalList()
val spots = jobList.map { it.spotId }.distinct().let { spotRepository.finLocalRichSpots(it) }?.associateBy({ it.spot.id }, { it })
val result = jobList.map { job ->
val richSpot = spots?.get(job.spotId)
val profile = richSpot?.printProfiles?.find { it.id == job.profileId }
PrintJobListItem(
id = job.id,
spotName = richSpot?.spot?.name ?: "Deleted spot",
cost = job.cost,
paperInfo = job.printSize.printSizeName,
isColor = profile?.grayscale != true,
statusColor = Color(0xFF1E88E5),
comment = job.comment,
copies = job.copies,
cover = job.sourceFiles?.firstOrNull()?.takeIf { File(it).exists() },
statusName = job.status.statusName.lowercase(),
accessCode = job.accessCode.orEmpty(),
createdAt = job.createdAt
)
}
emit(FlowResult.Success(result))
}
}
\ No newline at end of file
......@@ -6,23 +6,20 @@ import android.net.Uri
import androidx.compose.ui.unit.IntSize
import com.isidroid.c23.R
import com.isidroid.c23.SpotHasNoPrintProfilesException
import com.isidroid.rendering.model.RenderResult
import com.isidroid.c23.data.worker.job.SendJobWorker
import com.isidroid.c23.ext.transformToBitmapFiles
import com.isidroid.core.FlowResult
import com.isidroid.job.repository.JobRepository
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
import com.isidroid.rendering.model.RenderResult
import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.spot.model.PrintProfile
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.model.PrintProfile
import com.isidroid.spot.repository.ActiveSpotRepository
import com.isidroid.utils.getMimeType
import com.isidroid.utils.isMimeTypeImage
import com.isidroid.utils.isMimeTypePdf
import com.isidroid.utils.saveAsBitmapToFile
import com.isidroid.utils.savePdfAsBitmapFiles
import com.isidroid.utils.saveToFile
import com.isidroid.utils.toBitmap
import com.isidroid.utils.transformToBitmapFiles
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.io.File
import java.util.UUID
import javax.inject.Inject
......@@ -32,7 +29,8 @@ import javax.inject.Singleton
class RenderUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val renderRepository: RenderRepository,
private val spotRepository: ActiveSpotRepository
private val spotRepository: ActiveSpotRepository,
private val jobRepository: JobRepository
) {
fun loadSpot() = flow {
......@@ -108,5 +106,34 @@ class RenderUseCase @Inject constructor(
val files = uris.transformToBitmapFiles(context)
emit(FlowResult.Success(files))
}
fun createJob(
spotId: String?,
profileId: String?,
copies: Int,
@PrintSize printSize: Int,
@PrintOrientation orientation: Int,
sourceFiles: List<File>?
) = flow {
sourceFiles ?: throw IllegalStateException()
spotId ?: throw IllegalStateException()
profileId ?: throw IllegalStateException()
emit(FlowResult.Loading)
val job = jobRepository.createJob(
name = "",
spotId = spotId,
profileId = profileId,
copies = copies,
printSize = printSize,
printOrientation = orientation,
sourceFiles = sourceFiles,
clientName = ""
)
SendJobWorker.create(context)
emit(FlowResult.Success(null))
}
}
......@@ -12,13 +12,15 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.navArgument
import com.isidroid.c23.ui.navigation.Content
import com.isidroid.c23.ui.navigation.Map
import com.isidroid.c23.ui.navigation.PrintJobs
import com.isidroid.c23.ui.navigation.RenderPreview
val String?.isEdgeToEdge
get() = arrayOf<String>(
get() = arrayOf(
RenderPreview.route,
Content.route,
Map.route
Map.route,
PrintJobs.route
).any {
this?.contains(it) == true
}
......
package com.isidroid.c23.ext
import android.content.Context
import android.net.Uri
import com.isidroid.utils.getMimeType
import com.isidroid.utils.isMimeTypeImage
import com.isidroid.utils.isMimeTypePdf
import com.isidroid.utils.saveAsBitmapToFile
import com.isidroid.utils.savePdfAsBitmapFiles
import java.io.File
fun List<Uri>.transformToBitmapFiles(context: Context): List<File> {
val uris = this
val result = mutableListOf<File>()
uris.forEach { uri ->
val mime = uri.getMimeType(context)
val isImage = mime.isMimeTypeImage
val isPdf = mime.isMimeTypePdf
when {
isImage -> uri.saveAsBitmapToFile(context)?.also { result.add(it) }
isPdf -> uri.savePdfAsBitmapFiles(context)?.also { result.addAll(it) }
}
}
return result
}
\ No newline at end of file
package com.isidroid.sync_service.ext
import androidx.work.ListenableWorker
import androidx.work.WorkInfo
import androidx.work.WorkManager
internal fun WorkManager.isWorkerRunning(workerClass: Class<out ListenableWorker>): Boolean {
val data = getWorkInfosForUniqueWork(workerClass.name).get()
return data.any { it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED }
}
internal fun WorkManager.isWorkerPendingOrRunning(workerClass: Class<out ListenableWorker>): Boolean {
val data = getWorkInfosForUniqueWork(workerClass.name).get()
return data.any { it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED || it.state == WorkInfo.State.BLOCKED }
}
//internal fun Collection<SyncJob>.updateSyncJobsDataIds(vararg removeDataId: String) = mapNotNull { job ->
// val newDataList = job.data.split(",").filter { !removeDataId.contains(it) }
// if (newDataList.isEmpty()) null
// else {
// val data = newDataList.joinToString(",")
// job.copy(state = JobState.PENDING, id = UUID.randomUUID().toString(), data = data)
// }
//}
import android.Manifest
import android.content.Context
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.utils.hasPermission
import com.isidroid.utils.shouldShowRequestPermissionRationale
......
......@@ -42,7 +42,8 @@ fun ShortOfficeInfoComponent(
@StringRes titleRes: Int,
@StringRes buttonTextRes: Int,
bottomColor: Color = MaterialTheme.colorScheme.secondaryContainer,
paddingBottom: Dp? = null
paddingBottom: Dp? = null,
buttonDisabled: Boolean = false
) {
Column(modifier = modifier) {
Row(
......@@ -71,6 +72,7 @@ fun ShortOfficeInfoComponent(
Button(
shape = RoundedCornerShape(24.dp),
onClick = buttonClick,
enabled = !buttonDisabled
) {
Text(stringResource(id = buttonTextRes).uppercase())
}
......@@ -105,12 +107,13 @@ private fun footerText(title: String, officeName: String) = buildAnnotatedString
private fun FooterPreview() {
Surface {
ShortOfficeInfoComponent(
modifier = Modifier,
officeName = "Default Office Name",
modifier = Modifier,
containerClick = {},
bottomColor = MaterialTheme.colorScheme.secondaryContainer,
titleRes = R.string.app_name,
buttonTextRes = R.string.app_name,
bottomColor = MaterialTheme.colorScheme.secondaryContainer,
buttonDisabled = false,
)
}
}
\ No newline at end of file
package com.isidroid.c23.ui._component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft
......@@ -24,9 +25,11 @@ internal fun TopAppBarComponent(
modifier: Modifier = Modifier,
alpha: Float = 1f,
onNavigationClick: (() -> Unit)? = null,
colors: TopAppBarColors? = null
colors: TopAppBarColors? = null,
actions: @Composable RowScope.() -> Unit = {}
) {
CenterAlignedTopAppBar(
actions = actions,
modifier = modifier.height(64.dp),
title = {
Text(
......@@ -37,7 +40,8 @@ internal fun TopAppBarComponent(
)
},
colors = colors ?: TopAppBarDefaults.topAppBarColors().copy(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = alpha)),
colors = colors ?: TopAppBarDefaults.topAppBarColors()
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = alpha)),
navigationIcon = {
if (onNavigationClick != null)
Icon(Icons.AutoMirrored.Rounded.KeyboardArrowLeft, contentDescription = null, modifier = Modifier.clickable { onNavigationClick() })
......
......@@ -16,7 +16,10 @@ object Content: NavDirection {
override val route: String = "Content"
}
object Map: NavDirection {
override val route: String = "Map"
}
object PrintJobs: NavDirection {
override val route: String = "PrintJobs"
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ import com.isidroid.c23.ext.makeNavArgument
import com.isidroid.c23.ui.navigation.destinations.ContentScreenDestination
import com.isidroid.c23.ui.navigation.destinations.HomeScreenDestination
import com.isidroid.c23.ui.navigation.destinations.MapScreenDestination
import com.isidroid.c23.ui.navigation.destinations.PrintJobsScreenDestination
import com.isidroid.c23.ui.navigation.destinations.RenderScreenDestination
@Composable
......@@ -27,7 +28,8 @@ fun AppNavHost(
modifier = modifier,
) {
composable(route = Home.route) { HomeScreenDestination(navController) }
composable(route = Content.route) { ContentScreenDestination(navController) }
composable(route = routeSelectContent()) { ContentScreenDestination(navController) }
composable(route = routePrintJobs()) { PrintJobsScreenDestination(navController) }
composable(
route = routeRenderPreview(uris = "{${Argument.URI}}"),
......
......@@ -3,8 +3,8 @@ package com.isidroid.c23.ui.navigation
import androidx.core.net.toUri
import com.isidroid.c23.constant.Argument
internal fun routeSelectContent() = Content.route.toUri()
.toString()
internal fun routeSelectContent() = Content.route.toUri().toString()
internal fun routePrintJobs() = PrintJobs.route.toUri().toString()
internal fun routeRenderPreview(uris: String = Argument.URI) = RenderPreview.route.toUri().buildUpon()
.appendQueryParameter(Argument.URI, uris)
......
......@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.navigation.routeMap
import com.isidroid.c23.ui.navigation.routePrintJobs
import com.isidroid.c23.ui.navigation.routeRenderPreview
import com.isidroid.c23.ui.screen.content.ContentContract
import com.isidroid.c23.ui.screen.content.ContentScreen
......@@ -25,6 +26,7 @@ fun ContentScreenDestination(navController: NavHostController) {
is ContentContract.Effect.Navigation.ToRenderPreview -> navController.navigate(routeRenderPreview(uris = effect.uris))
is ContentContract.Effect.Navigation.ToMap -> navController.navigateSingleTopTo(routeMap(effect.lat?.toString(), effect.lng?.toString(), effect.spotId))
ContentContract.Effect.Navigation.ToBack -> navController.popBackStack()
ContentContract.Effect.Navigation.ToPrintJobList -> navController.navigate(routePrintJobs())
}
}
)
......
package com.isidroid.c23.ui.navigation.destinations
import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsContract
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsScreen
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsViewModel
@Composable
fun PrintJobsScreenDestination(navController: NavHostController) {
val viewModel: PrintJobsViewModel = hiltViewModel()
PrintJobsScreen(
state = viewModel.viewState,
effectFlow = viewModel.effect,
onEventSent = { event -> viewModel.setEvent(event) },
onNavigationRequested = { effect ->
when (effect) {
PrintJobsContract.Effect.Navigation.ToBack -> navController.popBackStack()
}
},
)
}
\ No newline at end of file
......@@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.isidroid.c23.ui.navigation.routeMap
import com.isidroid.c23.ui.navigation.routeSelectContent
import com.isidroid.c23.ui.screen.render_preview.RenderContract
import com.isidroid.c23.ui.screen.render_preview.RenderPreviewScreen
import com.isidroid.c23.ui.screen.render_preview.RenderViewModel
import com.isidroid.core.ext.navigateSingleTopTo
@Composable
fun RenderScreenDestination(navController: NavController) {
......@@ -21,6 +23,7 @@ fun RenderScreenDestination(navController: NavController) {
when (effect) {
RenderContract.Effect.Navigation.ToBack -> navController.popBackStack()
RenderContract.Effect.Navigation.ToMap -> navController.navigate(routeMap())
RenderContract.Effect.Navigation.ToSelectContent -> navController.navigateSingleTopTo(routeSelectContent())
}
}
)
......
......@@ -8,10 +8,11 @@ import com.isidroid.spot.model.RichSpot
class ContentContract {
sealed interface Event : ViewEvent {
data class PickContent(val photo: Boolean = false, val documents: Boolean = false) : Event
data class PickContent(val photo: Boolean = false, val documents: Boolean = false, val word: Boolean = false) : Event
data class MultipleContents(val uris: List<Uri>) : Event
data class OpenMap(val lat: Double? = null, val lng: Double? = null, val spotCode: String? = null) : Event
data object GoBack: Event
data object ToPrintJobList: Event
}
sealed interface Effect : ViewSideEffect {
......@@ -19,12 +20,14 @@ class ContentContract {
data class ToRenderPreview(val uris: String) : Navigation
data class ToMap(val lat: Double? = null, val lng: Double? = null, val spotId: String? = null) : Navigation
data object ToBack: Navigation
data object ToPrintJobList: Navigation
}
}
data class State(
val galleryHash: String? = null,
val documentHash: String? = null,
val wordHash: String? = null,
val richSpot: RichSpot? = null
) : ViewState
......
......@@ -9,18 +9,20 @@ import androidx.compose.foundation.clickable
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.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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ShoppingCart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
......@@ -34,7 +36,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
......@@ -62,6 +63,9 @@ fun ContentScreen(
LaunchedEffect(state.value.galleryHash) { if (state.value.galleryHash != null) launcher.launch("image/*") }
LaunchedEffect(state.value.documentHash) { if (state.value.documentHash != null) launcher.launch("application/pdf") }
LaunchedEffect(state.value.wordHash) { if (state.value.wordHash != null) launcher.launch("application/msword") }
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
......@@ -75,32 +79,35 @@ fun ContentScreen(
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.select_content),
colors = TopAppBarDefaults.topAppBarColors(
// containerColor = MaterialTheme.colorScheme.primary,
// titleContentColor = MaterialTheme.colorScheme.onPrimary
)
colors = TopAppBarDefaults.topAppBarColors(),
actions = { TopBarActions(onEventSent) }
)
}
) { paddingValues ->
},
) { paddingValues ->
Column(modifier = Modifier.fillMaxSize()) {
Row(
LazyVerticalGrid(
columns = GridCells.Fixed(2),
Modifier
.padding(paddingValues)
.fillMaxWidth()
) {
BlockComponent(
text = stringResource(id = R.string.select_content_photos),
image = painterResource(id = R.drawable.content_type_photo),
onClick = { onEventSent(ContentContract.Event.PickContent(photo = true)) },
)
BlockComponent(
text = stringResource(id = R.string.select_content_doc),
image = painterResource(id = R.drawable.content_type_doc),
onClick = { onEventSent(ContentContract.Event.PickContent(documents = true)) },
)
}
item {
BlockComponent(
text = stringResource(id = R.string.select_content_photos),
image = painterResource(id = R.drawable.content_type_photo),
onClick = { onEventSent(ContentContract.Event.PickContent(photo = true)) },
)
}
item {
BlockComponent(
text = stringResource(id = R.string.select_content_doc),
image = painterResource(id = R.drawable.content_type_doc),
onClick = { onEventSent(ContentContract.Event.PickContent(documents = true)) },
)
}
}
Spacer(modifier = Modifier.weight(1f))
Footer(state, paddingValues, onEventSent)
......@@ -109,6 +116,12 @@ fun ContentScreen(
}
@Composable
private fun TopBarActions(onEventSent: (event: ContentContract.Event) -> Unit) {
Icon(Icons.Rounded.ShoppingCart, contentDescription = null,
modifier = Modifier.clickable { onEventSent(ContentContract.Event.ToPrintJobList) })
}
@Composable
private fun Footer(
state: State<ContentContract.State>,
paddingValues: PaddingValues,
......@@ -128,14 +141,14 @@ private fun Footer(
}
@Composable
private fun RowScope.BlockComponent(
private fun BlockComponent(
text: String,
image: Painter,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier
.weight(1f)
.fillMaxWidth()
.height(160.dp)
.padding(2.dp)
.clip(RoundedCornerShape(6.dp))
......@@ -168,17 +181,3 @@ private fun RowScope.BlockComponent(
}
}
}
@Composable
@Preview
private fun BlockPreview() {
Surface {
Row {
BlockComponent(
text = "Photos",
image = painterResource(id = R.drawable.content_type_photo),
onClick = { }
)
}
}
}
\ No newline at end of file
......@@ -30,11 +30,13 @@ class ContentViewModel @Inject constructor(
copy(
galleryHash = if (event.photo) UUID.randomUUID().toString() else null,
documentHash = if (event.documents) UUID.randomUUID().toString() else null,
wordHash = if (event.word) UUID.randomUUID().toString() else null,
)
}
is ContentContract.Event.OpenMap -> setEffect { ContentContract.Effect.Navigation.ToMap(event.lat, event.lng, event.spotCode) }
ContentContract.Event.GoBack -> setEffect { ContentContract.Effect.Navigation.ToBack }
ContentContract.Event.ToPrintJobList -> setEffect { ContentContract.Effect.Navigation.ToPrintJobList }
}
}
......@@ -42,7 +44,7 @@ class ContentViewModel @Inject constructor(
if (uris.isEmpty())
return
setState { copy(galleryHash = null, documentHash = null) }
setState { copy(galleryHash = null, documentHash = null, wordHash = null) }
setEffect { ContentContract.Effect.Navigation.ToRenderPreview(uris.joinToString()) }
}
......
......@@ -6,16 +6,12 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
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.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
......
package com.isidroid.c23.ui.screen.print_jobs
import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.c23.ui.screen.content.ContentContract
import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState
import com.isidroid.job.model.PrintJob
class PrintJobsContract {
sealed interface Event : ViewEvent {
data object ToBack : Event
}
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
data object ToBack : Navigation
}
}
data class State(
val loading: Boolean = false,
val jobs: List<PrintJobListItem>? = null
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.print_jobs
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.ui.print_job.PrintJobListItemComponent
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PrintJobsScreen(
state: State<PrintJobsContract.State>,
effectFlow: Flow<PrintJobsContract.Effect>?,
onEventSent: (event: PrintJobsContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: PrintJobsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is PrintJobsContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.print_job_list),
colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(PrintJobsContract.Event.ToBack) }
)
}
) { paddingValues ->
ListComponent(
state = state,
modifier = Modifier.padding(paddingValues)
)
}
}
@Composable
private fun ListComponent(state: State<PrintJobsContract.State>, modifier: Modifier) {
val list = state.value.jobs ?: return
LazyColumn(modifier) {
items(list) { item ->
PrintJobListItemComponent(
spotName = item.spotName,
cost = item.cost,
statusName = item.statusName,
cover = item.cover,
accessCode = item.accessCode,
statusColor = item.statusColor,
createdAt = item.createdAt,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.print_jobs
import androidx.lifecycle.viewModelScope
import com.isidroid.c23.domain.use_case.PrintJobsUseCase
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class PrintJobsViewModel @Inject constructor(
private val useCase: PrintJobsUseCase
) : BaseViewModel<PrintJobsContract.Event, PrintJobsContract.State, PrintJobsContract.Effect>() {
init {
viewModelScope.launch { load() }
}
override val isDebug: Boolean = isDebug()
override fun setInitialState(): PrintJobsContract.State = PrintJobsContract.State()
override suspend fun handleEvents(event: PrintJobsContract.Event) {
when (event) {
PrintJobsContract.Event.ToBack -> setEffect { PrintJobsContract.Effect.Navigation.ToBack }
}
}
// handle events
private suspend fun load() {
useCase.load()
.flowOn(Dispatchers.IO)
.catchTimber { }
.collect { res ->
when (res) {
FlowResult.Loading -> setState { copy(loading = true) }
is FlowResult.Success -> setState { copy(loading = false, jobs = res.result) }
}
}
}
// handle callbacks
}
\ No newline at end of file
......@@ -21,6 +21,7 @@ class RenderContract {
data object ToBack : Event
data object OpenMap : Event
data object OpenListProfiles : Event
data object SendPrintJob: Event
data class OnPageOpened(val page: Int, val containerSize: IntSize?) : Event
data class UpdateRenderSettings(
......@@ -34,7 +35,9 @@ class RenderContract {
}
sealed interface Effect : ViewSideEffect {
data object PrintJobCreated: Effect
sealed interface Navigation : Effect {
data object ToSelectContent: Navigation
data object ToMap : Navigation
data object ToBack : Navigation
}
......@@ -52,6 +55,8 @@ class RenderContract {
var richSpot: RichSpot? = null,
val spotHasNoPrintProfiles: Boolean = false,
val printProfilesSelector: List<PrintProfile>? = null,
val pageCount: Int = 0
val pageCount: Int = 0,
val error: Throwable? = null,
val disablePrintButton: Boolean = false
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview
import android.util.SparseArray
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
......@@ -23,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
......@@ -30,11 +32,11 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.rendering.model.RenderResult
import com.isidroid.c23.ui.screen.render_preview._component.PagerPreviewComponent
import com.isidroid.c23.ui.screen.render_preview._component.PaperInfoComponent
import com.isidroid.c23.ui.screen.render_preview._component.PreviewFooterComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintCopiesModalComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintOptionsComponent
import com.isidroid.c23.ui.screen.render_preview._component.PrintOrientationModalComponent
......@@ -56,6 +58,7 @@ fun RenderPreviewScreen(
modifier: Modifier = Modifier,
effectFlow: Flow<RenderContract.Effect>?
) {
val context = LocalContext.current
val openPrintSize = state.value.openPrintSize
val openOrientation = state.value.openOrientation
val copies = state.value.openCopies
......@@ -69,6 +72,7 @@ fun RenderPreviewScreen(
effectFlow?.collect { effect ->
when (effect) {
is RenderContract.Effect.Navigation -> onNavigationRequested(effect)
RenderContract.Effect.PrintJobCreated -> Toast.makeText(context, R.string.print_job_sending, Toast.LENGTH_SHORT).show()
}
}
}
......@@ -167,15 +171,11 @@ fun RenderPreviewScreen(
)
ShortOfficeInfoComponent(
officeName = "officeName",
PreviewFooterComponent(
state = state,
bottomColor = bottomColor,
modifier = Modifier
.fillMaxWidth()
.constrainAs(spotView) { bottom.linkTo(footer.top) },
containerClick = {},
titleRes = R.string.preview_footer_title,
buttonTextRes = R.string.action_print
modifier = Modifier.constrainAs(spotView) { bottom.linkTo(footer.top) },
onEventSent = onEventSent
)
Box(modifier = Modifier
......
......@@ -14,12 +14,15 @@ import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import com.isidroid.rendering.constant.PrintOrientation
import com.isidroid.rendering.constant.PrintSize
import com.isidroid.rendering.constant.orientationName
import com.isidroid.rendering.constant.printSizeName
import com.isidroid.spot.model.RichSpot
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
......@@ -59,7 +62,7 @@ class RenderViewModel @Inject constructor(
RenderContract.Event.ToBack -> goBack()
RenderContract.Event.OpenMap -> openMap()
RenderContract.Event.OpenListProfiles -> setState { copy(printProfilesSelector = _richSpot?.printProfiles) }
RenderContract.Event.SendPrintJob -> sendPrintJob()
is RenderContract.Event.OnPageOpened -> onPageOpened(event.page, event.containerSize)
is RenderContract.Event.UpdateRenderSettings -> updateRenderSettings(
......@@ -170,6 +173,27 @@ class RenderViewModel @Inject constructor(
updateRenderSettings(increaseCopy = null, orientation = null, size = null, page = page)
}
private suspend fun sendPrintJob() {
val state = viewState.value
useCase.createJob(
spotId = _richSpot?.spot?.id,
profileId = state.printProfile?.id,
copies = state.renderSettings.copies,
printSize = state.renderSettings.printSize,
orientation = state.renderSettings.orientation,
sourceFiles = _sourceList
)
.flowOn(Dispatchers.IO)
.catchTimber { setState { copy(error = it) } }
.collect { res ->
when (res) {
FlowResult.Loading -> setState { copy(disablePrintButton = true) }
is FlowResult.Success -> onPrintJobCreated()
}
}
}
// callbacks
private suspend fun onSourceFileListReady(list: List<File>) {
_sourceList = list
......@@ -212,4 +236,9 @@ class RenderViewModel @Inject constructor(
if (isReset)
_renderResults.clear()
}
private fun onPrintJobCreated(){
setEffect { RenderContract.Effect.PrintJobCreated }
setEffect { RenderContract.Effect.Navigation.ToSelectContent }
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.render_preview._component
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui.screen.render_preview.RenderContract
@Composable
internal fun PreviewFooterComponent(
state: State<RenderContract.State>,
bottomColor: Color,
onEventSent: (event: RenderContract.Event) -> Unit,
modifier: Modifier,
) {
val stateValue = state.value
ShortOfficeInfoComponent(
officeName = stateValue.richSpot?.spot?.name.orEmpty(),
bottomColor = bottomColor,
modifier = modifier.fillMaxWidth(),
containerClick = {},
buttonClick = { onEventSent(RenderContract.Event.SendPrintJob) },
titleRes = R.string.preview_footer_title,
buttonTextRes = R.string.action_print,
buttonDisabled = stateValue.disablePrintButton
)
}
\ No newline at end of file
......@@ -34,4 +34,6 @@
<string name="appbar_print_preview">Print Preview</string>
<string name="error_spot_has_no_printing_profiles">You can\'t print in this Spot, please select another ones</string>
<string name="action_find_spot">Find spot</string>
<string name="print_job_sending">Print job is sending...</string>
<string name="print_job_list">My Print jobs</string>
</resources>
\ No newline at end of file
object BuildVersions {
const val MIN_SDK = 21
const val MIN_SDK = 26
const val COMPILE_SDK = 34
const val TARGET_SDK = 34
......@@ -13,7 +13,7 @@ object GoogleVersions {
const val activityCompose = "1.9.0"
const val appCompat = "1.7.0"
const val biometric = "1.1.0"
const val composeBom = "2024.05.00"
const val composeBom = "2024.06.00"
const val coreKtx = "1.13.1"
const val documentFile = "1.0.1"
const val firebaseBom = "32.8.0"
......@@ -21,22 +21,24 @@ object GoogleVersions {
const val hilt = "2.49"
const val hiltNavigationCompose = "1.2.0"
const val hiltWork = "1.0.0"
const val lifecycle = "2.8.1"
const val lifecycle = "2.8.2"
const val navigation = "2.7.0"
const val navigationCompose = "2.7.7"
const val preferences = "1.2.1"
const val room = "2.6.1"
const val roomCompiler = "2.5.1"
const val splash = "1.0.1"
const val work = "2.8.1"
const val work = "2.9.0"
const val services = "4.4.1"
const val constraint = "1.0.1"
const val paging = "3.2.1"
const val materialView = "1.11.0"
const val material = "1.6.7"
const val material = "1.6.8"
const val maps = "4.4.1"
const val location = "21.3.0"
const val mapUtils = "2.2.3"
const val work_hilt = "1.2.0"
}
object NetworkVersions {
......@@ -49,7 +51,7 @@ object NetworkVersions {
object FirebaseDependencies {
const val analytics = "com.google.firebase:firebase-analytics-ktx"
const val bom = "31.5.0"
const val bom = "33.1.0"
const val crashlytics = "com.google.firebase:firebase-crashlytics-ktx"
const val messaging = "com.google.firebase:firebase-messaging-ktx"
const val config = "com.google.firebase:firebase-config-ktx"
......
......@@ -61,6 +61,9 @@ class RepeatRule : TestRule {
}
}
val String?.isMethodPost get() = "post" == this?.lowercase()
val String?.isMethodPut get() = "put" == this?.lowercase()
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS)
annotation class RepeatTest(val value: Int = 1)
\ No newline at end of file
......@@ -6,12 +6,24 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.ParcelFileDescriptor
import timber.log.Timber
import java.io.File
fun Uri.savePdfAsBitmapFiles(context: Context): List<File>? {
val descriptor = context.contentResolver.openFileDescriptor(this, "r") ?: return null
val renderer = PdfRenderer(descriptor)
return descriptor.toBitmapList()
}
fun File.savePdfAsBitmapFiles(): List<File> {
Timber.i("==> file=$absolutePath, length=${length()}")
val descriptor = ParcelFileDescriptor.open(this, ParcelFileDescriptor.MODE_READ_ONLY)
return descriptor.toBitmapList()
}
private fun ParcelFileDescriptor.toBitmapList(): List<File> {
val renderer = PdfRenderer(this)
val result = (0 until renderer.pageCount).mapNotNull { pageNumber ->
val page = renderer.openPage(pageNumber)
......@@ -20,7 +32,6 @@ fun Uri.savePdfAsBitmapFiles(context: Context): List<File>? {
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)
bitmap.saveToTempFile()
} catch (t: Throwable) {
......@@ -31,7 +42,7 @@ fun Uri.savePdfAsBitmapFiles(context: Context): List<File>? {
}
}
descriptor.close()
close()
renderer.close()
return result
......
......@@ -6,7 +6,10 @@ import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
......@@ -48,6 +51,47 @@ fun Uri.getDate(context: Context): Date {
return date
}
fun Uri.saveUriToInternalStorage(context: Context): File? {
try {
val extension = getFileExtensionFromUri(context)
val inputStream: InputStream? = context.contentResolver.openInputStream(this)
inputStream?.use { input ->
val tempFile = File.createTempFile(UUID.randomUUID().toString(), ".${extension}", context.cacheDir)
FileOutputStream(tempFile).use { output ->
val buffer = ByteArray(4 * 1024)
var read: Int
while (input.read(buffer).also { read = it } != -1) {
output.write(buffer, 0, read)
}
output.flush()
}
return tempFile
}
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
fun Uri.getFileExtensionFromUri(context: Context): String? {
var extension: String? = null
val mimeType = context.contentResolver.getType(this)
if (mimeType != null) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
}
if (extension.isNullOrEmpty()) {
val path = path
if (path != null) {
extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(path)).toString())
}
}
return extension
}
fun Uri.getMimeType(context: Context): String? {
var mimeType: String?
mimeType = context.contentResolver.getType(this)
......@@ -60,7 +104,7 @@ fun Uri.getMimeType(context: Context): String? {
return mimeType
}
private val Uri.fileExtension: String?
val Uri.fileExtension: String?
get() {
val path = path ?: return null
val extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(path)).toString())
......@@ -75,7 +119,7 @@ fun Uri.saveAsBitmapToFile(context: Context): File? {
return bitmap.saveToTempFile()
}
internal fun Bitmap.saveToTempFile(): File {
fun Bitmap.saveToTempFile(): File {
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
saveToFile(file)
recycle()
......@@ -83,19 +127,3 @@ internal fun Bitmap.saveToTempFile(): File {
return file
}
fun List<Uri>.transformToBitmapFiles(context: Context): List<File> {
val uris = this
val result = mutableListOf<File>()
uris.forEach { uri ->
val mime = uri.getMimeType(context)
val isImage = mime.isMimeTypeImage
val isPdf = mime.isMimeTypePdf
when {
isImage -> uri.saveAsBitmapToFile(context)?.also { result.add(it) }
isPdf -> uri.savePdfAsBitmapFiles(context)?.also { result.addAll(it) }
}
}
return result
}
\ No newline at end of file
/build
\ No newline at end of file
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.isidroid.job"
compileSdk = BuildVersions.COMPILE_SDK
defaultConfig {
minSdk = BuildVersions.MIN_SDK
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
api(project(":core:network"))
api(project((":core:core")))
api(project((":core:utils")))
testApi(project((":core:test_utils")))
api("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
api("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
api("com.google.dagger:hilt-android:${GoogleVersions.hilt}")
api("androidx.work:work-runtime-ktx:${GoogleVersions.work}")
api("androidx.hilt:hilt-work:${GoogleVersions.work_hilt}")
api("androidx.room:room-runtime:${GoogleVersions.room}")
ksp("androidx.hilt:hilt-compiler:${GoogleVersions.work_hilt}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
// Firebase
api(platform("com.google.firebase:firebase-bom:${FirebaseDependencies.bom}"))
api("com.google.firebase:firebase-crashlytics-ktx")
// tools
api("com.jakewharton.timber:timber:${ToolsVersions.timber}")
testImplementation("junit:junit:${TestVersions.junit}")
androidTestImplementation("androidx.test.ext:junit:${TestVersions.junitExt}")
androidTestImplementation("androidx.test.espresso:espresso-core:${TestVersions.espressoCore}")
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.isidroid.job
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.isidroid.job_upload.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package com.isidroid.job
class CreateJobException(m: String? = null): Throwable(m)
\ No newline at end of file
package com.isidroid.job.constant
import androidx.annotation.IntDef
@Retention(AnnotationRetention.SOURCE)
@IntDef
annotation class JobStatus {
companion object {
const val CREATED = 1
const val CANCELED = 2
const val READY = 3
const val ACCEPTED = 4
const val REJECTED = 5
const val PRINTING = 6
const val COMPLETED = 7
const val ERROR = 8
}
}
@JobStatus
val Int.statusName
get() = when (this) {
JobStatus.CREATED -> "CREATED"
JobStatus.CANCELED -> "CANCELED"
JobStatus.READY -> "READY"
JobStatus.ACCEPTED -> "ACCEPTED"
JobStatus.REJECTED -> "REJECTED"
JobStatus.PRINTING -> "PRINTING"
JobStatus.COMPLETED -> "COMPLETED"
JobStatus.ERROR -> "ERROR"
else -> "Unknown"
}
\ No newline at end of file
package com.isidroid.job.data.mapper
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.data.source.remote.api.response.CreateJobResponse
import com.isidroid.job.model.PrintJob
internal fun CreateJobResponse.transform() = PrintJob(
id = id,
accessCode = accessCode,
accessToken = accessToken,
spotId = spotId,
profileId = profileId,
status = 0,
createdAt = created,
)
private fun String.transformToStatus() = when (this) {
"New" -> JobStatus.CREATED
"Canceled" -> JobStatus.CANCELED
"Ready" -> JobStatus.READY
"Accepted" -> JobStatus.ACCEPTED
"Rejected" -> JobStatus.REJECTED
"Printing" -> JobStatus.PRINTING
"Completed" -> JobStatus.COMPLETED
"Error" -> JobStatus.ERROR
else -> -1
}
package com.isidroid.job.data.source.local
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.model.PrintJob
@Dao
interface JobDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg printJob: PrintJob)
@Delete
fun delete(vararg printJob: PrintJob)
@Query("SELECT * FROM PrintJob WHERE (:spotId IS NULL OR spotId = :spotId) OR (status IS NULL OR status = :status) ORDER BY createdAt DESC")
fun find(
spotId: String? = null,
@JobStatus status: Int? = null
): List<PrintJob>
}
\ No newline at end of file
package com.isidroid.job.data.source.local
import com.isidroid.job.model.PrintJob
internal class JobLocalSource(private val dao: JobDao) : JobDao {
override fun insert(vararg printJob: PrintJob) = dao.insert(*printJob)
override fun delete(vararg printJob: PrintJob) = dao.delete(*printJob)
override fun find(spotId: String?, status: Int?) = dao.find(spotId, status)
}
\ No newline at end of file
package com.isidroid.job.data.source.remote
import com.isidroid.job.data.mapper.transform
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.data.source.remote.api.request.CreateJobRequest
import com.isidroid.job.model.PrintJob
internal class JobNetworkSource(private val api: ApiJob) {
fun createJob(spotId: String, profileId: String, clientName: String): PrintJob? {
val response = api.createJob(CreateJobRequest(spotId, profileId, senderName = clientName)).execute()
return response.body()?.transform()
}
}
\ No newline at end of file
package com.isidroid.job.data.source.remote
import android.content.Context
import com.isidroid.network.ext.createMockResponse
import okhttp3.Interceptor
import okhttp3.Response
internal class MockInterceptor(
private val context: Context,
private val isMock: Boolean
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (!isMock)
return chain.proceed(chain.request())
return createMockResponse(context, chain.request())
}
}
\ No newline at end of file
package com.isidroid.job.data.source.remote.api
import com.isidroid.job.data.source.remote.api.request.CreateJobRequest
import com.isidroid.job.data.source.remote.api.response.CreateJobResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
internal interface ApiJob {
@POST("api/job")
fun createJob(@Body request: CreateJobRequest): Call<CreateJobResponse>
}
\ No newline at end of file
package com.isidroid.job.data.source.remote.api.request
import com.google.gson.annotations.SerializedName
internal data class CreateJobRequest(
@SerializedName("spot_id") val spotId: String,
@SerializedName("profile_id") val profileId: String,
@SerializedName("sender_name") val senderName: String,
)
\ No newline at end of file
package com.isidroid.job.data.source.remote.api.response
import com.google.gson.annotations.SerializedName
import java.io.Serial
import java.util.Date
internal data class CreateJobResponse(
@SerializedName("id") val id: String,
@SerializedName("access_code") val accessCode: String,
@SerializedName("access_token") val accessToken: String,
@SerializedName("spot_id") val spotId: String,
@SerializedName("profile_id") val profileId: String,
@SerializedName("sender_name") val senderName: String,
@SerializedName("pages") val pages: Int,
@SerializedName("status") val status: String,
@SerializedName("created") val created: Date,
)
package com.isidroid.job.di
import android.content.Context
import com.google.gson.Gson
import com.isidroid.core.DiDebuggableBuild
import com.isidroid.core.DiMock
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.MockInterceptor
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.repository.JobRepository
import com.isidroid.job.repository.JobRepositoryImpl
import com.isidroid.network.ApiCacheControlInterceptor
import com.isidroid.network.ApiServerUrl
import com.isidroid.network.ApiTokenInterceptor
import com.isidroid.network.createRetrofitApiClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor
import okhttp3.logging.HttpLoggingInterceptor
import javax.inject.Qualifier
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object JobModule {
@Provides @Singleton
fun provideJobNetworkSource(api: ApiJob) = JobNetworkSource(api)
@Provides @Singleton
fun provideJobLocalSource(dao: JobDao) = JobLocalSource(dao)
@Provides @Singleton
fun provideJobRepository(
@ApplicationContext context: Context,
jobNetworkSource: JobNetworkSource,
jobLocalSource: JobLocalSource,
): JobRepository = JobRepositoryImpl(context, jobNetworkSource, jobLocalSource)
@Provides @DiJobMockInterceptor
fun provideMockInterceptor(
@ApplicationContext context: Context,
@DiMock isMock: Boolean
): Interceptor = MockInterceptor(context, isMock)
@Singleton @Provides
fun provideApiSpot(
@ApiServerUrl serverUrl: String,
@DiDebuggableBuild isDebugBuild: Boolean,
@ApiTokenInterceptor authInterceptor: Interceptor,
@DiJobMockInterceptor mockInterceptor: Interceptor,
@ApiCacheControlInterceptor cacheInterceptor: Interceptor,
gson: Gson
) = createRetrofitApiClient(
baseUrl = serverUrl,
cl = ApiJob::class.java,
logLevel = HttpLoggingInterceptor.Level.BODY,
gson = gson,
isDebugBuild = isDebugBuild,
interceptors = listOf(authInterceptor, mockInterceptor, cacheInterceptor)
)
}
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
internal annotation class DiJobMockInterceptor
\ No newline at end of file
package com.isidroid.job.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.isidroid.job.constant.JobStatus
import java.util.Date
import java.util.UUID
@Entity(indices = [Index("spotId"), Index("status")])
data class PrintJob(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val spotId: String,
val cost: Float = 0f,
val profileId: String,
val printSize: Int = -1,
val printOrientation: Int = -1,
val copies: Int = 1,
@JobStatus val status: Int,
val name: String = "",
val errorMessage: String? = null,
val comment: String? = null,
val createdAt: Date = Date(),
val updatedAt: Date = Date(),
val accessCode: String? = null,
val accessToken: String? = null,
val sourceFiles: List<String>? = null
)
\ No newline at end of file
package com.isidroid.job.repository
import com.isidroid.job.model.PrintJob
import java.io.File
interface JobRepository {
suspend fun createJob(
name: String,
clientName: String,
spotId: String,
profileId: String,
printSize: Int,
printOrientation: Int,
copies: Int? = 1,
sourceFiles: List<File>
): PrintJob
suspend fun readLocalList(): List<PrintJob>
}
\ No newline at end of file
package com.isidroid.job.repository
import android.content.Context
import com.isidroid.job.CreateJobException
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.model.PrintJob
import java.io.File
internal class JobRepositoryImpl(
private val context: Context,
private val jobNetworkSource: JobNetworkSource,
private val jobLocalSource: JobLocalSource,
) : JobRepository {
override suspend fun createJob(
name: String,
clientName: String,
spotId: String,
profileId: String,
printSize: Int,
printOrientation: Int,
copies: Int?,
sourceFiles: List<File>
): PrintJob {
val job = jobNetworkSource.createJob(spotId, profileId, clientName)
?.copy(printSize = printSize, printOrientation = printOrientation, sourceFiles = sourceFiles.map { it.absolutePath })
?: throw CreateJobException()
jobLocalSource.insert(job)
return job
}
override suspend fun readLocalList(): List<PrintJob> {
return jobLocalSource.find()
}
}
\ No newline at end of file
package com.isidroid.job
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.repository.JobRepository
import com.isidroid.job.repository.JobRepositoryImpl
import com.isidroid.test_utils.MockWebServerWrapper
import com.isidroid.test_utils.createApi
import com.isidroid.test_utils.isMethodPost
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class TestPrintJob {
private lateinit var api: ApiJob
private lateinit var localSource: JobLocalSource
private lateinit var networkSource: JobNetworkSource
private lateinit var jobRepository: JobRepositoryImpl
private lateinit var mockContext: Context
private lateinit var mockWebServer: MockWebServer
private val mockWrapper = MockWebServerWrapper { path, method, body ->
when {
path == "/api/job" && method.isMethodPost -> "create_job"
else -> ""
}
}
@Before
fun setup() {
val jobDao = mock(JobDao::class.java)
mockContext = InstrumentationRegistry.getInstrumentation().targetContext
mockWebServer = mockWrapper.create()
api = createApi(mockWebServer, ApiJob::class.java)
networkSource = JobNetworkSource(api)
localSource = JobLocalSource(jobDao)
jobRepository = JobRepositoryImpl(mockContext, networkSource, localSource)
}
@After
fun shutdown() {
mockWebServer.shutdown()
}
@Test
fun createPrintJob() = runBlocking {
val job = jobRepository.createJob(
name = "Hello world",
clientName = "Client Name",
printOrientation = 1,
printSize = 1,
profileId = "profileId",
sourceFiles = emptyList(),
spotId = "spotId"
)
Assert.assertEquals("Invalid response parsing", "27", job.id)
}
}
\ No newline at end of file
{
"spot_id": "1",
"profile_id": "1",
"sender_name": "",
"access_code": "457284",
"access_token": "023GJC9600OZA9R6BBML",
"pages": 0,
"status": "New",
"created": "2024-06-21T08:45:19.646Z",
"id": 27
}
\ No newline at end of file
/build
\ No newline at end of file
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.isidroid.ms_office"
compileSdk = BuildVersions.COMPILE_SDK
defaultConfig {
minSdk = BuildVersions.MIN_SDK
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
api(project(mapOf("path" to ":core:utils")))
api("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
api("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
api("androidx.documentfile:documentfile:${GoogleVersions.documentFile}")
// tools
api("com.jakewharton.timber:timber:${ToolsVersions.timber}")
api("org.apache.poi:poi:5.2.3")
api("org.apache.poi:poi-ooxml:5.2.3")
api("org.apache.poi:poi-scratchpad:5.2.3")
api("com.itextpdf:itext7-core:7.2.0")
testImplementation("junit:junit:${TestVersions.junit}")
androidTestImplementation("androidx.test.ext:junit:${TestVersions.junitExt}")
androidTestImplementation("androidx.test.espresso:espresso-core:${TestVersions.espressoCore}")
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.isidroid.ms_office
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.isidroid.ms_office.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package com.isidroid.ms_office
import com.itextpdf.kernel.pdf.PdfDocument
import com.itextpdf.kernel.pdf.PdfWriter
import com.itextpdf.layout.Document
import com.itextpdf.layout.element.Paragraph
import org.apache.poi.hwpf.HWPFDocument
import org.apache.poi.hwpf.extractor.WordExtractor
import org.apache.poi.openxml4j.opc.OPCPackage
import org.apache.poi.xwpf.usermodel.XWPFDocument
import org.apache.poi.xwpf.usermodel.XWPFParagraph
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
fun convertDocToPdf(docFile: File): File {
val pdfFile = File(docFile.parent, "${docFile.nameWithoutExtension}.pdf")
Timber.i("===> docFile=$docFile, exists=${docFile.exists()}")
// Read input DOC file
val doc = HWPFDocument(FileInputStream(docFile))
val extractor = WordExtractor(doc)
// Create output PDF file
val pdfWriter = PdfWriter(FileOutputStream(pdfFile))
val pdf = PdfDocument(pdfWriter)
val document = Document(pdf)
try {
// Extract text from DOC file
val text = extractor.text
// Split text into paragraphs
val paragraphs = text.split("\\\$\\{([^}]*)}")
// Add each paragraph to PDF document
for (paragraphText in paragraphs) {
val pdfParagraph = Paragraph(paragraphText)
document.add(pdfParagraph)
}
// Close the document
// document.close()
} catch (e: Exception) {
println("Error converting document to PDF: ${e.message}")
} finally {
// Close all resources
try {
// pdf.close()
} catch (e: Exception) {
println("Error closing PDF document: ${e.message}")
}
try {
// pdfWriter.close()
} catch (e: Exception) {
println("Error closing PDF writer: ${e.message}")
}
try {
// doc.close()
} catch (e: Exception) {
println("Error closing Word document: ${e.message}")
}
}
return pdfFile
}
\ No newline at end of file
package com.isidroid.ms_office
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
......@@ -10,4 +10,14 @@ annotation class PrintOrientation {
const val PORTRAIT = 1
const val LANDSCAPE = 2
}
}
\ No newline at end of file
}
@PrintOrientation val Int.orientationName
get() = when (this) {
PrintOrientation.AUTO -> "AUTO"
PrintOrientation.PORTRAIT -> "PORTRAIT"
PrintOrientation.LANDSCAPE -> "LANDSCAPE"
else -> "UNKNOWN"
}
......@@ -14,12 +14,12 @@ annotation class PrintSize {
}
@PrintSize val Int.name
@PrintSize val Int.printSizeName
get() = when (this) {
PrintSize.FILL_PAGE -> "FILL_PAGE"
PrintSize.PAPER_ORIGINAL_PAGE -> "FILL_PAGE"
PrintSize.FIT_TO_PAPER -> "FILL_PAGE"
PrintSize.FIT_TO_PRINTABLE_AREA -> "FILL_PAGE"
PrintSize.PAPER_ORIGINAL_PAGE -> "PAPER_ORIGINAL_PAGE"
PrintSize.FIT_TO_PAPER -> "FIT_TO_PAPER"
PrintSize.FIT_TO_PRINTABLE_AREA -> "FIT_TO_PRINTABLE_AREA"
else -> "UNKNOWN"
}
......@@ -5,7 +5,7 @@ import com.isidroid.network.ext.createMockResponse
import okhttp3.Interceptor
import okhttp3.Response
class MockInterceptor(
internal class MockInterceptor(
private val context: Context,
private val isMock: Boolean
) : Interceptor {
......
......@@ -83,4 +83,4 @@ internal object SpotModule {
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER)
annotation class DiSpotMockInterceptor
\ No newline at end of file
internal annotation class DiSpotMockInterceptor
\ No newline at end of file
......@@ -27,3 +27,6 @@ include(":ui:compose_components")
include(":feature:spot")
include(":library:location")
include(":ui:maps")
include(":feature:ms_office")
include(":feature:job")
include(":ui:print_job")
......@@ -43,13 +43,13 @@ dependencies {
api("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
// compose
implementation(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.constraintlayout:constraintlayout-compose:${GoogleVersions.constraint}")
implementation("androidx.hilt:hilt-navigation-compose:${GoogleVersions.hiltNavigationCompose}")
api(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
api("androidx.compose.ui:ui")
api("androidx.compose.ui:ui-graphics")
api("androidx.compose.ui:ui-tooling-preview")
api("androidx.compose.material3:material3")
api("androidx.constraintlayout:constraintlayout-compose:${GoogleVersions.constraint}")
api("androidx.hilt:hilt-navigation-compose:${GoogleVersions.hiltNavigationCompose}")
api("io.coil-kt:coil-compose:${ToolsVersions.coil}")
......
/build
\ No newline at end of file
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.isidroid.ui.print_job"
compileSdk = BuildVersions.COMPILE_SDK
defaultConfig {
minSdk = BuildVersions.MIN_SDK
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = BuildVersions.KOTLIN_COMPILER_EXT_VERSION
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
api(project((":ui:compose_components")))
api("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
api("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
// compose
api(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
api("androidx.compose.ui:ui")
api("androidx.compose.ui:ui-graphics")
api("androidx.compose.ui:ui-tooling-preview")
api("androidx.compose.material3:material3")
api("androidx.constraintlayout:constraintlayout-compose:${GoogleVersions.constraint}")
// tools
api("com.jakewharton.timber:timber:${ToolsVersions.timber}")
api("com.airbnb.android:lottie-compose:${ToolsVersions.lottie}")
// testing
testImplementation("junit:junit:${TestVersions.junit}")
androidTestImplementation("androidx.test.ext:junit:${TestVersions.junitExt}")
androidTestImplementation("androidx.test.espresso:espresso-core:${TestVersions.espressoCore}")
androidTestImplementation(platform("androidx.compose:compose-bom:${GoogleVersions.composeBom}"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.isidroid.ui.print_job
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.isidroid.ui.print_job.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package com.isidroid.ui.print_job
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.isidroid.ui.print_job._components.CardInfoComponent
import com.isidroid.ui.print_job._components.ListCardImageComponent
import com.isidroid.utils.asCost
import java.util.Date
@Composable
fun PrintJobListItemComponent(
cost: Float,
accessCode: String,
spotName: String,
cover: String?,
statusName: String,
statusColor: Color,
createdAt: Date,
modifier: Modifier = Modifier,
clickOnSpot: () -> Unit = {},
clickOnCard: () -> Unit = {},
) {
ElevatedCard(
onClick = clickOnCard,
modifier = modifier.padding(vertical = 4.dp),
colors = CardDefaults.elevatedCardColors()
) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
val (image, titleView, statusView, costView, accessCodeView) = createRefs()
ListCardImageComponent(cover = cover,
modifier = Modifier.constrainAs(image) {
start.linkTo(parent.start)
top.linkTo(parent.top)
})
Text(
text = spotName,
fontSize = 16.sp,
fontWeight = FontWeight.W600,
modifier = Modifier
.clickable { clickOnSpot() }
.constrainAs(titleView) {
start.linkTo(image.end, 8.dp)
end.linkTo(costView.start, 8.dp)
top.linkTo(image.top, 4.dp)
width = Dimension.fillToConstraints
}
)
CardInfoComponent(
code = accessCode,
createdAt = createdAt,
modifier = Modifier.constrainAs(accessCodeView) {
linkTo(start = titleView.start, end = titleView.end)
top.linkTo(titleView.bottom, 8.dp)
width = Dimension.fillToConstraints
}
)
Text(
text = cost.asCost(),
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
fontFamily = FontFamily.Monospace,
modifier = Modifier.constrainAs(costView) {
top.linkTo(parent.top, 8.dp)
end.linkTo(parent.end)
}
)
Text(
text = statusName,
fontSize = 11.sp,
fontWeight = FontWeight.W200,
letterSpacing = .76.sp,
color = Color.White,
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(statusColor)
.padding(horizontal = 6.dp, vertical = 2.dp)
.constrainAs(statusView) {
linkTo(start = costView.start, end = costView.end)
top.linkTo(costView.bottom, 6.dp)
}
)
}
}
}
@Preview
@Composable
private fun ListCardTitleComponentPreview() {
Surface {
Column(modifier = Modifier.fillMaxWidth()) {
PrintJobListItemComponent(
cost = 12f,
accessCode = "123412",
spotName = "Office Print at Joey",
cover = null,
statusName = "Sending",
statusColor = Color(0xFF1E88E5),
createdAt = Date(),
)
PrintJobListItemComponent(
cost = 12f,
accessCode = "123412",
spotName = "Office Print at Joey",
cover = null,
statusName = "Complete",
statusColor = Color(0xFF1E88E5),
createdAt = Date(),
)
}
}
}
\ No newline at end of file
package com.isidroid.ui.print_job._components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DateRange
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.ui.print_job.R
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
internal fun CardInfoComponent(
code: String,
modifier: Modifier = Modifier,
createdAt: Date = Date(),
) {
val itemSpace = 6.dp
Column(modifier = modifier.fillMaxWidth()) {
CardInfoItemComponent(
text = stringResource(id = R.string.label_access_code, code),
icon = Icons.Rounded.Lock
)
CardInfoItemComponent(
text = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT).format(createdAt),
icon = Icons.Rounded.DateRange,
modifier = Modifier.padding(top = itemSpace)
)
}
}
@Composable
private fun CardInfoItemComponent(text: String, icon: ImageVector, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(16.dp))
Text(
text = text,
modifier = Modifier.padding(start = 4.dp),
fontSize = 14.sp,
fontWeight = FontWeight.W400
)
}
}
@Composable
@Preview
private fun AccessCodeComponentPreview() {
Surface {
CardInfoComponent(code = "123411")
}
}
\ No newline at end of file
package com.isidroid.ui.print_job._components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
internal fun ColorizedComponent(isColor: Boolean, modifier: Modifier) {
Card(
modifier = modifier,
shape = RoundedCornerShape(8.dp)
) {
if (isColor)
SquareRGBGradientComponent()
else
CircularHalfBlackHalfWhite()
}
}
@Composable
private fun CircularHalfBlackHalfWhite() {
Box(
modifier = Modifier
) {
Canvas(modifier = Modifier.fillMaxSize()) {
drawRectWithGradient(
startColor = Color.Black,
endColor = Color.White,
startX = 0f,
startY = 0f,
width = size.width,
height = size.height
)
}
}
}
@Composable
private fun SquareRGBGradientComponent() {
Box(
modifier = Modifier
) {
Canvas(modifier = Modifier.fillMaxSize()) {
// Draw Blue to Red gradient (right side)
drawRectWithGradient(
startColor = Color.Blue,
endColor = Color.Red,
startX = 0f,
startY = 0f,
width = size.width,
height = size.height
)
}
}
}
/**
* Helper function to draw a rectangle with a linear gradient from startColor to endColor.
*/
private fun DrawScope.drawRectWithGradient(
startColor: Color,
endColor: Color,
startX: Float,
startY: Float,
width: Float,
height: Float
) {
drawRect(
brush = androidx.compose.ui.graphics.Brush.linearGradient(
colors = listOf(startColor, endColor),
start = Offset(startX, startY),
end = Offset(startX + width, startY + height)
),
topLeft = Offset(startX, startY),
size = androidx.compose.ui.geometry.Size(width, height)
)
}
@Composable
@Preview
private fun ColorizedComponentPreview() {
Surface {
Column {
ColorizedComponent(isColor = true, modifier = Modifier.size(120.dp))
ColorizedComponent(isColor = false, modifier = Modifier.size(120.dp))
}
}
}
\ No newline at end of file
package com.isidroid.ui.print_job._components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
internal fun ListCardButtonComponent(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
Card(
modifier = modifier.size(24.dp),
shape = CircleShape,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
IconButton(
onClick = onClick,
modifier = Modifier
.border(BorderStroke(.5.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = .35f)), CircleShape)
.background(MaterialTheme.colorScheme.surface, CircleShape),
) {
Icon(
Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = "Open",
)
}
}
}
@Composable
@Preview
fun ListCardButtonComponentPreview() {
Surface {
// Box(Modifier.padding(100.dp)) {
ListCardButtonComponent(onClick = {})
// }
}
}
\ No newline at end of file
package com.isidroid.ui.print_job._components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
@Composable
internal fun ListCardImageComponent(
cover: String?,
modifier: Modifier
) {
Card(
modifier = modifier.size(72.dp),
shape = RoundedCornerShape(12.dp),
border = BorderStroke((.5).dp, Color.Gray.copy(alpha = .35f)),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
if (!cover.isNullOrEmpty())
ImageComponent(cover)
else
NoImageComponent()
}
}
@Composable
private fun ImageComponent(cover: String?) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(cover)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
@Composable
private fun NoImageComponent() {
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(8.dp),
border = BorderStroke((.5).dp, Color.Gray.copy(alpha = .35f)),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "No Preview",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .75f)
)
}
}
}
@Composable
@Preview
fun NoImageComponentPreview() {
Surface(modifier = Modifier.padding(12.dp)) {
Box(Modifier.padding(40.dp)) {
ListCardImageComponent(cover = null, modifier = Modifier.size(96.dp))
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label_access_code">Access code: %s</string>
</resources>
\ No newline at end of file
package com.isidroid.ui.print_job
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment