Skip to content
Open
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
2 changes: 1 addition & 1 deletion .flutter-plugins-dependencies

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
exclude:
- build/**
- windows/**
include: package:flutter_lints/flutter.yaml

linter:
Expand Down
34 changes: 24 additions & 10 deletions assets/plugins/calendar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,25 @@ lw.register({
var startDow = first.getDay();
var lead = mondayFirst ? (startDow + 6) % 7 : startDow;

// 固定 6 行 42 格:月份切换时高度不跳变
// 固定 6 行 42 格:月份切换时高度不跳变。
// 日期格不能写死 36px:3x3/4x3 小卡片的网格行高可能只有 20~30px,
// 固定高度会触发 Flutter 的 RenderFlex overflow。按当前卡片的可用宽高
// 计算格子尺寸,并让文字随格子一起收缩。
var rows = ctx.grid.rows;
var extraHeight = rows >= 5 ? 88 : (rows >= 4 ? 26 : 0);
var gridHeight = Math.max(96, ctx.size.h - 56 - extraHeight);
var gridWidth = Math.max(84, ctx.size.w - 12);
var cellSize = Math.min(36, Math.min(gridWidth / 7 - 2, (gridHeight - 10) / 6));
cellSize = Math.max(18, cellSize);
var compact = cellSize < 26;

var cells = [];
for (var i = 0; i < 42; i++) {
var d = new Date(viewYear, viewMonth, 1 - lead + i);
var inMonth = d.getMonth() === viewMonth;
var col = i % 7;
cells.push(cell(d, inMonth, weekendCols.indexOf(col) >= 0,
showLunar, showFest));
showLunar, showFest, cellSize, compact));
}

var headCells = heads.map(function (h, i) {
Expand Down Expand Up @@ -89,9 +100,8 @@ lw.register({

// 尺寸分档:格子越多,能承载的信息越多。
// 小档只放月历;中档加今日一行;大档在下方展开今日详情与近期节气/节日。
var rows = ctx.grid.rows;
if (rows >= 4) body.children.splice(1, 0, todayStrip());
if (rows >= 5) body.children.push({ t: 'divider' }, upcoming());
if (rows >= 4 && !compact) body.children.splice(1, 0, todayStrip());
if (rows >= 5 && !compact) body.children.push({ t: 'divider' }, upcoming());

ctx.render(body);
}
Expand Down Expand Up @@ -152,7 +162,7 @@ lw.register({
}

// 一格:上面公历日,下面农历/节气/节日
function cell(d, inMonth, weekend, showLunar, showFest) {
function cell(d, inMonth, weekend, showLunar, showFest, cellSize, compact) {
var y = d.getFullYear(), m = d.getMonth() + 1, day = d.getDate();
var isToday = sameYMD(today, y, m - 1, day);

Expand Down Expand Up @@ -185,20 +195,24 @@ lw.register({
dayOpacity = 0.9;
}

// 紧凑格子只保留公历数字,避免副标题把行高再次撑开。
if (compact) sub = '';
var daySize = compact ? Math.max(10, cellSize * 0.48) : Math.min(15, cellSize * 0.42);
var subSize = Math.max(7, Math.min(9.5, cellSize * 0.27));
var inner = {
t: 'col', gap: 1, cross: 'center', main: 'center', children: [
{ t: 'text', v: '' + day, size: 15, weight: isToday ? 700 : 500,
t: 'col', gap: compact ? 0 : 1, cross: 'center', main: 'center', children: [
{ t: 'text', v: '' + day, size: daySize, weight: isToday ? 700 : 500,
align: 'center', color: dayColor, opacity: dayOpacity },
sub
? { t: 'text', v: sub, size: 9.5, align: 'center', maxLines: 1,
? { t: 'text', v: sub, size: subSize, align: 'center', maxLines: 1,
color: isToday ? '#0B1116' : subColor,
opacity: isToday ? 0.85 : (inMonth ? 0.5 : 0.22) }
: { t: 'box' }
]
};

return { t: 'row', main: 'center', children: [
{ t: 'box', w: 36, h: 36, radius: 18, center: true,
{ t: 'box', w: cellSize, h: cellSize, radius: cellSize / 2, center: true,
bg: isToday ? ACCENT : (marked ? '#FFFFFF12' : null),
child: inner }
] };
Expand Down
138 changes: 138 additions & 0 deletions lib/config/config_binding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/// 配置注册与绑定的基础类型。
///
/// 绑定只负责内存中的配置目标、类型校验和变更记录,不直接执行文件 IO。
library;

enum ConfigScope { app, ai, card }

enum ConfigValueType { boolean, number, text, select, json }

class ConfigOption<T> {
const ConfigOption({required this.value, required this.label});

final T value;
final String label;
}

class ConfigSpec<T> {
const ConfigSpec({
required this.key,
required this.scope,
required this.type,
required this.read,
required this.write,
this.defaultValue,
this.min,
this.max,
this.step,
this.options = const [],
});

final String key;
final ConfigScope scope;
final ConfigValueType type;
final T Function() read;
final void Function(T value) write;
final T? defaultValue;
final double? min;
final double? max;
final double? step;
final List<ConfigOption<T>> options;
}

class ConfigChange {
const ConfigChange({
required this.key,
required this.scope,
required this.previous,
required this.value,
});

final String key;
final ConfigScope scope;
final Object? previous;
final Object? value;
}

class ConfigValidationException implements Exception {
const ConfigValidationException(this.key, this.message);

final String key;
final String message;

@override
String toString() => 'Invalid configuration "$key": $message';
}

class ConfigBinding<T> {
ConfigBinding(this.spec);

final ConfigSpec<T> spec;

T get value => spec.read();

void set(T next) {
_validate(next);
spec.write(next);
}

void _validate(T next) {
switch (spec.type) {
case ConfigValueType.boolean:
if (next is! bool) {
throw ConfigValidationException(spec.key, 'expected a boolean');
}
case ConfigValueType.number:
if (next is! num) {
throw ConfigValidationException(spec.key, 'expected a number');
}
final number = next.toDouble();
if (spec.min != null && number < spec.min!) {
throw ConfigValidationException(spec.key, 'value is below the minimum');
}
if (spec.max != null && number > spec.max!) {
throw ConfigValidationException(spec.key, 'value is above the maximum');
}
if (spec.step != null && spec.step! > 0 && spec.min != null) {
final steps = (number - spec.min!) / spec.step!;
if ((steps - steps.round()).abs() > 0.000001) {
throw ConfigValidationException(spec.key, 'value does not match the step');
}
}
case ConfigValueType.text:
if (next is! String) {
throw ConfigValidationException(spec.key, 'expected text');
}
case ConfigValueType.select:
if (spec.options.isNotEmpty &&
!spec.options.any((option) => option.value == next)) {
throw ConfigValidationException(spec.key, 'value is not an available option');
}
case ConfigValueType.json:
break;
}
}
}

class ConfigRegistry {
final Map<String, ConfigBinding<Object?>> _bindings = {};

Iterable<ConfigBinding<Object?>> get bindings => _bindings.values;

ConfigBinding<T> register<T>(ConfigSpec<T> spec) {
if (_bindings.containsKey(spec.key)) {
throw ArgumentError.value(spec.key, 'key', 'configuration key is already registered');
}
final binding = ConfigBinding<T>(spec);
_bindings[spec.key] = binding as ConfigBinding<Object?>;
return binding;
}

ConfigBinding<T>? find<T>(String key) {
final binding = _bindings[key];
if (binding == null) return null;
return binding as ConfigBinding<T>;
}

void clear() => _bindings.clear();
}
74 changes: 74 additions & 0 deletions lib/config/config_coordinator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// 配置变更协调器。
///
/// 统一负责注册、内存更新、去抖持久化和宿主刷新通知;Store 仍是唯一文件持久化边界。
library;

import 'dart:async';

import '../store/store.dart';
import 'config_binding.dart';

class ConfigCoordinator {
ConfigCoordinator({
required this.store,
required this.state,
required this.onChanged,
this.onHotkeyChanged,
});

final Store store;
final AppState state;
final void Function() onChanged;
final void Function()? onHotkeyChanged;
final ConfigRegistry registry = ConfigRegistry();

final List<ConfigChange> _pending = [];
Timer? _notifyTimer;

List<ConfigChange> get pendingChanges => List.unmodifiable(_pending);
bool get isDirty => _pending.isNotEmpty;

ConfigBinding<T> register<T>(ConfigSpec<T> spec) => registry.register(spec);

bool set<T>(ConfigBinding<T> binding, T value, {bool hotkey = false}) {
final previous = binding.value;
if (previous == value) return false;
binding.set(value);
_pending.add(ConfigChange(
key: binding.spec.key,
scope: binding.spec.scope,
previous: previous,
value: value,
));
store.save(state);
if (hotkey) onHotkeyChanged?.call();
_notifyTimer?.cancel();
_notifyTimer = Timer(const Duration(milliseconds: 260), () {
_notifyTimer = null;
if (isDirty) onChanged();
});
return true;
}

void commit() {
store.save(state);
_notifyTimer?.cancel();
_notifyTimer = Timer(const Duration(milliseconds: 260), () {
_notifyTimer = null;
onChanged();
});
}

void markCommitted() => _pending.clear();

Future<void> commitNow() async {
_notifyTimer?.cancel();
await store.saveNow(state);
_pending.clear();
}

void dispose() {
_notifyTimer?.cancel();
if (isDirty) onChanged();
}
}
Loading
Loading