Update to Material3
This also adds a setting to change the URL for the scanner
This commit is contained in:
parent
c48b0e9440
commit
5941aaab38
14 changed files with 393 additions and 121 deletions
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="11" />
|
<bytecodeTargetLevel target="17" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -1,17 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="deploymentTargetDropDown">
|
|
||||||
<targetSelectedWithDropDown>
|
|
||||||
<Target>
|
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
|
||||||
<deviceKey>
|
|
||||||
<Key>
|
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
|
||||||
<value value="C:\Users\billy\.android\avd\Pixel_4_API_30.avd" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</targetSelectedWithDropDown>
|
|
||||||
<timeTargetWasSelectedWithDropDown value="2021-06-29T16:11:52.959392800Z" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -5,15 +5,15 @@
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="GRADLE" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -4,12 +4,13 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 30
|
namespace = "com.wbrawner.skerge"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.wbrawner.skerge"
|
applicationId = "com.wbrawner.skerge"
|
||||||
minSdk = 26
|
minSdk = 29
|
||||||
targetSdk = 30
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
|
@ -43,26 +44,30 @@ android {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String
|
kotlinCompilerExtensionVersion = "1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
|
||||||
implementation("androidx.core:core-ktx:1.5.0")
|
implementation(composeBom)
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("com.google.android.material:material:1.3.0")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.compose.ui:ui:${rootProject.extra["compose_version"]}")
|
implementation("com.google.android.material:material:1.10.0")
|
||||||
implementation("androidx.compose.material:material:${rootProject.extra["compose_version"]}")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-tooling:${rootProject.extra["compose_version"]}")
|
implementation("androidx.compose.material:material")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
|
implementation("androidx.compose.material3:material3")
|
||||||
implementation("androidx.activity:activity-compose:1.3.0-beta02")
|
implementation("androidx.compose.ui:ui-tooling")
|
||||||
val ktorVersion = "1.6.0"
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("androidx.activity:activity-compose:1.8.0")
|
||||||
|
val ktorVersion = "2.3.6"
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
implementation("io.ktor:ktor-client-android:$ktorVersion")
|
||||||
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
androidTestImplementation("io.ktor:ktor-client-mock:$ktorVersion")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.2")
|
androidTestImplementation(composeBom)
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:${rootProject.extra["compose_version"]}")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="com.wbrawner.skerge">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
@ -16,6 +15,7 @@
|
||||||
android:name="com.wbrawner.skerge.MainActivity"
|
android:name="com.wbrawner.skerge.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:windowSoftInputMode="adjustPan|adjustResize"
|
||||||
android:theme="@style/Theme.Skerge.NoActionBar">
|
android:theme="@style/Theme.Skerge.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
@ -5,99 +5,198 @@ import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.*
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.rememberSwipeableState
|
||||||
|
import androidx.compose.material.swipeable
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.core.view.WindowCompat
|
||||||
import com.wbrawner.skerge.ui.theme.SkergeTheme
|
import com.wbrawner.skerge.ui.theme.SkergeTheme
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModels {
|
||||||
private val scansDirectory: File by lazy { File(cacheDir, "scans") }
|
MainViewModel.Factory(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
setContent {
|
setContent {
|
||||||
|
val darkMode = isSystemInDarkTheme()
|
||||||
|
LaunchedEffect(darkMode) {
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
.isAppearanceLightNavigationBars = !darkMode
|
||||||
|
}
|
||||||
|
val pages by viewModel.pages.collectAsState(initial = emptyList())
|
||||||
|
val scannerUrl by viewModel.scannerUrl.collectAsState(initial = "")
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val (loading, setLoading) = remember { mutableStateOf(false) }
|
||||||
|
var pdfShareJob: Job? by remember { mutableStateOf(null) }
|
||||||
SkergeTheme {
|
SkergeTheme {
|
||||||
ScanScreen(
|
ScanScreen(
|
||||||
addButtonClicked = { viewModel.requestScan(scansDirectory) },
|
loading = loading,
|
||||||
shareButtonClicked = { sharePdf(viewModel.pages.replayCache.first()) },
|
setLoading = { loading ->
|
||||||
|
if (!loading) {
|
||||||
|
pdfShareJob?.cancel()
|
||||||
|
}
|
||||||
|
setLoading(loading)
|
||||||
|
},
|
||||||
|
scannerUrl = scannerUrl,
|
||||||
|
setScannerUrl = viewModel::setScannerUrl,
|
||||||
|
addButtonClicked = viewModel::requestScan,
|
||||||
|
shareButtonClicked = {
|
||||||
|
pdfShareJob = coroutineScope.launch {
|
||||||
|
delay(10_000)
|
||||||
|
val file = if (pages.size == 1) {
|
||||||
|
pages.first().file ?: return@launch
|
||||||
|
} else {
|
||||||
|
pages.merge()
|
||||||
|
}
|
||||||
|
startActivity(file.buildShareIntent(context))
|
||||||
|
}
|
||||||
|
},
|
||||||
removePage = viewModel::removePage,
|
removePage = viewModel::removePage,
|
||||||
pagesFlow = viewModel.pages
|
pages = pages
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sharePdf(pages: List<Page>) {
|
|
||||||
// TODO: Show loading dialog for this
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val file = if (pages.size == 1) {
|
|
||||||
pages.first().file ?: return@launch
|
|
||||||
} else {
|
|
||||||
pages.merge()
|
|
||||||
}
|
|
||||||
startActivity(file.buildShareIntent(this@MainActivity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ScanScreen(
|
fun ScanScreen(
|
||||||
|
loading: Boolean,
|
||||||
|
setLoading: (Boolean) -> Unit,
|
||||||
|
scannerUrl: String,
|
||||||
|
setScannerUrl: (String) -> Unit,
|
||||||
addButtonClicked: () -> Unit,
|
addButtonClicked: () -> Unit,
|
||||||
shareButtonClicked: () -> Unit,
|
shareButtonClicked: () -> Unit,
|
||||||
removePage: (Page) -> Unit,
|
removePage: (Page) -> Unit,
|
||||||
pagesFlow: SharedFlow<List<Page>>
|
pages: List<Page>
|
||||||
) {
|
) {
|
||||||
val pages = pagesFlow.collectAsState(initial = emptyList())
|
var showScannerUrlInput by remember { mutableStateOf(false) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
modifier = Modifier.statusBarsPadding(),
|
||||||
title = { Text("Skerge") },
|
title = { Text("Skerge") },
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = shareButtonClicked) {
|
IconButton(onClick = shareButtonClicked) {
|
||||||
Icon(imageVector = Icons.Default.Share, contentDescription = "Share")
|
Icon(imageVector = Icons.Default.Share, contentDescription = "Share")
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = { showScannerUrlInput = true }) {
|
||||||
|
Icon(imageVector = Icons.Default.Settings, contentDescription = "Settings")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (pages.value.none { it.file == null }) {
|
if (pages.none { it.file == null }) {
|
||||||
FloatingActionButton(onClick = addButtonClicked) {
|
FloatingActionButton(onClick = addButtonClicked) {
|
||||||
Icon(imageVector = Icons.Default.Add, contentDescription = "Add")
|
Icon(imageVector = Icons.Default.Add, contentDescription = "Add")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) { paddingValues ->
|
||||||
if (pages.value.isEmpty()) {
|
if (pages.isEmpty()) {
|
||||||
EmptyDocumentView()
|
EmptyDocumentView(modifier = Modifier.padding(paddingValues))
|
||||||
} else {
|
} else {
|
||||||
PageList(pages.value, removePage)
|
PageList(
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
pages = pages,
|
||||||
|
removePage = removePage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { setLoading(false) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { setLoading(false) }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text("Merging PDF...")
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else if (showScannerUrlInput) {
|
||||||
|
val (scannerInput, setScannerInput) = remember { mutableStateOf(scannerUrl) }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showScannerUrlInput = false },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showScannerUrlInput = false }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
setScannerUrl(scannerInput)
|
||||||
|
showScannerUrlInput = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = scannerInput,
|
||||||
|
onValueChange = setScannerInput,
|
||||||
|
label = {
|
||||||
|
Text("Scanner URL")
|
||||||
|
},
|
||||||
|
keyboardActions = KeyboardActions {
|
||||||
|
setScannerUrl(scannerInput)
|
||||||
|
showScannerUrlInput = false
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
autoCorrect = false,
|
||||||
|
capitalization = KeyboardCapitalization.None,
|
||||||
|
keyboardType = KeyboardType.Uri
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyDocumentView() {
|
fun EmptyDocumentView(modifier: Modifier) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
|
@ -113,8 +212,8 @@ fun EmptyDocumentView() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PageList(pages: List<Page>, removePage: (Page) -> Unit) {
|
fun PageList(pages: List<Page>, removePage: (Page) -> Unit, modifier: Modifier) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = modifier.fillMaxSize()) {
|
||||||
itemsIndexed(items = pages) { index, page ->
|
itemsIndexed(items = pages) { index, page ->
|
||||||
val topPadding = if (index == 0) 16.dp else 8.dp
|
val topPadding = if (index == 0) 16.dp else 8.dp
|
||||||
val bottomPadding = if (index == pages.size - 1) 16.dp else 8.dp
|
val bottomPadding = if (index == pages.size - 1) 16.dp else 8.dp
|
||||||
|
@ -130,6 +229,7 @@ fun PageList(pages: List<Page>, removePage: (Page) -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PagePreview(page: Page, removePage: (Page) -> Unit) {
|
fun PagePreview(page: Page, removePage: (Page) -> Unit) {
|
||||||
val (pageBitmap, setPageBitmap) = remember { mutableStateOf<Bitmap?>(null) }
|
val (pageBitmap, setPageBitmap) = remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
@ -137,11 +237,26 @@ fun PagePreview(page: Page, removePage: (Page) -> Unit) {
|
||||||
setPageBitmap(page.loadBitmap().first)
|
setPageBitmap(page.loadBitmap().first)
|
||||||
}
|
}
|
||||||
pageBitmap?.let {
|
pageBitmap?.let {
|
||||||
Image(
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
modifier = Modifier.fillMaxSize(),
|
Box(
|
||||||
bitmap = it.asImageBitmap(),
|
modifier = Modifier.fillMaxSize()
|
||||||
contentDescription = null
|
) {
|
||||||
)
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {
|
||||||
|
showMenu = true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||||
|
DropdownMenuItem(text = { Text("Remove") }, onClick = { removePage(page) })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?: page.error?.let {
|
?: page.error?.let {
|
||||||
Column(
|
Column(
|
||||||
|
@ -173,7 +288,7 @@ fun LoadingPage() {
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultPreview() {
|
fun DefaultPreview() {
|
||||||
SkergeTheme {
|
SkergeTheme {
|
||||||
ScanScreen({}, {}, {}, MutableSharedFlow())
|
ScanScreen(false, {}, "", {}, {}, {}, {}, emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package com.wbrawner.skerge
|
package com.wbrawner.skerge
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
@ -10,11 +14,21 @@ import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MainViewModel(private val scannerService: ScannerService = HpScannerService()) : ViewModel() {
|
class MainViewModel(
|
||||||
|
private val scannerService: ScannerService,
|
||||||
|
private val fileDirectory: File
|
||||||
|
) : ViewModel() {
|
||||||
private val _pages = MutableStateFlow<List<Page>>(emptyList())
|
private val _pages = MutableStateFlow<List<Page>>(emptyList())
|
||||||
val pages = _pages.asSharedFlow()
|
val pages = _pages.asSharedFlow()
|
||||||
|
private val _scannerUrl = MutableStateFlow(scannerService.url)
|
||||||
|
val scannerUrl = _scannerUrl.asSharedFlow()
|
||||||
|
|
||||||
fun requestScan(fileDirectory: File) {
|
fun setScannerUrl(url: String) {
|
||||||
|
scannerService.url = url
|
||||||
|
_scannerUrl.value = url
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestScan() {
|
||||||
val page = Page()
|
val page = Page()
|
||||||
_pages.value = _pages.value.toMutableList().apply {
|
_pages.value = _pages.value.toMutableList().apply {
|
||||||
add(page)
|
add(page)
|
||||||
|
@ -104,6 +118,20 @@ class MainViewModel(private val scannerService: ScannerService = HpScannerServic
|
||||||
}
|
}
|
||||||
_pages.value = updatedPages
|
_pages.value = updatedPages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Factory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(
|
||||||
|
modelClass: Class<T>,
|
||||||
|
extras: CreationExtras
|
||||||
|
): T {
|
||||||
|
val sharedPreferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
|
||||||
|
val scannerService = HpScannerService(sharedPreferences = sharedPreferences)
|
||||||
|
val fileDirectory = File(context.applicationContext.cacheDir, "scans")
|
||||||
|
return MainViewModel(scannerService, fileDirectory) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Page(
|
data class Page(
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
package com.wbrawner.skerge
|
package com.wbrawner.skerge
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.core.content.edit
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
|
import io.ktor.client.plugins.onDownload
|
||||||
|
import io.ktor.client.plugins.timeout
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.client.utils.*
|
import io.ktor.client.utils.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
interface ScannerService {
|
interface ScannerService {
|
||||||
|
var url: String
|
||||||
suspend fun requestScan(scanSettings: ScanSettings = ScanSettings()): String
|
suspend fun requestScan(scanSettings: ScanSettings = ScanSettings()): String
|
||||||
suspend fun getScanStatus(scanId: String): ScannerStatus
|
suspend fun getScanStatus(scanId: String): ScannerStatus
|
||||||
suspend fun downloadFile(
|
suspend fun downloadFile(
|
||||||
|
@ -21,20 +29,30 @@ interface ScannerService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: It would be cool to be able to autodiscover the printer(s) or manually enter the details
|
private const val KEY_SCANNER_URL = "scannerUrl"
|
||||||
const val SCANNER_URL = "http://brawner.print"
|
|
||||||
|
|
||||||
class HpScannerService(
|
class HpScannerService(
|
||||||
private val client: HttpClient = HttpClient(CIO) {
|
private val client: HttpClient = HttpClient(CIO) {
|
||||||
|
install(HttpTimeout) {
|
||||||
|
requestTimeoutMillis = TimeUnit.MINUTES.toMillis(2)
|
||||||
|
}
|
||||||
buildHeaders {
|
buildHeaders {
|
||||||
append("Content-Type", "text/xml")
|
append("Content-Type", "text/xml")
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
private val sharedPreferences: SharedPreferences
|
||||||
) : ScannerService {
|
) : ScannerService {
|
||||||
|
override var url: String
|
||||||
|
get() = sharedPreferences.getString(KEY_SCANNER_URL, null).orEmpty()
|
||||||
|
set(value) = sharedPreferences.edit { putString(KEY_SCANNER_URL, value) }
|
||||||
|
|
||||||
override suspend fun requestScan(scanSettings: ScanSettings): String {
|
override suspend fun requestScan(scanSettings: ScanSettings): String {
|
||||||
val url = URLBuilder(SCANNER_URL).path("eSCL", "ScanJobs").build()
|
val url = URLBuilder(url).run {
|
||||||
|
path("eSCL", "ScanJobs")
|
||||||
|
build()
|
||||||
|
}
|
||||||
val response: HttpResponse = client.post(url) {
|
val response: HttpResponse = client.post(url) {
|
||||||
body = scanSettings.toXml()
|
setBody(scanSettings.toXml())
|
||||||
}
|
}
|
||||||
val location = response.headers["Location"]
|
val location = response.headers["Location"]
|
||||||
?: throw IOException("Scanner didn't return location")
|
?: throw IOException("Scanner didn't return location")
|
||||||
|
@ -42,7 +60,13 @@ class HpScannerService(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getScanStatus(scanId: String): ScannerStatus = ScannerStatus.fromXml(
|
override suspend fun getScanStatus(scanId: String): ScannerStatus = ScannerStatus.fromXml(
|
||||||
client.get(URLBuilder(SCANNER_URL).path("eSCL", "ScannerStatus").build())
|
client.get(
|
||||||
|
URLBuilder(url)
|
||||||
|
.run {
|
||||||
|
path("eSCL", "ScannerStatus")
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
).body()
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun downloadFile(
|
override suspend fun downloadFile(
|
||||||
|
@ -51,13 +75,16 @@ class HpScannerService(
|
||||||
onProgress: (downloaded: Long, size: Long) -> Unit
|
onProgress: (downloaded: Long, size: Long) -> Unit
|
||||||
) {
|
) {
|
||||||
val httpResponse: HttpResponse = client.get(
|
val httpResponse: HttpResponse = client.get(
|
||||||
URLBuilder(SCANNER_URL).path("eSCL", "ScanJobs", uuid, "NextDocument").build()
|
URLBuilder(url).run {
|
||||||
|
path("eSCL", "ScanJobs", uuid, "NextDocument")
|
||||||
|
build()
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
onProgress(bytesSentTotal, contentLength)
|
onProgress(bytesSentTotal, contentLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val responseBody: ByteArray = httpResponse.receive()
|
val responseBody: ByteArray = httpResponse.body()
|
||||||
destination.writeBytes(responseBody)
|
destination.writeBytes(responseBody)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,67 @@
|
||||||
package com.wbrawner.skerge.ui.theme
|
package com.wbrawner.skerge.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val SkergeBlue = Color(0xFF0096d6)
|
val md_theme_light_primary = Color(0xFF006491)
|
||||||
|
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_primaryContainer = Color(0xFFC9E6FF)
|
||||||
|
val md_theme_light_onPrimaryContainer = Color(0xFF001E2F)
|
||||||
|
val md_theme_light_secondary = Color(0xFF4F606E)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_secondaryContainer = Color(0xFFD3E5F5)
|
||||||
|
val md_theme_light_onSecondaryContainer = Color(0xFF0C1D29)
|
||||||
|
val md_theme_light_tertiary = Color(0xFF64597C)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_tertiaryContainer = Color(0xFFEADDFF)
|
||||||
|
val md_theme_light_onTertiaryContainer = Color(0xFF201635)
|
||||||
|
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||||
|
val md_theme_light_background = Color(0xFFFCFCFF)
|
||||||
|
val md_theme_light_onBackground = Color(0xFF191C1E)
|
||||||
|
val md_theme_light_surface = Color(0xFFFCFCFF)
|
||||||
|
val md_theme_light_onSurface = Color(0xFF191C1E)
|
||||||
|
val md_theme_light_surfaceVariant = Color(0xFFDDE3EA)
|
||||||
|
val md_theme_light_onSurfaceVariant = Color(0xFF41474D)
|
||||||
|
val md_theme_light_outline = Color(0xFF71787E)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFF0F0F3)
|
||||||
|
val md_theme_light_inverseSurface = Color(0xFF2E3133)
|
||||||
|
val md_theme_light_inversePrimary = Color(0xFF8ACEFF)
|
||||||
|
val md_theme_light_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_light_surfaceTint = Color(0xFF006491)
|
||||||
|
val md_theme_light_outlineVariant = Color(0xFFC1C7CE)
|
||||||
|
val md_theme_light_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
val md_theme_dark_primary = Color(0xFF8ACEFF)
|
||||||
|
val md_theme_dark_onPrimary = Color(0xFF00344D)
|
||||||
|
val md_theme_dark_primaryContainer = Color(0xFF004C6E)
|
||||||
|
val md_theme_dark_onPrimaryContainer = Color(0xFFC9E6FF)
|
||||||
|
val md_theme_dark_secondary = Color(0xFFB7C9D9)
|
||||||
|
val md_theme_dark_onSecondary = Color(0xFF22323F)
|
||||||
|
val md_theme_dark_secondaryContainer = Color(0xFF384956)
|
||||||
|
val md_theme_dark_onSecondaryContainer = Color(0xFFD3E5F5)
|
||||||
|
val md_theme_dark_tertiary = Color(0xFFCEC0E8)
|
||||||
|
val md_theme_dark_onTertiary = Color(0xFF352B4B)
|
||||||
|
val md_theme_dark_tertiaryContainer = Color(0xFF4C4163)
|
||||||
|
val md_theme_dark_onTertiaryContainer = Color(0xFFEADDFF)
|
||||||
|
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||||
|
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||||
|
val md_theme_dark_onError = Color(0xFF690005)
|
||||||
|
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_dark_background = Color(0xFF191C1E)
|
||||||
|
val md_theme_dark_onBackground = Color(0xFFE2E2E5)
|
||||||
|
val md_theme_dark_surface = Color(0xFF191C1E)
|
||||||
|
val md_theme_dark_onSurface = Color(0xFFE2E2E5)
|
||||||
|
val md_theme_dark_surfaceVariant = Color(0xFF41474D)
|
||||||
|
val md_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE)
|
||||||
|
val md_theme_dark_outline = Color(0xFF8B9198)
|
||||||
|
val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
|
||||||
|
val md_theme_dark_inverseSurface = Color(0xFFE2E2E5)
|
||||||
|
val md_theme_dark_inversePrimary = Color(0xFF006491)
|
||||||
|
val md_theme_dark_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_dark_surfaceTint = Color(0xFF8ACEFF)
|
||||||
|
val md_theme_dark_outlineVariant = Color(0xFF41474D)
|
||||||
|
val md_theme_dark_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
|
||||||
|
val seed = Color(0xFF0096D6)
|
||||||
|
|
|
@ -1,35 +1,90 @@
|
||||||
package com.wbrawner.skerge.ui.theme
|
package com.wbrawner.skerge.ui.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material.darkColors
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.material.lightColors
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
private val DarkColorPalette = darkColors(
|
|
||||||
primary = SkergeBlue,
|
private val LightColors = lightColorScheme(
|
||||||
primaryVariant = SkergeBlue,
|
primary = md_theme_light_primary,
|
||||||
secondary = SkergeBlue
|
onPrimary = md_theme_light_onPrimary,
|
||||||
|
primaryContainer = md_theme_light_primaryContainer,
|
||||||
|
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||||
|
secondary = md_theme_light_secondary,
|
||||||
|
onSecondary = md_theme_light_onSecondary,
|
||||||
|
secondaryContainer = md_theme_light_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_light_tertiary,
|
||||||
|
onTertiary = md_theme_light_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
|
error = md_theme_light_error,
|
||||||
|
errorContainer = md_theme_light_errorContainer,
|
||||||
|
onError = md_theme_light_onError,
|
||||||
|
onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
|
background = md_theme_light_background,
|
||||||
|
onBackground = md_theme_light_onBackground,
|
||||||
|
surface = md_theme_light_surface,
|
||||||
|
onSurface = md_theme_light_onSurface,
|
||||||
|
surfaceVariant = md_theme_light_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||||
|
outline = md_theme_light_outline,
|
||||||
|
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_light_inverseSurface,
|
||||||
|
inversePrimary = md_theme_light_inversePrimary,
|
||||||
|
surfaceTint = md_theme_light_surfaceTint,
|
||||||
|
outlineVariant = md_theme_light_outlineVariant,
|
||||||
|
scrim = md_theme_light_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
|
||||||
primary = SkergeBlue,
|
private val DarkColors = darkColorScheme(
|
||||||
primaryVariant = SkergeBlue,
|
primary = md_theme_dark_primary,
|
||||||
secondary = SkergeBlue
|
onPrimary = md_theme_dark_onPrimary,
|
||||||
|
primaryContainer = md_theme_dark_primaryContainer,
|
||||||
|
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||||
|
secondary = md_theme_dark_secondary,
|
||||||
|
onSecondary = md_theme_dark_onSecondary,
|
||||||
|
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_dark_tertiary,
|
||||||
|
onTertiary = md_theme_dark_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||||
|
error = md_theme_dark_error,
|
||||||
|
errorContainer = md_theme_dark_errorContainer,
|
||||||
|
onError = md_theme_dark_onError,
|
||||||
|
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||||
|
background = md_theme_dark_background,
|
||||||
|
onBackground = md_theme_dark_onBackground,
|
||||||
|
surface = md_theme_dark_surface,
|
||||||
|
onSurface = md_theme_dark_onSurface,
|
||||||
|
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||||
|
outline = md_theme_dark_outline,
|
||||||
|
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_dark_inverseSurface,
|
||||||
|
inversePrimary = md_theme_dark_inversePrimary,
|
||||||
|
surfaceTint = md_theme_dark_surfaceTint,
|
||||||
|
outlineVariant = md_theme_dark_outlineVariant,
|
||||||
|
scrim = md_theme_dark_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SkergeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
fun SkergeTheme(
|
||||||
val colors = if (darkTheme) {
|
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
DarkColorPalette
|
content: @Composable() () -> Unit
|
||||||
} else {
|
) {
|
||||||
LightColorPalette
|
val colors = if (!useDarkTheme) {
|
||||||
}
|
LightColors
|
||||||
|
} else {
|
||||||
|
DarkColors
|
||||||
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colors = colors,
|
colorScheme = colors,
|
||||||
typography = Typography,
|
content = content
|
||||||
shapes = Shapes,
|
)
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
}
|
|
@ -5,13 +5,11 @@
|
||||||
<item name="colorPrimary">@color/skerge_blue</item>
|
<item name="colorPrimary">@color/skerge_blue</item>
|
||||||
<item name="colorPrimaryVariant">@color/skerge_blue</item>
|
<item name="colorPrimaryVariant">@color/skerge_blue</item>
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/skerge_blue</item>
|
<item name="colorSecondary">@color/skerge_blue</item>
|
||||||
<item name="colorSecondaryVariant">@color/skerge_blue</item>
|
<item name="colorSecondaryVariant">@color/skerge_blue</item>
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
<!-- Status bar color. -->
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Skerge.NoActionBar">
|
<style name="Theme.Skerge.NoActionBar">
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
val compose_version by extra("1.0.0-beta09")
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.0.0-beta05")
|
classpath("com.android.tools.build:gradle:8.1.2")
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
#Tue Jun 29 09:32:45 MDT 2021
|
#Tue Jun 29 09:32:45 MDT 2021
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
0
gradlew
vendored
Normal file → Executable file
0
gradlew
vendored
Normal file → Executable file
Loading…
Reference in a new issue