From bed62a7cc4359dfc72d6c76e43209e40ad952597 Mon Sep 17 00:00:00 2001 From: Alex Han Date: Tue, 25 Aug 2026 16:25:49 +0900 Subject: [PATCH 1/2] fix: polish v2.4.0 interface feedback --- CHANGELOG.md | 5 + .../dev/gf2log/app/DateTimePickerInput.kt | 2 +- .../main/java/dev/gf2log/app/MainActivity.kt | 40 +++--- .../dev/gf2log/app/MemberDetailActivity.kt | 31 ++--- .../java/dev/gf2log/app/ModernFeedback.kt | 35 ++++++ app/src/main/java/dev/gf2log/app/ModernUi.kt | 18 +++ .../java/dev/gf2log/app/OptionsActivity.kt | 117 +++++++++++------- .../dev/gf2log/app/PacketHistoryActivity.kt | 10 +- .../java/dev/gf2log/app/PlatoonActivity.kt | 14 +-- .../dev/gf2log/app/PlatoonProfileSelector.kt | 38 +++--- .../java/dev/gf2log/app/PrimaryActionStyle.kt | 2 + .../java/dev/gf2log/app/PrimaryNavigation.kt | 10 +- .../gf2log/app/SnapshotComparisonActivity.kt | 4 +- .../dev/gf2log/app/WeeklyReportActivity.kt | 102 +++++++++++---- .../dev/gf2log/app/WeeklySettingsActivity.kt | 4 +- .../app/WeeklyTableHistoryPreviewActivity.kt | 6 +- app/src/main/res/drawable/bg_dialog.xml | 14 +++ app/src/main/res/values-ko/strings.xml | 6 + app/src/main/res/values-night/styles.xml | 11 ++ app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/styles.xml | 11 ++ docs/UI_DESIGN_SYSTEM.md | 1 + 22 files changed, 338 insertions(+), 149 deletions(-) create mode 100644 app/src/main/java/dev/gf2log/app/ModernFeedback.kt create mode 100644 app/src/main/res/drawable/bg_dialog.xml 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..eda7fda 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,50 @@ 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)) + addView(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) } + }, 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) + }) + } + private fun confirmFullRestore() { if (CaptureStatus.isRunning) { showBackupMessage(R.string.stop_capture_before_backup) @@ -505,7 +528,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 +633,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 + + +