feat: extension store (#1301)

This commit is contained in:
Levi authored and GitHub committed 2026-03-06 21:53:19 +08:00
1 parent bc5c818eae
commit d36b345f92
12 files changed
+1791 -425

No files matched your search

+3 -3
View File
@@ -200,10 +200,10 @@ Future<void> putConfig(DownloaderConfig config) async {
return _parse(() => _client.dio.put("api/v1/config", data: config), null);
}
Future<void> installExtension(InstallExtension installExtension) async {
return _parse(
Future<String> installExtension(InstallExtension installExtension) async {
return _parse<String>(
() => _client.dio.post("api/v1/extensions", data: installExtension),
null);
(data) => data as String);
}
Future<List<Extension>> getExtensions() async {
+55
View File
@@ -0,0 +1,55 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'api.dart';
import 'model/store_extension.dart';
class GopeedSiteApi {
GopeedSiteApi._();
static final instance = GopeedSiteApi._();
static const _host = 'gopeed.com';
Future<Map<String, dynamic>> getRelease() async {
final json = await _getJson('/api/release');
return json as Map<String, dynamic>;
}
Future<StoreExtensionPage> getExtensions({
int page = 1,
int limit = 20,
StoreExtensionSort sort = StoreExtensionSort.stars,
StoreSortOrder order = StoreSortOrder.desc,
String? query,
}) async {
final json = await _getJson('/api/extensions', queryParameters: {
'page': page.toString(),
'limit': limit.clamp(1, 100).toString(),
'sort': sort.name,
'order': order.name,
if (query != null && query.trim().isNotEmpty) 'q': query.trim(),
});
return StoreExtensionPage.fromJson(json as Map<String, dynamic>);
}
Future<void> reportExtensionInstall(String id) async {
final uri = Uri.https(_host, '/api/extensions/install');
await proxyRequest(
uri.toString(),
data: {'id': id},
options: Options(method: 'POST', contentType: Headers.jsonContentType),
);
}
Future<dynamic> _getJson(String path,
{Map<String, String>? queryParameters}) async {
final uri = Uri.https(_host, path, queryParameters);
final Response<String> response = await proxyRequest(uri.toString());
if (response.data == null || response.data!.isEmpty) {
throw Exception('Empty response from $uri');
}
return jsonDecode(response.data!);
}
}
@@ -0,0 +1,149 @@
import 'dart:convert';
class StoreExtensionPage {
final List<StoreExtension> data;
final StorePagination pagination;
StoreExtensionPage({required this.data, required this.pagination});
factory StoreExtensionPage.fromJson(Map<String, dynamic> json) {
return StoreExtensionPage(
data: (json['data'] as List<dynamic>? ?? [])
.map((e) => StoreExtension.fromJson(e as Map<String, dynamic>))
.toList(),
pagination: StorePagination.fromJson(
json['pagination'] as Map<String, dynamic>? ?? {}),
);
}
}
class StorePagination {
final int page;
final int limit;
final int total;
final int totalPages;
final bool hasNext;
final bool hasPrev;
StorePagination({
required this.page,
required this.limit,
required this.total,
required this.totalPages,
required this.hasNext,
required this.hasPrev,
});
factory StorePagination.fromJson(Map<String, dynamic> json) {
return StorePagination(
page: (json['page'] as num?)?.toInt() ?? 1,
limit: (json['limit'] as num?)?.toInt() ?? 20,
total: (json['total'] as num?)?.toInt() ?? 0,
totalPages: (json['totalPages'] as num?)?.toInt() ?? 1,
hasNext: json['hasNext'] as bool? ?? false,
hasPrev: json['hasPrev'] as bool? ?? false,
);
}
}
class StoreExtension {
final String id;
final String repoFullName;
final String repoUrl;
final String? directory;
final String? commitSha;
final String name;
final String author;
final String title;
final String description;
final String? icon;
final String version;
final String? homepage;
final String? readme;
final int installCount;
final int stars;
final List<String> topics;
final DateTime? createdAt;
final DateTime? updatedAt;
StoreExtension({
required this.id,
required this.repoFullName,
required this.repoUrl,
this.directory,
this.commitSha,
required this.name,
required this.author,
required this.title,
required this.description,
this.icon,
required this.version,
this.homepage,
this.readme,
required this.installCount,
required this.stars,
required this.topics,
this.createdAt,
this.updatedAt,
});
factory StoreExtension.fromJson(Map<String, dynamic> json) {
return StoreExtension(
id: json['id'] as String? ?? '',
repoFullName: json['repoFullName'] as String? ?? '',
repoUrl: json['repoUrl'] as String? ?? '',
directory: json['directory'] as String?,
commitSha: json['commitSha'] as String?,
name: json['name'] as String? ?? '',
author: json['author'] as String? ?? '',
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
icon: json['icon'] as String?,
version: json['version'] as String? ?? '0.0.0',
homepage: json['homepage'] as String?,
readme: json['readme'] as String?,
installCount: (json['installCount'] as num?)?.toInt() ?? 0,
stars: (json['stars'] as num?)?.toInt() ?? 0,
topics: _parseTopics(json['topics']),
createdAt: _parseDate(json['createdAt']),
updatedAt: _parseDate(json['updatedAt']),
);
}
static List<String> _parseTopics(dynamic value) {
if (value is List) {
return value.map((e) => e.toString()).toList();
}
if (value is String && value.isNotEmpty) {
try {
final parsed = jsonDecode(value);
if (parsed is List) {
return parsed.map((e) => e.toString()).toList();
}
} catch (_) {}
}
return const [];
}
static DateTime? _parseDate(dynamic value) {
if (value == null) return null;
if (value is int) {
return DateTime.fromMillisecondsSinceEpoch(value);
}
if (value is String && value.isNotEmpty) {
return DateTime.tryParse(value);
}
return null;
}
}
enum StoreExtensionSort {
stars,
installs,
updated,
}
enum StoreSortOrder {
asc,
desc,
}