feat: vault-aligned server v1 + UX polish
All checks were successful
CI / Windows build (push) Successful in 7m47s
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:
139
lib/services/badnote_server_client.dart
Normal file
139
lib/services/badnote_server_client.dart
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user