4 Commits
Author SHA1 Message Date
rapafun 3a34979173 Add index.html 2026-08-16 12:44:21 +00:00
rapafun b79a44f2b0 Merge remote-tracking branch 'main/master' 2026-08-16 14:28:12 +02:00
rapafun cd09e21230 ver 1.1 2026-08-16 14:27:59 +02:00
rapafun 20c683370d ikona 2026-08-15 19:38:00 +02:00
9 changed files with 178 additions and 146 deletions
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.bartek.iwentaryzacja",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}
Binary file not shown.
-11
View File
@@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -15,16 +14,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.IwentARyzacja"> android:theme="@style/Theme.IwentARyzacja">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -15,15 +15,9 @@
package com.bartek.iwentaryzacja package com.bartek.iwentaryzacja
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.apache.poi.ss.usermodel.WorkbookFactory import org.apache.poi.ss.usermodel.WorkbookFactory
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Scanner
// ========================================== // ==========================================
// 1. MODEL DANYCH MASZYNY // 1. MODEL DANYCH MASZYNY
@@ -95,64 +80,60 @@ class InventoryViewModel : ViewModel() {
private val _notes = MutableStateFlow("") private val _notes = MutableStateFlow("")
val notes: StateFlow<String> = _notes.asStateFlow() val notes: StateFlow<String> = _notes.asStateFlow()
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null) fun loadState(context: Context) {
val updateInfo: StateFlow<UpdateInfo?> = _updateInfo.asStateFlow() val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
_notes.value = prefs.getString("notes", "") ?: ""
data class UpdateInfo(val versionCode: Int, val apkUrl: String)
val machinesJson = prefs.getString("machines", null)
fun checkForUpdates(context: Context) { if (machinesJson != null) {
viewModelScope.launch(Dispatchers.IO) {
try { try {
val url = URL("https://bbjsolution.org/AR/InwentARyzacja/src/branch/master/update.json") val jsonArray = JSONArray(machinesJson)
val connection = url.openConnection() as HttpURLConnection val loadedList = mutableListOf<Machine>()
connection.connectTimeout = 5000 for (i in 0 until jsonArray.length()) {
connection.readTimeout = 5000 val obj = jsonArray.getJSONObject(i)
loadedList.add(
if (connection.responseCode == 200) { Machine(
val scanner = Scanner(connection.inputStream).useDelimiter("\\A") id = obj.getString("id"),
val result = if (scanner.hasNext()) scanner.next() else "" name = obj.getString("name"),
val json = JSONObject(result) category = obj.getString("category"),
originalStatus = obj.getString("originalStatus"),
val newVersionCode = json.getInt("versionCode") isChecked = obj.getBoolean("isChecked")
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)
}
} }
_machines.value = loadedList
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }
fun downloadAndInstall(context: Context, apkUrl: String) { private fun saveState(context: Context) {
val request = DownloadManager.Request(Uri.parse(apkUrl)) val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
.setTitle("Aktualizacja inwentARyzacja") val editor = prefs.edit()
.setDescription("Pobieranie nowej wersji...") editor.putString("notes", _notes.value)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk") val jsonArray = JSONArray()
.setAllowedOverMetered(true) _machines.value.forEach { machine ->
.setAllowedOverRoaming(true) val obj = JSONObject()
obj.put("id", machine.id)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager obj.put("name", machine.name)
downloadManager.enqueue(request) 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) { fun setSearchQuery(query: String) {
_searchQuery.value = query _searchQuery.value = query
} }
fun setNotes(newNotes: String) { fun setNotes(context: Context, newNotes: String) {
_notes.value = newNotes _notes.value = newNotes
saveState(context)
} }
fun appendDigit(digit: String) { fun appendDigit(digit: String) {
@@ -181,7 +162,7 @@ class InventoryViewModel : ViewModel() {
.sorted() .sorted()
} }
fun markMachinesByStatuses(statuses: Set<String>, isChecked: Boolean) { fun markMachinesByStatuses(context: Context, statuses: Set<String>, isChecked: Boolean) {
_machines.value = _machines.value.map { machine -> _machines.value = _machines.value.map { machine ->
if (statuses.contains(machine.originalStatus)) { if (statuses.contains(machine.originalStatus)) {
machine.copy(isChecked = isChecked) machine.copy(isChecked = isChecked)
@@ -189,9 +170,10 @@ class InventoryViewModel : ViewModel() {
machine machine
} }
} }
saveState(context)
} }
fun toggleMachineCheck(machineId: String) { fun toggleMachineCheck(context: Context, machineId: String) {
_machines.value = _machines.value.map { machine -> _machines.value = _machines.value.map { machine ->
if (machine.id == machineId) { if (machine.id == machineId) {
machine.copy(isChecked = !machine.isChecked) machine.copy(isChecked = !machine.isChecked)
@@ -199,10 +181,11 @@ class InventoryViewModel : ViewModel() {
machine machine
} }
} }
saveState(context)
} }
// Parser zoptymalizowany pod strukturę raportu z systemu // Parser zoptymalizowany pod strukturę raportu z systemu
fun parseXlsFile(inputStream: InputStream) { fun parseXlsFile(context: Context, inputStream: InputStream) {
try { try {
// WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx // WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx
val workbook = WorkbookFactory.create(inputStream) val workbook = WorkbookFactory.create(inputStream)
@@ -232,6 +215,7 @@ class InventoryViewModel : ViewModel() {
} }
_machines.value = parsedList _machines.value = parsedList
workbook.close() workbook.close()
saveState(context)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -242,38 +226,10 @@ class InventoryViewModel : ViewModel() {
// 3. GŁÓWNA AKTYWNOŚĆ // 3. GŁÓWNA AKTYWNOŚĆ
// ========================================== // ==========================================
class MainActivity : ComponentActivity() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
ContextCompat.registerReceiver(
this,
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
setContent { setContent {
MaterialTheme( MaterialTheme(
colorScheme = darkColorScheme( 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 searchQuery by viewModel.searchQuery.collectAsState()
val filterMissing by viewModel.filterMissing.collectAsState() val filterMissing by viewModel.filterMissing.collectAsState()
val notes by viewModel.notes.collectAsState() val notes by viewModel.notes.collectAsState()
val updateInfo by viewModel.updateInfo.collectAsState()
var showStatusDialog by remember { mutableStateOf(false) } var showStatusDialog by remember { mutableStateOf(false) }
var showNotesDialog by remember { mutableStateOf(false) } var showNotesDialog by remember { mutableStateOf(false) }
var showUpdateDialog by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.checkForUpdates(context) viewModel.loadState(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)
)
} }
// Launcher akceptujący pliki .xlsx oraz .xls // Launcher akceptujący pliki .xlsx oraz .xls
@@ -352,7 +274,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
) { uri: Uri? -> ) { uri: Uri? ->
uri?.let { uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream -> 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(), statuses = viewModel.getUniqueStatuses(),
onDismiss = { showStatusDialog = false }, onDismiss = { showStatusDialog = false },
onConfirm = { selectedStatuses -> onConfirm = { selectedStatuses ->
viewModel.markMachinesByStatuses(selectedStatuses, true) viewModel.markMachinesByStatuses(context, selectedStatuses, true)
showStatusDialog = false showStatusDialog = false
} }
) )
@@ -371,7 +293,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
if (showNotesDialog) { if (showNotesDialog) {
NotesDialog( NotesDialog(
notes = notes, notes = notes,
onNotesChange = { viewModel.setNotes(it) }, onNotesChange = { viewModel.setNotes(context, it) },
onDismiss = { showNotesDialog = false } onDismiss = { showNotesDialog = false }
) )
} }
@@ -484,7 +406,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
items(filteredMachines, key = { it.id }) { machine -> items(filteredMachines, key = { it.id }) { machine ->
MachineItem( MachineItem(
machine = machine, machine = machine,
onToggle = { viewModel.toggleMachineCheck(machine.id) } onToggle = { viewModel.toggleMachineCheck(context, machine.id) }
) )
} }
} }
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<cache-path name="cache_files" path="." />
<files-path name="internal_files" path="." />
</paths>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BBJ Solution - Pobierz Aplikację</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
body {
background-color: #0f172a;
color: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.card {
background-color: #1e293b;
border-radius: 16px;
padding: 32px 24px;
max-width: 420px;
width: 100%;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
border: 1px solid #334155;
}
h1 {
font-size: 24px;
margin-bottom: 8px;
color: #38bdf8;
}
p {
color: #94a3b8;
font-size: 14px;
margin-bottom: 24px;
line-height: 1.5;
}
.download-btn {
display: inline-block;
width: 100%;
padding: 14px 20px;
background-color: #0284c7;
color: #ffffff;
font-weight: bold;
font-size: 16px;
text-decoration: none;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.download-btn:active {
background-color: #0369a1;
}
.info {
margin-top: 16px;
font-size: 12px;
color: #64748b;
}
</style>
</head>
<body>
<div class="card">
<h1>BBJ Solution</h1>
<p>Aplikacja mobilna do obsługi magazynu i urządzeń.</p>
<!-- Podmień app-release.apk na dokładną nazwę swojego pliku APK -->
<a href="app-release.apk" class="download-btn" download>Pobierz aplikację (APK)</a>
<p class="info">Po pobraniu zezwól w telefonie na instalację z nieznanych źródeł.</p>
</div>
</body>
</html>