Compare commits
12 Commits
v0.3.2
...
26628dc0bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 26628dc0bb | |||
| d2e3832ef2 | |||
| 93857135b3 | |||
| 36beb2d0ad | |||
| 976d496d21 | |||
| 245f1db536 | |||
| 623e533547 | |||
| 2e50356f81 | |||
| e6f503c02a | |||
| c53511196d | |||
| a8595e26b4 | |||
| 411e27659f |
@@ -370,11 +370,13 @@ jobs:
|
|||||||
# release. Needs the CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
|
# release. Needs the CODEBERG_RELEASE_TOKEN secret; skips cleanly if unset.
|
||||||
- name: Publish release to Codeberg
|
- name: Publish release to Codeberg
|
||||||
if: env.IS_RELEASE == 'true'
|
if: env.IS_RELEASE == 'true'
|
||||||
continue-on-error: true
|
# NOT continue-on-error: this step reported green through 0.2.1, 0.2.2,
|
||||||
|
# 0.3.0, 0.3.1 and 0.3.2 while never once publishing, which is how a
|
||||||
|
# crash-fix release reached F-Droid but not the Codeberg/Obtainium
|
||||||
|
# users who needed it. A broken mirror must fail the release loudly.
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
TOKEN: ${{ secrets.CODEBERG_RELEASE_TOKEN }}
|
||||||
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
|
API: https://codeberg.org/api/v1/repos/jlmakiola/agendula
|
||||||
SHA: ${{ github.sha }}
|
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
if [ -z "${TOKEN:-}" ]; then
|
if [ -z "${TOKEN:-}" ]; then
|
||||||
@@ -399,29 +401,39 @@ jobs:
|
|||||||
sed -i -e '/./,$!d' release-notes.md
|
sed -i -e '/./,$!d' release-notes.md
|
||||||
fi
|
fi
|
||||||
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
|
[ -s release-notes.md ] || echo "_See CHANGELOG.md for ${VERSION}._" > release-notes.md
|
||||||
# Forgejo 500s on POST /releases when the tag ALREADY exists, and a
|
# Never mint the tag here. Gitea's push mirror owns getting it to
|
||||||
# bare tag is not a release — so GET /releases/tags then 404s and the
|
# Codeberg; this step's only job is to attach a release to a tag that
|
||||||
# upsert has no id to fall back on ("Could not resolve Codeberg
|
# has already landed. That split matters because every way of creating
|
||||||
# release id", the 0.3.1 failure). Pushing the tag first therefore
|
# a tag from here — git push, or a release POST carrying
|
||||||
# guarantees the 500 rather than avoiding it; 0.3.0 published because
|
# target_commitish for a tag Codeberg lacks — is a ref WRITE, and ref
|
||||||
# it POSTed while the tag was still absent. So: POST with the tag
|
# writes are what fail on this repo ("cannot lock references" on push,
|
||||||
# ABSENT and let the API mint tag + release together from
|
# an empty-bodied 500 on the API). Attaching to a tag that is already
|
||||||
# target_commitish. Branches mirror reliably, so the commit is already
|
# present needs no ref write and succeeds.
|
||||||
# on Codeberg — only a tag the mirror raced in ahead of us is in the
|
#
|
||||||
# way, and clearing it is safe precisely because no release owns it.
|
# So: wait for the mirror, verify, then attach. If the tag never shows
|
||||||
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
# up, fail — do NOT fall back to creating it, which is what produced
|
||||||
if [ -z "$ID" ]; then
|
# the silent breakage across 0.2.1 through 0.3.2.
|
||||||
curl -s -o /dev/null -w "codeberg tag DELETE HTTP %{http_code}\n" -X DELETE \
|
TAG_OK=""
|
||||||
-H "Authorization: token $TOKEN" "$API/tags/$TAG"
|
for i in $(seq 1 30); do
|
||||||
|
if [ "$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token $TOKEN" "$API/tags/$TAG")" = "200" ]; then
|
||||||
|
TAG_OK=1; echo "Codeberg has $TAG (after ~$((i*10))s)"; break
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
if [ -z "$TAG_OK" ]; then
|
||||||
|
echo "Codeberg never received $TAG from the push mirror (waited 300s)." >&2
|
||||||
|
echo "Not creating it here: ref writes to this repo fail, so that" >&2
|
||||||
|
echo "would 500. Check the mirror, then re-run once the tag is there." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
python3 - "$TAG" "$SHA" "$PRERELEASE" <<'PY' > cb-payload.json
|
# No target_commitish: the tag exists, so the API must attach to it
|
||||||
|
# rather than resolve a commit and mint one.
|
||||||
|
python3 - "$TAG" "$PRERELEASE" <<'PY' > cb-payload.json
|
||||||
import json, sys
|
import json, sys
|
||||||
tag, sha, pre = sys.argv[1:4]
|
tag, pre = sys.argv[1:3]
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
# Recreate the tag as part of the release; the commit is on
|
|
||||||
# Codeberg already via the branch mirror.
|
|
||||||
"target_commitish": sha,
|
|
||||||
"name": tag,
|
"name": tag,
|
||||||
"body": open("release-notes.md").read(),
|
"body": open("release-notes.md").read(),
|
||||||
"draft": False,
|
"draft": False,
|
||||||
@@ -429,9 +441,9 @@ jobs:
|
|||||||
"prerelease": pre == "true",
|
"prerelease": pre == "true",
|
||||||
}))
|
}))
|
||||||
PY
|
PY
|
||||||
# Upsert (re-run safe): a release that already exists is PATCHed in
|
# Upsert (re-run safe): a release already attached to this tag is
|
||||||
# place — the delete above is skipped in that case, so re-running
|
# PATCHed in place, so re-running never disturbs a published release.
|
||||||
# never disturbs a published release.
|
ID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | jq -r '.id // empty')
|
||||||
if [ -n "$ID" ]; then
|
if [ -n "$ID" ]; then
|
||||||
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
curl -s -o /dev/null -w "release PATCH HTTP %{http_code}\n" -X PATCH \
|
||||||
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import de.jeanlucmakiola.agendula.domain.TaskFormField
|
|||||||
import de.jeanlucmakiola.floret.reminders.ReminderOverride
|
import de.jeanlucmakiola.floret.reminders.ReminderOverride
|
||||||
import de.jeanlucmakiola.floret.reminders.ReminderOverrideCodec
|
import de.jeanlucmakiola.floret.reminders.ReminderOverrideCodec
|
||||||
import de.jeanlucmakiola.floret.reminders.applyReminderOverride
|
import de.jeanlucmakiola.floret.reminders.applyReminderOverride
|
||||||
import de.jeanlucmakiola.floret.reminders.reminderLeadFor
|
import de.jeanlucmakiola.floret.reminders.reminderLeadsFor
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -39,15 +39,17 @@ data class Settings(
|
|||||||
val bottomAddBar: Boolean = false,
|
val bottomAddBar: Boolean = false,
|
||||||
/**
|
/**
|
||||||
* Per-list overrides of [reminderLeadMinutes]: a list present in the map
|
* Per-list overrides of [reminderLeadMinutes]: a list present in the map
|
||||||
* overrides the global default (a null value = no reminder); absent = inherit.
|
* overrides the global default (an empty list = no reminder); absent =
|
||||||
|
* inherit. Agendula offers a single reminder, so each override is a
|
||||||
|
* one-element (or empty) list.
|
||||||
*/
|
*/
|
||||||
val perListReminderOverride: Map<Long, Int?> = emptyMap(),
|
val perListReminderOverride: Map<Long, List<Int>> = emptyMap(),
|
||||||
/** Optional edit-form fields shown by default; the rest sit behind "More fields". */
|
/** Optional edit-form fields shown by default; the rest sit behind "More fields". */
|
||||||
val defaultEditFields: Set<TaskFormField> = emptySet(),
|
val defaultEditFields: Set<TaskFormField> = emptySet(),
|
||||||
) {
|
) {
|
||||||
/** The lead time for a task in [listId]: its override if set, else the global default. */
|
/** The lead time for a task in [listId]: its override if set, else the global default. */
|
||||||
fun reminderLeadFor(listId: Long): Int? =
|
fun reminderLeadFor(listId: Long): Int? =
|
||||||
perListReminderOverride.reminderLeadFor(listId, reminderLeadMinutes)
|
perListReminderOverride.reminderLeadsFor(listId, listOf(reminderLeadMinutes)).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** App preferences, backed by DataStore. Mirrors Calendula's prefs shape. */
|
/** App preferences, backed by DataStore. Mirrors Calendula's prefs shape. */
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ fun ReminderLeadPicker(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
presets: List<Int> = REMINDER_PRESETS,
|
presets: List<Int> = REMINDER_PRESETS,
|
||||||
) {
|
) {
|
||||||
val selectedMinutes = (selected as? ReminderOverride.Minutes)?.minutes
|
// Agendula is single-reminder: an override carries a one-element list, so
|
||||||
|
// take the single value for this single-select picker.
|
||||||
|
val selectedMinutes = (selected as? ReminderOverride.Minutes)?.minutes?.firstOrNull()
|
||||||
val customSelected = selectedMinutes != null && selectedMinutes !in presets
|
val customSelected = selectedMinutes != null && selectedMinutes !in presets
|
||||||
val seed = decomposeReminderMinutes(selectedMinutes?.takeIf { customSelected })
|
val seed = decomposeReminderMinutes(selectedMinutes?.takeIf { customSelected })
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ fun ReminderLeadPicker(
|
|||||||
val options = buildList {
|
val options = buildList {
|
||||||
if (allowInherit) add(ReminderOverride.Inherit)
|
if (allowInherit) add(ReminderOverride.Inherit)
|
||||||
if (allowNone) add(ReminderOverride.None)
|
if (allowNone) add(ReminderOverride.None)
|
||||||
presets.forEach { add(ReminderOverride.Minutes(it)) }
|
presets.forEach { add(ReminderOverride.Minutes(listOf(it))) }
|
||||||
}
|
}
|
||||||
val rowCount = options.size + 1 // + the custom row
|
val rowCount = options.size + 1 // + the custom row
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ fun ReminderLeadPicker(
|
|||||||
unit = unit,
|
unit = unit,
|
||||||
onUnitChange = { unit = it },
|
onUnitChange = { unit = it },
|
||||||
onConfirm = { minutes ->
|
onConfirm = { minutes ->
|
||||||
onSelect(ReminderOverride.Minutes(minutes))
|
onSelect(ReminderOverride.Minutes(listOf(minutes)))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -168,5 +170,5 @@ private fun CustomReminderEditor(
|
|||||||
private fun reminderOverrideLabel(override: ReminderOverride): String = when (override) {
|
private fun reminderOverrideLabel(override: ReminderOverride): String = when (override) {
|
||||||
ReminderOverride.Inherit -> stringResource(R.string.reminder_use_default)
|
ReminderOverride.Inherit -> stringResource(R.string.reminder_use_default)
|
||||||
ReminderOverride.None -> stringResource(R.string.reminder_none)
|
ReminderOverride.None -> stringResource(R.string.reminder_none)
|
||||||
is ReminderOverride.Minutes -> reminderLeadTimeLabel(override.minutes)
|
is ReminderOverride.Minutes -> reminderLeadTimeLabel(override.minutes.first())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import de.jeanlucmakiola.floret.components.positionOf
|
|||||||
import de.jeanlucmakiola.floret.identity.collapseExit
|
import de.jeanlucmakiola.floret.identity.collapseExit
|
||||||
import de.jeanlucmakiola.floret.identity.expandEnter
|
import de.jeanlucmakiola.floret.identity.expandEnter
|
||||||
import de.jeanlucmakiola.floret.reminders.ReminderOverride
|
import de.jeanlucmakiola.floret.reminders.ReminderOverride
|
||||||
|
import de.jeanlucmakiola.floret.reminders.reminderOverrideFor
|
||||||
import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel
|
import de.jeanlucmakiola.agendula.ui.common.reminderLeadTimeLabel
|
||||||
|
|
||||||
/** The settings sub-screens reached from the hub's category rows. */
|
/** The settings sub-screens reached from the hub's category rows. */
|
||||||
@@ -483,10 +484,10 @@ private fun RemindersScreen(
|
|||||||
if (showOffset) {
|
if (showOffset) {
|
||||||
ReminderLeadPicker(
|
ReminderLeadPicker(
|
||||||
title = stringResource(R.string.settings_default_reminder),
|
title = stringResource(R.string.settings_default_reminder),
|
||||||
selected = ReminderOverride.Minutes(state.settings.reminderLeadMinutes),
|
selected = ReminderOverride.Minutes(listOf(state.settings.reminderLeadMinutes)),
|
||||||
allowInherit = false,
|
allowInherit = false,
|
||||||
allowNone = false,
|
allowNone = false,
|
||||||
onSelect = { if (it is ReminderOverride.Minutes) viewModel.setReminderLeadMinutes(it.minutes) },
|
onSelect = { if (it is ReminderOverride.Minutes) viewModel.setReminderLeadMinutes(it.minutes.first()) },
|
||||||
onDismiss = { showOffset = false },
|
onDismiss = { showOffset = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -504,14 +505,8 @@ private fun RemindersScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The stored override for [listId], as a picker choice (absent → inherit). */
|
/** The stored override for [listId], as a picker choice (absent → inherit). */
|
||||||
private fun listOverrideChoice(state: SettingsUiState, listId: Long): ReminderOverride {
|
private fun listOverrideChoice(state: SettingsUiState, listId: Long): ReminderOverride =
|
||||||
val map = state.settings.perListReminderOverride
|
state.settings.perListReminderOverride.reminderOverrideFor(listId)
|
||||||
return when {
|
|
||||||
!map.containsKey(listId) -> ReminderOverride.Inherit
|
|
||||||
map[listId] == null -> ReminderOverride.None
|
|
||||||
else -> ReminderOverride.Minutes(map.getValue(listId)!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Row summary for a list: its override, or the inherited global default. */
|
/** Row summary for a list: its override, or the inherited global default. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -519,7 +514,7 @@ private fun listOverrideSummary(choice: ReminderOverride, globalDefault: Int): S
|
|||||||
ReminderOverride.Inherit ->
|
ReminderOverride.Inherit ->
|
||||||
stringResource(R.string.settings_list_reminder_inherits, reminderLeadTimeLabel(globalDefault))
|
stringResource(R.string.settings_list_reminder_inherits, reminderLeadTimeLabel(globalDefault))
|
||||||
ReminderOverride.None -> stringResource(R.string.reminder_none)
|
ReminderOverride.None -> stringResource(R.string.reminder_none)
|
||||||
is ReminderOverride.Minutes -> reminderLeadTimeLabel(choice.minutes)
|
is ReminderOverride.Minutes -> reminderLeadTimeLabel(choice.minutes.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Submodule floret-kit updated: 566caf4305...5a576c4d28
5
release-notes.md
Normal file
5
release-notes.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
### Fixed
|
||||||
|
- Agendula no longer crashes on launch. Every 0.3.0 install was affected: the
|
||||||
|
release build stripped a constructor that the background-work scheduler needs
|
||||||
|
to open its database, and that happens before the app draws anything.
|
||||||
|
|
||||||
Reference in New Issue
Block a user