feat: add data models and api service

This commit is contained in:
Andrew 2026-06-17 04:01:20 +07:00
parent a85c5e833e
commit 32fd7da5be
2 changed files with 242 additions and 0 deletions

135
lib/models/models.dart Normal file
View file

@ -0,0 +1,135 @@
class AuthResponse {
final String token;
final String userId;
final String email;
AuthResponse({required this.token, required this.userId, required this.email});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
token: json['token'] as String,
userId: json['userId'] as String,
email: json['email'] as String,
);
}
}
class AppsResponse {
final List<TeamWithApps> teams;
AppsResponse({required this.teams});
factory AppsResponse.fromJson(Map<String, dynamic> json) {
return AppsResponse(
teams: (json['teams'] as List)
.map((e) => TeamWithApps.fromJson(e))
.toList(),
);
}
List<AppInfo> get allApps => teams.expand((t) => t.apps).toList();
}
class TeamWithApps {
final String id;
final String name;
final List<AppInfo> apps;
TeamWithApps({required this.id, required this.name, required this.apps});
factory TeamWithApps.fromJson(Map<String, dynamic> json) {
return TeamWithApps(
id: json['id'],
name: json['name'],
apps: (json['apps'] as List).map((e) => AppInfo.fromJson(e)).toList(),
);
}
}
class AppInfo {
final String id;
final String title;
final String packageName;
final String icon;
final Track release;
final Track debug;
final Track profile;
AppInfo({
required this.id,
required this.title,
required this.packageName,
required this.icon,
required this.release,
required this.debug,
required this.profile,
});
factory AppInfo.fromJson(Map<String, dynamic> json) {
return AppInfo(
id: json['id'],
title: json['title'],
packageName: json['packageName'],
icon: json['icon'],
release: Track.fromJson(json['release']),
debug: Track.fromJson(json['debug']),
profile: Track.fromJson(json['profile']),
);
}
Track track(String name) {
switch (name) {
case 'release':
return release;
case 'debug':
return debug;
case 'profile':
return profile;
default:
return release;
}
}
}
class Track {
final VersionInfo? current;
final VersionInfo? pending;
Track({this.current, this.pending});
factory Track.fromJson(Map<String, dynamic> json) {
return Track(
current: json['current'] != null ? VersionInfo.fromJson(json['current']) : null,
pending: json['pending'] != null ? VersionInfo.fromJson(json['pending']) : null,
);
}
}
class VersionInfo {
final String id;
final String version;
final String name;
final bool public;
final bool hasFile;
final DateTime created;
VersionInfo({
required this.id,
required this.version,
required this.name,
required this.public,
required this.hasFile,
required this.created,
});
factory VersionInfo.fromJson(Map<String, dynamic> json) {
return VersionInfo(
id: json['id'],
version: json['version'],
name: json['name'] ?? '',
public: json['public'] ?? false,
hasFile: json['hasFile'] ?? false,
created: DateTime.parse(json['created']),
);
}
}

View file

@ -0,0 +1,107 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/models.dart';
class ApiService {
String baseUrl;
String? _token;
ApiService({required this.baseUrl});
String? get token => _token;
bool get isAuthenticated => _token != null;
void setToken(String token) {
_token = token;
}
Map<String, String> get _headers => {
if (_token != null) 'Authorization': 'Bearer $_token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
Future<AuthResponse> login(String email, String password) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/auth'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': email, 'password': password}),
);
if (response.statusCode == 200) {
final data = AuthResponse.fromJson(jsonDecode(response.body));
setToken(data.token);
return data;
}
final body = jsonDecode(response.body);
throw ApiException(
statusCode: response.statusCode,
message: body['error'] ?? 'Unknown error',
);
}
Future<AppsResponse> getApps() async {
final response = await http.get(
Uri.parse('$baseUrl/api/v1/apps'),
headers: _headers,
);
if (response.statusCode == 200) {
return AppsResponse.fromJson(jsonDecode(response.body));
}
if (response.statusCode == 401) {
throw AuthException();
}
final body = jsonDecode(response.body);
throw ApiException(
statusCode: response.statusCode,
message: body['error'] ?? 'Unknown error',
);
}
Future<http.StreamedResponse> downloadVersion(String versionId) async {
final request = http.Request(
'GET',
Uri.parse('$baseUrl/api/v1/versions/$versionId/download'),
);
request.headers.addAll(_headers);
final response = await http.Client().send(request);
if (response.statusCode == 200) {
return response;
}
if (response.statusCode == 401) {
throw AuthException();
}
response.stream.drain();
final body = await response.stream.bytesToString();
final parsed = jsonDecode(body);
throw ApiException(
statusCode: response.statusCode,
message: parsed['error'] ?? 'Download failed',
);
}
String iconUrl(String iconPath) {
if (iconPath.startsWith('http')) return iconPath;
return '$baseUrl$iconPath';
}
}
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException({required this.statusCode, required this.message});
@override
String toString() => message;
}
class AuthException implements Exception {}