Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion SecRandom.Core.Tests/ClassIslandScheduleSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,58 @@ public async Task Refresh_WhenPreClassEnableWindowOpens_UnrestrictsWithoutNotify
Assert.Equal(notifications, harness.StateChangedCount);
}

[Fact]
public async Task OnClass_OutdoorSubject_RestrictsWhenEnabled()
{
var harness = new LinkageHarness(instantDrawDisable: false, disableOnOutdoorCourse: true);
harness.Lessons.CurrentState = TimeState.OnClass;
harness.Lessons.CurrentSubject = new Subject { Name = "体育", IsOutDoor = true };
await harness.RefreshAsync();

Assert.True(harness.Snapshot.IsAvailable, $"error={harness.Snapshot.Error}");
Assert.True(harness.Snapshot.IsOutdoorCourse);
Assert.True(harness.OutdoorRestricted);
// 非上课时段限制与户外课程限制是两个独立开关
Assert.False(harness.Restricted);
}

[Fact]
public async Task OnClass_IndoorSubject_NotRestricted()
{
var harness = new LinkageHarness(instantDrawDisable: false, disableOnOutdoorCourse: true);
harness.Lessons.CurrentState = TimeState.OnClass;
harness.Lessons.CurrentSubject = new Subject { Name = "数学", IsOutDoor = false };
await harness.RefreshAsync();

Assert.False(harness.Snapshot.IsOutdoorCourse);
Assert.False(harness.OutdoorRestricted);
}

[Fact]
public async Task OnClass_OutdoorSubject_SettingOff_NotRestricted()
{
var harness = new LinkageHarness();
harness.Lessons.CurrentState = TimeState.OnClass;
harness.Lessons.CurrentSubject = new Subject { Name = "体育", IsOutDoor = true };
await harness.RefreshAsync();

Assert.True(harness.Snapshot.IsOutdoorCourse);
Assert.False(harness.OutdoorRestricted);
}

[Fact]
public async Task Break_OutdoorFlagFromBreakLabel_NotRestricted()
{
var harness = new LinkageHarness(instantDrawDisable: false, disableOnOutdoorCourse: true);
harness.Lessons.CurrentState = TimeState.Breaking;
harness.Lessons.CurrentSubject = new Subject { Name = "课间休息", IsOutDoor = true };
await harness.RefreshAsync();

// 课间的 CurrentSubject 是课间标签而非科目,户外标记必须忽略
Assert.False(harness.Snapshot.IsOutdoorCourse);
Assert.False(harness.OutdoorRestricted);
}

