diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b96fe6..a5c1054 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,9 @@ All notable changes to mobileGF2logger are documented here.
### Changed
+- Refine the Home capture card and active-Platoon selector, use cohesive
+ rounded dialogs, compact packet-history settings, and replace queued transient
+ messages so repeated actions do not leave stale feedback behind.
- Quarantine up to 32 decoded payloads per TCP flow until both its supported
Android client and valid Platoon identity are known; unverified flows never
enter management storage.
@@ -42,6 +45,8 @@ All notable changes to mobileGF2logger are documented here.
### Fixed
+- Improve weekly evidence guidance and restore consistent spacing around weekly
+ notes and member-detail fields in both light and dark themes.
- Resolve all six review findings from v2.3.3: clean flow metadata without a
parser, order known same-day joins by instant, bound manual weekly notes,
preserve captured member names, replace timezone-derived history atomically,
diff --git a/app/src/main/java/dev/gf2log/app/DateTimePickerInput.kt b/app/src/main/java/dev/gf2log/app/DateTimePickerInput.kt
index 00662f6..f9a35e1 100644
--- a/app/src/main/java/dev/gf2log/app/DateTimePickerInput.kt
+++ b/app/src/main/java/dev/gf2log/app/DateTimePickerInput.kt
@@ -53,7 +53,7 @@ internal class DateTimePickerInput(
}
private val timeInput = pickerField(context.getString(R.string.time_optional)) {
if (selection.date == null) {
- Toast.makeText(context, R.string.select_date_first, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(context, R.string.select_date_first)
} else {
showTimePicker()
}
diff --git a/app/src/main/java/dev/gf2log/app/MainActivity.kt b/app/src/main/java/dev/gf2log/app/MainActivity.kt
index 7e34748..959c37a 100644
--- a/app/src/main/java/dev/gf2log/app/MainActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/MainActivity.kt
@@ -299,7 +299,7 @@ class MainActivity : LocalizedActivity() {
textSize = 22f
setTextColor(getColor(R.color.success_text))
setTypeface(typeface, Typeface.BOLD)
- setPadding(0, dp(2), 0, 0)
+ setPadding(0, dp(4), 0, 0)
}
addView(captureStateText, matchWidth())
captureStatusText = TextView(context).apply {
@@ -307,25 +307,30 @@ class MainActivity : LocalizedActivity() {
setTextColor(getColor(R.color.text_secondary))
}
addView(captureStatusText, matchWidth())
- }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply {
+ }, LinearLayout.LayoutParams(dp(124), ViewGroup.LayoutParams.WRAP_CONTENT).apply {
marginEnd = dp(8)
})
addView(
- PlatoonProfileSelector.controls(this@MainActivity, compact = true),
+ PlatoonProfileSelector.controls(
+ this@MainActivity,
+ compact = true,
+ showManageButton = false,
+ ),
LinearLayout.LayoutParams(
- dp(200),
+ 0,
ViewGroup.LayoutParams.WRAP_CONTENT,
- ),
+ 1f,
+ ).apply { topMargin = dp(18) },
)
}, matchWidth())
addView(LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.TOP
+ gravity = Gravity.BOTTOM
addView(TextView(context).apply {
text = getString(R.string.capture_target)
textSize = 12f
setTextColor(getColor(R.color.text_secondary))
- setPadding(0, dp(4), 0, 0)
+ setPadding(0, 0, 0, dp(4))
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
addView(ImageButton(context).apply {
setImageResource(R.drawable.ic_info_outline)
@@ -333,7 +338,10 @@ class MainActivity : LocalizedActivity() {
useModernIconStyle()
setOnClickListener(::showTargetPackageInfo)
}, LinearLayout.LayoutParams(dp(48), dp(48)))
- }, matchWidth())
+ }, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ dp(48),
+ ))
addView(fixedTargetField(SupportedGamePackages.HAOPLAY), matchWidth())
addView(
fixedTargetField(SupportedGamePackages.DARKWINTER),
@@ -509,11 +517,14 @@ class MainActivity : LocalizedActivity() {
},
)
val status = CaptureStatus.read()
- captureStatusText.text = when (status) {
- "Capture is stopped" -> getString(R.string.capture_stopped_detail)
- "Preparing capture" -> getString(R.string.status_preparing)
+ val detail = when {
+ status.startsWith("Capturing only ") -> ""
+ status == "Capture is stopped" -> getString(R.string.capture_stopped_detail)
+ status == "Preparing capture" -> getString(R.string.status_preparing)
else -> status
}
+ captureStatusText.text = detail
+ captureStatusText.visibility = if (detail.isBlank()) View.GONE else View.VISIBLE
captureStateText.contentDescription = captureStateText.text
captureStatusText.contentDescription = captureStatusText.text
val busy = starting || running
@@ -638,10 +649,7 @@ class MainActivity : LocalizedActivity() {
}
private fun showNoPlatoonMessage() {
- AlertDialog.Builder(this)
- .setMessage(R.string.no_platoon_detected_detail)
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ TransientMessage.show(this, R.string.no_platoon_detected_detail, android.widget.Toast.LENGTH_LONG)
}
private fun showCsvImportSelector() {
@@ -961,7 +969,7 @@ class MainActivity : LocalizedActivity() {
}
val scope = requireActiveScope() ?: return
if (!CsvImportCheckpointManager(this, scope).canUndo()) {
- statusText.text = getString(R.string.no_csv_import_checkpoint)
+ TransientMessage.show(this, R.string.no_csv_import_checkpoint)
return
}
AlertDialog.Builder(this)
diff --git a/app/src/main/java/dev/gf2log/app/MemberDetailActivity.kt b/app/src/main/java/dev/gf2log/app/MemberDetailActivity.kt
index 49a7d2e..593a39c 100644
--- a/app/src/main/java/dev/gf2log/app/MemberDetailActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/MemberDetailActivity.kt
@@ -71,7 +71,10 @@ class MemberDetailActivity : LocalizedActivity() {
setPadding(0, dp(4), 0, dp(8))
}, matchWidth())
addView(nameInput, matchWidth())
- addView(noteInput, matchWidth())
+ addView(noteInput, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ).apply { topMargin = dp(8) })
addView(LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
addView(Button(context).apply {
@@ -85,11 +88,11 @@ class MemberDetailActivity : LocalizedActivity() {
noteInput.text.toString(),
)
}.getOrDefault(false)
- Toast.makeText(
+ TransientMessage.show(
this@MemberDetailActivity,
getString(if (saved) R.string.saved else R.string.save_failed),
Toast.LENGTH_SHORT,
- ).show()
+ )
}
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply {
marginEnd = dp(4)
@@ -138,18 +141,18 @@ class MemberDetailActivity : LocalizedActivity() {
}.getOrDefault(false)
if (deleted) {
dialog.dismiss()
- Toast.makeText(
+ TransientMessage.show(
this@MemberDetailActivity,
R.string.member_deleted,
Toast.LENGTH_SHORT,
- ).show()
+ )
finish()
} else {
- Toast.makeText(
+ TransientMessage.show(
this@MemberDetailActivity,
R.string.member_delete_failed,
Toast.LENGTH_SHORT,
- ).show()
+ )
}
}
}
@@ -246,11 +249,11 @@ class MemberDetailActivity : LocalizedActivity() {
note.text.toString(),
)
}.getOrDefault(false)
- Toast.makeText(
+ TransientMessage.show(
this,
getString(if (saved) R.string.saved else R.string.invalid_date),
Toast.LENGTH_SHORT,
- ).show()
+ )
if (saved) {
dialog.dismiss()
render()
@@ -280,18 +283,18 @@ class MemberDetailActivity : LocalizedActivity() {
if (deleted) {
confirmation.dismiss()
editor.dismiss()
- Toast.makeText(
+ TransientMessage.show(
this@MemberDetailActivity,
R.string.membership_period_deleted,
Toast.LENGTH_SHORT,
- ).show()
+ )
render()
} else {
- Toast.makeText(
+ TransientMessage.show(
this@MemberDetailActivity,
R.string.membership_period_delete_failed,
Toast.LENGTH_SHORT,
- ).show()
+ )
}
}
}
@@ -346,7 +349,7 @@ class MemberDetailActivity : LocalizedActivity() {
dialog.dismiss()
render()
} else {
- Toast.makeText(this, R.string.invalid_date, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.invalid_date)
}
}
}
diff --git a/app/src/main/java/dev/gf2log/app/ModernFeedback.kt b/app/src/main/java/dev/gf2log/app/ModernFeedback.kt
new file mode 100644
index 0000000..774b10a
--- /dev/null
+++ b/app/src/main/java/dev/gf2log/app/ModernFeedback.kt
@@ -0,0 +1,35 @@
+package dev.gf2log.app
+
+import android.app.AlertDialog
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.StringRes
+
+/** Keeps app-owned dialogs visually consistent while preserving Android dialog behavior. */
+internal fun AlertDialog.applyModernDialogStyle(): AlertDialog = apply {
+ window?.setBackgroundDrawable(ModernUi.dialogBackground(context))
+ getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(context.getColor(R.color.accent))
+ getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(context.getColor(R.color.text_secondary))
+ getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(context.getColor(R.color.text_secondary))
+}
+
+internal fun AlertDialog.Builder.showModern(): AlertDialog = show().applyModernDialogStyle()
+
+/**
+ * Replaces the currently visible transient message instead of extending a long Toast queue.
+ * Android does not expose its internal queue, so cancelling the prior app-owned Toast is the
+ * deterministic way to keep rapid repeated actions responsive.
+ */
+internal object TransientMessage {
+ private var active: Toast? = null
+
+ @Synchronized
+ fun show(context: Context, message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
+ active?.cancel()
+ active = Toast.makeText(context.applicationContext, message, duration).also(Toast::show)
+ }
+
+ fun show(context: Context, @StringRes message: Int, duration: Int = Toast.LENGTH_SHORT) {
+ show(context, context.getString(message), duration)
+ }
+}
diff --git a/app/src/main/java/dev/gf2log/app/ModernUi.kt b/app/src/main/java/dev/gf2log/app/ModernUi.kt
index 4012bc7..ccd95e9 100644
--- a/app/src/main/java/dev/gf2log/app/ModernUi.kt
+++ b/app/src/main/java/dev/gf2log/app/ModernUi.kt
@@ -35,6 +35,7 @@ object ModernUi {
SECONDARY,
TERTIARY,
NAVIGATION,
+ SELECTOR,
DESTRUCTIVE,
DESTRUCTIVE_TEXT,
}
@@ -51,6 +52,13 @@ object ModernUi {
cornerRadius = context.dp(16).toFloat()
}
+ /** Theme-aware surface used by app-owned dialogs without replacing native semantics. */
+ fun dialogBackground(context: Context): GradientDrawable = GradientDrawable().apply {
+ setColor(context.getColor(R.color.surface))
+ cornerRadius = context.dp(20).toFloat()
+ setStroke(context.dp(1), context.getColor(R.color.outline))
+ }
+
/** Flat list navigation used by dashboard utilities and grouped Settings rows. */
fun listRow(
context: Context,
@@ -414,6 +422,16 @@ object ModernUi {
insetVertical = 4,
radiusDp = 12,
)
+ ControlRole.SELECTOR -> ButtonColors(
+ normal = R.color.surface,
+ pressed = R.color.surface_pressed,
+ disabled = R.color.surface_variant,
+ stroke = R.color.outline_strong,
+ pressedStroke = R.color.accent,
+ text = R.color.text_primary,
+ insetVertical = 4,
+ radiusDp = 12,
+ )
ControlRole.SECONDARY -> ButtonColors(
normal = R.color.surface,
pressed = R.color.surface_pressed,
diff --git a/app/src/main/java/dev/gf2log/app/OptionsActivity.kt b/app/src/main/java/dev/gf2log/app/OptionsActivity.kt
index 8df719e..a49f11c 100644
--- a/app/src/main/java/dev/gf2log/app/OptionsActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/OptionsActivity.kt
@@ -13,6 +13,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.OpenableColumns
import android.text.InputType
+import android.view.View
import android.view.ViewGroup
import android.view.Gravity
import android.widget.Button
@@ -208,11 +209,7 @@ class OptionsActivity : LocalizedActivity() {
icon = R.drawable.ic_calendar,
onClick = {
if (resetScope == null) {
- Toast.makeText(
- context,
- R.string.no_platoon_detected_detail,
- Toast.LENGTH_SHORT,
- ).show()
+ TransientMessage.show(context, R.string.no_platoon_detected_detail)
} else {
chooseGameServerRegion()
}
@@ -324,7 +321,7 @@ class OptionsActivity : LocalizedActivity() {
val saved = runCatching {
webhookStore.save(webhookInput.text.toString())
}.isSuccess
- Toast.makeText(
+ TransientMessage.show(
this@OptionsActivity,
if (saved) {
R.string.discord_webhook_saved
@@ -332,7 +329,7 @@ class OptionsActivity : LocalizedActivity() {
R.string.discord_webhook_invalid
},
Toast.LENGTH_LONG,
- ).show()
+ )
if (saved) recreate()
}
}, matchWidth())
@@ -342,11 +339,11 @@ class OptionsActivity : LocalizedActivity() {
isEnabled = webhookConfigured
setOnClickListener {
val cleared = runCatching(webhookStore::clear).isSuccess
- Toast.makeText(
+ TransientMessage.show(
this@OptionsActivity,
if (cleared) R.string.discord_webhook_cleared else R.string.discord_webhook_clear_failed,
Toast.LENGTH_LONG,
- ).show()
+ )
if (cleared) recreate()
}
}, matchWidth())
@@ -355,55 +352,37 @@ class OptionsActivity : LocalizedActivity() {
text = getString(R.string.payload_history)
textSize = 15f
setTypeface(typeface, Typeface.BOLD)
- setPadding(0, spacing, 0, dp(4))
+ setPadding(0, spacing, 0, dp(2))
}, matchWidth())
val capturePreferences = CapturePreferences(context)
- addView(CheckBox(context).apply {
- text = getString(R.string.detailed_notifications)
- isChecked = capturePreferences.detailedNotifications
- setOnCheckedChangeListener { _, enabled ->
- capturePreferences.detailedNotifications = enabled
- }
- }, matchWidth())
- addView(TextView(context).apply {
- text = getString(R.string.notification_required_explanation)
- textSize = 13f
- setPadding(dp(48), 0, 0, spacing)
- }, matchWidth())
+ addView(packetHistoryOptionRow(
+ title = getString(R.string.detailed_notifications),
+ detail = getString(R.string.notification_required_explanation),
+ checked = capturePreferences.detailedNotifications,
+ enabled = true,
+ onChanged = { capturePreferences.detailedNotifications = it },
+ ), matchWidth())
PayloadCatalog.categories.forEach { category ->
- addView(CheckBox(context).apply {
- text = getString(
+ addView(packetHistoryOptionRow(
+ title = getString(
R.string.payload_option_label,
payloadName(category.payloadType),
category.payloadType,
- )
- textSize = 14f
- isChecked = preferences.isEnabled(category.payloadType)
- isEnabled = !category.isRequired
- if (category.isRequired) {
- buttonTintList = ColorStateList(
- arrayOf(intArrayOf(-android.R.attr.state_enabled), intArrayOf()),
- intArrayOf(getColor(R.color.primary), getColor(R.color.primary)),
- )
- }
- setOnCheckedChangeListener { _, enabled ->
- preferences.setEnabled(category.payloadType, enabled)
- }
- }, matchWidth())
- addView(TextView(context).apply {
- text = if (category.isRequired) {
+ ),
+ detail = if (category.isRequired) {
getString(
R.string.required_payload_description,
payloadDescription(category.payloadType),
)
} else {
payloadDescription(category.payloadType)
- }
- textSize = 14f
- setPadding(dp(48), 0, 0, spacing)
- }, matchWidth())
+ },
+ checked = preferences.isEnabled(category.payloadType),
+ enabled = !category.isRequired,
+ onChanged = { preferences.setEnabled(category.payloadType, it) },
+ ), matchWidth())
}
addView(TextView(context).apply {
@@ -434,6 +413,55 @@ class OptionsActivity : LocalizedActivity() {
return ScrollView(this).apply { addView(container, matchWidth()) }
}
+ /** Keeps a packet-history choice and its explanation in one compact visual group. */
+ private fun packetHistoryOptionRow(
+ title: CharSequence,
+ detail: CharSequence,
+ checked: Boolean,
+ enabled: Boolean,
+ onChanged: (Boolean) -> Unit,
+ ): View = LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.TOP
+ minimumHeight = dp(52)
+ setPadding(0, dp(2), 0, dp(2))
+ val checkBox = CheckBox(context).apply {
+ contentDescription = title
+ isChecked = checked
+ isEnabled = enabled
+ if (!enabled) {
+ buttonTintList = ColorStateList(
+ arrayOf(intArrayOf(-android.R.attr.state_enabled), intArrayOf()),
+ intArrayOf(getColor(R.color.primary), getColor(R.color.primary)),
+ )
+ }
+ setOnCheckedChangeListener { _, value -> onChanged(value) }
+ }
+ addView(checkBox, LinearLayout.LayoutParams(dp(48), dp(48)))
+ addView(LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ addView(TextView(context).apply {
+ text = title
+ textSize = 14f
+ setTextColor(getColor(if (enabled) R.color.text_primary else R.color.text_secondary))
+ }, matchWidth())
+ addView(TextView(context).apply {
+ text = detail
+ textSize = 12f
+ setTextColor(getColor(R.color.text_secondary))
+ setLineSpacing(0f, 1.08f)
+ setPadding(0, dp(2), 0, 0)
+ }, matchWidth())
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply {
+ topMargin = dp(4)
+ })
+ if (enabled) {
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ setOnClickListener { checkBox.performClick() }
+ }
+ }
+
private fun confirmFullRestore() {
if (CaptureStatus.isRunning) {
showBackupMessage(R.string.stop_capture_before_backup)
@@ -505,7 +533,7 @@ class OptionsActivity : LocalizedActivity() {
}
private fun showBackupMessage(message: Int) {
- android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
+ TransientMessage.show(this, message, android.widget.Toast.LENGTH_LONG)
}
private fun requireActiveScope(): dev.gf2log.app.management.PlatoonStorageScope? =
@@ -610,12 +638,12 @@ class OptionsActivity : LocalizedActivity() {
}
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
- Toast.makeText(
+ TransientMessage.show(
this,
if (result.isSuccess) R.string.game_timezone_updated
else R.string.game_timezone_update_failed,
Toast.LENGTH_SHORT,
- ).show()
+ )
recreate()
}
}
diff --git a/app/src/main/java/dev/gf2log/app/PacketHistoryActivity.kt b/app/src/main/java/dev/gf2log/app/PacketHistoryActivity.kt
index ddfc05c..799e27b 100644
--- a/app/src/main/java/dev/gf2log/app/PacketHistoryActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/PacketHistoryActivity.kt
@@ -177,7 +177,7 @@ class PacketHistoryActivity : LocalizedActivity() {
private fun confirmSendOriginalCsv() {
val csv = runCatching { OriginalCsvPayload.extract(rawContent) }.getOrElse {
- Toast.makeText(this, R.string.discord_csv_invalid, Toast.LENGTH_LONG).show()
+ TransientMessage.show(this, R.string.discord_csv_invalid, Toast.LENGTH_LONG)
return
}
val webhook = DiscordWebhookSecretStore(this).read() ?: run {
@@ -206,17 +206,17 @@ class PacketHistoryActivity : LocalizedActivity() {
private fun sendOriginalCsv(webhook: String, csv: String) {
sendButton.alpha = 0.5f
sendButton.isEnabled = false
- Toast.makeText(this, R.string.discord_csv_sending, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.discord_csv_sending)
sendExecutor.execute {
val sent = runCatching { DiscordWebhookSender().send(webhook, csv) }.isSuccess
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
refreshSendAvailability()
- Toast.makeText(
+ TransientMessage.show(
this,
if (sent) R.string.discord_csv_sent else R.string.discord_csv_send_failed,
Toast.LENGTH_LONG,
- ).show()
+ )
}
}
}
@@ -273,7 +273,7 @@ class PacketHistoryActivity : LocalizedActivity() {
private fun copyToClipboard(content: String) {
getSystemService(ClipboardManager::class.java)
.setPrimaryClip(ClipData.newPlainText(getString(R.string.clipboard_label), content))
- Toast.makeText(this, getString(R.string.status_packet_copied), Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.status_packet_copied)
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
diff --git a/app/src/main/java/dev/gf2log/app/PlatoonActivity.kt b/app/src/main/java/dev/gf2log/app/PlatoonActivity.kt
index d6e1669..b088957 100644
--- a/app/src/main/java/dev/gf2log/app/PlatoonActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/PlatoonActivity.kt
@@ -49,7 +49,7 @@ class PlatoonActivity : LocalizedActivity() {
super.onCreate(savedInstanceState)
profileBinding = ActivePlatoonScopeBinding(this)
val scope = profileBinding.scope ?: run {
- Toast.makeText(this, R.string.no_platoon_detected_detail, Toast.LENGTH_LONG).show()
+ TransientMessage.show(this, R.string.no_platoon_detected_detail, Toast.LENGTH_LONG)
finish()
return
}
@@ -327,18 +327,18 @@ class PlatoonActivity : LocalizedActivity() {
?: error("Document provider did not open an output stream")
output.writer(Charsets.UTF_8).use { it.write(content) }
}.isSuccess
- Toast.makeText(
+ TransientMessage.show(
this,
getString(if (exported) R.string.members_exported else R.string.status_export_failed),
Toast.LENGTH_SHORT,
- ).show()
+ )
}
@Suppress("DEPRECATION")
private fun exportSelectedMembers() {
val selected = statuses.filter { it.uid in selectedUids }
if (selected.isEmpty()) {
- Toast.makeText(this, R.string.select_members_first, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.select_members_first)
return
}
pendingMemberCsv = PlatoonMemberCsv.format(
@@ -416,13 +416,13 @@ class PlatoonActivity : LocalizedActivity() {
if (saved) {
dialog.dismiss()
refresh()
- Toast.makeText(this, R.string.saved, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.saved)
} else {
- Toast.makeText(
+ TransientMessage.show(
this,
R.string.invalid_withdrawn_member,
Toast.LENGTH_SHORT,
- ).show()
+ )
}
}
}
diff --git a/app/src/main/java/dev/gf2log/app/PlatoonProfileSelector.kt b/app/src/main/java/dev/gf2log/app/PlatoonProfileSelector.kt
index 53c7d60..723fd50 100644
--- a/app/src/main/java/dev/gf2log/app/PlatoonProfileSelector.kt
+++ b/app/src/main/java/dev/gf2log/app/PlatoonProfileSelector.kt
@@ -17,7 +17,6 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
-import android.widget.Toast
import dev.gf2log.app.capture.CaptureStatus
import dev.gf2log.app.management.PlatoonProfile
import dev.gf2log.app.management.PlatoonProfileAdministration
@@ -28,7 +27,11 @@ import java.util.concurrent.Executors
/** Shared, presentation-only selector and manager for isolated Platoon scopes. */
internal object PlatoonProfileSelector {
- fun controls(activity: Activity, compact: Boolean = false): LinearLayout =
+ fun controls(
+ activity: Activity,
+ compact: Boolean = false,
+ showManageButton: Boolean = true,
+ ): LinearLayout =
LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
@@ -36,12 +39,14 @@ internal object PlatoonProfileSelector {
selectorButton(activity, compact),
LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f),
)
- addView(
- manageButton(activity),
- LinearLayout.LayoutParams(dp(activity, 48), dp(activity, 48)).apply {
- marginStart = dp(activity, 8)
- },
- )
+ if (showManageButton) {
+ addView(
+ manageButton(activity),
+ LinearLayout.LayoutParams(dp(activity, 48), dp(activity, 48)).apply {
+ marginStart = dp(activity, 8)
+ },
+ )
+ }
}
private fun selectorButton(activity: Activity, compact: Boolean): Button {
@@ -56,9 +61,7 @@ internal object PlatoonProfileSelector {
textSize = 11f
maxWidth = dp(activity, 210)
setPadding(dp(activity, 10), 0, dp(activity, 10), 0)
- background = ModernUi.panelBackground(activity).apply {
- setStroke(dp(activity, 1), activity.getColor(R.color.outline))
- }
+ useSelectorActionStyle()
} else {
useNavigationActionStyle()
}
@@ -199,8 +202,7 @@ internal object PlatoonProfileSelector {
private fun chooseProfileRegion(activity: Activity, profile: PlatoonProfile) {
if (CaptureStatus.isRunning) {
- Toast.makeText(activity, R.string.stop_capture_before_profile_change, Toast.LENGTH_LONG)
- .show()
+ TransientMessage.show(activity, R.string.stop_capture_before_profile_change, android.widget.Toast.LENGTH_LONG)
return
}
val regions = ClientServerRegionPreferences.allowedFor(profile.client.packageName)
@@ -232,8 +234,7 @@ internal object PlatoonProfileSelector {
private fun confirmDeleteFirst(activity: Activity, profile: PlatoonProfile) {
if (CaptureStatus.isRunning) {
- Toast.makeText(activity, R.string.stop_capture_before_profile_change, Toast.LENGTH_LONG)
- .show()
+ TransientMessage.show(activity, R.string.stop_capture_before_profile_change, android.widget.Toast.LENGTH_LONG)
return
}
val warning = AlertDialog.Builder(activity)
@@ -327,17 +328,14 @@ internal object PlatoonProfileSelector {
if (result.isSuccess) {
activity.recreate()
} else {
- Toast.makeText(activity, failureMessage, Toast.LENGTH_LONG).show()
+ TransientMessage.show(activity, failureMessage, android.widget.Toast.LENGTH_LONG)
}
}
}
}
private fun showNoPlatoonMessage(activity: Activity) {
- AlertDialog.Builder(activity)
- .setMessage(R.string.no_platoon_detected_detail)
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ TransientMessage.show(activity, R.string.no_platoon_detected_detail, android.widget.Toast.LENGTH_LONG)
}
private fun Activity.serverRegionLabel(region: GameServerRegion): String = getString(
diff --git a/app/src/main/java/dev/gf2log/app/PrimaryActionStyle.kt b/app/src/main/java/dev/gf2log/app/PrimaryActionStyle.kt
index 2fbf861..8ce9b23 100644
--- a/app/src/main/java/dev/gf2log/app/PrimaryActionStyle.kt
+++ b/app/src/main/java/dev/gf2log/app/PrimaryActionStyle.kt
@@ -14,6 +14,8 @@ fun Button.useTertiaryActionStyle() = useActionRole(ModernUi.ControlRole.TERTIAR
fun Button.useNavigationActionStyle() = useActionRole(ModernUi.ControlRole.NAVIGATION)
+fun Button.useSelectorActionStyle() = useActionRole(ModernUi.ControlRole.SELECTOR)
+
fun Button.useEvidenceActionStyle() = useActionRole(ModernUi.ControlRole.EVIDENCE)
fun Button.useDestructiveActionStyle() = useActionRole(ModernUi.ControlRole.DESTRUCTIVE)
diff --git a/app/src/main/java/dev/gf2log/app/PrimaryNavigation.kt b/app/src/main/java/dev/gf2log/app/PrimaryNavigation.kt
index f6cbee6..3a034ff 100644
--- a/app/src/main/java/dev/gf2log/app/PrimaryNavigation.kt
+++ b/app/src/main/java/dev/gf2log/app/PrimaryNavigation.kt
@@ -1,7 +1,6 @@
package dev.gf2log.app
import android.app.Activity
-import android.app.AlertDialog
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Typeface
@@ -115,10 +114,11 @@ internal object PrimaryNavigation {
if (destination != Destination.HOME &&
PlatoonProfileRegistry(activity).active() == null
) {
- AlertDialog.Builder(activity)
- .setMessage(R.string.no_platoon_detected_detail)
- .setPositiveButton(android.R.string.ok, null)
- .show()
+ TransientMessage.show(
+ activity,
+ R.string.no_platoon_detected_detail,
+ android.widget.Toast.LENGTH_LONG,
+ )
} else {
activity.startActivity(
Intent(activity, activityClass).addFlags(
diff --git a/app/src/main/java/dev/gf2log/app/SnapshotComparisonActivity.kt b/app/src/main/java/dev/gf2log/app/SnapshotComparisonActivity.kt
index 901c2c1..69451bc 100644
--- a/app/src/main/java/dev/gf2log/app/SnapshotComparisonActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/SnapshotComparisonActivity.kt
@@ -49,11 +49,11 @@ class SnapshotComparisonActivity : LocalizedActivity() {
SnapshotComparisonCsv.format(result),
),
)
- Toast.makeText(
+ TransientMessage.show(
this@SnapshotComparisonActivity,
R.string.snapshot_csv_copied,
Toast.LENGTH_SHORT,
- ).show()
+ )
}
})
addSection(
diff --git a/app/src/main/java/dev/gf2log/app/WeeklyReportActivity.kt b/app/src/main/java/dev/gf2log/app/WeeklyReportActivity.kt
index a61ffd5..e196aaf 100644
--- a/app/src/main/java/dev/gf2log/app/WeeklyReportActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/WeeklyReportActivity.kt
@@ -124,7 +124,7 @@ class WeeklyReportActivity : LocalizedActivity() {
super.onCreate(savedInstanceState)
profileBinding = ActivePlatoonScopeBinding(this)
val scope = profileBinding.scope ?: run {
- Toast.makeText(this, R.string.no_platoon_detected_detail, Toast.LENGTH_LONG).show()
+ TransientMessage.show(this, R.string.no_platoon_detected_detail, Toast.LENGTH_LONG)
finish()
return
}
@@ -1094,17 +1094,62 @@ class WeeklyReportActivity : LocalizedActivity() {
}
private fun showWeeklyEvidenceGuide(isGunsmokeWeek: Boolean) {
+ fun guideRow(symbol: String, description: CharSequence): View =
+ LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER_VERTICAL
+ addView(TextView(context).apply {
+ text = symbol
+ textSize = if (symbol.length <= 2) 20f else 15f
+ gravity = Gravity.CENTER
+ setTypeface(typeface, Typeface.BOLD)
+ setTextColor(getColor(R.color.accent_text))
+ background = ModernUi.panelBackground(context, emphasized = true)
+ setPadding(dp(8), dp(8), dp(8), dp(8))
+ }, LinearLayout.LayoutParams(dp(76), dp(48)).apply {
+ marginEnd = dp(12)
+ })
+ addView(TextView(context).apply {
+ text = description
+ textSize = 14f
+ setTextColor(getColor(R.color.text_primary))
+ setLineSpacing(0f, 1.12f)
+ }, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
+ }
+ val guide = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(dp(20), dp(8), dp(20), dp(12))
+ val rows = listOf(
+ "✓" to getString(R.string.evidence_guide_exact),
+ "≥" to getString(R.string.evidence_guide_minimum),
+ "?" to getString(R.string.evidence_guide_unknown),
+ (if (isGunsmokeWeek) "TOTAL" else "0 / 50 / 90") to getString(
+ if (isGunsmokeWeek) {
+ R.string.evidence_guide_gunsmoke_total
+ } else {
+ R.string.evidence_guide_standard_allocation
+ },
+ ),
+ )
+ rows.forEachIndexed { index, (symbol, explanation) ->
+ addView(guideRow(symbol, explanation), LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ).apply { if (index > 0) topMargin = dp(12) })
+ }
+ addView(TextView(context).apply {
+ text = getString(R.string.evidence_guide_gap_warning)
+ textSize = 13f
+ setTextColor(getColor(R.color.text_secondary))
+ setLineSpacing(0f, 1.12f)
+ setPadding(0, dp(16), 0, 0)
+ }, matchWidth())
+ }
AlertDialog.Builder(this)
.setTitle(R.string.weekly_evidence_guide)
- .setMessage(
- if (isGunsmokeWeek) {
- R.string.incomplete_daily_evidence_gunsmoke
- } else {
- R.string.incomplete_daily_evidence_standard
- },
- )
+ .setView(ScrollView(this).apply { addView(guide, matchWidth()) })
.setPositiveButton(android.R.string.ok, null)
- .show()
+ .showModern()
}
private fun showEvidenceExplanation(
@@ -1285,12 +1330,12 @@ class WeeklyReportActivity : LocalizedActivity() {
).takeIf { it.hasAnyValue() }
}
}.getOrElse {
- Toast.makeText(this, R.string.invalid_weekly_edit, Toast.LENGTH_LONG).show()
+ TransientMessage.show(this, R.string.invalid_weekly_edit, Toast.LENGTH_LONG)
return
}
repository.replaceWeeklyOverrides(report.periodStart.toEpochDay(), overrides)
cancelWeeklyEditing()
- Toast.makeText(this, R.string.weekly_edits_saved, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.weekly_edits_saved)
requestRender()
}
@@ -1329,7 +1374,7 @@ class WeeklyReportActivity : LocalizedActivity() {
WeeklyReportCsv.format(report),
),
)
- Toast.makeText(this, R.string.weekly_csv_copied, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.weekly_csv_copied)
}
private fun addMembershipEvents(
@@ -1440,7 +1485,7 @@ class WeeklyReportActivity : LocalizedActivity() {
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ ->
if (repository.deleteWeeklyNote(note.id)) {
- Toast.makeText(this, R.string.note_deleted, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.note_deleted)
requestRender()
}
}
@@ -1496,7 +1541,7 @@ class WeeklyReportActivity : LocalizedActivity() {
privacy: WeeklyShareProjection.Privacy,
shareAfter: Boolean,
) {
- Toast.makeText(this, R.string.weekly_png_rendering, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.weekly_png_rendering)
workerExecutor.execute {
val result = runCatching {
val document = WeeklyShareProjection.build(
@@ -1514,11 +1559,11 @@ class WeeklyReportActivity : LocalizedActivity() {
if (shareAfter) shareWeeklyPng(file) else saveWeeklyPng(file, model.report.periodStart)
},
onFailure = {
- Toast.makeText(
+ TransientMessage.show(
this,
R.string.weekly_png_failed,
Toast.LENGTH_LONG,
- ).show()
+ )
},
)
}
@@ -1605,13 +1650,13 @@ class WeeklyReportActivity : LocalizedActivity() {
}
}.isSuccess
source.delete()
- Toast.makeText(
+ TransientMessage.show(
this,
getString(
if (exported) R.string.weekly_png_saved else R.string.weekly_png_failed,
),
Toast.LENGTH_SHORT,
- ).show()
+ )
return
}
@@ -1626,11 +1671,11 @@ class WeeklyReportActivity : LocalizedActivity() {
?: error("Document provider did not open an output stream")
output.writer(Charsets.UTF_8).use { it.write(content) }
}.isSuccess
- Toast.makeText(
+ TransientMessage.show(
this,
getString(if (exported) R.string.weekly_csv_exported else R.string.status_export_failed),
Toast.LENGTH_SHORT,
- ).show()
+ )
}
@Suppress("DEPRECATION")
@@ -1656,8 +1701,7 @@ class WeeklyReportActivity : LocalizedActivity() {
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
if (content == null) {
- Toast.makeText(this, R.string.no_weekly_tables_to_export, Toast.LENGTH_SHORT)
- .show()
+ TransientMessage.show(this, R.string.no_weekly_tables_to_export)
return@runOnUiThread
}
pendingCsv = content
@@ -1687,6 +1731,7 @@ class WeeklyReportActivity : LocalizedActivity() {
body.addView(note, matchWidth())
body.addView(Button(this).apply {
text = getString(R.string.add_note)
+ usePrimaryActionStyle()
setOnClickListener {
val text = note.text.toString().trim()
if (text.isBlank()) return@setOnClickListener
@@ -1699,15 +1744,15 @@ class WeeklyReportActivity : LocalizedActivity() {
)
}.fold(
onSuccess = {
- Toast.makeText(
+ TransientMessage.show(
this@WeeklyReportActivity,
getString(R.string.saved),
Toast.LENGTH_SHORT,
- ).show()
+ )
requestRender()
},
onFailure = { error ->
- Toast.makeText(
+ TransientMessage.show(
this@WeeklyReportActivity,
getString(
if (error is WeeklyNoteLimitException) {
@@ -1717,11 +1762,14 @@ class WeeklyReportActivity : LocalizedActivity() {
},
),
Toast.LENGTH_SHORT,
- ).show()
+ )
},
)
}
- }, matchWidth())
+ }, LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ).apply { topMargin = dp(8) })
}
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
diff --git a/app/src/main/java/dev/gf2log/app/WeeklySettingsActivity.kt b/app/src/main/java/dev/gf2log/app/WeeklySettingsActivity.kt
index bf912c9..6822577 100644
--- a/app/src/main/java/dev/gf2log/app/WeeklySettingsActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/WeeklySettingsActivity.kt
@@ -146,7 +146,7 @@ class WeeklySettingsActivity : LocalizedActivity() {
patrolDays,
).map { runCatching { it.read() }.getOrNull() }
if (values.any { it == INVALID }) {
- Toast.makeText(this, R.string.invalid_cutline, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.invalid_cutline)
return
}
WeeklyCutlinePreferences(this).write(
@@ -161,7 +161,7 @@ class WeeklySettingsActivity : LocalizedActivity() {
weeklyPatrolDays = values[7]?.toInt(),
),
)
- Toast.makeText(this, R.string.saved, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.saved)
finish()
}
diff --git a/app/src/main/java/dev/gf2log/app/WeeklyTableHistoryPreviewActivity.kt b/app/src/main/java/dev/gf2log/app/WeeklyTableHistoryPreviewActivity.kt
index 12a7bf9..d6e2a87 100644
--- a/app/src/main/java/dev/gf2log/app/WeeklyTableHistoryPreviewActivity.kt
+++ b/app/src/main/java/dev/gf2log/app/WeeklyTableHistoryPreviewActivity.kt
@@ -151,15 +151,15 @@ class WeeklyTableHistoryPreviewActivity : LocalizedActivity() {
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
if (restored) {
- Toast.makeText(this, R.string.weekly_table_history_restored, Toast.LENGTH_SHORT).show()
+ TransientMessage.show(this, R.string.weekly_table_history_restored)
setResult(Activity.RESULT_OK)
finish()
} else {
- Toast.makeText(
+ TransientMessage.show(
this,
R.string.weekly_table_history_restore_failed,
Toast.LENGTH_LONG,
- ).show()
+ )
}
}
}
diff --git a/app/src/main/res/drawable/bg_dialog.xml b/app/src/main/res/drawable/bg_dialog.xml
new file mode 100644
index 0000000..b1f454f
--- /dev/null
+++ b/app/src/main/res/drawable/bg_dialog.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 5a1578f..feb0c40 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -364,6 +364,12 @@
근거 상태
근거 상태 읽는 법
✓ 확정 · ≥ 확인된 최솟값 · ? 알 수 없음 — 자세히 보기
+ 마감 경계, 직접 이벤트, 수동 수정 또는 가능한 모든 배치의 일치로 정확한 값이 확정되었습니다.
+ 현재 캡처로 최소 이 값은 확인되지만, 누락된 근거에 따라 최종 값은 더 높을 수 있습니다.
+ 현재 캡처만으로 안전하게 확정할 수 없어 GF2logger가 임의로 추측하지 않습니다.
+ 완료된 날짜에는 유효한 일일 공적 배치만 사용합니다. 가능한 0/50/90 조합이 여러 개면 알 수 없음으로 유지합니다.
+ 누락된 흙먼지 날짜를 안전하게 나눌 수 없어도 카운터로 확인된 주간 합계는 계속 활용할 수 있습니다.
+ 캡처 공백이 길면 이전 항목이 알 수 없음으로 남을 수 있습니다. 각 셀을 누르면 사용된 근거를 확인할 수 있습니다.
관측 일수 %1$d/%2$d · 확정 %3$d · 최솟값 %4$d · 알 수 없음 %5$d\n직접 로그인 %6$d일 · 직접 과업 %7$d일 · 마감 경계 %8$d개
%1$s · %2$s · %3$s
판정: %1$s\n\n근거:\n%2$s
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index 4cd7146..112d950 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -14,5 +14,16 @@
- false
- true
- true
+ - @style/GF2DialogTheme
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e3936e7..b6476da 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -368,6 +368,12 @@
Evidence Health
How evidence states work
✓ exact · ≥ confirmed minimum · ? unknown — tap for details
+ An exact value is confirmed by a closing boundary, direct event, manual edit, or unanimous valid allocation.
+ Captures prove at least this value, but missing evidence may make the final value higher.
+ The available captures cannot determine this field safely, so GF2logger does not guess.
+ Completed days can only use valid daily merit allocations. If several 0/50/90 combinations still fit, the day remains unknown.
+ The counter-backed weekly total remains useful when missing Gunsmoke days cannot be divided safely.
+ Long capture gaps can leave earlier fields unknown. Tap any weekly cell to inspect the exact evidence used.
Observed days %1$d/%2$d · Exact %3$d · Minimum %4$d · Unknown %5$d\nDirect login %6$d days · Direct patrol %7$d days · Closing boundaries %8$d
%1$s · %2$s · %3$s
Answer: %1$s\n\nWhy:\n%2$s
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e9e29c0..6fce432 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -16,6 +16,17 @@
- true
- true
- true
+ - @style/GF2DialogTheme
+
+
+