Clean up: move docs to Obsidian vault, remove outdated md files
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Модель файла из Gitea Contents API
|
||||
class GiteaFile {
|
||||
final String name;
|
||||
final String path;
|
||||
final String type; // 'file' | 'dir'
|
||||
final String? sha;
|
||||
final String? content; // base64 encoded
|
||||
final int? size;
|
||||
final String? downloadUrl;
|
||||
|
||||
GiteaFile({
|
||||
required this.name,
|
||||
required this.path,
|
||||
required this.type,
|
||||
this.sha,
|
||||
this.content,
|
||||
this.size,
|
||||
this.downloadUrl,
|
||||
});
|
||||
|
||||
factory GiteaFile.fromJson(Map<String, dynamic> json) {
|
||||
return GiteaFile(
|
||||
name: json['name'] ?? '',
|
||||
path: json['path'] ?? '',
|
||||
type: json['type'] ?? 'file',
|
||||
sha: json['sha'],
|
||||
content: json['content'],
|
||||
size: json['size'],
|
||||
downloadUrl: json['download_url'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Декодирует base64 content (Gitea возвращает base64 с переносами)
|
||||
String? get decodedContent {
|
||||
if (content == null || content!.isEmpty) return null;
|
||||
try {
|
||||
final clean = content!.replaceAll('\n', '');
|
||||
return utf8.decode(base64Decode(clean));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Сервис прямой синхронизации с Gitea REST API
|
||||
/// Используется в мобильном приложении вместо Obsidian Git plugin
|
||||
class GiteaSyncService {
|
||||
static final GiteaSyncService _instance = GiteaSyncService._internal();
|
||||
factory GiteaSyncService() => _instance;
|
||||
GiteaSyncService._internal();
|
||||
|
||||
String? _baseUrl;
|
||||
String? _token;
|
||||
String? _owner;
|
||||
String? _repo;
|
||||
|
||||
static const String _prefsBaseUrlKey = 'gitea_base_url';
|
||||
static const String _prefsTokenKey = 'gitea_token';
|
||||
static const String _prefsOwnerKey = 'gitea_owner';
|
||||
static const String _prefsRepoKey = 'gitea_repo';
|
||||
|
||||
/// Инициализация из SharedPreferences
|
||||
Future<void> initialize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_baseUrl = prefs.getString(_prefsBaseUrlKey);
|
||||
_token = prefs.getString(_prefsTokenKey);
|
||||
_owner = prefs.getString(_prefsOwnerKey);
|
||||
_repo = prefs.getString(_prefsRepoKey);
|
||||
}
|
||||
|
||||
/// Настройка подключения
|
||||
Future<void> configure({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required String owner,
|
||||
required String repo,
|
||||
}) async {
|
||||
_baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), '');
|
||||
_token = token;
|
||||
_owner = owner;
|
||||
_repo = repo;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsBaseUrlKey, _baseUrl!);
|
||||
await prefs.setString(_prefsTokenKey, _token!);
|
||||
await prefs.setString(_prefsOwnerKey, _owner!);
|
||||
await prefs.setString(_prefsRepoKey, _repo!);
|
||||
}
|
||||
|
||||
bool get isConfigured =>
|
||||
_baseUrl != null && _token != null && _owner != null && _repo != null;
|
||||
|
||||
// ==================== AUTH / TEST ====================
|
||||
|
||||
/// Проверка соединения и валидности токена
|
||||
Future<bool> testConnection() async {
|
||||
if (!isConfigured) return false;
|
||||
try {
|
||||
final response = await _get('/api/v1/user');
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить информацию о текущем пользователе
|
||||
Future<Map<String, dynamic>?> getCurrentUser() async {
|
||||
final response = await _get('/api/v1/user');
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== REPOSITORY CONTENTS ====================
|
||||
|
||||
/// Получить список файлов в директории репозитория
|
||||
Future<List<GiteaFile>> listFiles({
|
||||
String path = '',
|
||||
String branch = 'main',
|
||||
}) async {
|
||||
final response = await _get(
|
||||
'/api/v1/repos/$_owner/$_repo/contents/$path?ref=$branch',
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to list files: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data
|
||||
.map((e) => GiteaFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Получить содержимое одного файла
|
||||
Future<GiteaFile> getFile({
|
||||
required String path,
|
||||
String branch = 'main',
|
||||
}) async {
|
||||
final response = await _get(
|
||||
'/api/v1/repos/$_owner/$_repo/contents/$path?ref=$branch',
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to get file: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return GiteaFile.fromJson(data);
|
||||
}
|
||||
|
||||
/// Обновить/создать файл в репозитории
|
||||
Future<void> updateFile({
|
||||
required String path,
|
||||
required String content,
|
||||
required String commitMessage,
|
||||
String branch = 'main',
|
||||
String? authorName,
|
||||
String? authorEmail,
|
||||
}) async {
|
||||
// Получаем текущий sha если файл существует
|
||||
String? currentSha;
|
||||
try {
|
||||
final existing = await getFile(path: path, branch: branch);
|
||||
currentSha = existing.sha;
|
||||
} catch (_) {
|
||||
// файл не существует — создаём новый
|
||||
}
|
||||
|
||||
final body = <String, dynamic>{
|
||||
'content': base64Encode(utf8.encode(content)),
|
||||
'message': commitMessage,
|
||||
'branch': branch,
|
||||
};
|
||||
|
||||
if (currentSha != null) {
|
||||
body['sha'] = currentSha;
|
||||
}
|
||||
|
||||
if (authorName != null && authorEmail != null) {
|
||||
body['author'] = {
|
||||
'name': authorName,
|
||||
'email': authorEmail,
|
||||
};
|
||||
}
|
||||
|
||||
final response = await _put(
|
||||
'/api/v1/repos/$_owner/$_repo/contents/$path',
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
throw Exception(
|
||||
'Failed to update file: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Удалить файл из репозитория
|
||||
Future<void> deleteFile({
|
||||
required String path,
|
||||
required String commitMessage,
|
||||
String branch = 'main',
|
||||
}) async {
|
||||
final existing = await getFile(path: path, branch: branch);
|
||||
|
||||
final response = await _delete(
|
||||
'/api/v1/repos/$_owner/$_repo/contents/$path',
|
||||
body: {
|
||||
'message': commitMessage,
|
||||
'sha': existing.sha,
|
||||
'branch': branch,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception(
|
||||
'Failed to delete file: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GIT OPERATIONS ====================
|
||||
|
||||
/// Получить дерево файлов (рекурсивно)
|
||||
Future<List<GiteaFile>> getTree({
|
||||
String branch = 'main',
|
||||
bool recursive = true,
|
||||
}) async {
|
||||
final response = await _get(
|
||||
'/api/v1/repos/$_owner/$_repo/git/trees/$branch?recursive=${recursive ? 1 : 0}',
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to get tree: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final List<dynamic> tree = data['tree'] ?? [];
|
||||
|
||||
return tree
|
||||
.where((e) => e['type'] == 'blob')
|
||||
.map((e) => GiteaFile(
|
||||
name: e['path'].toString().split('/').last,
|
||||
path: e['path'] ?? '',
|
||||
type: 'file',
|
||||
sha: e['sha'],
|
||||
size: e['size'],
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Получить последний коммит ветки
|
||||
Future<Map<String, dynamic>?> getLatestCommit(
|
||||
{String branch = 'main'}) async {
|
||||
final response = await _get(
|
||||
'/api/v1/repos/$_owner/$_repo/commits?sha=$branch&limit=1',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
if (data.isNotEmpty) return data.first as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Получить историю коммитов
|
||||
Future<List<Map<String, dynamic>>> getCommits({
|
||||
String branch = 'main',
|
||||
int page = 1,
|
||||
int limit = 30,
|
||||
}) async {
|
||||
final response = await _get(
|
||||
'/api/v1/repos/$_owner/$_repo/commits?sha=$branch&page=$page&limit=$limit',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ==================== HELPERS ====================
|
||||
|
||||
Map<String, String> _getHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'token $_token',
|
||||
};
|
||||
}
|
||||
|
||||
Future<http.Response> _get(String path) async {
|
||||
final uri = Uri.parse('$_baseUrl$path');
|
||||
return http.get(uri, headers: _getHeaders());
|
||||
}
|
||||
|
||||
Future<http.Response> _put(String path,
|
||||
{required Map<String, dynamic> body}) async {
|
||||
final uri = Uri.parse('$_baseUrl$path');
|
||||
return http.put(
|
||||
uri,
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
}
|
||||
|
||||
Future<http.Response> _delete(String path,
|
||||
{required Map<String, dynamic> body}) async {
|
||||
final uri = Uri.parse('$_baseUrl$path');
|
||||
final request = http.Request('DELETE', uri)
|
||||
..headers.addAll(_getHeaders())
|
||||
..body = jsonEncode(body);
|
||||
return http.Client().send(request).then((r) => http.Response.fromStream(r));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user