This commit is contained in:
2026-08-16 14:27:59 +02:00
parent 20c683370d
commit cd09e21230
6 changed files with 51 additions and 146 deletions
@@ -15,15 +15,9 @@
package com.bartek.iwentaryzacja
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -49,24 +43,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Scanner
// ==========================================
// 1. MODEL DANYCH MASZYNY
@@ -95,64 +80,60 @@ class InventoryViewModel : ViewModel() {
private val _notes = MutableStateFlow("")
val notes: StateFlow<String> = _notes.asStateFlow()
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null)
val updateInfo: StateFlow<UpdateInfo?> = _updateInfo.asStateFlow()
data class UpdateInfo(val versionCode: Int, val apkUrl: String)
fun checkForUpdates(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
fun loadState(context: Context) {
val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
_notes.value = prefs.getString("notes", "") ?: ""
val machinesJson = prefs.getString("machines", null)
if (machinesJson != null) {
try {
val url = URL("https://bbjsolution.org/AR/InwentARyzacja/src/branch/master/update.json")
val connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.readTimeout = 5000
if (connection.responseCode == 200) {
val scanner = Scanner(connection.inputStream).useDelimiter("\\A")
val result = if (scanner.hasNext()) scanner.next() else ""
val json = JSONObject(result)
val newVersionCode = json.getInt("versionCode")
val apkUrl = json.getString("url")
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
@Suppress("DEPRECATION")
val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode.toInt()
} else {
packageInfo.versionCode
}
if (newVersionCode > currentVersionCode) {
_updateInfo.value = UpdateInfo(newVersionCode, apkUrl)
}
val jsonArray = JSONArray(machinesJson)
val loadedList = mutableListOf<Machine>()
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
loadedList.add(
Machine(
id = obj.getString("id"),
name = obj.getString("name"),
category = obj.getString("category"),
originalStatus = obj.getString("originalStatus"),
isChecked = obj.getBoolean("isChecked")
)
)
}
_machines.value = loadedList
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun downloadAndInstall(context: Context, apkUrl: String) {
val request = DownloadManager.Request(Uri.parse(apkUrl))
.setTitle("Aktualizacja inwentARyzacja")
.setDescription("Pobieranie nowej wersji...")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk")
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.enqueue(request)
private fun saveState(context: Context) {
val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("notes", _notes.value)
val jsonArray = JSONArray()
_machines.value.forEach { machine ->
val obj = JSONObject()
obj.put("id", machine.id)
obj.put("name", machine.name)
obj.put("category", machine.category)
obj.put("originalStatus", machine.originalStatus)
obj.put("isChecked", machine.isChecked)
jsonArray.put(obj)
}
editor.putString("machines", jsonArray.toString())
editor.apply()
}
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
fun setNotes(newNotes: String) {
fun setNotes(context: Context, newNotes: String) {
_notes.value = newNotes
saveState(context)
}
fun appendDigit(digit: String) {
@@ -181,7 +162,7 @@ class InventoryViewModel : ViewModel() {
.sorted()
}
fun markMachinesByStatuses(statuses: Set<String>, isChecked: Boolean) {
fun markMachinesByStatuses(context: Context, statuses: Set<String>, isChecked: Boolean) {
_machines.value = _machines.value.map { machine ->
if (statuses.contains(machine.originalStatus)) {
machine.copy(isChecked = isChecked)
@@ -189,9 +170,10 @@ class InventoryViewModel : ViewModel() {
machine
}
}
saveState(context)
}
fun toggleMachineCheck(machineId: String) {
fun toggleMachineCheck(context: Context, machineId: String) {
_machines.value = _machines.value.map { machine ->
if (machine.id == machineId) {
machine.copy(isChecked = !machine.isChecked)
@@ -199,10 +181,11 @@ class InventoryViewModel : ViewModel() {
machine
}
}
saveState(context)
}
// Parser zoptymalizowany pod strukturę raportu z systemu
fun parseXlsFile(inputStream: InputStream) {
fun parseXlsFile(context: Context, inputStream: InputStream) {
try {
// WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx
val workbook = WorkbookFactory.create(inputStream)
@@ -232,6 +215,7 @@ class InventoryViewModel : ViewModel() {
}
_machines.value = parsedList
workbook.close()
saveState(context)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -242,38 +226,10 @@ class InventoryViewModel : ViewModel() {
// 3. GŁÓWNA AKTYWNOŚĆ
// ==========================================
class MainActivity : ComponentActivity() {
private val onDownloadComplete = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id != -1L) {
installApk(context)
}
}
}
private fun installApk(context: Context) {
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "update.apk")
if (file.exists()) {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
ContextCompat.registerReceiver(
this,
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
setContent {
MaterialTheme(
colorScheme = darkColorScheme(
@@ -291,10 +247,6 @@ class MainActivity : ComponentActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(onDownloadComplete)
}
}
// ==========================================
@@ -308,42 +260,12 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
val searchQuery by viewModel.searchQuery.collectAsState()
val filterMissing by viewModel.filterMissing.collectAsState()
val notes by viewModel.notes.collectAsState()
val updateInfo by viewModel.updateInfo.collectAsState()
var showStatusDialog by remember { mutableStateOf(false) }
var showNotesDialog by remember { mutableStateOf(false) }
var showUpdateDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.checkForUpdates(context)
}
LaunchedEffect(updateInfo) {
if (updateInfo != null) {
showUpdateDialog = true
}
}
if (showUpdateDialog && updateInfo != null) {
AlertDialog(
onDismissRequest = { showUpdateDialog = false },
title = { Text("Dostępna aktualizacja", color = Color.White) },
text = { Text("Czy chcesz pobrać i zainstalować nową wersję aplikacji?", color = Color.White) },
confirmButton = {
Button(onClick = {
viewModel.downloadAndInstall(context, updateInfo!!.apkUrl)
showUpdateDialog = false
}) {
Text("Pobierz")
}
},
dismissButton = {
TextButton(onClick = { showUpdateDialog = false }) {
Text("Później")
}
},
containerColor = Color(0xFF1E2638)
)
viewModel.loadState(context)
}
// Launcher akceptujący pliki .xlsx oraz .xls
@@ -352,7 +274,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
) { uri: Uri? ->
uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
viewModel.parseXlsFile(inputStream)
viewModel.parseXlsFile(context, inputStream)
}
}
}
@@ -362,7 +284,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
statuses = viewModel.getUniqueStatuses(),
onDismiss = { showStatusDialog = false },
onConfirm = { selectedStatuses ->
viewModel.markMachinesByStatuses(selectedStatuses, true)
viewModel.markMachinesByStatuses(context, selectedStatuses, true)
showStatusDialog = false
}
)
@@ -371,7 +293,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
if (showNotesDialog) {
NotesDialog(
notes = notes,
onNotesChange = { viewModel.setNotes(it) },
onNotesChange = { viewModel.setNotes(context, it) },
onDismiss = { showNotesDialog = false }
)
}
@@ -484,7 +406,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
items(filteredMachines, key = { it.id }) { machine ->
MachineItem(
machine = machine,
onToggle = { viewModel.toggleMachineCheck(machine.id) }
onToggle = { viewModel.toggleMachineCheck(context, machine.id) }
)
}
}