Close the gap between the flashcards study screen and the mockup

Add real session-history navigation (Previous/Next/Shuffle), compact
side panels, and a segmented progress row to match the mockup layout.
Flashcards now opens straight into Study, with library management
moved into the Options menu ("Manage Flashcards") and Export hidden
from the UI (Import stays). Shuffle is backed by a real, due-before-new
randomized pick on the backend, not a decorative toggle.
This commit is contained in:
hz
2026-07-22 00:43:52 -05:00
parent 70aa425fde
commit 09b670d5d4
21 changed files with 555 additions and 70 deletions
@@ -102,4 +102,76 @@ public sealed class FlashcardSchedulingTests
Assert.Equal(1, FlashcardScheduling.ComputeStreakDays(timestamps, Today));
}
[Fact]
public void PickIndex_NonShuffle_ReturnsFirstNonExcluded()
{
var pool = new List<string> { "a", "b", "c" };
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string> { "a" }, shuffle: false, n => throw new InvalidOperationException("should not randomize"));
Assert.Equal(1, picked);
}
[Fact]
public void PickIndex_NonShuffle_EverythingExcluded_ReturnsNull()
{
var pool = new List<string> { "a", "b" };
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string> { "a", "b" }, shuffle: false, n => 0);
Assert.Null(picked);
}
[Fact]
public void PickIndex_EmptyPool_ReturnsNullRegardlessOfShuffle()
{
Assert.Null(FlashcardScheduling.PickIndex([], new HashSet<string>(), shuffle: false, n => 0));
Assert.Null(FlashcardScheduling.PickIndex([], new HashSet<string>(), shuffle: true, n => 0));
}
[Fact]
public void PickIndex_Shuffle_StubReturningFirstCandidate_PicksFirstNonExcluded()
{
var pool = new List<string> { "a", "b", "c" };
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string> { "a" }, shuffle: true, n => 0);
// Candidates (excluding "a") are ["b", "c"]; stub index 0 -> "b" -> original pool index 1.
Assert.Equal(1, picked);
}
[Fact]
public void PickIndex_Shuffle_CanReturnSomethingOtherThanTheOldestCandidate()
{
var pool = new List<string> { "a", "b", "c" };
// Stub picks the last candidate instead of always the first (oldest), proving this isn't
// hardcoded to FirstOrDefault-style behavior.
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string>(), shuffle: true, n => n - 1);
Assert.Equal(2, picked);
}
[Fact]
public void PickIndex_Shuffle_ExcludedItemsDoNotShiftTheOriginalIndexMapping()
{
var pool = new List<string> { "a", "b", "c", "d" };
// Candidates (excluding "b") are ["a", "c", "d"] at original indices [0, 2, 3]; stub
// index 2 should map back to original index 3 ("d"), not 2 ("c").
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string> { "b" }, shuffle: true, n => 2);
Assert.Equal(3, picked);
}
[Fact]
public void PickIndex_Shuffle_EverythingExcluded_ReturnsNull()
{
var pool = new List<string> { "a", "b" };
var picked = FlashcardScheduling.PickIndex(pool, new HashSet<string> { "a", "b" }, shuffle: true, n => 0);
Assert.Null(picked);
}
}