diff --git a/assets/locale/en.json b/assets/locale/en.json index 9ff4e5fa7..c170c69c7 100644 --- a/assets/locale/en.json +++ b/assets/locale/en.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "Database is already up to date.", "importingbookmark": "Import Bookmark", "exportingbookmark": "Export Bookmark", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "You do not have permission to run it.", "noselectedb": "No database selected.", "importbookmark": "Bookmark Imported!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/assets/locale/eo.json b/assets/locale/eo.json index 779c02fc7..e92aa48e1 100644 --- a/assets/locale/eo.json +++ b/assets/locale/eo.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "Los datos ya están actualizados.", "importingbookmark": "Importar Marcador", "exportingbookmark": "Exportar Marcador", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "No tienes permiso para iniciar.", "noselectedb": "No se ha seleccionado ninguna base de datos.", "importbookmark": "¡Marcador Importado!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/assets/locale/it.json b/assets/locale/it.json index 7942b1e3c..de93ef4d7 100644 --- a/assets/locale/it.json +++ b/assets/locale/it.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "Il database è aggiornato.", "importingbookmark": "Importa Segnalibri", "exportingbookmark": "Esporta Segnalibri", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "Non hai il permesso di avviarlo.", "noselectedb": "Nessun database selezionato.", "importbookmark": "Segnalibri Importati!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/assets/locale/ja.json b/assets/locale/ja.json index 2cc9bc25e..edb00568a 100644 --- a/assets/locale/ja.json +++ b/assets/locale/ja.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "データベースがすでに最新の状態です。", "importingbookmark": "ブックマークを読み込む", "exportingbookmark": "ブックマークの書き出す", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "権限がないため、実行できません。", "noselectedb": "選択されたデータベースがありません。", "importbookmark": "ブックマークを読み込みしました!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "ボタンを下に移動させる", "showslider": "スライダー表示", "importfromeh": "E/Ex-Hentaiから読み込む", + "importfromgit": "Import From Git Repository", "setcookiefirst": "クッキーを設定してください!", "bookmarkisempty": "ブックマークがありません!", "ensurecreatebookmark": "$1個の項目をブックマークに追加しますか?(Favorite別のグループが作成されます。)", diff --git a/assets/locale/ko.json b/assets/locale/ko.json index 4363d2325..c0de5d734 100644 --- a/assets/locale/ko.json +++ b/assets/locale/ko.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "데이터베이스가 이미 최신상태입니다.", "importingbookmark": "북마크 가져오기", "exportingbookmark": "북마크 내보내기", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "권한이 없어서 실행할 수 없습니다.", "noselectedb": "선택된 데이터베이스가 없습니다.", "importbookmark": "북마크를 가져왔습니다!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "버튼 밑으로 내리기", "showslider": "슬라이더 보여주기", "importfromeh": "이/익헨에서 불러오기", + "importfromgit": "Import From Git Repository", "setcookiefirst": "쿠키를 먼저 설정하세요!", "bookmarkisempty": "북마크가 비어있어요!", "ensurecreatebookmark": "$1개 항목을 북마크에 추가할까요? (각 Favorite별로 그룹이 생성됩니다.)", diff --git a/assets/locale/pt.json b/assets/locale/pt.json index e1650a518..f53f84b62 100644 --- a/assets/locale/pt.json +++ b/assets/locale/pt.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "O banco de dados já está atualizado.", "importingbookmark": "Importar marcador", "exportingbookmark": "Exportar marcador", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "Você não tem permissão para executar isso.", "noselectedb": "Nenhum banco de dados selecionado.", "importbookmark": "Marcador importado!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Mover botões para baixo", "showslider": "Mostrar controle deslizante", "importfromeh": "Importar de E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Defina o cookie primeiro!", "bookmarkisempty": "O marcador está vazio!", "ensurecreatebookmark": "Gostaria de adicionar $ 1 item aos seus marcadores? (Grupos são criados para cada Favorito.)", diff --git a/assets/locale/zh.json b/assets/locale/zh.json index b33041f22..68018e8fc 100644 --- a/assets/locale/zh.json +++ b/assets/locale/zh.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "Database is already up to date.", "importingbookmark": "Import Bookmark", "exportingbookmark": "Export Bookmark", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "You do not have permission to run it.", "noselectedb": "No database selected.", "importbookmark": "Bookmark Imported!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/assets/locale/zh_Hans.json b/assets/locale/zh_Hans.json index e90a3731a..352fd23cf 100644 --- a/assets/locale/zh_Hans.json +++ b/assets/locale/zh_Hans.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "数据库已经是最新的.", "importingbookmark": "导入书签", "exportingbookmark": "导出书签", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "你没有权限执行此操作.", "noselectedb": "没有数据库被选中.", "importbookmark": "书签导入成功!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/assets/locale/zh_Hant.json b/assets/locale/zh_Hant.json index deb534e17..256cf32d8 100644 --- a/assets/locale/zh_Hant.json +++ b/assets/locale/zh_Hant.json @@ -152,6 +152,7 @@ "thisislatestbookmark": "Database is already up to date.", "importingbookmark": "Import Bookmark", "exportingbookmark": "Export Bookmark", + "exportingbookmarkgit": "Export Bookmark to Git Repository", "noauth": "You do not have permission to run it.", "noselectedb": "No database selected.", "importbookmark": "Bookmark Imported!", @@ -228,6 +229,7 @@ "movetoappbartobottom": "Move Buttons To Bottom", "showslider": "Show Slider", "importfromeh": "Import From E/Ex-Hentai", + "importfromgit": "Import From Git Repository", "setcookiefirst": "Set Cookie First!", "bookmarkisempty": "Bookmark is empty!", "ensurecreatebookmark": "Would you like to add $1 item to your bookmarks? (Groups are created for each Favorite.)", diff --git a/lib/component/git/git_bookmark.dart b/lib/component/git/git_bookmark.dart new file mode 100644 index 000000000..2be045289 --- /dev/null +++ b/lib/component/git/git_bookmark.dart @@ -0,0 +1,120 @@ +// This source code is a part of Project Violet. +// Copyright (C) 2020-2024. violet-team. Licensed under the Apache-2.0 License. + +import 'dart:collection'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:violet/util/git.dart'; + +class BookmarkGroupKeyVal { + int? id; + String? name; + String? dateTime; + String? description; + int? color; + int? gorder; +} + +class BookmarkArticleKeyVal { + int? id; + int? article; + String? dateTime; + int? groupId; +} + +class GitBookmark { + static Map>? bookmarkInfo; + static Future>?> + process() async { + // https://e-hentai.org/favorites.php?page=0&favcat=0 + // https://exhentai.org/favorites.php?page=0&favcat=0 + + Map> result = + >{}; + + final gitPath = + '${(await getTemporaryDirectory()).path}/_tmp_bookmark_from_git'; + if (await Directory(gitPath).exists()) { + await Directory(gitPath).delete(recursive: true); + } + BookmarkGit git = BookmarkGit(); + await git.clone(gitPath); + String getRelatedPath(String absolutePath) { + return absolutePath + .replaceAll(gitPath, '') + .split('/') + .where((p) => p.isNotEmpty) + .join('/'); + } + + List getList() { + return Directory(gitPath) + .listSync(recursive: true, followLinks: false) + .where((absolutePath) => + getRelatedPath(absolutePath.path) + .split('/') + .firstOrNull + ?.isNotEmpty ?? + false) + .where((absolutePath) => + getRelatedPath(absolutePath.path).split('/').firstOrNull != + '.git') + .toList(); + } + + if (await Directory(gitPath).exists()) { + final listInPath = getList(); + print(listInPath); + await Future.forEach(listInPath, (absolutePath) async { + final relativePath = getRelatedPath(absolutePath.path); + if (relativePath.split('/').isNotEmpty) { + if (relativePath.split('/').firstOrNull != '.git') { + if (relativePath.split('/').lastOrNull?.endsWith('.db') ?? false) { + Database db = await openDatabase(absolutePath.path); + final bookmarkGroups = await db.query('BookmarkGroup'); + for (var bookmarkGroup in bookmarkGroups) { + BookmarkGroupKeyVal group = BookmarkGroupKeyVal(); + // "Id", "Name", "DateTime", "Description", "Color", "Gorder" + // Int, String, String , String , Int , Int + bookmarkGroup.forEach((key, value) { + if (key == 'Id') group.id = int.tryParse(value.toString()); + if (key == 'Name') group.name = value.toString(); + if (key == 'DataTime') group.dateTime = value.toString(); + if (key == 'Description') + group.description = value.toString(); + if (key == 'Color') + group.color = int.tryParse(value.toString()); + if (key == 'Gorder') + group.gorder = int.tryParse(value.toString()); + }); + final bookmarkArticles = await db.query('BookmarkArticle'); + // "Id", "Article", "DateTime", "GroupId" + // Int, Int , String , Int + for (final bookmarkArticle in bookmarkArticles) { + BookmarkArticleKeyVal article = BookmarkArticleKeyVal(); + bookmarkArticle.forEach((key, value) { + if (key == 'Id') + article.id = int.tryParse(value.toString()); + if (key == 'Article') + article.article = int.tryParse(value.toString()); + if (key == 'DateTime') article.dateTime = value.toString(); + if (key == 'GroupId') + article.groupId = int.tryParse(value.toString()); + }); + if (group.gorder == article.groupId) { + result[group] ??= []; + result[group]!.add(article); + } + } + } + await db.close(); + } + } + } + }); + } + return bookmarkInfo = result; + } +} diff --git a/lib/pages/settings/import_from_git.dart b/lib/pages/settings/import_from_git.dart new file mode 100644 index 000000000..fdcb40171 --- /dev/null +++ b/lib/pages/settings/import_from_git.dart @@ -0,0 +1,104 @@ +// This source code is a part of Project Violet. +// Copyright (C) 2020-2024. violet-team. Licensed under the Apache-2.0 License. + +import 'package:flutter/material.dart'; +import 'package:violet/component/git/git_bookmark.dart'; +import 'package:violet/settings/settings.dart'; +import 'package:violet/style/palette.dart'; + +class ImportFromGitPage extends StatefulWidget { + const ImportFromGitPage({super.key}); + + @override + State createState() => _ImportFromGitState(); +} + +class _ImportFromGitState extends State { + @override + void initState() { + super.initState(); + + Future.delayed(const Duration(milliseconds: 100)).then((value) async { + await GitBookmark.process(); + + Navigator.pop(context); + }); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(1)), + boxShadow: [ + BoxShadow( + color: Settings.themeWhat + ? Colors.black.withOpacity(0.4) + : Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 1, + offset: const Offset(0, 3), // changes position of shadow + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + color: Settings.themeWhat + ? Palette.darkThemeBackground + : Palette.lightThemeBackground, + elevation: 100, + child: SizedBox( + child: SizedBox( + width: 280, + height: (56 * 4 + 16).toDouble(), + child: const Padding( + padding: EdgeInsets.fromLTRB(0, 8, 0, 8), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // CircularProgressIndicator(), + // Expanded( + // child: Container(child: Text('초기화 중...')), + // ) + // ], + // ), + child: Stack( + children: [ + Center( + child: CircularProgressIndicator(), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.only(bottom: 33), + child: Text( + '가져오는 중', + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.only(bottom: 15), + child: Text( + '잠시만 기다려주세요...', + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 56f4eee11..358e19697 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -18,6 +18,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_windowmanager/flutter_windowmanager.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_git_dart/go_git_dart.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:mdi/mdi.dart'; import 'package:path_provider/path_provider.dart'; @@ -26,6 +27,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:violet/component/eh/eh_bookmark.dart'; +import 'package:violet/component/git/git_bookmark.dart'; import 'package:violet/component/hitomi/hitomi.dart'; import 'package:violet/component/hitomi/indexs.dart'; import 'package:violet/database/database.dart'; @@ -49,6 +51,7 @@ import 'package:violet/pages/segment/platform_navigator.dart'; import 'package:violet/pages/settings/bookmark_version_select.dart'; import 'package:violet/pages/settings/db_rebuild_page.dart'; import 'package:violet/pages/settings/import_from_eh.dart'; +import 'package:violet/pages/settings/import_from_git.dart'; import 'package:violet/pages/settings/license_page.dart'; import 'package:violet/pages/settings/lock_setting_page.dart'; import 'package:violet/pages/settings/log_page.dart'; @@ -63,6 +66,7 @@ import 'package:violet/platform/misc.dart'; import 'package:violet/server/violet.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/style/palette.dart'; +import 'package:violet/util/git.dart'; import 'package:violet/util/helper.dart'; import 'package:violet/variables.dart'; import 'package:violet/version/sync.dart'; @@ -2057,6 +2061,147 @@ class _SettingsPageState extends State }, ), _buildDivider(), + ListTile( + leading: Icon( + MdiIcons.export, + color: Settings.majorColor, + ), + title: Text(Translations.of(context).trans('exportingbookmarkgit')), + trailing: const Icon(Icons.keyboard_arrow_right), + onTap: () async { + final dir = Platform.isIOS + ? await getApplicationSupportDirectory() + : (await getApplicationDocumentsDirectory()); + final bookmarkDatabaseFile = File('${dir.path}/user.db'); + + final gitPath = + '${(await getTemporaryDirectory()).path}/_tmp_git_bookmark'; + if (await Directory(gitPath).exists()) { + await Directory(gitPath).delete(recursive: true); + } + + final git = BookmarkGit(); + await git.clone(gitPath); + final extpath = '$gitPath/bookmark.db'; + await bookmarkDatabaseFile.copy(extpath); + await git.addAll(gitPath); + await git.commit(gitPath); + await git.push(gitPath); + if (await Directory(gitPath).exists()) { + await Directory(gitPath).delete(recursive: true); + } + }, + ), + _buildDivider(), + InkWell( + child: ListTile( + leading: Icon( + MdiIcons.cloudSearchOutline, + color: Settings.majorColor, + ), + title: Text(Translations.of(context).trans('importfromgit')), + trailing: const Icon(Icons.keyboard_arrow_right), + ), + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) => const ImportFromGitPage(), + ); + + if (GitBookmark.bookmarkInfo == null) { + flutterToast.showToast( + child: ToastWrapper( + isCheck: false, + isWarning: true, + msg: Translations.of(context).trans('bookmarkisempty'), + ), + ignorePointer: true, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 4), + ); + return; + } + + int count = 0; + + GitBookmark.bookmarkInfo?.forEach((description, bookmark) { + count += bookmark.length; + }); + + var qqq = await showYesNoDialog( + context, + Translations.of(context) + .trans('ensurecreatebookmark') + .replaceAll('\$1', count.toString())); + if (qqq) { + var bookmark = await Bookmark.getInstance(); + for (int i = 0; + i < (GitBookmark.bookmarkInfo?.keys.length ?? 0); + i++) { + if (GitBookmark + .bookmarkInfo![ + GitBookmark.bookmarkInfo?.keys.elementAt(i)] + ?.isEmpty ?? + true) continue; + final name = + GitBookmark.bookmarkInfo?.keys.elementAtOrNull(i)?.name ?? + 'Favorite $i'; + final description = GitBookmark.bookmarkInfo?.keys + .elementAtOrNull(i) + ?.description ?? + ''; + final color = Color(GitBookmark.bookmarkInfo?.keys + .elementAtOrNull(i) + ?.color ?? + Colors.deepOrange.value); + // final datetime = DateTime.tryParse(GitBookmark.bookmarkInfo?.keys?.elementAtOrNull(i)?.dateTime ?? '') ?? DateTime.now(); + final datetime = DateTime.now(); + await bookmark.createGroup( + name, description, color, datetime); + var group = (await bookmark.getGroup()) + .where((element) => (element.name() == name && + element.description() == description && + // element.color() == color.value && + DateTime.tryParse(element.datetime()) == datetime)) + .first + .id(); + for (int j = 0; + j < + (GitBookmark + .bookmarkInfo![GitBookmark.bookmarkInfo?.keys + .elementAt(i)] + ?.length ?? + 0); + j++) { + try { + await bookmark.insertArticle( + GitBookmark.bookmarkInfo![ + GitBookmark.bookmarkInfo?.keys.elementAt(i)]! + .elementAt(j) + .article + .toString(), + DateTime.now(), + group); + } catch (_) { + Logger.error(''); + } + } + } + flutterToast.showToast( + child: ToastWrapper( + isCheck: true, + isWarning: false, + msg: Translations.of(context) + .trans('completeimportbookmark'), + ), + ignorePointer: true, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 4), + ); + } + }, + ), + _buildDivider(), InkWell( child: ListTile( leading: Icon( @@ -2444,6 +2589,134 @@ class _SettingsPageState extends State ), ], ), + _buildItems( + [ + InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: 'https://git-scm.com/favicon.ico', + width: 25, + ), + title: const Text('Git Config'), + trailing: const Icon(Icons.keyboard_arrow_right), + ), + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + var bookmarkRepository = prefs.getString('bookmarkRepository'); + var bookmarkHost = prefs.getString('bookmarkHost'); + var bookmarkPrivateKey = prefs.getString('bookmarkPrivateKey'); + var bookmarkPublicKey = prefs.getString('bookmarkPublicKey'); + var bookmarkPrivateKeyPassword = + prefs.getString('bookmarkPrivateKeyPassword'); + + var rController = TextEditingController(text: bookmarkRepository); + var hController = TextEditingController(text: bookmarkHost); + var kController = TextEditingController(text: bookmarkPrivateKey); + var pkController = TextEditingController(text: bookmarkPublicKey); + var pController = + TextEditingController(text: bookmarkPrivateKeyPassword); + Widget okButton = TextButton( + style: + TextButton.styleFrom(foregroundColor: Settings.majorColor), + child: Text(Translations.of(context).trans('ok')), + onPressed: () async { + await Settings.setBookmarkHost(hController.text); + await Settings.setBookmarkRepository(rController.text); + await Settings.setBookmarkPrivateKey(kController.text); + await Settings.setBookmarkPublicKey(pkController.text); + await Settings.setBookmarkPrivateKeyPassword( + pController.text); + Navigator.pop(context, true); + flutterToast.showToast( + child: ToastWrapper( + isWarning: true, + msg: Translations.of(context).trans('bookmarkexportgit'), + ), + ignorePointer: true, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 4), + ); + }, + ); + Widget generateButton = TextButton( + style: + TextButton.styleFrom(foregroundColor: Settings.majorColor), + child: Text(Translations.of(context).trans('generate')), + onPressed: () async { + (String, String) keyPair = GitBindings().generateRsaKeys(); + pkController.text = keyPair.$1; + kController.text = keyPair.$2; + }, + ); + + Widget cancelButton = TextButton( + style: + TextButton.styleFrom(foregroundColor: Settings.majorColor), + child: Text(Translations.of(context).trans('cancel')), + onPressed: () async { + Navigator.pop(context, false); + }, + ); + await showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + actions: [generateButton, okButton, cancelButton], + title: const Text('Git Config'), + contentPadding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row(children: [ + const Text('Repository: '), + Expanded( + child: TextField( + controller: rController, + ), + ), + ]), + Row(children: [ + const Text('Host: '), + Expanded( + child: TextField( + controller: hController, + ), + ), + ]), + Row(children: [ + const Text('Private Key: '), + Expanded( + child: TextField( + controller: kController, + ), + ), + ]), + Row(children: [ + const Text('Public Key: '), + Expanded( + child: TextField( + controller: pkController, + ), + ), + ]), + Row(children: [ + const Text('Private Key Password: '), + Expanded( + child: TextField( + controller: pController, + ), + ), + ]), + ], + ), + ), + ); + }, + ), + ], + ), ]; } diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index ded814f28..b42d891ae 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -20,6 +20,14 @@ import 'package:violet/settings/device_type.dart'; class Settings { static late final SharedPreferences prefs; + // Bookmark Git Settings + static late String bookmarkRepository; // default 'example/bookmark' + static late String bookmarkHost; // default 'gitee.com' + static late String + bookmarkPrivateKey; // default '-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----' + static late String bookmarkPublicKey; // default '' + static late String bookmarkPrivateKeyPassword; // default '' + // Timeout Settings static late bool ignoreTimeout; // default false @@ -303,6 +311,13 @@ class Settings { searchMessageAPI = await _getString( 'searchmessageapi', 'https://koromo.xyz/api/search/msg'); + bookmarkRepository = + await _getString('bookmarkRepository', 'example/bookmark'); + bookmarkHost = await _getString('bookmarkHost', 'gitee.com'); + bookmarkPrivateKey = await _getString('bookmarkPrivateKey', + '-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----'); + bookmarkPrivateKeyPassword = + await _getString('bookmarkPrivateKeyPassword', ''); ignoreTimeout = await _getBool('ignoretimeout'); useVioletServer = await _getBool('usevioletserver'); useDrawer = await _getBool('usedrawer'); @@ -471,6 +486,37 @@ class Settings { majorAccentColor = accent; } + static Future setBookmarkRepository(String value) async { + bookmarkRepository = value; + + await prefs.setString('bookmarkRepository', bookmarkRepository); + } + + static Future setBookmarkHost(String value) async { + bookmarkHost = value; + + await prefs.setString('bookmarkHost', bookmarkHost); + } + + static Future setBookmarkPrivateKey(String value) async { + bookmarkPrivateKey = value; + + await prefs.setString('bookmarkPrivateKey', bookmarkPrivateKey); + } + + static Future setBookmarkPublicKey(String value) async { + bookmarkPublicKey = value; + + await prefs.setString('bookmarkPublicKey', bookmarkPublicKey); + } + + static Future setBookmarkPrivateKeyPassword(String value) async { + bookmarkPrivateKeyPassword = value; + + await prefs.setString( + 'bookmarkPrivateKeyPassword', bookmarkPrivateKeyPassword); + } + static Future setSearchResultType(int wh) async { searchResultType = wh; diff --git a/lib/util/git.dart b/lib/util/git.dart new file mode 100644 index 000000000..ebd97b3ea --- /dev/null +++ b/lib/util/git.dart @@ -0,0 +1,143 @@ +import 'dart:io'; +import 'dart:convert'; + +import 'package:dart_git/exceptions.dart'; +import 'package:dart_git/git.dart'; +import 'package:dart_git/plumbing/objects/commit.dart'; +import 'package:go_git_dart/go_git_dart.dart' as go_git_dart; +import 'package:dart_git/dart_git.dart' as dart_git; +import 'package:violet/log/log.dart'; +import 'package:violet/settings/settings.dart'; + +class BookmarkGit { + String? repo; + String? host; + String? key; + String? pass; + go_git_dart.GitBindings? gitBindings; + Future _init() async { + repo = Settings.bookmarkRepository; + host = Settings.bookmarkHost; + key = Settings.bookmarkPrivateKey; + pass = Settings.bookmarkPrivateKeyPassword; + } + + Future init(String path) async { + await _init(); + try { + GitRepository.init(path); + Logger.info('[BookmarkGit.init] Successfully init repo'); + } catch (e, st) { + Logger.error('[BookmarkGit.init] $e\n' + '$st'); + } + GitRepository gitRepo = await load(path); + if (gitRepo.config.remote('origin') == null) { + try { + gitRepo.addRemote('origin', 'git@$host:$repo.git'); + Logger.info( + '[BookmarkGit.init] Successfully add remote origin to git@$host:$repo.git'); + } catch (e, st) { + Logger.error('[BookmarkGit.clone] $e\n' + '$st'); + } + } + return gitRepo; + } + + Future load(String path) async { + await _init(); + late GitRepository gitRepo; + try { + gitRepo = GitRepository.load(path); + } catch (e) { + rethrow; + } + return gitRepo; + } + + Future clone(String path) async { + await _init(); + var gitBindings = go_git_dart.GitBindings(); + try { + gitBindings.clone('git@$host:$repo.git', path, utf8.encode(key!), pass!); + Logger.info( + '[BookmarkGit.clone] Successfully cloned from git@$host:$repo.git'); + } catch (e, st) { + Logger.error('[BookmarkGit.clone] $e\n' + '$st'); + await init(path); + } + GitRepository gitRepo = await load(path); + if (gitRepo.config.remote('origin') == null) { + try { + gitRepo.addRemote('origin', 'git@$host:$repo.git'); + } catch (e, st) { + Logger.error('[BookmarkGit.clone] $e\n' + '$st'); + } + } + return gitRepo; + } + + Future addAll(String path) async { + GitRepository gitRepo = await load(path); + if (Directory(path).existsSync()) { + List listInPath = + Directory(path).listSync(recursive: true, followLinks: false); + // ignore: avoid_function_literals_in_foreach_calls + listInPath.forEach((absolutePath) { + final relativePath = absolutePath.path + .replaceFirst(path, '') // It removes the current path in string + .split('/') // It handles '/a//b/c' to ['','a','','b','c'] + .where((p) => + p.isNotEmpty) // It handles ['','a','','b','c'] to ['a','b','c'] + .join('/'); // It handles ['a','b','c'] to 'a/b/c' + if (relativePath.split('/').isNotEmpty) { + if (relativePath.split('/').firstOrNull != '.git') { + gitRepo.add(absolutePath.path); + } + } + }); + } + return gitRepo; + } + + Future commit(String path) async { + await _init(); + GitRepository gitRepo = GitRepository.load(path); + gitRepo.config.user = GitAuthor( + name: 'Violet Committer', // It handles git config user.name + email: 'violet@no-reply.koromo.xyz' // It handles git config user.email + ); + try { + gitRepo.commit( + // It handles commit + message: DateTime.now().toIso8601String(), // Message with DateTime + author: gitRepo.config.user!, // Author with Violet Committer + committer: gitRepo.config.user! // Committer with VioletCommitter + ); + } catch (e, st) { + if (e is GitEmptyCommit) { + Logger.warning('[BookmarkGit.commit] Nothing to commit'); + } else { + Logger.error('[BookmarkGit.commit] $e\n' + '$st'); + } + } + return gitRepo; + } + + Future push(String path) async { + await _init(); + var gitBindings = go_git_dart.GitBindings(); + try { + gitBindings.push('origin', path, utf8.encode(key!), pass!); + Logger.info('[BookmarkGit.push] Successfully pushed\n' + 'to git@$host:$repo.git'); + } catch (e, st) { + Logger.error('[BookmarkGit.push] $e\n' + '$st'); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 270c16c26..51772f311 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.6" + buffer: + dependency: transitive + description: + name: buffer + sha256: "94f60815065a8f0fd4f05be51faf86cf86519327e039d5c2aac72e1d1cc1dad4" + url: "https://pub.dev" + source: hosted + version: "1.2.2" build: dependency: transitive description: @@ -225,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -353,6 +369,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + dart_git: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "2ca0f3e2b71f1e327e951a2afc9f073519119e14" + url: "https://github.com/GitJournal/dart-git" + source: git + version: "0.0.2" dart_style: dependency: transitive description: @@ -377,6 +402,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -434,6 +467,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" expandable: dependency: "direct main" description: @@ -466,6 +507,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fast_immutable_collections: + dependency: transitive + description: + name: fast_immutable_collections + sha256: "3eb1d7495c70598964add20e10666003fad6e855b108fe684ebcbf8ad0c8e120" + url: "https://pub.dev" + source: hosted + version: "9.2.0" ffi: dependency: "direct main" description: @@ -847,6 +896,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_git_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "9110267d46fd0c8079885d746662db216ef0bb36" + url: "https://github.com/GitJournal/go_git_dart" + source: git + version: "0.0.1" gradient_widgets: dependency: "direct main" description: @@ -1822,5 +1880,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7d03cd7fa..991c443ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -125,6 +125,10 @@ dependencies: webview_flutter: ^3.0.0 collection: ^1.16.0 octo_image: ^2.0.0 + dart_git: + git: https://github.com/GitJournal/dart-git + go_git_dart: + git: https://github.com/GitJournal/go_git_dart dev_dependencies: flutter_test: