using CommunityToolkit.Mvvm.ComponentModel; namespace ClassHelper.ClassIslandPlugin.Models; /// /// 插件设置。 /// /// 存储由 ClassIsland 负责:插件入口把它保存在 PluginConfigFolder/Settings.json /// (**不要**放在插件安装目录,那里不会随档案备份、升级时还会被清掉)。 /// 因此这里必须继承 并让每个属性都走 SetProperty: /// ClassIsland 通过监听 PropertyChanged 来即时保存。 /// public class PluginSettings : ObservableObject { private string _serverUrl = "http://127.0.0.1:4000"; private string _deviceToken = ""; private string _deviceKey = ""; private bool _autoReport = true; private int _reportIntervalSeconds = 60; private bool _uploadSchedule = true; private bool _mirrorSchedule = false; private bool _receiveNotifications = true; private int _notificationDurationSeconds = 8; private int _notificationPollSeconds = 10; private bool _speechEnabled = false; private bool _showDebugInfo = false; /// /// 班级小助手后端地址,例如 http://127.0.0.1:4000。 /// public string ServerUrl { get => _serverUrl; set => SetProperty(ref _serverUrl, value); } /// /// 设备令牌(形如 chci_...):在班级小助手 Web 端「ClassIsland 联动」页面生成。 /// public string DeviceToken { get => _deviceToken; // 在 setter 里就去掉首尾空白:粘贴令牌时带前导空格非常常见, // 而 Normalize() 只在启动加载时跑一次 —— 运行期粘进来的脏值会让 IsConfigured // 一直为 false,用户看着明明填好了却查不出原因。 set => SetProperty(ref _deviceToken, (value ?? "").Trim()); } /// /// 本机标识(首次生成后固定):服务端据此把同一台机器识别为同一设备,重装插件不会产生重复设备。 /// public string DeviceKey { get => _deviceKey; set => SetProperty(ref _deviceKey, value); } /// /// 是否自动上报:启动后、课表变化时、以及每隔 秒各上报一次。 /// public bool AutoReport { get => _autoReport; set => SetProperty(ref _autoReport, value); } /// /// 定时上报间隔(秒),最小 15 秒。 /// public int ReportIntervalSeconds { get => _reportIntervalSeconds; set => SetProperty(ref _reportIntervalSeconds, Math.Clamp(value, 15, 3600)); } /// /// 是否把 ClassIsland 的课表同步到班级小助手。 /// 关闭后仍会上报"当前上什么课"的状态,只是不再写课表。 /// public bool UploadSchedule { get => _uploadSchedule; set => SetProperty(ref _uploadSchedule, value); } /// /// 是否把班级小助手上排好的课表镜像回 ClassIsland /// (新建一份名为「班级小助手-<班级名>」的档案课表,不覆盖老师原有的课表)。 /// public bool MirrorSchedule { get => _mirrorSchedule; set => SetProperty(ref _mirrorSchedule, value); } /// /// 是否接收老师在班级小助手上发布的提醒,并在 ClassIsland 上弹出。 /// public bool ReceiveNotifications { get => _receiveNotifications; set => SetProperty(ref _receiveNotifications, value); } /// /// 提醒轮询间隔(秒),最小 5 秒。 /// /// 插件与后端之间没有长连接(刻意不引入 Socket 依赖),提醒是"轮询取回"的, /// 因此这个值就是**老师按下发送到教室弹出之间的最大延迟**。 /// 单独一个轻量轮询(只查待提醒,不上传课表)比把上报间隔改小划算得多。 /// public int NotificationPollSeconds { get => _notificationPollSeconds; set => SetProperty(ref _notificationPollSeconds, Math.Clamp(value, 5, 600)); } /// /// 默认提醒显示时长(秒);老师在下发时可以单独指定更长的时长。 /// public int NotificationDurationSeconds { get => _notificationDurationSeconds; set => SetProperty(ref _notificationDurationSeconds, Math.Clamp(value, 1, 120)); } /// /// 提醒弹出时是否语音朗读(沿用 ClassIsland 的语音设置与语音提供方)。 /// public bool SpeechEnabled { get => _speechEnabled; set => SetProperty(ref _speechEnabled, value); } /// /// 是否在设置页显示调试信息(最后一次上报结果、课表条目数等),排查问题时打开。 /// public bool ShowDebugInfo { get => _showDebugInfo; set => SetProperty(ref _showDebugInfo, value); } /// /// 兼容旧配置:反序列化后把 DeviceKey 补齐(首次运行时由服务生成)。 /// public void Normalize() { ServerUrl = (ServerUrl ?? "").Trim().TrimEnd('/'); DeviceToken = (DeviceToken ?? "").Trim(); DeviceKey = (DeviceKey ?? "").Trim(); ReportIntervalSeconds = Math.Clamp(ReportIntervalSeconds, 15, 3600); NotificationDurationSeconds = Math.Clamp(NotificationDurationSeconds, 1, 120); NotificationPollSeconds = Math.Clamp(NotificationPollSeconds, 5, 600); } /// 是否已经配置好(地址 + 令牌都填了)。 public bool IsConfigured => TryGetServerUri(out _) && DeviceToken.StartsWith("chci_", StringComparison.Ordinal); /// /// 解析服务器地址,**只接受 http/https 绝对地址**。 /// /// 不用裸的 `Uri.TryCreate(..., UriKind.Absolute)`:它会接受 `file://` / `ftp://`, /// 那种地址会在发请求时抛一句难以理解的 NotSupportedException,用户根本猜不到是地址填错了。 /// public bool TryGetServerUri(out Uri uri) { uri = null!; if (!Uri.TryCreate((ServerUrl ?? "").Trim(), UriKind.Absolute, out var parsed)) return false; if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps) return false; uri = parsed; return true; } }