private static TimeLayoutItem ClassTime(DateTime now, TimeSpan startedAgo, TimeSpan endedAgo) => new()
{
TimeType = 0,
Expand All @@ -180,11 +232,13 @@ private sealed class LinkageHarness
public LinkageHarness(
int preClassEnableSeconds = 0,
int postClassDelaySeconds = 0,
bool instantDrawDisable = true)
bool instantDrawDisable = true,
bool disableOnOutdoorCourse = false)
{
var config = new MainConfigModel();
config.LinkageSettings.DataSource = LinkageDataSource.ClassIsland;
config.LinkageSettings.InstantDrawDisable = instantDrawDisable;
config.LinkageSettings.DisableOnOutdoorCourse = disableOnOutdoorCourse;
config.LinkageSettings.PreClassEnableTime = preClassEnableSeconds;
config.LinkageSettings.PostClassDisableDelay = postClassDelaySeconds;

Expand Down Expand Up @@ -215,6 +269,7 @@ public LinkageHarness(
public int StateChangedCount { get; private set; }
public CourseScheduleSnapshot Snapshot => Service.Snapshot;
public bool Restricted => Service.IsConfirmedNonClassTime;
public bool OutdoorRestricted => Service.IsConfirmedOutdoorCourse;

public Task RefreshAsync() => Service.RefreshAsync();
}
Expand Down
4 changes: 3 additions & 1 deletion SecRandom.Core/Models/Linkage/CourseModels.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace SecRandom.Core.Models.Linkage;
namespace SecRandom.Core.Models.Linkage;

public enum CourseTimeState
{
Expand Down Expand Up @@ -26,6 +26,7 @@ public sealed record CourseScheduleSnapshot(
TimeSpan? TimeSincePreviousCourseEnd,
string Source,
string Version,
bool IsOutdoorCourse = false,
string? Error = null)
{
public static CourseScheduleSnapshot Unavailable(string source, string? error = null) => new(
Expand All @@ -39,5 +40,6 @@ public sealed record CourseScheduleSnapshot(
null,
source,
string.Empty,
false,
error);
}
2 changes: 2 additions & 0 deletions SecRandom.Core/Models/SubConfigs/LinkageSettingsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ public partial class LinkageSettingsConfig : ObservableObject
{
[ObservableProperty] private bool _verificationRequired = true;
[ObservableProperty] private bool _instantDrawDisable = false;
[ObservableProperty] private bool _disableOnOutdoorCourse = false;
[ObservableProperty] private LinkageDataSource _dataSource = LinkageDataSource.Off;
[ObservableProperty] private bool _hideFloatingWindowOnClassEnd = false;
[ObservableProperty] private bool _hideFloatingWindowOnOutdoorCourse = false;
[ObservableProperty] private bool _preClassResetEnabled = false;
[ObservableProperty] private int _preClassResetTime = 120;
[ObservableProperty] private int _preClassEnableTime = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ private static readonly (string Key, CultureInfo[] Fallback)[] PublishedLanguage
["instant_draw_disable"] = R(Linkage, "S_External_InstantDrawDisable"),
["verification_required"] = R(Linkage, "S_External_VerificationRequired"),
["hide_floating_window_on_class_end"] = R(Linkage, "S_ClassTime_HideFloatingWindow"),
["hide_floating_window_on_outdoor_course"] = R(Linkage, "S_ClassTime_HideFloatingWindowOnOutdoorCourse"),
["disable_on_outdoor_course"] = R(Linkage, "S_External_OutdoorCourse"),
["pre_class_reset_enabled"] = R(Linkage, "S_ClassTime_PreClassReset"),
["pre_class_reset_time"] = R(Linkage, "S_ClassTime_PreClassResetTime"),
["post_class_disable_delay"] = R(Linkage, "S_ClassTime_PostClassDisableDelay"),
Expand Down
40 changes: 38 additions & 2 deletions SecRandom/Langs/SettingsPages/Linkage/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions SecRandom/Langs/SettingsPages/Linkage/Resources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
<data name="S_External_InstantDrawDisable_D">
<value>Restrict drawing and reset when course data confirms a break or non-class period; disable for no restriction</value>
</data>
<data name="S_External_OutdoorCourse">
<value>Disable draws during outdoor courses</value>
</data>
<data name="S_External_OutdoorCourse_D">
<value>Reads each subject's outdoor-course flag from the ClassIsland timetable: while the current course is marked outdoor, drawing and resetting stay limited even during class; shares the verification exception with the non-class-time restriction</value>
</data>
<data name="S_External_DataSource">
<value>Data source</value>
</data>
Expand Down Expand Up @@ -75,6 +81,12 @@
<data name="S_ClassTime_HideFloatingWindow_D">
<value>Automatically hide the floating window when class ends</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse">
<value>Hide floating window on outdoor courses</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse_D">
<value>Automatically hide the floating window when the current course is marked as an outdoor course</value>
</data>
<data name="S_ClassTime_PreClassReset">
<value>Reset before class</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions SecRandom/Langs/SettingsPages/Linkage/Resources.ja-JP.resx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
<data name="S_External_InstantDrawDisable_D">
<value>休み時間や授業外と確認された場合に抽選とリセットを制限します</value>
</data>
<data name="S_External_OutdoorCourse">
<value>授業時間中は屋外科目で抽選を無効化</value>
</data>
<data name="S_External_OutdoorCourse_D">
<value>ClassIsland 時間割の科目についている屋外フラグを読み取り、現在の授業が屋外とマークされている場合は授業時間中でも抽選とリセットを制限します。検証の例外は「授業時間外の操作制限」と共通です</value>
</data>
<data name="S_External_DataSource">
<value>データソース</value>
</data>
Expand Down Expand Up @@ -75,6 +81,12 @@
<data name="S_ClassTime_HideFloatingWindow_D">
<value>授業終了時に自動で非表示にします</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse">
<value>屋外授業中は非表示</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse_D">
<value>現在の授業が屋外授業の場合、フローティングウィンドウを自動で非表示にします</value>
</data>
<data name="S_ClassTime_PreClassReset">
<value>授業前にリセット</value>
</data>
Expand Down
12 changes: 12 additions & 0 deletions SecRandom/Langs/SettingsPages/Linkage/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<data name="S_External_InstantDrawDisable_D" xml:space="preserve">
<value>课程数据源明确显示处于课间或非上课时段时,限制抽取和重置;关闭后不作限制</value>
</data>
<data name="S_External_OutdoorCourse" xml:space="preserve">
<value>户外课程上课时段禁用</value>
</data>
<data name="S_External_OutdoorCourse_D" xml:space="preserve">
<value>读取 ClassIsland 课表科目的户外课程标记,当前课程被标记为户外课程时,即使处于上课时段也限制抽取和重置</value>
</data>
<data name="S_External_DataSource" xml:space="preserve">
<value>数据源</value>
</data>
Expand Down Expand Up @@ -90,6 +96,12 @@
<data name="S_ClassTime_HideFloatingWindow_D" xml:space="preserve">
<value>课程结束时自动隐藏悬浮窗</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse" xml:space="preserve">
<value>户外课程隐藏悬浮窗</value>
</data>
<data name="S_ClassTime_HideFloatingWindowOnOutdoorCourse_D" xml:space="preserve">
<value>上课时段当前课程为户外课程时自动隐藏悬浮窗</value>
</data>
<data name="S_ClassTime_PreClassReset" xml:space="preserve">
<value>课前重置</value>
</data>
Expand Down
7 changes: 6 additions & 1 deletion SecRandom/Services/Linkage/ClassIslandScheduleSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public async Task<CourseScheduleSnapshot> GetSnapshotAsync(CancellationToken can
var currentName = state == CourseTimeState.OnClass
? NormalizeSubjectName(lessons.CurrentSubject?.Name)
: string.Empty;
// 户外课程标记只在真实科目上有效;课间(CurrentSubject 是课间标签)与后备科目都不算
var isOutdoorCourse = state == CourseTimeState.OnClass
&& !string.IsNullOrEmpty(currentName)
&& lessons.CurrentSubject?.IsOutDoor == true;
var nextName = NormalizeSubjectName(lessons.NextClassSubject?.Name);
var now = DateTime.Now;
if (!string.IsNullOrEmpty(currentName))
Expand Down Expand Up @@ -101,7 +105,8 @@ public async Task<CourseScheduleSnapshot> GetSnapshotAsync(CancellationToken can
nextCourseIn,
sincePreviousEnd,
SourceName,
$"{lessons.CurrentSelectedIndex}:{lessons.CurrentState}");
$"{lessons.CurrentSelectedIndex}:{lessons.CurrentState}",
isOutdoorCourse);
}
catch (Exception exception)
{
Expand Down
17 changes: 17 additions & 0 deletions SecRandom/Services/Linkage/CourseLinkageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ private static bool SnapshotsEqual(CourseScheduleSnapshot a, CourseScheduleSnaps
&& a.State == b.State
&& a.Source == b.Source
&& a.Version == b.Version
&& a.IsOutdoorCourse == b.IsOutdoorCourse
&& a.CurrentCourse?.Name == b.CurrentCourse?.Name
&& a.PreviousCourse?.Name == b.PreviousCourse?.Name
&& a.NextCourse?.Name == b.NextCourse?.Name
Expand All @@ -108,6 +109,22 @@ private static bool SnapshotsEqual(CourseScheduleSnapshot a, CourseScheduleSnaps
public bool IsConfirmedNonClassTime => Settings.InstantDrawDisable
&& IsConfirmedBreakTime;

/// <summary>
/// 户外课程上课时段禁用:只有 ClassIsland 数据源携带科目的户外课程标记。开启后当前
/// 课程被标记为户外课程时,即使处于上课时段也限制抽取和重置,验证例外与
/// <see cref="IsConfirmedNonClassTime" /> 共用。
/// </summary>
public bool IsConfirmedOutdoorCourse => Settings.DisableOnOutdoorCourse
&& IsOutdoorCourseOnClass;

/// <summary>
/// 当前处于上课时段且课程被标记为户外课程的纯状态判断,不依赖任何开关;
/// 抽取限制看 <see cref="IsConfirmedOutdoorCourse" />,悬浮窗隐藏由调用方结合自己的开关判断。
/// </summary>
public bool IsOutdoorCourseOnClass => _snapshot.IsAvailable
&& _snapshot.State == CourseTimeState.OnClass
&& _snapshot.IsOutdoorCourse;

public string GetSubjectFilter()
{
if (!Settings.SubjectHistoryFilterEnabled || !_snapshot.IsAvailable)
Expand Down
15 changes: 12 additions & 3 deletions SecRandom/Services/Linkage/LinkageDrawCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task<bool> AuthorizeAsync(
if (controlDrawGate.IsDrawLocked)
return false;

if (!linkageService.IsConfirmedNonClassTime)
if (!IsLinkageRestricted)
return await securityService.AuthorizeAsync(operations, action, cancellationToken).ConfigureAwait(false);

if (!linkageService.Settings.VerificationRequired)
Expand Down Expand Up @@ -79,17 +79,26 @@ public LinkageDrawGate EvaluateGate(IReadOnlyCollection<SecurityOperation> opera
if (controlDrawGate.IsDrawLocked)
return LinkageDrawGate.BlockedByRemoteLock;

if (!linkageService.IsConfirmedNonClassTime)
if (!IsLinkageRestricted)
return operations.Any(securityService.RequiresVerification)
? LinkageDrawGate.RequiresLocalVerification
: LinkageDrawGate.Allowed;

if (!linkageService.Settings.VerificationRequired)
return LinkageDrawGate.BlockedByClassTime;

// 确认非上课时间后的抽取要额外过 BypassClassTimeRestriction,而它永远需要当场验证。
// 联动限制后的抽取要额外过 BypassClassTimeRestriction,而它永远需要当场验证。
// 户外课程限制沿用 BlockedByClassTime:它本就发生在上课时段,远程控制台的
// "上课时间"文案仍然准确。
return LinkageDrawGate.RequiresLocalVerification;
}

/// <summary>
/// 联动闸门是否在当前时刻限制抽取:非上课时段限制与上课时段户外课程限制共用同一套
/// 验证例外(<see cref="SecurityOperation.BypassClassTimeRestriction" />)。
/// </summary>
private bool IsLinkageRestricted => linkageService.IsConfirmedNonClassTime
|| linkageService.IsConfirmedOutdoorCourse;

public string GetCourseName() => linkageService.GetSubjectFilter();
}
7 changes: 5 additions & 2 deletions SecRandom/Views/FloatingWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,11 @@ public void SetUserVisibilityIntent(bool visible)

private void ApplyCourseLinkageVisibility()
{
var shouldHide = ViewModel.Config.LinkageSettings.HideFloatingWindowOnClassEnd
&& _linkageService.IsConfirmedBreakTime;
var linkageSettings = ViewModel.Config.LinkageSettings;
var shouldHide = (linkageSettings.HideFloatingWindowOnClassEnd
&& _linkageService.IsConfirmedBreakTime)
|| (linkageSettings.HideFloatingWindowOnOutdoorCourse
&& _linkageService.IsOutdoorCourseOnClass);
if (shouldHide == _hiddenByCourseLinkage)
return;

Expand Down
20 changes: 20 additions & 0 deletions SecRandom/Views/SettingsPages/Linkage/LinkageSettingsPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@
</fa:FASettingsExpander.Footer>
</fa:FASettingsExpander>

<fa:FASettingsExpander x:Name="S_External_OutdoorCourse"
Header="{x:Static lsp:Resources.S_External_OutdoorCourse}"
Description="{x:Static lsp:Resources.S_External_OutdoorCourse_D}"
IsVisible="{Binding IsOutdoorCourseRowVisible}"
IconSource="{sr:FluentIconSource {sr:Fi RunFilled}}">
<fa:FASettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding Settings.DisableOnOutdoorCourse}" />
</fa:FASettingsExpander.Footer>
</fa:FASettingsExpander>

<fa:FASettingsExpander x:Name="S_External_DataSource"
Header="{x:Static lsp:Resources.S_External_DataSource}"
Description="{x:Static lsp:Resources.S_External_DataSource_D}"
Expand Down Expand Up @@ -78,6 +88,16 @@
</fa:FASettingsExpander.Footer>
</fa:FASettingsExpander>

<fa:FASettingsExpander x:Name="S_ClassTime_HideFloatingWindowOnOutdoorCourse"
Header="{x:Static lsp:Resources.S_ClassTime_HideFloatingWindowOnOutdoorCourse}"
Description="{x:Static lsp:Resources.S_ClassTime_HideFloatingWindowOnOutdoorCourse_D}"
IsVisible="{Binding IsOutdoorCourseRowVisible}"
IconSource="{sr:FluentIconSource {sr:Fi WindowFilled}}">
<fa:FASettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding Settings.HideFloatingWindowOnOutdoorCourse}" />
</fa:FASettingsExpander.Footer>
</fa:FASettingsExpander>

<fa:FASettingsExpander x:Name="S_ClassTime_PreClassReset"
Header="{x:Static lsp:Resources.S_ClassTime_PreClassReset}"
Description="{x:Static lsp:Resources.S_ClassTime_PreClassReset_D}"
Expand Down
Loading
Loading