Upload via GUI (7854 Dateien)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package de.deskhub.staff.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
private const val MAX_IMAGE_BYTES = 2 * 1024 * 1024
|
||||
|
||||
/** Lädt ein Bild nur über https, höchstens 2 MB und verkleinert auf ca. 600 px; bei jedem Fehler null (dann bleibt nur der Text). */
|
||||
internal fun loadSignatureBitmap(url: String): Bitmap? {
|
||||
if (!url.startsWith("https://", ignoreCase = true)) return null
|
||||
return try {
|
||||
val conn = URL(url).openConnection() as HttpURLConnection
|
||||
conn.connectTimeout = 8000
|
||||
conn.readTimeout = 8000
|
||||
conn.instanceFollowRedirects = false
|
||||
try {
|
||||
if (conn.responseCode != 200) return null
|
||||
val bytes = conn.inputStream.use { input ->
|
||||
val out = java.io.ByteArrayOutputStream()
|
||||
val buf = ByteArray(8192)
|
||||
while (true) {
|
||||
val n = input.read(buf)
|
||||
if (n < 0) break
|
||||
out.write(buf, 0, n)
|
||||
if (out.size() > MAX_IMAGE_BYTES) return null
|
||||
}
|
||||
out.toByteArray()
|
||||
}
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds)
|
||||
if (bounds.outWidth <= 0) return null
|
||||
var sample = 1
|
||||
while (bounds.outWidth / sample > 1200) sample *= 2
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, BitmapFactory.Options().apply { inSampleSize = sample })
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignatureImage(url: String, modifier: Modifier = Modifier) {
|
||||
if (url.isBlank()) return
|
||||
val bitmap by produceState<Bitmap?>(initialValue = null, url) {
|
||||
value = withContext(Dispatchers.IO) { loadSignatureBitmap(url) }
|
||||
}
|
||||
bitmap?.let {
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = "Bild aus der Signatur",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier.widthIn(max = 220.dp).heightIn(max = 120.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#28405A" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="#33FFFFFF" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="DeskHub"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_refresh"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="Aktualisieren"
|
||||
android:padding="4dp"
|
||||
android:text="⟳"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="Lade …"
|
||||
android:textColor="#D6DEE8"
|
||||
android:textSize="13sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_tiles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_tile"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_my_open"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="–"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Meine"
|
||||
android:textColor="#D6DEE8"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_tile"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_unassigned"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="–"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Offen"
|
||||
android:textColor="#D6DEE8"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/widget_tile"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_overdue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="–"
|
||||
android:textColor="#FFB4A8"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Überfällig"
|
||||
android:textColor="#D6DEE8"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_time"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="end"
|
||||
android:textColor="#A9B6C6"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
@@ -1,4 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">deskhub</string>
|
||||
<string name="server_label">deskhub-Server</string>
|
||||
<string name="widget_description">Zeigt deine offenen, nicht zugewiesenen und überfälligen Tickets.</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_description"
|
||||
android:initialLayout="@layout/widget_deskhub"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:previewLayout="@layout/widget_deskhub"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:widgetCategory="home_screen" />
|
||||
@@ -42,6 +42,30 @@ class ApiDtoParsingTest {
|
||||
assertEquals(false, t.permissions?.get("close_ticket"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ticket detail carries reply signatures and message request sends the choice`() {
|
||||
val t = gson.fromJson(
|
||||
"""{"id":5,"number":"T-1","subject":"S","department":{"id":2,"name":"Technik"},
|
||||
"customFields":[{"label":"Titel","type":"text","value":"Bär"}],"replySignatures":{"default":"own","own":{"available":true,"text":"Grüße\nMax","hasImage":false},
|
||||
"department":{"available":true,"name":"Technik","text":"Team","hasImage":true,"imageUrl":"https://x.de/l.png"}}}""",
|
||||
TicketDetailDto::class.java,
|
||||
)
|
||||
val sigs = t.replySignatures!!
|
||||
assertEquals("own", sigs.default)
|
||||
assertTrue(sigs.own.available)
|
||||
assertEquals("Grüße\nMax", sigs.own.text)
|
||||
assertEquals("Technik", sigs.department.name)
|
||||
assertTrue(sigs.department.hasImage)
|
||||
assertEquals("https://x.de/l.png", sigs.department.imageUrl)
|
||||
assertEquals("Bär", t.customFields.single().value)
|
||||
// ältere Server liefern keine Signaturen: keine Auswahl
|
||||
assertNull(gson.fromJson("""{"id":5,"number":"T-1"}""", TicketDetailDto::class.java).replySignatures)
|
||||
// die Wahl geht als Feld "signature" an den Server
|
||||
val body = gson.toJson(MessageRequest("T-1", "reply", "Hallo", "answered", "dept"))
|
||||
assertTrue(body.contains("\"signature\":\"dept\""))
|
||||
assertFalse(gson.toJson(MessageRequest("T-1", "note", "Notiz")).contains("signature"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `activity response reports health`() {
|
||||
val a = gson.fromJson("""{"newReplies":2,"newUnassigned":1,"serverTime":"2026-10-15 10:00:00","health":[{"level":"warning","code":"fetch_stale","message":"Abruf veraltet"}]}""", ActivityResponseDto::class.java)
|
||||
|
||||
Reference in New Issue
Block a user