worldhopper/lib/models/server.dart
Felipe M. 58a562abe8
feat: refactor server software abstraction with Kavita REST API integration
Rename all OPDS-prefixed models to generic names (Server, Feed, Entry,
Link, StreamLink), expand the ServerSoftware interface to cover all
server interactions, and implement a full Kavita REST API client that
replaces the OPDS delegation.

- Rename OPDS* models to generic names across ~60 files
- Add database migrations 13 (opds_id → entry_id) and 14 (unify credentials)
- Create OPDSServerSoftware wrapping existing OPDS services
- Create KavitaApiClient for direct Kavita REST API calls
- Create KavitaFeedMapper to convert Kavita JSON to Feed/Entry models
- Rewrite KavitaServerSoftware to use native API (no OPDS delegation)
- Unify server credentials (remove softwareUsername/softwarePassword)
- Simplify add/edit server UI to single auth section
- Add test connection button to server add/edit screens
- Add progress indicator to PublicationCard using local and server data
- Eliminate softwareType branching in reader screens
- Add EntryProgress and fetchEntryProgress to ServerSoftware interface
- Fix Entry.acquisitionLink crash on empty links
- Add 59 new tests covering models, services, and providers
2026-04-06 17:46:48 +02:00

87 lines
2.5 KiB
Dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:worldhopper/services/server_software/server_software_type.dart';
import 'package:worldhopper/services/url_helper.dart';
part 'server.freezed.dart';
part 'server.g.dart';
/// Represents a server configuration
@freezed
class Server with _$Server {
const factory Server({
/// Unique identifier for the server
required String id,
/// Display name for the server
required String name,
/// Root URL of the server
required String url,
/// Optional username for authentication
String? username,
/// Optional password for authentication
String? password,
/// Timestamp when the server was added
required DateTime createdAt,
/// Timestamp of the last successful sync
DateTime? lastSyncedAt,
/// Server software type (null = not yet detected)
ServerSoftwareType? softwareType,
}) = _Server;
/// Create Server from JSON
factory Server.fromJson(Map<String, dynamic> json) => _$ServerFromJson(json);
/// Create Server from database row
factory Server.fromDatabase(Map<String, dynamic> row) {
final softwareTypeStr = row['software_type'] as String?;
return Server(
id: row['id'] as String,
name: row['name'] as String,
url: row['url'] as String,
username: row['username'] as String?,
password: row['password'] as String?,
createdAt: DateTime.fromMillisecondsSinceEpoch(row['created_at'] as int),
lastSyncedAt: row['last_synced_at'] != null
? DateTime.fromMillisecondsSinceEpoch(row['last_synced_at'] as int)
: null,
softwareType: softwareTypeStr != null
? ServerSoftwareType.values.asNameMap()[softwareTypeStr]
: null,
);
}
}
extension ServerX on Server {
/// Convert Server to database row
Map<String, dynamic> toDatabase() {
return {
'id': id,
'name': name,
'url': url,
'username': username,
'password': password,
'created_at': createdAt.millisecondsSinceEpoch,
'last_synced_at': lastSyncedAt?.millisecondsSinceEpoch,
'software_type': softwareType?.name,
};
}
/// Check if server requires authentication
bool get requiresAuth => username != null && password != null;
/// Resolve a relative URL against this server's base URL
String resolveUrl(String relativeUrl) {
return UrlHelper.resolveUrl(url, relativeUrl);
}
/// Make a request URL by resolving it against the server base
String makeRequest(String path) {
return UrlHelper.resolveUrl(url, path);
}
}