Adds two settings exposed in the PWA frame-settings sheet:
- rotationMode (enum: random | least_recently_shown | oldest_upload |
newest_upload). Default oldest_upload preserves the legacy
hard-coded sort, so existing devices behave identically until the
user changes it.
- prioritizeNeverShown (bool). When set, the candidate set is narrowed
to never-shown images first (if any exist) before the mode runs —
useful for "burn through new uploads before re-shuffling the catalog."
RotationService pipeline:
1. Pull approved/ready pool.
2. Drop the last `uniquenessWindow` served (existing).
3. If prioritizeNeverShown AND any candidates have never been served,
narrow to those.
4. Apply the selection mode.
Backend: enum, entity columns + accessors, migration, serializer,
PATCH validator. Frontend: types, stores, settings sheet section
(dropdown + checkbox), test fixtures, save-flow test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,29 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="home-view__sheet-field">
|
||||
<p class="home-view__sheet-label">Image selection</p>
|
||||
|
||||
<select
|
||||
class="home-view__mode-select"
|
||||
v-model="editRotationMode"
|
||||
aria-label="Image selection mode"
|
||||
>
|
||||
<option value="oldest_upload">Oldest upload first</option>
|
||||
<option value="newest_upload">Newest upload first</option>
|
||||
<option value="least_recently_shown">Least recently shown</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
|
||||
<label class="home-view__rotation-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="editPrioritizeNeverShown"
|
||||
/>
|
||||
<span>Show never-shown images first</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
class="home-view__sheet-save"
|
||||
@@ -465,6 +488,8 @@ const editFrequencyMode = ref<FrequencyMode>('interval')
|
||||
const editWakeTimes = ref<number[]>([])
|
||||
const editIntervalMinutes = ref<number>(60)
|
||||
const editTimezone = ref('UTC')
|
||||
const editRotationMode = ref<Device['rotationMode']>('oldest_upload')
|
||||
const editPrioritizeNeverShown = ref<boolean>(false)
|
||||
|
||||
// Default candidates tried (in order) when adding a new time slot — picks the
|
||||
// first one that isn't already in the list, so repeated +Add gives a sensible
|
||||
@@ -602,6 +627,8 @@ function onEdit(deviceId: number) {
|
||||
editIntervalMinutes.value = device.rotationIntervalMinutes
|
||||
editWakeTimes.value = [...device.wakeTimes]
|
||||
editFrequencyMode.value = device.wakeTimes.length > 0 ? 'times' : 'interval'
|
||||
editRotationMode.value = device.rotationMode
|
||||
editPrioritizeNeverShown.value = device.prioritizeNeverShown
|
||||
sheetOpen.value = true
|
||||
}
|
||||
|
||||
@@ -613,6 +640,8 @@ async function saveSettings() {
|
||||
name: editName.value.trim() || editingDevice.value.name,
|
||||
orientation: editOrientation.value,
|
||||
timezone: editTimezone.value,
|
||||
rotationMode: editRotationMode.value,
|
||||
prioritizeNeverShown: editPrioritizeNeverShown.value,
|
||||
}
|
||||
if (editFrequencyMode.value === 'times') {
|
||||
patch.wakeTimes = [...editWakeTimes.value]
|
||||
@@ -908,6 +937,24 @@ async function saveSettings() {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__rotation-checkbox {
|
||||
margin-top: var(--space-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
min-height: var(--touch-min);
|
||||
|
||||
input[type="checkbox"] {
|
||||
// Bigger than browser default so it remains a comfortable touch target.
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__propagation-note {
|
||||
margin-top: var(--space-1);
|
||||
font-size: var(--text-xs);
|
||||
|
||||
Reference in New Issue
Block a user