feat: vault-aligned server v1 + UX polish
All checks were successful
CI / Windows build (push) Successful in 7m47s

Redesign the optional FastAPI companion around vault files (manifest /
PUT/GET/DELETE + OCR jobs) instead of legacy strokes_json notes. Wire a
client Server settings panel for health/login. Polish shell UX: l10n for
settings/home/board, sticky-board empty state, and a narrow-screen
diagnostics FAB.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 19:04:48 +08:00
parent d346cc2670
commit 198da00ecd
20 changed files with 1325 additions and 90 deletions

View File

@@ -0,0 +1,139 @@
// HTTP client for the optional self-hosted BadNote Server (/api/v1).
// WebDAV remains the primary NAS sync path; this client covers health, auth,
// vault manifest assist, and OCR job submit/poll.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class BadNoteServerConfig {
const BadNoteServerConfig({
this.baseUrl = '',
this.username = '',
this.password = '',
this.token = '',
this.userId = '',
});
final String baseUrl;
final String username;
final String password;
final String token;
final String userId;
bool get isConfigured => baseUrl.trim().isNotEmpty;
bool get isLoggedIn => token.isNotEmpty;
Uri? uri(String path) {
final base = baseUrl.trim().replaceAll(RegExp(r'/+$'), '');
if (base.isEmpty) return null;
final p = path.startsWith('/') ? path : '/$path';
return Uri.parse('$base$p');
}
BadNoteServerConfig copyWith({
String? baseUrl,
String? username,
String? password,
String? token,
String? userId,
}) =>
BadNoteServerConfig(
baseUrl: baseUrl ?? this.baseUrl,
username: username ?? this.username,
password: password ?? this.password,
token: token ?? this.token,
userId: userId ?? this.userId,
);
static const _kBase = 'badnote.server.baseUrl';
static const _kUser = 'badnote.server.username';
static const _kPass = 'badnote.server.password';
static const _kToken = 'badnote.server.token';
static const _kUid = 'badnote.server.userId';
static Future<BadNoteServerConfig> load(SharedPreferences prefs) async {
return BadNoteServerConfig(
baseUrl: prefs.getString(_kBase) ?? '',
username: prefs.getString(_kUser) ?? '',
password: prefs.getString(_kPass) ?? '',
token: prefs.getString(_kToken) ?? '',
userId: prefs.getString(_kUid) ?? '',
);
}
Future<void> save(SharedPreferences prefs) async {
await prefs.setString(_kBase, baseUrl);
await prefs.setString(_kUser, username);
await prefs.setString(_kPass, password);
await prefs.setString(_kToken, token);
await prefs.setString(_kUid, userId);
}
}
class BadNoteServerClient {
BadNoteServerClient(this.config, {http.Client? httpClient})
: _http = httpClient ?? http.Client();
BadNoteServerConfig config;
final http.Client _http;
Map<String, String> get _authHeaders => {
if (config.token.isNotEmpty) 'Authorization': 'Bearer ${config.token}',
};
Future<Map<String, dynamic>> health() async {
final uri = config.uri('/api/v1/health');
if (uri == null) throw StateError('server URL not set');
final res = await _http.get(uri).timeout(const Duration(seconds: 8));
if (res.statusCode != 200) {
throw StateError('health ${res.statusCode}: ${res.body}');
}
return jsonDecode(res.body) as Map<String, dynamic>;
}
Future<BadNoteServerConfig> registerOrLogin({
required String username,
required String password,
}) async {
final registerUri = config.uri('/api/v1/auth/register');
if (registerUri == null) throw StateError('server URL not set');
var res = await _http.post(
registerUri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'username': username, 'password': password}),
);
if (res.statusCode == 409) {
final loginUri = config.uri('/api/v1/auth/login')!;
res = await _http.post(
loginUri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'username': username, 'password': password}),
);
}
if (res.statusCode != 200 && res.statusCode != 201) {
throw StateError('auth ${res.statusCode}: ${res.body}');
}
final body = jsonDecode(res.body) as Map<String, dynamic>;
config = config.copyWith(
username: username,
password: password,
token: body['token'] as String? ?? '',
userId: body['user_id'] as String? ?? '',
);
return config;
}
Future<Map<String, dynamic>> vaultManifest() async {
final uri = config.uri('/api/v1/vault/manifest');
if (uri == null) throw StateError('server URL not set');
final res = await _http.get(uri, headers: _authHeaders);
if (res.statusCode != 200) {
throw StateError('manifest ${res.statusCode}: ${res.body}');
}
return jsonDecode(res.body) as Map<String, dynamic>;
}
void close() => _http.close();
}