From 32fd7da5be3313ad6d27a91e35df92cd983efa76 Mon Sep 17 00:00:00 2001 From: Andrew nuark G Date: Wed, 17 Jun 2026 04:01:20 +0700 Subject: [PATCH] feat: add data models and api service --- lib/models/models.dart | 135 ++++++++++++++++++++++++++++++++++ lib/services/api_service.dart | 107 +++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 lib/models/models.dart create mode 100644 lib/services/api_service.dart diff --git a/lib/models/models.dart b/lib/models/models.dart new file mode 100644 index 0000000..4a5e20b --- /dev/null +++ b/lib/models/models.dart @@ -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 json) { + return AuthResponse( + token: json['token'] as String, + userId: json['userId'] as String, + email: json['email'] as String, + ); + } +} + +class AppsResponse { + final List teams; + + AppsResponse({required this.teams}); + + factory AppsResponse.fromJson(Map json) { + return AppsResponse( + teams: (json['teams'] as List) + .map((e) => TeamWithApps.fromJson(e)) + .toList(), + ); + } + + List get allApps => teams.expand((t) => t.apps).toList(); +} + +class TeamWithApps { + final String id; + final String name; + final List apps; + + TeamWithApps({required this.id, required this.name, required this.apps}); + + factory TeamWithApps.fromJson(Map 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 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 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 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']), + ); + } +} diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart new file mode 100644 index 0000000..77a86da --- /dev/null +++ b/lib/services/api_service.dart @@ -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 get _headers => { + if (_token != null) 'Authorization': 'Bearer $_token', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + Future 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 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 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 {}